Compare commits
BIN
build/icon.icns
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 105 KiB |
44724
package-lock.json
generated
|
|
@ -1,11 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "Crimata",
|
"name": "Crimata",
|
||||||
"version": "1.0.0-beta.0",
|
"version": "0.9.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Cross-platform messenger application built with electron, vue3, and TS.",
|
"description": "Cross-platform messenger application built with electron, vue3, and TS.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Enrique Hernandez",
|
"name": "Enrique Hernandez"
|
||||||
"name": "Andrew Gundersen"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vue-cli-service electron:build",
|
"build": "vue-cli-service electron:build",
|
||||||
|
|
@ -13,7 +12,7 @@
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"postuninstall": "electron-builder install-app-deps"
|
"postuninstall": "electron-builder install-app-deps"
|
||||||
},
|
},
|
||||||
"main": "background.js",
|
"main": "init.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google-cloud/speech": "^4.2.0",
|
"@google-cloud/speech": "^4.2.0",
|
||||||
"@types/animejs": "^3.1.2",
|
"@types/animejs": "^3.1.2",
|
||||||
|
|
@ -55,7 +54,7 @@
|
||||||
"@vue/eslint-config-typescript": "^5.0.2",
|
"@vue/eslint-config-typescript": "^5.0.2",
|
||||||
"@vue/test-utils": "^2.0.0-0",
|
"@vue/test-utils": "^2.0.0-0",
|
||||||
"@wasm-tool/wasm-pack-plugin": "^1.3.1",
|
"@wasm-tool/wasm-pack-plugin": "^1.3.1",
|
||||||
"electron": "11.4.10",
|
"electron": "^9.0.0",
|
||||||
"electron-devtools-installer": "^3.1.0",
|
"electron-devtools-installer": "^3.1.0",
|
||||||
"electron-log": "^4.3.4",
|
"electron-log": "^4.3.4",
|
||||||
"eslint": "^6.7.2",
|
"eslint": "^6.7.2",
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 4 KiB |
100
src/account.ts
|
|
@ -1,92 +1,75 @@
|
||||||
|
|
||||||
import { app } from "electron";
|
|
||||||
import { parseAuthRes } from "@/auth";
|
|
||||||
import { postAuth, postLogin, postLogout } from "@/api/account";
|
import { postAuth, postLogin, postLogout } from "@/api/account";
|
||||||
import { updateAppUI, launchSession, endSession } from "@/session";
|
import { endSession, launchSession } from "@/session";
|
||||||
import { getToken, setToken, clearToken } from "./store";
|
import { getToken, setToken, setProfile, getProfile, clearStore } from "./store";
|
||||||
import { backgroundMitt, ipcEmit } from "@/composables/useEmitter";
|
import { parseAuthRes } from "./auth";
|
||||||
|
import { ipcEmit } from "@/composables/useEmitter";
|
||||||
|
|
||||||
|
export const accountAuth = async (): Promise<Error | AuthState> => {
|
||||||
/* Either null or a crimataId */
|
|
||||||
let account: string | null = null;
|
|
||||||
|
|
||||||
|
|
||||||
export const accountAuth = async (): Promise<void> => {
|
|
||||||
|
|
||||||
/* attempt to get a login token from the store */
|
/* attempt to get a login token from the store */
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
|
||||||
try {
|
/* try to login with it, returns platform secret and new token on success */
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
|
||||||
if (token) {
|
|
||||||
|
|
||||||
// attempt to login with token
|
|
||||||
const res = await postAuth(token);
|
const res = await postAuth(token);
|
||||||
|
|
||||||
const parsed = parseAuthRes(res);
|
const parsed = parseAuthRes(res);
|
||||||
|
|
||||||
// save jwt token and profile
|
setToken(parsed.token)
|
||||||
setToken(parsed.token);
|
setProfile(parsed.profile);
|
||||||
account = parsed.crimataId
|
|
||||||
|
|
||||||
// launch session
|
return {
|
||||||
launchSession(parsed.token);
|
profile: parsed.profile,
|
||||||
|
token: parsed.token
|
||||||
|
};
|
||||||
|
|
||||||
} else throw(new Error('Failed to authenticate (no token).'));
|
} catch(e) {
|
||||||
|
console.log('[ACCOUNT]', e);
|
||||||
} catch (e) {
|
clearStore();
|
||||||
console.log(e);
|
throw(new Error('Failed to authenticate.'));
|
||||||
clearToken();
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
|
|
||||||
// push state changes to the frontend
|
|
||||||
updateAppState();
|
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw(new Error('Unable to authenticate.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const accountLogin = async (payload: any): Promise<Error | void> => {
|
export const accountLogin: IpcHandlerCallback<AccountCredentials, Profile> = async (payload) => {
|
||||||
|
const account = payload as AccountCredentials;
|
||||||
const creds = payload as AccountCredentials;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// attempt login with email password
|
// attempt login with email password
|
||||||
const res = await postLogin(creds.email, creds.password);
|
const res = await postLogin(account.email, account.password);
|
||||||
const parsed = parseAuthRes(res);
|
const parsed = parseAuthRes(res);
|
||||||
|
|
||||||
// save jwt token and profile
|
// save jwt token and profile
|
||||||
setToken(parsed.token);
|
setToken(parsed.token);
|
||||||
account = parsed.crimataId;
|
setProfile(parsed.profile);
|
||||||
|
|
||||||
// launch session
|
// launch session
|
||||||
launchSession(parsed.token);
|
launchSession(parsed.token);
|
||||||
|
|
||||||
// push state changes to the frontend
|
// return profile to renderer
|
||||||
updateAppState();
|
return parsed.profile;
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
clearToken();
|
clearStore();
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const accountLogout = async (): Promise<Error | void> => {
|
export const accountLogout = async (): Promise<Error | void> => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// post logout to backend
|
// post logout to backend
|
||||||
await postLogout();
|
await postLogout();
|
||||||
|
|
||||||
// remove key and account
|
// remove key and crimataId
|
||||||
clearToken();
|
clearStore();
|
||||||
account = null;
|
|
||||||
|
|
||||||
// push account state to browser
|
|
||||||
ipcEmit("set-account", account);
|
|
||||||
|
|
||||||
// kill crimata platform session
|
// kill crimata platform session
|
||||||
endSession();
|
endSession();
|
||||||
|
|
@ -100,24 +83,13 @@ export const accountLogout = async (): Promise<Error | void> => {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// logout when auth fails on platform end
|
export const updateAppState = (): void => {
|
||||||
backgroundMitt.on("close-auth-fail", async (_payload: any) => {
|
|
||||||
|
|
||||||
await accountLogout();
|
const profile = getProfile();
|
||||||
|
|
||||||
});
|
ipcEmit("set-profile", profile);
|
||||||
|
|
||||||
// emits state of account, version, and UI
|
// ipcEmit('messages') etc
|
||||||
export const updateAppState = () => {
|
|
||||||
|
|
||||||
ipcEmit("set-account", account);
|
|
||||||
|
|
||||||
ipcEmit("set-version", app.getVersion());
|
|
||||||
|
|
||||||
updateAppUI();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
187
src/audio.ts
Normal 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 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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
14
src/auth.ts
|
|
@ -1,5 +1,13 @@
|
||||||
|
|
||||||
export const parseAuthRes = (authRes: any) => {
|
export const parseAuthRes = (authRes: any) => {
|
||||||
const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string;
|
const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string;
|
||||||
const crimataId = authRes.data as string;
|
const profile = authRes.data as Profile;
|
||||||
return {token, crimataId}
|
return {
|
||||||
};
|
token,
|
||||||
|
profile
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,10 +71,12 @@ export class IpcListener<InputParam> implements IIpcListener<InputParam> {
|
||||||
ipcMain.removeAllListeners(this.channel);
|
ipcMain.removeAllListeners(this.channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onPost = (_e: IpcMainEvent, payload: InputParam): void => {
|
private _onPost = (_e: IpcMainEvent, payload?: string | null): void => {
|
||||||
|
|
||||||
console.log(`[IPC] Post: ${this.channel}`);
|
console.log(`[IPC] Post: ${this.channel}`);
|
||||||
this._listenerCallback(payload);
|
|
||||||
|
const params = payload ? JSON.parse(payload) : null;
|
||||||
|
this._listenerCallback(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -11,13 +11,7 @@ let _connectionCheckInterval: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
export default function useWebSockets(
|
export default function useWebSockets(
|
||||||
messageCallback: (message: string) => void,
|
messageCallback: (message: string) => void,
|
||||||
connectionStatusCallback: (status: string) => void,
|
connectionStatusCallback: (alive: boolean) => void,
|
||||||
statusOptions?: {
|
|
||||||
openMessage: string;
|
|
||||||
pongMessage: string;
|
|
||||||
closeMessage: string;
|
|
||||||
pingErrorMessage: string;
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
let socket: WebSocket;
|
let socket: WebSocket;
|
||||||
|
|
@ -47,15 +41,13 @@ export default function useWebSockets(
|
||||||
|
|
||||||
socket.send(JSON.stringify({key: secret}));
|
socket.send(JSON.stringify({key: secret}));
|
||||||
|
|
||||||
connectionStatusCallback(statusOptions ? statusOptions.openMessage : "Connection Opened");
|
|
||||||
|
|
||||||
// ping server
|
// ping server
|
||||||
_connectionCheckInterval = setInterval(() => {
|
_connectionCheckInterval = setInterval(() => {
|
||||||
|
|
||||||
socket.ping(null, true, (e: Error) => {
|
socket.ping(null, true, (e: Error) => {
|
||||||
if (e) {
|
if (e) {
|
||||||
socket.close();
|
socket.close();
|
||||||
connectionStatusCallback(statusOptions ? statusOptions.pingErrorMessage : "Connection Lost");
|
connectionStatusCallback(false);
|
||||||
setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
|
setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -69,28 +61,24 @@ export default function useWebSockets(
|
||||||
messageCallback(event.toString())
|
messageCallback(event.toString())
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("close", (code: number, reason: string) => {
|
socket.on("close", (event: WebSocket.CloseEvent) => {
|
||||||
connectionStatusCallback(statusOptions ? statusOptions.closeMessage : "Connection Closed");
|
connectionStatusCallback(false);
|
||||||
clearInterval(_connectionCheckInterval);
|
clearInterval(_connectionCheckInterval);
|
||||||
if (code !== 1000 || reason !== 'session-logout') {
|
if (!event.wasClean) {
|
||||||
setTimeout(() => {
|
setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
|
||||||
connect(socketUrl, secret), _reconnectTimeout
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("pong", () => connectionStatusCallback(statusOptions ? statusOptions.pongMessage : "Pong"));
|
socket.on("pong", () => connectionStatusCallback(true));
|
||||||
|
|
||||||
socket.on('error', () => {});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const close = (code: number, reason: string) => {
|
const close = () => {
|
||||||
if (socket) {
|
if (socket) {
|
||||||
socket.close(code, reason);
|
socket.close();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connect,
|
connect,
|
||||||
|
|
|
||||||
|
|
@ -13,5 +13,5 @@ export const config = {
|
||||||
PLATFORM_URL: `${PLATFORM_IP}:${PLATFORM_PORT}`,
|
PLATFORM_URL: `${PLATFORM_IP}:${PLATFORM_PORT}`,
|
||||||
BUSINESS_URL: `${BUSINESS_IP}:${BUSINESS_PORT}`,
|
BUSINESS_URL: `${BUSINESS_IP}:${BUSINESS_PORT}`,
|
||||||
BUSINESS_PREFIX: '/api',
|
BUSINESS_PREFIX: '/api',
|
||||||
configPath: app.getPath('userData'),
|
configPath: app.getPath('userData')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import { app, protocol } from "electron";
|
||||||
import createWindow from "./window";
|
import createWindow from "./window";
|
||||||
import main from "./main";
|
import main from "./main";
|
||||||
import { backgroundMitt } from '@/composables/useEmitter';
|
import { backgroundMitt } from '@/composables/useEmitter';
|
||||||
import {setWindowOpen, getWindowOpen } from './store';
|
|
||||||
|
|
||||||
console.log('Starting Crimata electron app.');
|
console.log('Starting Crimata electron app.');
|
||||||
|
|
||||||
|
|
@ -20,9 +19,11 @@ protocol.registerSchemesAsPrivileged([
|
||||||
|
|
||||||
const isDev = require('electron-is-dev');
|
const isDev = require('electron-is-dev');
|
||||||
|
|
||||||
|
let win: boolean;
|
||||||
|
|
||||||
// Listen for window creation.
|
// Listen for window creation.
|
||||||
backgroundMitt.on('window-active', (state: boolean) => {
|
backgroundMitt.on('window-active', (state: boolean) => {
|
||||||
setWindowOpen(state)
|
win = state;
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Start main process on ready */
|
/* Start main process on ready */
|
||||||
|
|
@ -40,7 +41,7 @@ app.on("window-all-closed", () => {
|
||||||
|
|
||||||
// When user clicks app icon (re-open)
|
// When user clicks app icon (re-open)
|
||||||
app.on("activate", () => {
|
app.on("activate", () => {
|
||||||
if (!getWindowOpen()) createWindow();
|
if (!win) createWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Exit cleanly on request from parent process in development mode.
|
// Exit cleanly on request from parent process in development mode.
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import { accountLogin, accountLogout } from "@/account";
|
import { accountLogin, accountLogout, accountProfile } from "@/account";
|
||||||
import { IpcHandler } from "@/composables/useIpcMain";
|
import { IpcHandler } from "@/composables/useIpcMain";
|
||||||
|
import { flush } from "@/audio";
|
||||||
|
|
||||||
|
|
||||||
const LOGIN_CHANNEL = "invoke-account-login";
|
const LOGIN_CHANNEL = "invoke-account-login";
|
||||||
const LOGOUT_CHANNEL = "invoke-account-logout";
|
const LOGOUT_CHANNEL = "invoke-account-logout";
|
||||||
|
const GET_AUDIO_CHANNEL = "invoke-audio-flush";
|
||||||
|
|
||||||
export const loginHandler = new IpcHandler({
|
export const loginHandler = new IpcHandler({
|
||||||
channel: LOGIN_CHANNEL,
|
channel: LOGIN_CHANNEL,
|
||||||
|
|
@ -17,3 +19,10 @@ export const logoutHandler = new IpcHandler({
|
||||||
channel: LOGOUT_CHANNEL,
|
channel: LOGOUT_CHANNEL,
|
||||||
handlerCallback: accountLogout
|
handlerCallback: accountLogout
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getAudioHandler = new IpcHandler({
|
||||||
|
channel: GET_AUDIO_CHANNEL,
|
||||||
|
handlerCallback: flush
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,24 @@
|
||||||
|
|
||||||
import { IpcListener } from "@/composables/useIpcMain"
|
import { IpcListener } from "@/composables/useIpcMain"
|
||||||
import { sendMessage } from '@/session';
|
import { sendMessage } from '@/session';
|
||||||
|
import { collect } from "@/audio";
|
||||||
import { updateAppState } from "@/account";
|
import { updateAppState } from "@/account";
|
||||||
import { onNavBar } from "@/window";
|
|
||||||
import { setWindowFocus } from '@/store';
|
|
||||||
|
|
||||||
const CLIENT_MESSAGE_CHANNEL = "post-session-send"
|
const CLIENT_MESSAGE_CHANNEL = "post-session-send"
|
||||||
|
const GET_AUDIO_CHANNEL = "post-audio-collect";
|
||||||
const APP_MOUNT_CHANNEL = "post-app-mount";
|
const APP_MOUNT_CHANNEL = "post-app-mount";
|
||||||
const NAV_BAR_CHANNEL = "post-nav-bar";
|
|
||||||
const POST_WINDOW_FOCUS = 'post-window-focus';
|
|
||||||
|
|
||||||
export const messageListener = new IpcListener<Raw | Request>({
|
export const messageListener = new IpcListener<Message>({
|
||||||
channel: CLIENT_MESSAGE_CHANNEL,
|
channel: CLIENT_MESSAGE_CHANNEL,
|
||||||
listenerCallback: sendMessage
|
listenerCallback: sendMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const audioChunkListener = new IpcListener<ArrayBuffer>({
|
||||||
|
channel: GET_AUDIO_CHANNEL,
|
||||||
|
listenerCallback: collect
|
||||||
|
});
|
||||||
|
|
||||||
export const appMountListener = new IpcListener<null>({
|
export const appMountListener = new IpcListener<null>({
|
||||||
channel: APP_MOUNT_CHANNEL,
|
channel: APP_MOUNT_CHANNEL,
|
||||||
listenerCallback: updateAppState
|
listenerCallback: updateAppState
|
||||||
});
|
});
|
||||||
|
|
||||||
export const navBarListener = new IpcListener({
|
|
||||||
channel: NAV_BAR_CHANNEL,
|
|
||||||
listenerCallback: onNavBar
|
|
||||||
});
|
|
||||||
|
|
||||||
export const windowFocusListener = new IpcListener<FocusPayload>({
|
|
||||||
channel: POST_WINDOW_FOCUS,
|
|
||||||
listenerCallback: ({ isFocused }) => setWindowFocus(isFocused)
|
|
||||||
});
|
|
||||||
|
|
|
||||||
24
src/main.ts
|
|
@ -10,22 +10,30 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import initIpcMain from "@/ipc/index";
|
import initIpcMain from "@/ipc/index";
|
||||||
import { accountAuth } from "./account";
|
import { accountAuth, updateAppState } from "./account";
|
||||||
|
import { launchSession } from "./session";
|
||||||
import createWindow from "./window";
|
import createWindow from "./window";
|
||||||
import { initTray } from './tray';
|
|
||||||
|
let authState: AuthState | null;
|
||||||
|
|
||||||
export default async function main() {
|
export default async function main() {
|
||||||
|
|
||||||
/* initiate render process event listeners & handlers */
|
/* initiate controls for frontend to use when needed */
|
||||||
initIpcMain();
|
initIpcMain();
|
||||||
|
|
||||||
/* initiate tray icon in default state */
|
|
||||||
initTray();
|
|
||||||
|
|
||||||
/* launch browser window */
|
/* launch browser window */
|
||||||
await createWindow();
|
await createWindow();
|
||||||
|
|
||||||
/* try to authenticate with token */
|
try {
|
||||||
accountAuth();
|
authState = await accountAuth() as AuthState;
|
||||||
|
} catch(e) {
|
||||||
|
console.log('AUTH:', e);
|
||||||
|
authState = null;
|
||||||
|
} finally {
|
||||||
|
if (authState) {
|
||||||
|
launchSession(authState.token as string);
|
||||||
|
}
|
||||||
|
updateAppState();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import { ipcEmit } from "@/composables/useEmitter";
|
|
||||||
|
|
||||||
|
|
||||||
export default class ProfileHandler {
|
|
||||||
|
|
||||||
profile: Profile;
|
|
||||||
|
|
||||||
constructor(profile: Profile) {
|
|
||||||
this.profile = profile;
|
|
||||||
this.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
emit() {
|
|
||||||
ipcEmit("set-profile", this.profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
update(update: Update) {
|
|
||||||
this.profile = update.data as Profile;
|
|
||||||
this.emit()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- Only render when profile has been set -->
|
<!-- Only render when profile has been set -->
|
||||||
<main id="app" v-if="ready">
|
<main id="app" v-if="authComplete">
|
||||||
|
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<Messenger v-if="account"/>
|
<Messenger
|
||||||
|
v-if="profile"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Main Components
|
||||||
|
-->
|
||||||
<Login v-else />
|
<Login v-else />
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
@ -17,13 +22,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import { defineComponent, onMounted } from "vue";
|
import { defineComponent, onMounted } from "vue";
|
||||||
|
import { IpcRendererEvent } from "electron";
|
||||||
|
|
||||||
import Splash from "@/render/components/splash.vue";
|
import Splash from "@/render/components/splash.vue";
|
||||||
import Messenger from "@/render/components/messenger.vue";
|
import Messenger from "@/render/components/messenger.vue";
|
||||||
import Login from "@/render/components/login.vue";
|
import Login from "@/render/components/login.vue";
|
||||||
import Header from "@/render/components/header.vue";
|
import Header from "@/render/components/header.vue";
|
||||||
|
|
||||||
import { ready, account, setAccount, clearAccount } from "@/render/shared/account";
|
import { profile, authComplete } from "@/render/composables/useProfile";
|
||||||
import { postAppMount } from "@/render/ipc";
|
import { postAppMount } from "@/render/ipc";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
@ -37,15 +43,11 @@ export default defineComponent({
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => postAppMount());
|
||||||
|
|
||||||
postAppMount()
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
account,
|
authComplete,
|
||||||
ready
|
profile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24.87" height="23.001" viewBox="0 0 24.87 23.001">
|
|
||||||
<g id="Group_541" data-name="Group 541" transform="translate(-4127 -507)">
|
|
||||||
<g id="Group_33" data-name="Group 33" transform="translate(4127 507)">
|
|
||||||
<g id="Group_32" data-name="Group 32" transform="translate(0 0)">
|
|
||||||
<g id="Group_31" data-name="Group 31" transform="translate(0 11.948)">
|
|
||||||
<circle id="Ellipse_50" data-name="Ellipse 50" cx="5.527" cy="5.527" r="5.527" transform="translate(0 0)" fill="#383838"/>
|
|
||||||
<circle id="Ellipse_51" data-name="Ellipse 51" cx="5.527" cy="5.527" r="5.527" transform="translate(13.817 0)" fill="#383838"/>
|
|
||||||
</g>
|
|
||||||
<circle id="Ellipse_52" data-name="Ellipse 52" cx="5.527" cy="5.527" r="5.527" transform="translate(6.898 0)" fill="#383838"/>
|
|
||||||
<path id="Path_150" data-name="Path 150" d="M36.27,83.67" transform="translate(-30.733 -66.196)" fill="#ff0"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 973 B |
|
|
@ -1,10 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30">
|
|
||||||
<g id="Group_604" data-name="Group 604" transform="translate(-366 -464)">
|
|
||||||
<circle id="Ellipse_69" data-name="Ellipse 69" cx="15" cy="15" r="15" transform="translate(366 464)" fill="#fff"/>
|
|
||||||
<g id="Group_603" data-name="Group 603">
|
|
||||||
<circle id="Ellipse_50" data-name="Ellipse 50" cx="3.116" cy="3.116" r="3.116" transform="translate(374 479.252)" fill="#383838"/>
|
|
||||||
<circle id="Ellipse_51" data-name="Ellipse 51" cx="3.116" cy="3.116" r="3.116" transform="translate(381.789 479.252)" fill="#383838"/>
|
|
||||||
<circle id="Ellipse_52" data-name="Ellipse 52" cx="3.116" cy="3.116" r="3.116" transform="translate(377.889 472.517)" fill="#383838"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 766 B |
|
|
@ -1,7 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="5" viewBox="0 0 25 5">
|
|
||||||
<g id="Group_611" data-name="Group 611" transform="translate(-4032 499)">
|
|
||||||
<circle id="Ellipse_71" data-name="Ellipse 71" cx="2.5" cy="2.5" r="2.5" transform="translate(4032 -499)" fill="#9b9b9b"/>
|
|
||||||
<circle id="Ellipse_72" data-name="Ellipse 72" cx="2.5" cy="2.5" r="2.5" transform="translate(4042 -499)" fill="#9b9b9b"/>
|
|
||||||
<circle id="Ellipse_73" data-name="Ellipse 73" cx="2.5" cy="2.5" r="2.5" transform="translate(4052 -499)" fill="#9b9b9b"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 553 B |
|
|
@ -1,18 +1,53 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="`bubble ${modifier}-bubble ${modifier}-${child}`">
|
<div :class="`${type}-message`">
|
||||||
<div v-show="!seen" class="notify"></div>
|
|
||||||
{{ text }}
|
<!-- message bubble -->
|
||||||
|
<div :class="`${type}-${child}-bubble`">
|
||||||
|
<div class="notification-dot"/>
|
||||||
|
<div class="error-dot"/>
|
||||||
|
{{ text }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- message context -->
|
||||||
|
<span class="context">
|
||||||
|
<div :class="`${type}-icon`"/>
|
||||||
|
{{ context }}
|
||||||
|
</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang='ts'>
|
<script lang='ts'>
|
||||||
|
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent, ref, onMounted } from 'vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Bubble",
|
name: "Bubble",
|
||||||
|
|
||||||
props: ["modifier", "text", "child", "seen"],
|
props: ["text", "context", "type", "position"],
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
|
||||||
|
const seen = ref(false);
|
||||||
|
/* initialize seen state */
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
seen.value = true;
|
||||||
|
} else seen.value = false;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
|
||||||
|
if(seen.value)
|
||||||
|
document.addEventListener("visibilitychange", () => {
|
||||||
|
seen.value = true
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
seen
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -20,79 +55,277 @@
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
.bubble {
|
.message {
|
||||||
position: relative;
|
width: 100vw;
|
||||||
max-width: 66vw;
|
display: flex;
|
||||||
font-family: "SF Pro Text";
|
flex-direction: column;
|
||||||
font-size: 14px;
|
padding-top: 9px;
|
||||||
padding: 10px;
|
padding-bottom: 9px;
|
||||||
border-radius: 18px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-bubble {
|
|
||||||
background-color: #FFFFFF;
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-bubble {
|
|
||||||
color: white;
|
|
||||||
background-color: #58C4FD;
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-first-child {
|
|
||||||
border-bottom-left-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-middle-child {
|
|
||||||
border-top-left-radius: 9px;
|
|
||||||
border-bottom-left-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-last-child {
|
|
||||||
border-top-left-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-first-child {
|
|
||||||
border-bottom-right-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-middle-child {
|
|
||||||
border-top-right-radius: 9px;
|
|
||||||
border-bottom-right-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-last-child {
|
|
||||||
border-top-right-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-none-child, .client-none-child {
|
|
||||||
border-top-right-radius: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notify {
|
|
||||||
position: absolute;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #58D9FF;
|
|
||||||
top: -5px;
|
|
||||||
left: -5px;
|
|
||||||
border: 2px solid #EBEBEB;
|
|
||||||
// transform: scale(0);
|
|
||||||
|
|
||||||
// animation-name: notify-anim;
|
|
||||||
// animation-duration: 5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes notify-anim {
|
|
||||||
0%, 90% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
}
|
||||||
100% {
|
|
||||||
|
.message:first-child {
|
||||||
|
margin-top: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message:last-child {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-message {
|
||||||
|
@extend .message;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-message, .fr-message {
|
||||||
|
@extend .message;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
position: relative;
|
||||||
|
max-width: 66vw;
|
||||||
|
font-family: "SF Pro Text";
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-bubble {
|
||||||
|
@extend .bubble;
|
||||||
|
background-color: #58c4fd;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.firstChildMessage {
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.middleChildMessage {
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lastChildMessage {
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#aiMessage {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sfMessage {
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
#frMessage {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageBox {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
// Animate on render.
|
||||||
|
animation-name: appear;
|
||||||
|
animation-duration: 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#aiMessageBox {
|
||||||
|
margin-left: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sfMessageBox {
|
||||||
|
margin-right: 20px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
#frMessageBox {
|
||||||
|
margin-left: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
max-width: 66vw;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
font-family: "SF Pro Text";
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes appear {
|
||||||
|
10% {
|
||||||
|
transform: scale(0.3);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#aiBubble {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sfBubble {
|
||||||
|
background-color: #58c4fd;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#frBubble {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-firstChildBubble {
|
||||||
|
border-bottom-right-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-middleChildBubble {
|
||||||
|
border-top-right-radius: 9px;
|
||||||
|
border-bottom-right-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-lastChildBubble {
|
||||||
|
border-top-right-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-firstChildBubble, .fr-firstChildBubble {
|
||||||
|
border-bottom-left-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-middleChildBubble, .fr-middleChildBubble {
|
||||||
|
border-top-left-radius: 9px;
|
||||||
|
border-bottom-left-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-lastChildBubble, .fr-lastChildBubble {
|
||||||
|
border-top-left-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notify {
|
||||||
|
position: absolute;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #58D9FF;
|
||||||
|
top: -5px;
|
||||||
|
left: -5px;
|
||||||
|
border: 2px solid #EBEBEB;
|
||||||
transform: scale(0);
|
transform: scale(0);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
animation-name: notify-anim;
|
||||||
|
animation-duration: 5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes notify-anim {
|
||||||
|
0%, 90% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.context {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
font-family: "SF Compact Display";
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
margin-right: 5px;
|
||||||
|
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply for audio message playback.
|
||||||
|
.playing {
|
||||||
|
animation-name: circle1;
|
||||||
|
animation-duration: 2s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes circle1 {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.4), 0 0 0 0.25em rgba(195, 195, 195, 0.15);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.15), 0 0 0 0.4em rgba(195, 195, 195, 0.3);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.55), 0 0 0 0.15em rgba(195, 195, 195, 0.05);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.25), 0 0 0 0.55em rgba(195, 195, 195, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentLoader {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentLoaderDot {
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
|
||||||
|
margin: 2px;
|
||||||
|
border-radius: 2.5px;
|
||||||
|
background-color: #357CA2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.questionMark {
|
||||||
|
font-weight: 900;
|
||||||
|
color: #357CA2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// .divider {
|
||||||
|
// width: 100vw;
|
||||||
|
// display: flex;
|
||||||
|
// justify-content: center;
|
||||||
|
// align-items: center;
|
||||||
|
|
||||||
|
// font-family: "SF Compact Display";
|
||||||
|
// font-size: 12px;
|
||||||
|
// font-weight: bold;
|
||||||
|
// color: #9B9B9B;
|
||||||
|
|
||||||
|
// margin-bottom: 18px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
<template>
|
|
||||||
<div id="container">
|
|
||||||
<img id="logo" src="@/render/assets/connectLogo.svg">
|
|
||||||
<div id="connect">
|
|
||||||
<div class="dot"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
|
|
||||||
import { defineComponent, onMounted, watch } from 'vue'
|
|
||||||
import { hiddenState, useAnim, status, show, hide } from "@/render/shared/connectionStatus";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "ConnectionStatus",
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
|
|
||||||
watch(status, (val, _oldval) => {
|
|
||||||
if (val === "Connected" || val === "Nominal") {
|
|
||||||
if (!hiddenState) hide();
|
|
||||||
} else {
|
|
||||||
if (hiddenState)
|
|
||||||
show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(useAnim);
|
|
||||||
|
|
||||||
return {
|
|
||||||
status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
#container {
|
|
||||||
position: fixed;
|
|
||||||
width: 100vw;
|
|
||||||
height: 54px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#connect {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
shape {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #48E065;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot {
|
|
||||||
@extend shape;
|
|
||||||
position: relative;
|
|
||||||
transform: translateX(-15px);
|
|
||||||
animation: flashing 1s infinite linear alternate;
|
|
||||||
animation-delay: .25s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot::before, .dot::after {
|
|
||||||
content: '';
|
|
||||||
display: inline-block;
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot::before {
|
|
||||||
@extend shape;
|
|
||||||
left: -9px;
|
|
||||||
animation: flashing 1s infinite alternate;
|
|
||||||
animation-delay: 0s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dot::after {
|
|
||||||
@extend shape;
|
|
||||||
left: 9px;
|
|
||||||
animation: flashing 1s infinite alternate;
|
|
||||||
animation-delay: 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes flashing {
|
|
||||||
0% {
|
|
||||||
background-color: #48E065;
|
|
||||||
}
|
|
||||||
50%,
|
|
||||||
100% {
|
|
||||||
background-color: #9B9B9B;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
<template>
|
|
||||||
|
|
||||||
<span :class="`context ${modifier}-context`">
|
|
||||||
|
|
||||||
<div v-if="context.img" class="avatar">
|
|
||||||
<img v-if="context.img === 'crimata'" :src="require('@/render/assets/crimataAvatar.svg')">
|
|
||||||
<div v-else>{{ context.img }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ context.text == false ? " " : context.text }}
|
|
||||||
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang='ts'>
|
|
||||||
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Context",
|
|
||||||
|
|
||||||
props: ["modifier", "context", "uid"],
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
.context {
|
|
||||||
position: relative;
|
|
||||||
min-height: 14px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-family: "SF Compact Display";
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-context {
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-context {
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 15px;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playing {
|
|
||||||
animation-name: message-playback-anim;
|
|
||||||
animation-duration: 2s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes message-playback-anim {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.4), 0 0 0 0.25em rgba(195, 195, 195, 0.15);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.15), 0 0 0 0.4em rgba(195, 195, 195, 0.3);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.55), 0 0 0 0.15em rgba(195, 195, 195, 0.05);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.25), 0 0 0 0.55em rgba(195, 195, 195, 0.45);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
0
src/render/components/controllers/bubble.control.ts
Normal file
|
|
@ -1,5 +1,6 @@
|
||||||
import { ref, Ref } from "vue";
|
import { ref } from "vue";
|
||||||
import anime from "animejs";
|
import anime from "animejs";
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
|
||||||
export function animateTextInput () {
|
export function animateTextInput () {
|
||||||
|
|
@ -85,15 +86,17 @@ export function animateAudioInput () {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Given index and length of content, return message child status.
|
|
||||||
export function calcChild (index: number, len: number) {
|
export function newMessage ({
|
||||||
if (len === 1) {
|
text=false,
|
||||||
return "none";
|
audio=false,
|
||||||
} else if (index === 0) {
|
context=false,
|
||||||
return "first-child";
|
uid=uuidv4()
|
||||||
} else if (index === len - 1) {
|
}) {
|
||||||
return "last-child";
|
return {
|
||||||
} else {
|
text: text,
|
||||||
return "middle-child";
|
audio: audio,
|
||||||
}
|
context: context,
|
||||||
}
|
uid: uid
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { onMounted, onUnmounted, ref, Ref } from "vue";
|
import { onMounted, onUnmounted, ref, Ref } from "vue";
|
||||||
import { postMessage } from "@/render/ipc";
|
import { postMessage } from "@/render/ipc";
|
||||||
import { animateAudioInput } from "./helpers";
|
import { newMessage, animateAudioInput } from "./helpers";
|
||||||
import { invokeReturnAudio, postAudioChunk } from "@/render/ipc";
|
import { invokeReturnAudio, postAudioChunk } from "@/render/ipc";
|
||||||
|
|
||||||
export default function useAudioInputController (typing: Ref) {
|
export default function useAudioInputController (typing: Ref) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Ref, ref, watch, onMounted, onUnmounted } from "vue";
|
import { Ref, ref, watch, onMounted, onUnmounted } from "vue";
|
||||||
import { postMessage } from "@/render/ipc";
|
import { postMessage } from "@/render/ipc";
|
||||||
import { animateTextInput } from "./helpers";
|
import { newMessage, animateTextInput } from "./helpers";
|
||||||
|
|
||||||
|
|
||||||
export default function useTextInputController(elementX: Ref) {
|
export default function useTextInputController(elementX: Ref) {
|
||||||
|
|
@ -36,12 +36,11 @@ export default function useTextInputController(elementX: Ref) {
|
||||||
if (textInput) {
|
if (textInput) {
|
||||||
|
|
||||||
// Send it to the backend for processing.
|
// Send it to the backend for processing.
|
||||||
const message: Raw = {
|
const message = newMessage({
|
||||||
text: textInput.value,
|
text: false
|
||||||
audio: false
|
});
|
||||||
};
|
|
||||||
|
|
||||||
postMessage(message);
|
// postMessage(message);
|
||||||
|
|
||||||
clearInput()
|
clearInput()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
38
src/render/components/controllers/messenger.control.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import useScroll from "@/render/composables/useScroll";
|
||||||
|
|
||||||
|
const messagesRef = ref();
|
||||||
|
|
||||||
|
/* seed the canvas with messages */
|
||||||
|
const seedCanvas = (messages: Message[]) => {
|
||||||
|
messagesRef.value = messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addMessage = (message: Message) => {
|
||||||
|
messagesRef.value.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateMessage = (message: Message) => {
|
||||||
|
|
||||||
|
let target_message = messagesRef.value.filter((m: Message) => {
|
||||||
|
return m.uid = message.uid;
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
if (target_message) {
|
||||||
|
target_message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useMessages() {
|
||||||
|
|
||||||
|
const { updateScrollRef, adjustScroll } = useScroll("messenger");
|
||||||
|
|
||||||
|
return {
|
||||||
|
messagesRef,
|
||||||
|
seedCanvas,
|
||||||
|
addMessage,
|
||||||
|
updateMessage
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -16,17 +16,16 @@
|
||||||
|
|
||||||
// import { postNavBarExit, postNavBarMin } from "@/render/ipc";
|
// import { postNavBarExit, postNavBarMin } from "@/render/ipc";
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import { postNavBar } from "@/render/ipc";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Header",
|
name: "Header",
|
||||||
setup() {
|
setup() {
|
||||||
const postNavBarExit = () => postNavBar('close');
|
const postNavBarExit = () => {};
|
||||||
const postNavBarMin = () => postNavBar('min');
|
const postNavBarMin = () => {};
|
||||||
return {
|
return {
|
||||||
postNavBarExit,
|
postNavBarExit,
|
||||||
postNavBarMin
|
postNavBarMin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,8 @@
|
||||||
:class="{ playing: recording }"
|
:class="{ playing: recording }"
|
||||||
:style="{ top: `${elementY}px`, left: `${elementX}px` }"
|
:style="{ top: `${elementY}px`, left: `${elementX}px` }"
|
||||||
>
|
>
|
||||||
|
<div>{{ initials }}</div>
|
||||||
|
|
||||||
<!-- show the profile photo in the center of the input item -->
|
|
||||||
<div>{{ profile.photo }}</div>
|
|
||||||
|
|
||||||
<!-- Recording animation on space bar -->
|
<!-- Recording animation on space bar -->
|
||||||
<span v-if="recording" class="play"></span>
|
<span v-if="recording" class="play"></span>
|
||||||
|
|
@ -29,16 +28,18 @@
|
||||||
|
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import draggify from "@/render/composables/useDraggify";
|
import draggify from "@/render/composables/useDraggify";
|
||||||
import { profile } from "@/render/shared/profile";
|
|
||||||
|
|
||||||
import useTextInputController from
|
import useTextInputController from
|
||||||
"@/render/components/controllers/inputItem.control.text";
|
"@/render/components/controllers/inputItem.control.text";
|
||||||
// import useAudioInputController from
|
import useAudioInputController from
|
||||||
"@/render/components/controllers/inputItem.control.audio";
|
"@/render/components/controllers/inputItem.control.audio";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "InputItem",
|
name: "InputItem",
|
||||||
|
|
||||||
|
props: ["initials"],
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
|
|
||||||
// Default values for position.
|
// Default values for position.
|
||||||
|
|
@ -50,11 +51,9 @@ export default defineComponent({
|
||||||
|
|
||||||
// Controllers for text and audio.
|
// Controllers for text and audio.
|
||||||
const { typing } = useTextInputController(elementX)
|
const { typing } = useTextInputController(elementX)
|
||||||
// const { recording } = useAudioInputController(typing)
|
const { recording } = useAudioInputController(typing)
|
||||||
const recording = false;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
profile,
|
|
||||||
elementX,
|
elementX,
|
||||||
elementY,
|
elementY,
|
||||||
recording
|
recording
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<form id="login" @submit.prevent="submitForm">
|
<form id="login" @submit.prevent="submitForm">
|
||||||
|
|
||||||
|
|
||||||
|
<!-- login title -->
|
||||||
|
<div id="loginTitle">
|
||||||
|
<div>Login</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- username and password forms -->
|
<!-- username and password forms -->
|
||||||
<div id="form">
|
<div id="loginBox">
|
||||||
<!-- username -->
|
<!-- username -->
|
||||||
<div class="inputBox">
|
<div class="inputBox">
|
||||||
<input class="input" type="text" v-model="usr" />
|
<input class="input" type="text" v-model="usr" />
|
||||||
|
|
@ -18,15 +24,18 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- submit button; position: fixed -->
|
<!-- submit button; position: fixed -->
|
||||||
<button class="submit button" type="submit">Login</button>
|
<button class="submitButton button" type="submit">Submit</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- back to login button: position: fixed -->
|
||||||
|
<!-- <button class="createAccountButton button" @click.prevent="switchView">Create account</button> -->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import { defineComponent, ref } from "vue";
|
import { defineComponent, ref } from "vue";
|
||||||
|
import { setProfile } from '@/render/composables/useProfile';
|
||||||
import { invokeLogin } from "@/render/ipc";
|
import { invokeLogin } from "@/render/ipc";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
@ -41,26 +50,21 @@ export default defineComponent({
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await invokeLogin({email: usr.value, password: pwd.value});
|
|
||||||
|
const profile = await invokeLogin({
|
||||||
|
email: usr.value,
|
||||||
|
password: pwd.value
|
||||||
|
}) as Profile;
|
||||||
|
|
||||||
|
setProfile(profile)
|
||||||
|
|
||||||
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
shake("form");
|
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const shake = (elementId: string) => {
|
|
||||||
const el = document.getElementById(elementId);
|
|
||||||
|
|
||||||
if (el)
|
|
||||||
el.classList.remove("shake");
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (el) el.classList.add("shake");
|
|
||||||
}, 250);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
usr,
|
usr,
|
||||||
pwd,
|
pwd,
|
||||||
|
|
@ -83,7 +87,16 @@ export default defineComponent({
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#form {
|
#loginTitle {
|
||||||
|
font-family: "SF Pro Text";
|
||||||
|
min-width: 181px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: left;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loginBox {
|
||||||
font-family: "SF Compact Display";
|
font-family: "SF Compact Display";
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -119,7 +132,7 @@ export default defineComponent({
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit {
|
.submitButton {
|
||||||
font-family: "SF Compact Display";
|
font-family: "SF Compact Display";
|
||||||
position: fixed;
|
position: fixed;
|
||||||
margin-top: 141px;
|
margin-top: 141px;
|
||||||
|
|
@ -131,10 +144,21 @@ export default defineComponent({
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.submit:active {
|
.submitButton:active {
|
||||||
background-color: #4296C3;
|
background-color: #4296C3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.createAccountButton {
|
||||||
|
position: absolute;
|
||||||
|
border: none;
|
||||||
|
background-color: #EBEBEB;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #58C4FD;
|
||||||
|
font-weight: bold;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
@ -145,29 +169,12 @@ export default defineComponent({
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shake {
|
.invalid {
|
||||||
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
|
margin-bottom: 30px;
|
||||||
transform: translate3d(0, 0, 0);
|
font-family: "SF Compact Display";
|
||||||
backface-visibility: hidden;
|
font-weight: bold;
|
||||||
perspective: 1000px;
|
font-size: 14px;
|
||||||
}
|
color: #F53737;
|
||||||
|
|
||||||
@keyframes shake {
|
|
||||||
10%, 90% {
|
|
||||||
transform: translate3d(-1px, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
20%, 80% {
|
|
||||||
transform: translate3d(2px, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
30%, 50%, 70% {
|
|
||||||
transform: translate3d(-4px, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
40%, 60% {
|
|
||||||
transform: translate3d(4px, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
<template>
|
|
||||||
<div :id="uid" :class="`${modifier}-message`">
|
|
||||||
|
|
||||||
<Bubble
|
|
||||||
v-for="(val, index) in content"
|
|
||||||
:modifier="modifier"
|
|
||||||
:text="val.text"
|
|
||||||
:child="calcChild(index, content.length)"
|
|
||||||
:seen="seen"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Context
|
|
||||||
:modifier="modifier"
|
|
||||||
:context="context"
|
|
||||||
:uid="uid"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang='ts'>
|
|
||||||
|
|
||||||
import { defineComponent, onMounted } from 'vue';
|
|
||||||
import Bubble from "@/render/components/bubble.vue";
|
|
||||||
import Context from "@/render/components/context.vue";
|
|
||||||
import { calcChild } from "@/render/components/controllers/helpers";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: "Message",
|
|
||||||
|
|
||||||
props: ["modifier", "content", "context", "seen", "uid"],
|
|
||||||
|
|
||||||
components: {
|
|
||||||
Bubble,
|
|
||||||
Context
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
|
|
||||||
const messengerEl = document.getElementById("messenger");
|
|
||||||
|
|
||||||
if (messengerEl)
|
|
||||||
messengerEl.dispatchEvent(new CustomEvent('adjust-scroll'))
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
calcChild
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
.message {
|
|
||||||
width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding-top: 9px;
|
|
||||||
padding-bottom: 9px;
|
|
||||||
animation-name: message-init-anim;
|
|
||||||
animation-duration: 0.25s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes message-init-anim {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
} to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message:first-child {
|
|
||||||
margin-top: 55px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message:last-child {
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.client-message {
|
|
||||||
@extend .message;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-message {
|
|
||||||
@extend .message;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,20 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<!-- Position fixed items -->
|
<!-- Position fixed items -->
|
||||||
|
<InputItem :initials="profile.initials"/>
|
||||||
<Settings />
|
<Settings />
|
||||||
<ConnectionStatus />
|
|
||||||
<div id="recIcon" />
|
<div id="recIcon" />
|
||||||
<InputItem />
|
|
||||||
|
|
||||||
<!-- List of message bubbles. -->
|
<!-- List of message bubbles. -->
|
||||||
<div id="messenger">
|
<div id="messenger">
|
||||||
<Message
|
<Bubble
|
||||||
v-for="message in messages"
|
v-for="message in messages"
|
||||||
:modifier="message.modifier"
|
:text="message.content.text"
|
||||||
:content="message.content"
|
|
||||||
:context="message.context"
|
:context="message.context"
|
||||||
:seen="message.seen"
|
:key="message[0]"
|
||||||
:uid="message.uid"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -25,62 +22,21 @@
|
||||||
import { defineComponent, onMounted, onUnmounted } from "vue";
|
import { defineComponent, onMounted, onUnmounted } from "vue";
|
||||||
import InputItem from "@/render/components/inputItem.vue";
|
import InputItem from "@/render/components/inputItem.vue";
|
||||||
import Settings from "@/render/components/settings.vue";
|
import Settings from "@/render/components/settings.vue";
|
||||||
import Message from "@/render/components/message.vue";
|
import Bubble from "@/render/components/bubble.vue";
|
||||||
import ConnectionStatus from "./connectionStatus.vue";
|
|
||||||
|
|
||||||
import { profile } from "@/render/shared/profile";
|
import { profile } from "@/render/composables/useProfile";
|
||||||
import { messages } from "@/render/shared/messages";
|
import { messages } from "@/render/composables/useMessages";
|
||||||
import useScroll from "@/render/composables/useScroll";
|
|
||||||
import { postMessage, postWindowFocus } from "@/render/ipc";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Messenger",
|
name: "Messenger",
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
ConnectionStatus,
|
|
||||||
InputItem,
|
InputItem,
|
||||||
Settings,
|
Settings,
|
||||||
Message,
|
Bubble
|
||||||
},
|
},
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
|
|
||||||
const onFocus = () => {
|
|
||||||
postWindowFocus({ isFocused: true });
|
|
||||||
postMessage({
|
|
||||||
intent: "focus",
|
|
||||||
params: { "focus": true },
|
|
||||||
epic: false,
|
|
||||||
confidence: 1.0
|
|
||||||
} as PlatformRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onBlur = () => {
|
|
||||||
postWindowFocus({ isFocused: false });
|
|
||||||
postMessage({
|
|
||||||
intent: "focus",
|
|
||||||
params: { "focus": false },
|
|
||||||
epic: false,
|
|
||||||
confidence: 1.0
|
|
||||||
} as PlatformRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
|
|
||||||
useScroll("messenger");
|
|
||||||
|
|
||||||
window.addEventListener('blur', onBlur);
|
|
||||||
window.addEventListener('focus', onFocus);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
|
|
||||||
window.removeEventListener("blur", onBlur);
|
|
||||||
window.removeEventListener("focus", onFocus);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messages,
|
messages,
|
||||||
profile
|
profile
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,35 @@
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
<!-- Settings content -->
|
<!-- Settings icon. -->
|
||||||
<div v-if="toggleSettings" id="settings" >
|
<button
|
||||||
|
class="settingsIcon"
|
||||||
|
v-if="toggleSettings == false"
|
||||||
|
@click="onActive"
|
||||||
|
>
|
||||||
|
<div class="settingsButtonDot"></div>
|
||||||
|
<div class="settingsButtonDot"></div>
|
||||||
|
<div class="settingsButtonDot"></div>
|
||||||
|
</button>
|
||||||
|
|
||||||
<p class="account">{{ `Hi, ${account}` }}</p>
|
<!-- Settings div. -->
|
||||||
|
<div id="settings" v-show="toggleSettings">
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="logout"
|
v-if="toggleSettings"
|
||||||
|
class="settingsOption"
|
||||||
@click="onLogout"
|
@click="onLogout"
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p class="version">{{ `Version ${version}` }}</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Icon -->
|
|
||||||
<button
|
|
||||||
v-else
|
|
||||||
class="icon"
|
|
||||||
@click="onActive"
|
|
||||||
>
|
|
||||||
<img src="@/render/assets/settingsIcon.svg">
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import { defineComponent, ref } from "vue";
|
import { defineComponent, ref } from "vue";
|
||||||
|
import { clearProfile } from "@/render/composables/useProfile"
|
||||||
import { invokeLogout } from "@/render/ipc";
|
import { invokeLogout } from "@/render/ipc";
|
||||||
import { version } from "@/render/shared/version";
|
|
||||||
import { ready, account, setAccount, clearAccount } from "@/render/shared/account";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Settings",
|
name: "Settings",
|
||||||
|
|
@ -61,6 +58,7 @@
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await invokeLogout();
|
await invokeLogout();
|
||||||
|
clearProfile();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.log('error')
|
console.log('error')
|
||||||
}
|
}
|
||||||
|
|
@ -68,8 +66,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
account,
|
|
||||||
version,
|
|
||||||
onActive,
|
onActive,
|
||||||
toggleSettings,
|
toggleSettings,
|
||||||
onLogout
|
onLogout
|
||||||
|
|
@ -83,52 +79,67 @@
|
||||||
|
|
||||||
#settings {
|
#settings {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: rgba(235, 235, 235, 0.75);
|
|
||||||
z-index: 4;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
background-color: #EBEBEB;
|
||||||
|
|
||||||
|
opacity: 0.75;
|
||||||
|
|
||||||
|
z-index: 4;
|
||||||
|
|
||||||
animation-name: appear;
|
animation-name: appear;
|
||||||
animation-duration: 0.5s;
|
animation-duration: 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes appear {
|
@keyframes appear {
|
||||||
from {
|
from {
|
||||||
background-color: rgba(235, 235, 235, 0);
|
opacity: 0
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
background-color: rgba(235, 235, 235, 0.75);
|
opacity: 0.75
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.settingsIcon {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
margin-top: 19px;
|
margin-top: 19px;
|
||||||
|
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
background-color: Transparent;
|
background-color: Transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon:hover {
|
.settingsIcon:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account {
|
.settingsButtonDot {
|
||||||
font-size: 12px;
|
width: 5px;
|
||||||
font-weight: bold;
|
height: 5px;
|
||||||
margin-bottom: 25px;
|
|
||||||
|
margin: 2px;
|
||||||
|
border-radius: 2.5px;
|
||||||
|
background-color: #9B9B9B;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout {
|
.settingsOption {
|
||||||
font-family: "SF Compact Display";
|
font-family: "SF Compact Display";
|
||||||
background-color: #B7B7B7;
|
background-color: #B7B7B7;
|
||||||
padding: 10px 30px;
|
padding: 10px 30px;
|
||||||
|
|
@ -138,20 +149,15 @@
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
|
||||||
|
|
||||||
.logout:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.version {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
top: 75%;
|
top: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
font-size: 12px;
|
z-index: 5;
|
||||||
font-weight: bold;
|
}
|
||||||
color: #575757;
|
|
||||||
|
.settingsOption:hover {
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export class IpcRendererListener<InputParam> implements IIpcListener<InputParam>
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onPost = (_e: IpcRendererEvent, payload: InputParam): void => {
|
private _onPost = (_e: IpcRendererEvent, payload: InputParam): void => {
|
||||||
console.log(`[IPC] Post: ${this.channel}`, payload);
|
console.log(`[IPC] Post: ${this.channel}`);
|
||||||
this._listenerCallback(payload);
|
this._listenerCallback(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,16 +34,16 @@ export class IpcRendererListener<InputParam> implements IIpcListener<InputParam>
|
||||||
|
|
||||||
export default function useIpcRenderer () {
|
export default function useIpcRenderer () {
|
||||||
|
|
||||||
const invoke = async <T>(endpoint: string, payload: T) => {
|
const invoke = async (endpoint: string, payload: any) => {
|
||||||
try {
|
try {
|
||||||
const res = await window.ipcRenderer.invoke(endpoint, payload? JSON.stringify(payload) : null);
|
const res = await window.ipcRenderer.invoke(endpoint, payload);
|
||||||
return res;
|
return res;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = <T>(endpoint: string, payload: T) => {
|
const post = (endpoint: string, payload: any) => {
|
||||||
window.ipcRenderer.send(endpoint, payload);
|
window.ipcRenderer.send(endpoint, payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
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
|
|
@ -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,25 +1,29 @@
|
||||||
|
|
||||||
export default function useScroll(elementId: string) {
|
|
||||||
|
|
||||||
const el = document.getElementById(elementId);
|
export default function useScroll(element: string) {
|
||||||
|
|
||||||
const isBottom = () => {
|
let isScrolledToBottom: boolean;
|
||||||
if (el) {
|
const view = document.getElementById(element)
|
||||||
return el.scrollHeight - el.clientHeight <= el.scrollTop + 1;
|
|
||||||
}
|
// Update isScrolledToBottom
|
||||||
|
const updateScrollRef = () => {
|
||||||
|
if (view) isScrolledToBottom = view.scrollHeight - view.clientHeight <= view.scrollTop + 1;
|
||||||
|
return isScrolledToBottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adjust scroll after we add content to the messenger.
|
||||||
const adjustScroll = () => {
|
const adjustScroll = () => {
|
||||||
if (el) {
|
if (view) {
|
||||||
el.scrollTo({
|
view.scrollTo({
|
||||||
top: el.scrollHeight - el.clientHeight,
|
top: view.scrollHeight - view.clientHeight,
|
||||||
behavior: 'smooth'
|
behavior: 'smooth'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el)
|
return {
|
||||||
el.addEventListener('adjust-scroll', adjustScroll);
|
updateScrollRef,
|
||||||
window.addEventListener('resize', adjustScroll);
|
adjustScroll,
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -12,9 +12,9 @@ const { post, invoke } = useIpc();
|
||||||
|
|
||||||
|
|
||||||
export const invokeLogin = async (
|
export const invokeLogin = async (
|
||||||
payload: AccountCredentials
|
payload: LoginPayload
|
||||||
): Promise<Profile | Error> => (
|
): Promise<Profile | Error> => (
|
||||||
await invoke('invoke-account-login', payload)
|
await invoke('invoke-account-login', JSON.stringify(payload))
|
||||||
);
|
);
|
||||||
|
|
||||||
export const invokeLogout = async (): Promise<void> => (
|
export const invokeLogout = async (): Promise<void> => (
|
||||||
|
|
@ -41,7 +41,11 @@ export const invokeReturnAudio = async (): Promise<ArrayBuffer[] | Error> => (
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const postMessage = (payload: Raw | PlatformRequest): void => (
|
export const invokeSession = async (cid: string): Promise<Profile | Error> => (
|
||||||
|
await invoke("messenger-init", cid)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const postMessage = (payload: Message): void => (
|
||||||
post('post-session-send', payload)
|
post('post-session-send', payload)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -49,25 +53,6 @@ export const postAppMount = (): void => (
|
||||||
post('post-app-mount', null)
|
post('post-app-mount', null)
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Nav bar endpoints
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const postNavBar = (payload: string): void => (
|
|
||||||
post('post-nav-bar', payload)
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Window focus endpoint
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const postWindowFocus = (payload: { isFocused: boolean }): void => (
|
|
||||||
post('post-window-focus', payload)
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,13 @@
|
||||||
|
|
||||||
import { IpcRendererListener } from "./composables/useIpcRend"
|
import { IpcRendererListener } from "./composables/useIpcRend"
|
||||||
/*
|
import { setProfile } from "./composables/useProfile";
|
||||||
* Shared state imports
|
import { setMessages, addMessage, updateMessage } from "./composables/useMessages";
|
||||||
* */
|
|
||||||
import {
|
|
||||||
setMessages,
|
|
||||||
addMessage,
|
|
||||||
updateMessage,
|
|
||||||
deleteMessage
|
|
||||||
} from "./shared/messages";
|
|
||||||
import { setAccount } from "./shared/account";
|
|
||||||
import { setProfile } from "./shared/profile";
|
|
||||||
import { setVersion } from "./shared/version";
|
|
||||||
import { setConnectionStatus } from "./shared/connectionStatus";
|
|
||||||
|
|
||||||
const SET_ACCOUNT_CHANNEL = "set-account";
|
|
||||||
const SET_PROFILE_CHANNEL = "set-profile";
|
const SET_PROFILE_CHANNEL = "set-profile";
|
||||||
const SET_VERSION_CHANNEL = "set-version";
|
|
||||||
|
|
||||||
const INIT_MESSAGES_CHANNEL = "init-messages";
|
const INIT_MESSAGES_CHANNEL = "init-messages";
|
||||||
const ADD_MESSAGE_CHANNEL = "add-message";
|
const ADD_MESSAGE_CHANNEL = "add-message";
|
||||||
const UPDATE_MESSAGE_CHANNEL = "update-message";
|
const UPDATE_MESSAGE_CHANNEL = "update-message";
|
||||||
const DELETE_MESSAGE_CHANNEL = "delete-message";
|
|
||||||
const CONNECTION_STATUS_CHANNEL = "set-connection-status";
|
|
||||||
|
|
||||||
export const setAccountListener = new IpcRendererListener({
|
|
||||||
channel: SET_ACCOUNT_CHANNEL,
|
|
||||||
listenerCallback: setAccount
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setVersionListener = new IpcRendererListener({
|
|
||||||
channel: SET_VERSION_CHANNEL,
|
|
||||||
listenerCallback: setVersion
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setProfileListener = new IpcRendererListener({
|
export const setProfileListener = new IpcRendererListener({
|
||||||
channel: SET_PROFILE_CHANNEL,
|
channel: SET_PROFILE_CHANNEL,
|
||||||
|
|
@ -55,12 +30,3 @@ export const updateMessagesListener = new IpcRendererListener({
|
||||||
listenerCallback: updateMessage
|
listenerCallback: updateMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
export const deleteMessagesListener = new IpcRendererListener({
|
|
||||||
channel: DELETE_MESSAGE_CHANNEL,
|
|
||||||
listenerCallback: deleteMessage
|
|
||||||
});
|
|
||||||
|
|
||||||
export const connectionStatusListener = new IpcRendererListener({
|
|
||||||
channel: CONNECTION_STATUS_CHANNEL,
|
|
||||||
listenerCallback: setConnectionStatus
|
|
||||||
});
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
// shared
|
|
||||||
import { ref } from "vue";
|
|
||||||
|
|
||||||
export const ready = ref(false);
|
|
||||||
export const account = ref();
|
|
||||||
|
|
||||||
export const setAccount: IpcListenerCallback<string | null> = (payload) => {
|
|
||||||
payload ? account.value = payload : clearAccount();
|
|
||||||
showRender();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clearAccount = () => {
|
|
||||||
console.log("Clearing account");
|
|
||||||
account.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const showRender = () => {
|
|
||||||
ready.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
ready,
|
|
||||||
account,
|
|
||||||
setAccount,
|
|
||||||
clearAccount
|
|
||||||
};
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
// shared
|
|
||||||
import { ref } from "vue";
|
|
||||||
import anime from "animejs";
|
|
||||||
|
|
||||||
export const status = ref("");
|
|
||||||
|
|
||||||
export let hiddenState = true;
|
|
||||||
|
|
||||||
let animateLogo: anime.AnimeInstance;
|
|
||||||
let animateConnect: anime.AnimeInstance;
|
|
||||||
|
|
||||||
export function useAnim() {
|
|
||||||
|
|
||||||
animateLogo = anime({
|
|
||||||
targets: '#logo',
|
|
||||||
translateX: [0, 15],
|
|
||||||
duration: 500,
|
|
||||||
autoplay: false,
|
|
||||||
easing: 'easeInOutBack'
|
|
||||||
});
|
|
||||||
|
|
||||||
animateConnect = anime({
|
|
||||||
targets: '#connect',
|
|
||||||
translateX: [0, -15],
|
|
||||||
opacity: [0, 1],
|
|
||||||
duration: 500,
|
|
||||||
autoplay: false,
|
|
||||||
easing: 'easeInOutBack'
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function show() {
|
|
||||||
if (animateLogo && animateConnect) {
|
|
||||||
animateLogo.direction = "normal";
|
|
||||||
animateConnect.direction = "normal";
|
|
||||||
animateLogo.play();
|
|
||||||
animateConnect.play();
|
|
||||||
hiddenState = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hide() {
|
|
||||||
if (animateLogo && animateConnect) {
|
|
||||||
animateLogo.direction = "reverse";
|
|
||||||
animateConnect.direction = "reverse";
|
|
||||||
animateLogo.play();
|
|
||||||
animateConnect.play();
|
|
||||||
hiddenState = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setConnectionStatus: IpcListenerCallback<string> = (payload) => {
|
|
||||||
status.value = payload;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
hiddenState,
|
|
||||||
useAnim,
|
|
||||||
status,
|
|
||||||
show,
|
|
||||||
hide
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
// shared
|
|
||||||
import { ref, Ref } from "vue";
|
|
||||||
|
|
||||||
export const messages: Ref<Array<Message>> = ref([]);
|
|
||||||
|
|
||||||
export const setMessages: IpcListenerCallback<Array<Message>> = (payload) => {
|
|
||||||
messages.value = payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const addMessage: IpcListenerCallback<Message> = (payload) => {
|
|
||||||
messages.value.push(payload as Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updateMessage: IpcListenerCallback<Annotation> = (payload) => {
|
|
||||||
const annotation = payload as Annotation;
|
|
||||||
|
|
||||||
for (const i in messages.value) {
|
|
||||||
|
|
||||||
if (messages.value[i].uid == annotation.uid) {
|
|
||||||
|
|
||||||
if (annotation.name == "content") {
|
|
||||||
messages.value[i].content = annotation.data as Content[];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (annotation.name == "context") {
|
|
||||||
messages.value[i].context = annotation.data as Context;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (annotation.name == "seen") {
|
|
||||||
messages.value[i].seen = annotation.data as boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteMessage: IpcListenerCallback<string> = (payload) => {
|
|
||||||
const uid = payload as string;
|
|
||||||
|
|
||||||
for (let i = 0; i < messages.value.length; i++) {
|
|
||||||
if (messages.value[i].uid === uid) {
|
|
||||||
messages.value.splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export const resetMessages = () => {
|
|
||||||
messages.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
messages,
|
|
||||||
setMessages,
|
|
||||||
addMessage,
|
|
||||||
updateMessage
|
|
||||||
};
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
// shared
|
|
||||||
import { ref } from "vue";
|
|
||||||
|
|
||||||
class Profile implements Profile{
|
|
||||||
crimata_id = "";
|
|
||||||
name = "";
|
|
||||||
photo = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const profile = ref(new Profile());
|
|
||||||
|
|
||||||
export const setProfile: IpcListenerCallback<Profile> = (payload) => {
|
|
||||||
profile.value = payload;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
profile,
|
|
||||||
setProfile
|
|
||||||
};
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
// shared
|
|
||||||
import { ref } from "vue";
|
|
||||||
|
|
||||||
export const version = ref();
|
|
||||||
|
|
||||||
export const setVersion: IpcListenerCallback<Profile | null> = (payload) => {
|
|
||||||
version.value = payload;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
setVersion
|
|
||||||
}
|
|
||||||
127
src/session.ts
|
|
@ -1,97 +1,84 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// import useAudio from "@/audio";
|
// import useAudio from "@/audio";
|
||||||
import UIState from "@/uiState";
|
import { ipcEmit } from "@/composables/useEmitter";
|
||||||
import { config } from "@/config";
|
|
||||||
import useWebsockets from "./composables/useWebsockets";
|
import useWebsockets from "./composables/useWebsockets";
|
||||||
import { backgroundMitt, ipcEmit } from "@/composables/useEmitter";
|
import {config} from "@/config";
|
||||||
import { Notification } from 'electron';
|
|
||||||
import { getWindowFocus, getWindowOpen } from './store';
|
|
||||||
|
|
||||||
export const CONNECTION_STATUS = {
|
/* data structure of messages that's tied to the UI */
|
||||||
connected: 'Connected',
|
const uiState: any | null = null;
|
||||||
nominal: 'Nominal',
|
|
||||||
reconnecting: 'Reconnecting',
|
/* start and stop audio functionality */
|
||||||
lost: 'Connection Lost',
|
// const { initAudio, closeAudio } = useAudio();
|
||||||
|
|
||||||
|
const isInitMessage = (message: any): boolean => {
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
function showNotification (options: {
|
const deauthenticate = (): void => {
|
||||||
title: string;
|
console.log('deauthenticating')
|
||||||
subtitle?: string;
|
};
|
||||||
body: string;
|
|
||||||
}) {
|
|
||||||
new Notification(
|
|
||||||
{
|
|
||||||
title: options.title,
|
|
||||||
subtitle: options.subtitle,
|
|
||||||
body: options.body,
|
|
||||||
}).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI state
|
const isAddMessage = (message: any): boolean => {
|
||||||
const uiState = new UIState();
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls for interfacing with the platform.
|
||||||
|
* Takes an onMessage callback which we define below.
|
||||||
|
*/
|
||||||
|
|
||||||
// let audio = new Audio();
|
|
||||||
|
|
||||||
const onMessageCallback = (payload: string) => {
|
const onMessageCallback = (payload: string) => {
|
||||||
|
|
||||||
if (payload === "CLOSE_AUTH_FAIL") {
|
const message = JSON.parse(payload);
|
||||||
backgroundMitt.emit("close-auth-fail");
|
console.log(typeof message);
|
||||||
|
|
||||||
|
/* if the platform fails to authenticate, we must back down */
|
||||||
|
if (message === "CLOSE_AUTH_FAIL") {
|
||||||
|
deauthenticate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = JSON.parse(payload) as ClientProtocol;
|
ipcEmit("add-message", message)
|
||||||
|
return
|
||||||
|
|
||||||
if (message.header === "init") {
|
/* on init, platform sends state, used to init canvas */
|
||||||
uiState.set(message.body as Init);
|
if (isInitMessage(message)) {
|
||||||
} else {
|
ipcEmit("init-messages", message)
|
||||||
const body = message.body as Update;
|
}
|
||||||
uiState.update(body);
|
|
||||||
|
|
||||||
const isFocused = getWindowFocus();
|
|
||||||
const isOpen = getWindowOpen();
|
|
||||||
// show notification if window is not active
|
|
||||||
if (!isFocused || !isOpen) {
|
|
||||||
if (body.name === "add") {
|
|
||||||
const data = body.data as Message;
|
|
||||||
showNotification({
|
|
||||||
title: 'New Message',
|
|
||||||
subtitle: data.context.text as string,
|
|
||||||
body: data.content[0].text,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
else if (isAddMessage(message)) {
|
||||||
|
ipcEmit("add-messages", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
ipcEmit("update-messages", message)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onConnectionStatusCallback = (alive: boolean) => {
|
||||||
const connectionStatusCallback = (status: string) => {
|
// console.log('[Session]: Connection Alive: ', alive);
|
||||||
// update icon
|
// ipcEmit('connection-state', alive);
|
||||||
// backgroundMitt.emit("update-icon-status", status);
|
|
||||||
ipcEmit('set-connection-status', status);
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusOptions = {
|
|
||||||
openMessage: CONNECTION_STATUS.connected,
|
|
||||||
pongMessage: CONNECTION_STATUS.nominal,
|
|
||||||
closeMessage: CONNECTION_STATUS.reconnecting,
|
|
||||||
pingErrorMessage: CONNECTION_STATUS.lost,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { connect, send, close } = useWebsockets(
|
const { connect, send, close } = useWebsockets(
|
||||||
onMessageCallback,
|
onMessageCallback,
|
||||||
connectionStatusCallback,
|
onConnectionStatusCallback
|
||||||
statusOptions
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const updateAppUI = (): void => {
|
/* send a message to the platform */
|
||||||
uiState.emit();
|
export function sendMessage<Message>(message: Message): void {
|
||||||
|
|
||||||
|
/* socket send */
|
||||||
|
send(message);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendMessage(message: Raw | Request): void {
|
|
||||||
send(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* launch a new session (the main process for authenticated users) */
|
/* launch a new session (the main process for authenticated users) */
|
||||||
export function launchSession(platformKey: string) {
|
export function launchSession(platformKey: string) {
|
||||||
|
|
@ -99,16 +86,16 @@ export function launchSession(platformKey: string) {
|
||||||
/* connect to the platform */
|
/* connect to the platform */
|
||||||
connect(config.PLATFORM_URL, platformKey);
|
connect(config.PLATFORM_URL, platformKey);
|
||||||
|
|
||||||
// initialize the audio streams
|
/* initialize the audio streams */
|
||||||
// audio.record();
|
// initAudio();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function endSession() {
|
export function endSession() {
|
||||||
|
|
||||||
close(1000, 'session-logout');
|
// closeAudio();
|
||||||
|
|
||||||
uiState.reset();
|
close();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
59
src/store.ts
|
|
@ -4,6 +4,7 @@ const schema = {
|
||||||
token: {
|
token: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
|
profile: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
const store = new Store({
|
const store = new Store({
|
||||||
|
|
@ -11,48 +12,20 @@ const store = new Store({
|
||||||
encryptionKey: "super user test"
|
encryptionKey: "super user test"
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
export const getToken = (): string | undefined => (store.get("token"));
|
||||||
* Token state management
|
|
||||||
* */
|
export const clearToken = (): void => (store.delete("token"));
|
||||||
export const setToken = (token: string): void => {
|
|
||||||
store.set("token", 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getToken = (): string | undefined => {
|
|
||||||
return store.get("token");
|
|
||||||
}
|
|
||||||
|
|
||||||
export const clearToken = (): void => {
|
|
||||||
store.delete("token");
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Window state management
|
|
||||||
* */
|
|
||||||
export const setWindowOpen = (state: boolean): void => {
|
|
||||||
store.set("window.isOpen", state);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getWindowOpen = (): boolean | undefined => {
|
|
||||||
return store.get("window.isOpen");
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setWindowFocus = (state: boolean): void => {
|
|
||||||
store.set("window.isFocused", state);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getWindowFocus = (): boolean | undefined => {
|
|
||||||
return store.get("window.isFocused");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Icon state management
|
|
||||||
* */
|
|
||||||
export const setIconState = (state: string): void => {
|
|
||||||
store.set("iconStatus", state);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getIconState = (): string | undefined => {
|
|
||||||
return store.get("iconStatus");
|
|
||||||
}
|
|
||||||
|
|
|
||||||
90
src/tray.ts
|
|
@ -1,90 +0,0 @@
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import fs from 'fs'
|
|
||||||
import path from 'path'
|
|
||||||
import { Tray } from 'electron';
|
|
||||||
const nativeImage = require('electron').nativeImage
|
|
||||||
import {NativeImage} from 'electron'
|
|
||||||
import { backgroundMitt } from "@/composables/useEmitter";
|
|
||||||
import { getIconState, setIconState } from "@/store";
|
|
||||||
import { CONNECTION_STATUS } from '@/session';
|
|
||||||
|
|
||||||
declare const __static: string;
|
|
||||||
|
|
||||||
interface Icons {
|
|
||||||
[name: string]: NativeImage;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tray: Tray | null = null;
|
|
||||||
const TRAY_ICON_DIR = 'trayIcon';
|
|
||||||
|
|
||||||
const ICON_NAMES = {
|
|
||||||
default: 'icon.png',
|
|
||||||
// TODO: need icons for different states
|
|
||||||
// TODO: need higher density icons @3x, @4x, @5x
|
|
||||||
// TODO: need to test with 20x20 & 24x24 icon sizes
|
|
||||||
recording: 'recording.png',
|
|
||||||
playback: 'playback.png',
|
|
||||||
connected: 'connected.png',
|
|
||||||
nominal: 'nominal.png',
|
|
||||||
reconnecting: 'reconnecting.png',
|
|
||||||
lost: 'lost.png',
|
|
||||||
}
|
|
||||||
|
|
||||||
const TRAY_STATUS = {
|
|
||||||
playback: 'playback',
|
|
||||||
recording: 'recording',
|
|
||||||
...CONNECTION_STATUS
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const ICONS: Icons = {
|
|
||||||
default: iconFactory(ICON_NAMES.default),
|
|
||||||
[TRAY_STATUS.recording]: iconFactory(ICON_NAMES.recording),
|
|
||||||
[TRAY_STATUS.playback]: iconFactory(ICON_NAMES.playback),
|
|
||||||
[TRAY_STATUS.connected]: iconFactory(ICON_NAMES.connected),
|
|
||||||
[TRAY_STATUS.nominal]: iconFactory(ICON_NAMES.nominal),
|
|
||||||
[TRAY_STATUS.reconnecting]: iconFactory(ICON_NAMES.reconnecting),
|
|
||||||
[TRAY_STATUS.lost]: iconFactory(ICON_NAMES.lost),
|
|
||||||
};
|
|
||||||
|
|
||||||
backgroundMitt.on('update-icon-status', (payload: string) => {
|
|
||||||
updateTray(payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
const changeIcon = (icon: NativeImage): void => tray ? tray.setImage(icon): undefined;
|
|
||||||
|
|
||||||
function iconFactory(iconName: string): NativeImage {
|
|
||||||
if (iconName !== 'icon.png') {
|
|
||||||
console.log('ERROR: Missing Icons: ', iconName);
|
|
||||||
}
|
|
||||||
// TODO: remove hard code, need icons for different states
|
|
||||||
const iconBuffer = fs.readFileSync(path.join(__static, TRAY_ICON_DIR, 'icon.png'));
|
|
||||||
const icon = nativeImage.createFromBuffer(iconBuffer);
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTray(status: string): void {
|
|
||||||
// read icon state
|
|
||||||
const current = getIconState();
|
|
||||||
if (current !== status) {
|
|
||||||
setIconState(status);
|
|
||||||
if (status in ICONS) {
|
|
||||||
changeIcon(ICONS[status]);
|
|
||||||
} else {
|
|
||||||
changeIcon(ICONS.default);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initTray(): void {
|
|
||||||
if (!tray) {
|
|
||||||
try {
|
|
||||||
tray = new Tray(ICONS.default);
|
|
||||||
} catch(e) {
|
|
||||||
console.log('Error initilizing tray', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
94
src/types.ts
|
|
@ -1,74 +1,14 @@
|
||||||
|
|
||||||
/**
|
|
||||||
* Messages from platform except for CLOSE_AUTH_FAIL
|
|
||||||
*/
|
|
||||||
interface ClientProtocol {
|
|
||||||
header: string;
|
|
||||||
body: Init | Update;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initial message from platform to seed uiState
|
|
||||||
*/
|
|
||||||
interface Init {
|
|
||||||
profile: Profile;
|
|
||||||
messages: Message[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update something in uiState
|
|
||||||
*/
|
|
||||||
interface Update {
|
|
||||||
name: string;
|
|
||||||
data: Profile | Message[] | Message | Annotation | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A standard message in Messenger
|
|
||||||
*/
|
|
||||||
interface Message {
|
interface Message {
|
||||||
modifier: string;
|
text: boolean | string;
|
||||||
content: Content[];
|
context: boolean | string;
|
||||||
context: Context;
|
audio: boolean | string;
|
||||||
seen: boolean;
|
type: 1 | 2 | 3;
|
||||||
uid: string;
|
uid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Content {
|
interface ViewMessage extends Message {
|
||||||
text: string;
|
child: string;
|
||||||
audio: string | boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Context {
|
|
||||||
img: string | boolean;
|
|
||||||
text: string | boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a specific attribute in a given message
|
|
||||||
*/
|
|
||||||
interface Annotation {
|
|
||||||
name: string;
|
|
||||||
data: Context | Content[] | boolean;
|
|
||||||
uid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send raw, uncategorized data to the platform
|
|
||||||
*/
|
|
||||||
interface Raw {
|
|
||||||
text: string | boolean;
|
|
||||||
audio: string | boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The oppisite of Raw, when you know exactly what needs to get done
|
|
||||||
*/
|
|
||||||
interface PlatformRequest {
|
|
||||||
intent: string;
|
|
||||||
params: any;
|
|
||||||
epic: string | boolean;
|
|
||||||
confidence: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WindowState {
|
interface WindowState {
|
||||||
|
|
@ -79,9 +19,19 @@ interface WindowState {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Profile {
|
interface Profile {
|
||||||
crimata_id: string;
|
crimataId: string;
|
||||||
name: string;
|
alias: string;
|
||||||
photo: string;
|
initials: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginPayload {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthState {
|
||||||
|
profile: Profile | null;
|
||||||
|
token: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AccountCredentials {
|
interface AccountCredentials {
|
||||||
|
|
@ -97,7 +47,7 @@ interface IpcHandlerCallback<I, O> {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IpcListenerCallback<T> {
|
interface IpcListenerCallback<T> {
|
||||||
(payload: T): void;
|
(payload: T | null): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IIpcHandler<I, O> {
|
interface IIpcHandler<I, O> {
|
||||||
|
|
@ -125,7 +75,3 @@ interface IpcRendererEvent<T> {
|
||||||
payload: T | null;
|
payload: T | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FocusPayload {
|
|
||||||
isFocused: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
import { ipcEmit } from "@/composables/useEmitter";
|
|
||||||
|
|
||||||
class Profile implements Profile{
|
|
||||||
crimata_id = "";
|
|
||||||
name = "";
|
|
||||||
photo = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class UIState {
|
|
||||||
|
|
||||||
profile: Profile;
|
|
||||||
messages: Message[];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.profile = new Profile();
|
|
||||||
this.messages = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
emit() {
|
|
||||||
ipcEmit("set-profile", this.profile);
|
|
||||||
ipcEmit("init-messages", this.messages);
|
|
||||||
}
|
|
||||||
|
|
||||||
set (message: Init) {
|
|
||||||
this.profile = message.profile;
|
|
||||||
this.messages = message.messages;
|
|
||||||
this.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
update(update: Update) {
|
|
||||||
|
|
||||||
if (update.name === "profile") {
|
|
||||||
this.profile = update.data as Profile;
|
|
||||||
ipcEmit("set-profile", this.profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (update.name === "add") {
|
|
||||||
this.messages.push(update.data as Message);
|
|
||||||
ipcEmit("add-message", update.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (update.name === "annotate") {
|
|
||||||
this._annotate(update.data as Annotation);
|
|
||||||
ipcEmit("update-message", update.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
this._delete(update.data as string);
|
|
||||||
ipcEmit("delete-message", update.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.profile = new Profile();
|
|
||||||
this.messages = [];
|
|
||||||
this.emit();
|
|
||||||
}
|
|
||||||
|
|
||||||
_annotate(annotation: Annotation) {
|
|
||||||
|
|
||||||
for (const i in this.messages) {
|
|
||||||
|
|
||||||
if (this.messages[i].uid == annotation.uid) {
|
|
||||||
|
|
||||||
if (annotation.name == "content") {
|
|
||||||
this.messages[i].content = annotation.data as Content[];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (annotation.name == "context") {
|
|
||||||
this.messages[i].context = annotation.data as Context;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (annotation.name == "seen") {
|
|
||||||
this.messages[i].seen = annotation.data as boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcEmit("update-message", annotation);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
_delete(uid: string) {
|
|
||||||
for (let i = 0; i < this.messages.length; i++) {
|
|
||||||
if (this.messages[i].uid === uid) {
|
|
||||||
this.messages.splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -33,20 +33,20 @@ const loadWinState = (fileName: string): WindowState => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called when a NavBar button is pressed.
|
// Called when a NavBar button is pressed.
|
||||||
export const onNavBar: IpcListenerCallback<string> = (payload): void => {
|
const onNavBar = (_event: any, action: string): void => {
|
||||||
if (win) {
|
if (win) {
|
||||||
if (payload === "close") {
|
if (action === "close") {
|
||||||
win.close();
|
win.close()
|
||||||
} else {
|
} else {
|
||||||
win.minimize();
|
win.minimize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Util function to render message on ipc-renderer event.
|
// Util function to render message on ipc-renderer event.
|
||||||
const postToWindow = <T>(e: IpcRendererEvent<T>): void => {
|
const postToWindow = <T>(event: IpcRendererEvent<T>): void => {
|
||||||
if (win) {
|
if (win) {
|
||||||
win.webContents.send(e.channel, e.payload);
|
win.webContents.send(event.channel, event.payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,6 +75,10 @@ const onWindowMount = (): void => {
|
||||||
// Must tell initApp that window exists.
|
// Must tell initApp that window exists.
|
||||||
backgroundMitt.emit('window-active', true);
|
backgroundMitt.emit('window-active', true);
|
||||||
|
|
||||||
|
// Handle win nav-bar event.
|
||||||
|
ipcMain.removeAllListeners("nav-bar") // avoid setting duplicate handlers
|
||||||
|
ipcMain.on("nav-bar", onNavBar);
|
||||||
|
|
||||||
// Gateway for messages to the frontend.
|
// Gateway for messages to the frontend.
|
||||||
backgroundMitt.removeAllListeners("ipc-renderer")
|
backgroundMitt.removeAllListeners("ipc-renderer")
|
||||||
backgroundMitt.on("ipc-renderer", postToWindow);
|
backgroundMitt.on("ipc-renderer", postToWindow);
|
||||||
|
|
|
||||||
19
yarn.lock
|
|
@ -1482,9 +1482,9 @@
|
||||||
integrity sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw==
|
integrity sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw==
|
||||||
|
|
||||||
"@types/node@^12.0.12":
|
"@types/node@^12.0.12":
|
||||||
version "12.20.17"
|
version "12.19.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.17.tgz#ffd44c2801fc527a6fe6e86bc9b900261df1c87e"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.3.tgz#a6e252973214079155f749e8bef99cc80af182fa"
|
||||||
integrity sha512-so8EHl4S6MmatPS0f9sE1ND94/ocbcEshW5OpyYthRqeRpiYyW2uXYTo/84kmfdfeNrDycARkvuiXl6nO40NGg==
|
integrity sha512-8Jduo8wvvwDzEVJCOvS/G6sgilOLvvhn1eMmK3TW8/T217O7u1jdrK6ImKLv80tVryaPSVeKu6sjDEiFjd4/eg==
|
||||||
|
|
||||||
"@types/node@^12.12.47":
|
"@types/node@^12.12.47":
|
||||||
version "12.19.15"
|
version "12.19.15"
|
||||||
|
|
@ -4898,6 +4898,11 @@ dotenv-expand@^5.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
|
resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
|
||||||
integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
|
integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
|
||||||
|
|
||||||
|
dotenv@^10.0.0:
|
||||||
|
version "10.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
|
||||||
|
integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==
|
||||||
|
|
||||||
dotenv@^8.2.0:
|
dotenv@^8.2.0:
|
||||||
version "8.2.0"
|
version "8.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a"
|
||||||
|
|
@ -5077,10 +5082,10 @@ electron-updater@^4.3.8:
|
||||||
lodash.isequal "^4.5.0"
|
lodash.isequal "^4.5.0"
|
||||||
semver "^7.3.4"
|
semver "^7.3.4"
|
||||||
|
|
||||||
electron@11.4.10:
|
electron@^9.0.0:
|
||||||
version "11.4.10"
|
version "9.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/electron/-/electron-11.4.10.tgz#7bcbca82810b82c2f2765824c5e11e3061272ea4"
|
resolved "https://registry.yarnpkg.com/electron/-/electron-9.3.3.tgz#99a6619d5df68f97697a5d1d82ef3a8a63fcdf36"
|
||||||
integrity sha512-aQTRgRdHwCW68gxz9qvGCfOUvR4NBbdecLB/mEWX8fMncDFvPMmm+dq2D6zSWWVEKywmsj3+wMMVn3UV2Cl2CQ==
|
integrity sha512-xghKeUY1qgnEcJ5w2rXo/toH+8NT2Dktx2aAxBNPV7CIJr3mejJJAPwLbycwtddzr37tgKxHeHlc8ivfKtMkJQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@electron/get" "^1.0.1"
|
"@electron/get" "^1.0.1"
|
||||||
"@types/node" "^12.0.12"
|
"@types/node" "^12.0.12"
|
||||||
|
|
|
||||||