Compare commits

..
63 changed files with 1031 additions and 46084 deletions

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

44724
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

View file

@ -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
View file

@ -0,0 +1,187 @@
/* eslint @typescript-eslint/no-var-requires: "off" */
"use strict";
// where the audio goes
let buffer: ArrayBuffer[] = [];
// place audio data in buffer
export const collect: IpcListenerCallback<ArrayBuffer> = (chunk) => {
if (chunk) buffer.push(chunk);
}
// return audio and clear buffer
export const flush = async () => {
const bufferCopy = buffer;
buffer = [];
return bufferCopy;
}
// import { backgroundMitt } from '@/modules/emitter';
// const portAudio = require('naudiodon');
// // Audio in and out stream objects.
// let ai: typeof portAudio.AudioIO | boolean = false;
// let ao: typeof portAudio.AudioIO | boolean = false;
// // Whether activly recording.
// let record = false;
// const audioContainer = {
// input: '',
// }
// const audioOptions = {
// channelCount: 1,
// sampleFormat: 16,
// sampleRate: 16000,
// deviceId: -1,
// closeOnError: false,
// }
// export const toggleRecord = (): void => { record = !record };
// export const fetchAudioInput = (): Promise<Error | string> => (
// new Promise((resolve, reject) => {
// try {
// resolve(audioContainer.input);
// toggleRecord();
// } catch (e) {
// reject(new Error('Failed to fetch the audio.'))
// }
// })
// )
// // Main audio function run by run.ts module.
// export function initAudioIO(): void {
// console.log("AUDIO:Starting io streams.")
// if (!ai) {
// // Initialize and start input stream.
// ai = new portAudio.AudioIO({ inOptions: audioOptions });
// ai.setEncoding("hex");
// ai.start();
// // On each data chunk...
// ai.on('data', (chunk: string) => {
// // If recording, we capture the data.
// if (record) {
// console.log('AUDIO:Recording...')
// audioContainer.input += chunk;
// }
// // Else, we don't capture and also clear audioContainer.
// else {
// if (audioContainer.input.length) {
// audioContainer.input = "";
// }
// }
// });
// }
// if (!ao) {
// // Initialize and start input stream.
// ao = new portAudio.AudioIO({ outOptions: audioOptions });
// ao.start();
// }
// }
// // ---Audio playback--------------------------------------------
// // Split Buffer into an array of len-sized Buffers.
// function bufSplit(buf: Buffer, len: number): Array<Buffer> {
// const chunks = [];
// let i = 0;
// let L = len;
// while(i < buf.byteLength) {
// chunks.push(buf.slice(i, L));
// i = L;
// L += len;
// }
// return chunks;
// }
// // Audio playback.
// export function play(input: string): void {
// // Format the audio.
// const audio = bufSplit(
// Buffer.from(input as string, 'hex'),
// 8192
// );
// // Called on end of write.
// const callback = () => {
// // We stop audio playback anim.
// backgroundMitt.emit('ipc-renderer', {
// endpoint: 'stop-playback-anim'
// });
// }
// write();
// // Iterate through audio array and write buffers to portAudio writable.
// function write() {
// let chunk: Buffer;
// let ok = true;
// let i = 0;
// do {
// chunk = audio[i];
// if (i === audio.length - 1) {
// // write last chunk.
// ao.write(chunk, null, callback);
// } else {
// // check for backpreassure.
// ok = ao.write(chunk, null);
// }
// i++;
// } while (i < audio.length && ok);
// if (i < audio.length) {
// // Had to stop early!
// // Write some more once it drains.
// ao.once('drain', write);
// }
// }
// }
// // -------------------------------------------------------------
// // Get's called on window close.
// export async function stopStream() {
// console.log("AUDIO:Stopping audio stream.")
// if (ai) {
// try {
// await ai.quit()
// } catch(e){
// console.log('AUDIO: Failed to shutdown audio input.');
// throw e;
// }
// }
// if (ao) {
// try {
// await ao.quit()
// } catch(e){
// console.log('AUDIO: Failed to shutdown audio output.');
// throw e;
// }
// }
// }

View file

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

View file

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

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

View file

@ -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,

View file

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

View file

@ -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.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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
};
}

View file

@ -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) {

View file

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

View 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
};
}

View file

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

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

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

View 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
};

View 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,
};

View file

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

View file

@ -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)
);
/** /**
* *

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"