updated protocols

This commit is contained in:
Andrew Gundersen 2021-07-11 11:54:39 -05:00
commit 531a32a5a5
37 changed files with 682 additions and 650 deletions

BIN
build/icon.icns Normal file

Binary file not shown.

View file

@ -1,66 +1,79 @@
import { app } from "electron";
import { parseAuthRes } from "@/auth";
import { postAuth, postLogin, postLogout } from "@/api/account";
import { endSession, launchSession } from "@/session";
import { getToken, setToken, setProfile, clearStore } from "./store";
import { parseAuthRes } from "./auth";
import { ipcEmit } from "@/composables/useEmitter";
import { updateAppUI, launchSession, endSession } from "@/session";
import { getToken, setToken, clearToken } from "./store";
import { backgroundMitt, ipcEmit } from "@/composables/useEmitter";
export const accountAuth = async (): Promise<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 */
const token = getToken();
/* try to login with it, returns platform secret and new token on success */
if (token) {
try {
try {
if (token) {
// attempt to login with token
const res = await postAuth(token);
const parsed = parseAuthRes(res);
setToken(parsed.token)
setProfile(parsed.profile);
// save jwt token and profile
setToken(parsed.token);
account = parsed.crimataId
return {
profile: parsed.profile,
token: parsed.token
};
// launch session
launchSession(parsed.token);
} catch(e) {
console.log('[ACCOUNT]', e);
clearStore();
throw(new Error('Failed to authenticate.'));
} else throw(new Error('Failed to authenticate (no token).'));
} catch (e) {
console.log(e);
clearToken();
} finally {
// push state changes to the frontend
updateAppState();
}
} else {
throw(new Error('Unable to authenticate.'));
}
};
export const accountLogin: IpcHandlerCallback<AccountCredentials, Profile> = async (payload) => {
const account = payload as AccountCredentials;
export const accountLogin = async (payload: any): Promise<Error | void> => {
const creds = payload as AccountCredentials;
try {
// attempt login with email password
const res = await postLogin(account.email, account.password);
const res = await postLogin(creds.email, creds.password);
const parsed = parseAuthRes(res);
// save jwt token and profile
setToken(parsed.token);
setProfile(parsed.profile);
account = parsed.crimataId;
// launch session
launchSession(parsed.token);
// return profile to renderer
return parsed.profile;
// push state changes to the frontend
updateAppState();
return;
} catch(e) {
clearStore();
clearToken();
throw e;
}
}
}
export const accountLogout = async (): Promise<Error | void> => {
@ -68,8 +81,12 @@ export const accountLogout = async (): Promise<Error | void> => {
// post logout to backend
await postLogout();
// remove key and crimataId
clearStore();
// remove key and account
clearToken();
account = null;
// push account state to browser
ipcEmit("set-account", account);
// kill crimata platform session
endSession();
@ -83,4 +100,24 @@ export const accountLogout = async (): Promise<Error | void> => {
}
// logout when auth fails on platform end
backgroundMitt.on("close-auth-fail", async (_payload: any) => {
await accountLogout();
});
// emits state of account, version, and UI
export const updateAppState = () => {
ipcEmit("set-account", account);
ipcEmit("set-version", app.getVersion());
updateAppUI();
}

View file

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

View file

@ -1,13 +1,5 @@
export const parseAuthRes = (authRes: any) => {
const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string;
const profile = authRes.data as Profile;
return {
token,
profile
}
};
const crimataId = authRes.data as string;
return {token, crimataId}
};

View file

@ -1,36 +0,0 @@
export default class Canvas {
messages: Message[];
/* seed canvas with messages on init */
constructor(messages: Message[]) {
this.messages = messages;
ipcEmit("seed-view", this.messages);
}
/* add a new message to the canvas */
add(message: Message) {
this.messages.push(message);
ipcEmit("update-view", message);
}
/* update an existing message */
update(message: Message) {
/* get the target message */
let target_message = this.messages.filter((m: Message) => {
return m.uid = message.uid;
})[0];
/* replace the target message */
if (target_message) {
target_message = message;
ipcEmit("update-view", message);
}
}
}

View file

@ -76,7 +76,6 @@ export default function useWebSockets(
setTimeout(() => {
connect(socketUrl, secret), _reconnectTimeout
});
}
});

View file

@ -1,7 +1,7 @@
"use strict";
import { accountLogin, accountLogout, accountProfile } from "@/account";
import { accountLogin, accountLogout } from "@/account";
import { IpcHandler } from "@/composables/useIpcMain";
import { flush } from "@/audio";
@ -24,5 +24,3 @@ export const getAudioHandler = new IpcHandler({
channel: GET_AUDIO_CHANNEL,
handlerCallback: flush
});

View file

@ -2,25 +2,19 @@
import { IpcListener } from "@/composables/useIpcMain"
import { sendMessage } from '@/session';
import { collect } from "@/audio";
import { updateAppState } from "@/session";
import { updateAppState } from "@/account";
import { onNavBar } from "@/window";
import { toggleRecord } from "@/audio";
const CLIENT_MESSAGE_CHANNEL = "post-session-send"
const GET_AUDIO_CHANNEL = "post-audio-collect";
const APP_MOUNT_CHANNEL = "post-app-mount";
const NAV_BAR_CHANNEL = "post-nav-bar";
const WINDOW_BLUR_CHANNEL = "post-window-focus";
export const messageListener = new IpcListener<Message>({
export const messageListener = new IpcListener<Raw | Request>({
channel: CLIENT_MESSAGE_CHANNEL,
listenerCallback: sendMessage
});
export const audioChunkListener = new IpcListener<ArrayBuffer>({
channel: GET_AUDIO_CHANNEL,
listenerCallback: collect
});
export const appMountListener = new IpcListener<null>({
channel: APP_MOUNT_CHANNEL,
listenerCallback: updateAppState
@ -31,16 +25,7 @@ export const navBarListener = new IpcListener({
listenerCallback: onNavBar
});
const onWindowFocus: IpcListenerCallback<{ isFocused: boolean }> = (payload) => {
if (payload.isFocused) {
console.log('window is focused')
} else {
console.log('window is blurred')
}
}
export const windowFocusListener = new IpcListener({
channel: WINDOW_BLUR_CHANNEL,
listenerCallback: onWindowFocus
});
export const recordingListener = new IpcListener({
channel: RECORDING_CHANNEL,
listenerCallback: toggleRecord
});

View file

@ -11,10 +11,8 @@
import initIpcMain from "@/ipc/index";
import { accountAuth } from "./account";
import { launchSession, updateAppState } from "./session";
import createWindow from "./window";
let authState: AuthState | null;
export default async function main() {
@ -24,16 +22,7 @@ export default async function main() {
/* launch browser window */
await createWindow();
try {
authState = await accountAuth() as AuthState;
} catch(e) {
console.log('AUTH:', e);
authState = null;
} finally {
if (authState) {
launchSession(authState.token as string);
}
updateAppState();
}
/* try to authenticate with token */
await accountAuth();
}

22
src/profile.ts Normal file
View file

@ -0,0 +1,22 @@
import { ipcEmit } from "@/composables/useEmitter";
export default class ProfileHandler {
profile: Profile;
constructor(profile: Profile) {
this.profile = profile;
this.emit();
}
emit() {
ipcEmit("set-profile", this.profile);
}
update(update: Update) {
this.profile = update.data as Profile;
this.emit()
}
}

View file

@ -1,15 +1,10 @@
<template>
<!-- Only render when profile has been set -->
<main id="app" v-if="authComplete">
<main id="app" v-if="ready">
<Header />
<Messenger
v-if="profile"
/>
<!-- Main Components
-->
<Messenger v-if="account"/>
<Login v-else />
</main>
@ -21,15 +16,14 @@
<script lang="ts">
import { defineComponent, onMounted, onUnmounted } from "vue";
import { IpcRendererEvent } from "electron";
import { defineComponent, onMounted } from "vue";
import Splash from "@/render/components/splash.vue";
import Messenger from "@/render/components/messenger.vue";
import Login from "@/render/components/login.vue";
import Header from "@/render/components/header.vue";
import { profile, authComplete } from "@/render/shared/profile";
import { ready, account, setAccount, clearAccount } from "@/render/shared/account";
import { postAppMount } from "@/render/ipc";
export default defineComponent({
@ -43,30 +37,15 @@ export default defineComponent({
setup() {
const onFocus = () => {
console.log('window is focused');
}
const onBlur = () => {
console.log('window is blurred');
}
onMounted(() => {
// subscribe to visibility change events
window.addEventListener('blur', onBlur);
window.addEventListener('focus', onFocus);
postAppMount()
});
onUnmounted(() => {
window.removeEventListener("blur", onBlur);
window.removeEventListener("focus", onFocus);
});
return {
authComplete,
profile
account,
ready
}
}
})

View file

@ -0,0 +1,14 @@
<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>

After

Width:  |  Height:  |  Size: 973 B

View file

@ -0,0 +1,10 @@
<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>

After

Width:  |  Height:  |  Size: 766 B

View file

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

After

Width:  |  Height:  |  Size: 553 B

View file

@ -1,6 +1,7 @@
<template>
<div :class="`bubble ${modifier}-bubble ${modifier}-${child}`">
{{ content }}
<div v-show="!seen" class="notify"></div>
{{ text }}
</div>
</template>
@ -11,7 +12,7 @@
export default defineComponent({
name: "Bubble",
props: ["modifier", "content", "child"],
props: ["modifier", "text", "child", "seen"],
})
@ -70,4 +71,28 @@
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% {
transform: scale(0);
}
}
</style>

View file

@ -1,20 +1,34 @@
<template>
<div class="statusContainer">
<div>{{status}}</div>
<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 } from 'vue'
import { status } from "@/render/shared/connectionStatus";
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
}
@ -25,13 +39,65 @@ export default defineComponent({
</script>
<style lang="scss">
.statusContainer {
width: 100vw;
display: flex;
justify-content: center;
:first-child {
margin-right: 2px;
}
<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,35 +1,27 @@
<template>
<span :class="`context ${modifier}-context`">
<div v-if="modifier==='ai'" class="avatar"></div>
{{ contextRef }}
<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, ref, watch } from 'vue';
import { annotateAnim } from "@/render/components/controllers/helpers";
import { defineComponent } from 'vue';
export default defineComponent({
name: "Context",
props: ["modifier", "context", "uid"],
setup(props) {
const contextRef = ref(" ");
contextRef.value = props.context;
watch(() => props.context, (val: string, _oldval: string) => {
annotateAnim(props.uid, val, contextRef);
});
return {
contextRef
};
}
})
</script>
@ -56,11 +48,14 @@
}
.avatar {
margin-right: 5px;
display: flex;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
background-color: white;
border-radius: 15px;
margin-right: 5px;
}
.playing {

View file

@ -85,31 +85,6 @@ export function animateAudioInput () {
}
// animate a message annotation.
//! probably could be improved.
export function annotateAnim (uid: string, val: string, contextRef: Ref) {
anime({
targets: `#${uid} span`,
opacity: [1, 0],
duration: 250,
easing: 'easeOutExpo'
})
.finished.then(() => {
contextRef.value = val;
})
.then(() => {
anime({
targets: `#${uid} span`,
opacity: [0, 1],
duration: 250,
easing: 'easeOutExpo'
})
})
}
// Given index and length of content, return message child status.
export function calcChild (index: number, len: number) {
if (len === 1) {

View file

@ -9,7 +9,6 @@
class="menuButton minimizeButton"
@click.prevent="postNavBarMin"
/>
<connection-status />
</span>
</template>
@ -19,11 +18,8 @@
import { defineComponent } from "vue";
import { postNavBar } from "@/render/ipc";
import ConnectionStatus from "./connectionStatus.vue";
export default defineComponent({
name: "Header",
components: { ConnectionStatus },
setup() {
const postNavBarExit = () => postNavBar('close');
const postNavBarMin = () => postNavBar('min');

View file

@ -5,8 +5,9 @@
:class="{ playing: recording }"
: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 -->
<span v-if="recording" class="play"></span>
@ -28,7 +29,7 @@
import { defineComponent } from "vue";
import draggify from "@/render/composables/useDraggify";
import { profile } from "@/render/shared/profile";
import useTextInputController from
"@/render/components/controllers/inputItem.control.text";
@ -38,8 +39,6 @@ import useTextInputController from
export default defineComponent({
name: "InputItem",
props: ["initials"],
setup() {
// Default values for position.
@ -55,6 +54,7 @@ export default defineComponent({
const recording = false;
return {
profile,
elementX,
elementY,
recording

View file

@ -1,14 +1,8 @@
<template>
<form id="login" @submit.prevent="submitForm">
<!-- login title -->
<div id="loginTitle">
<div>Login</div>
</div>
<!-- username and password forms -->
<div id="loginBox">
<div id="form">
<!-- username -->
<div class="inputBox">
<input class="input" type="text" v-model="usr" />
@ -24,18 +18,15 @@
</div>
<!-- submit button; position: fixed -->
<button class="submitButton button" type="submit">Submit</button>
<button class="submit button" type="submit">Login</button>
</form>
<!-- back to login button: position: fixed -->
<!-- <button class="createAccountButton button" @click.prevent="switchView">Create account</button> -->
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { setProfile } from '@/render/shared/profile';
import { invokeLogin } from "@/render/ipc";
export default defineComponent({
@ -50,21 +41,26 @@ export default defineComponent({
const submitForm = async () => {
try {
const profile = await invokeLogin({
email: usr.value,
password: pwd.value
}) as Profile;
setProfile(profile)
await invokeLogin({email: usr.value, password: pwd.value});
} catch(e) {
shake("form");
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 {
usr,
pwd,
@ -87,16 +83,7 @@ export default defineComponent({
justify-content: center;
}
#loginTitle {
font-family: "SF Pro Text";
min-width: 181px;
font-size: 24px;
font-weight: bold;
text-align: left;
padding-bottom: 10px;
}
#loginBox {
#form {
font-family: "SF Compact Display";
display: flex;
flex-direction: column;
@ -132,7 +119,7 @@ export default defineComponent({
margin-left: 15px;
}
.submitButton {
.submit {
font-family: "SF Compact Display";
position: fixed;
margin-top: 141px;
@ -144,21 +131,10 @@ export default defineComponent({
font-weight: bold;
}
.submitButton:active {
.submit:active {
background-color: #4296C3;
}
.createAccountButton {
position: absolute;
border: none;
background-color: #EBEBEB;
text-decoration: none;
color: #58C4FD;
font-weight: bold;
bottom: 20px;
right: 20px;
}
.button {
border: none;
outline: none;
@ -169,12 +145,29 @@ export default defineComponent({
cursor: pointer;
}
.invalid {
margin-bottom: 30px;
font-family: "SF Compact Display";
font-weight: bold;
font-size: 14px;
color: #F53737;
.shake {
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
@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>

View file

@ -4,13 +4,14 @@
<Bubble
v-for="(val, index) in content"
:modifier="modifier"
:content="val"
:text="val.text"
:child="calcChild(index, content.length)"
:seen="seen"
/>
<Context
<Context
:modifier="modifier"
:context="context"
:context="context"
:uid="uid"
/>
@ -19,7 +20,7 @@
<script lang='ts'>
import { defineComponent } from 'vue';
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";
@ -27,7 +28,7 @@
export default defineComponent({
name: "Message",
props: ["modifier", "content", "context", "uid"],
props: ["modifier", "content", "context", "seen", "uid"],
components: {
Bubble,
@ -36,6 +37,15 @@
setup() {
onMounted(() => {
const messengerEl = document.getElementById("messenger");
if (messengerEl)
messengerEl.dispatchEvent(new CustomEvent('adjust-scroll'))
});
return {
calcChild
};
@ -55,7 +65,7 @@
padding-top: 9px;
padding-bottom: 9px;
animation-name: message-init-anim;
animation-duration: 0.25s;
animation-duration: 0.25s;
}
@keyframes message-init-anim {

View file

@ -1,9 +1,10 @@
<template>
<!-- Position fixed items -->
<InputItem :initials="profile.initials"/>
<Settings />
<ConnectionStatus />
<div id="recIcon" />
<InputItem />
<!-- List of message bubbles. -->
<div id="messenger">
@ -12,6 +13,7 @@
:modifier="message.modifier"
:content="message.content"
:context="message.context"
:seen="message.seen"
:uid="message.uid"
/>
</div>
@ -20,24 +22,63 @@
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, onMounted, onUnmounted } from "vue";
import InputItem from "@/render/components/inputItem.vue";
import Settings from "@/render/components/settings.vue";
import Message from "@/render/components/message.vue";
import ConnectionStatus from "./connectionStatus.vue";
import { profile } from "@/render/shared/profile";
import { messages } from "@/render/shared/messages";
import useScroll from "@/render/composables/useScroll";
import { postMessage } from "@/render/ipc";
export default defineComponent({
name: "Messenger",
components: {
ConnectionStatus,
InputItem,
Settings,
Message
Message,
},
setup() {
const onFocus = () => {
postMessage({
intent: "focus",
params: { "focus": true },
epic: false,
confidence: 1.0
} as PlatformRequest);
}
const onBlur = () => {
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 {
messages,
profile

View file

@ -1,35 +1,38 @@
<template>
<!-- Settings icon. -->
<button
class="settingsIcon"
v-if="toggleSettings == false"
@click="onActive"
>
<div class="settingsButtonDot"></div>
<div class="settingsButtonDot"></div>
<div class="settingsButtonDot"></div>
</button>
<!-- Settings content -->
<div v-if="toggleSettings" id="settings" >
<!-- Settings div. -->
<div id="settings" v-show="toggleSettings">
</div>
<p class="account">{{ `Hi, ${account}` }}</p>
<button
v-if="toggleSettings"
class="settingsOption"
class="logout"
@click="onLogout"
>
Logout
</button>
<p class="version">{{ `Version ${version}` }}</p>
</div>
<!-- Icon -->
<button
v-else
class="icon"
@click="onActive"
>
<img src="@/render/assets/settingsIcon.svg">
</button>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { clearProfile } from "@/render/shared/profile"
import { invokeLogout } from "@/render/ipc";
import { version } from "@/render/shared/version";
import { ready, account, setAccount, clearAccount } from "@/render/shared/account";
export default defineComponent({
name: "Settings",
@ -58,7 +61,6 @@
try {
await invokeLogout();
clearProfile();
} catch(e) {
console.log('error')
}
@ -66,6 +68,8 @@
}
return {
account,
version,
onActive,
toggleSettings,
onLogout
@ -79,67 +83,52 @@
#settings {
position: fixed;
width: 100vw;
height: 100vh;
background-color: rgba(235, 235, 235, 0.75);
z-index: 4;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
background-color: #EBEBEB;
opacity: 0.75;
z-index: 4;
animation-name: appear;
animation-duration: 0.5s;
}
@keyframes appear {
from {
opacity: 0
background-color: rgba(235, 235, 235, 0);
}
to {
opacity: 0.75
background-color: rgba(235, 235, 235, 0.75);
}
}
.settingsIcon {
.icon {
position: fixed;
right: 0;
border: none;
outline: none;
display: flex;
flex-direction: row;
padding: 5px;
margin-right: 20px;
margin-top: 19px;
z-index: 2;
background-color: Transparent;
}
.settingsIcon:hover {
.icon:hover {
cursor: pointer;
}
.settingsButtonDot {
width: 5px;
height: 5px;
margin: 2px;
border-radius: 2.5px;
background-color: #9B9B9B;
.account {
font-size: 12px;
font-weight: bold;
margin-bottom: 25px;
}
.settingsOption {
.logout {
font-family: "SF Compact Display";
background-color: #B7B7B7;
padding: 10px 30px;
@ -149,16 +138,21 @@
border: none;
outline: none;
text-decoration: none;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 5;
}
.settingsOption:hover {
.logout:hover {
cursor: pointer;
}
.version {
position: absolute;
left: 50%;
top: 75%;
transform: translate(-50%, -50%);
font-size: 12px;
font-weight: bold;
color: #575757;
}
</style>

View file

@ -1,33 +1,25 @@
export default function useScroll(elementId: string) {
// Update isScrolledToBottom
const isScrolledToBottom = () => {
const el = document.getElementById(elementId);
const el = document.getElementById(elementId);
const isBottom = () => {
if (el) {
return el.scrollHeight - el.clientHeight <= el.scrollTop + 30;
return el.scrollHeight - el.clientHeight <= el.scrollTop + 1;
}
}
// Adjust scroll after we add content to the messenger.
const adjustScroll = () => {
const el = document.getElementById(elementId);
if (el) {
el.scrollTo({
top: el.scrollHeight - el.clientHeight,
behavior: 'smooth'
});
}
}
return {
isScrolledToBottom,
adjustScroll,
};
if (el)
el.addEventListener('adjust-scroll', adjustScroll);
window.addEventListener('resize', adjustScroll);
}
}

View file

@ -12,7 +12,7 @@ const { post, invoke } = useIpc();
export const invokeLogin = async (
payload: LoginPayload
payload: AccountCredentials
): Promise<Profile | Error> => (
await invoke('invoke-account-login', payload)
);
@ -41,7 +41,7 @@ export const invokeReturnAudio = async (): Promise<ArrayBuffer[] | Error> => (
*
*/
export const postMessage = (payload: Raw): void => (
export const postMessage = (payload: Raw | PlatformRequest): void => (
post('post-session-send', payload)
);

View file

@ -3,11 +3,20 @@ import { IpcRendererListener } from "./composables/useIpcRend"
/*
* Shared state imports
* */
import { setProfile } from "./shared/profile";
import { setMessages, addMessage, updateMessage, deleteMessage } from "./shared/messages";
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_VERSION_CHANNEL = "set-version";
const INIT_MESSAGES_CHANNEL = "init-messages";
const ADD_MESSAGE_CHANNEL = "add-message";
@ -15,6 +24,16 @@ 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({
channel: SET_PROFILE_CHANNEL,
listenerCallback: setProfile
@ -44,4 +63,4 @@ export const deleteMessagesListener = new IpcRendererListener({
export const connectionStatusListener = new IpcRendererListener({
channel: CONNECTION_STATUS_CHANNEL,
listenerCallback: setConnectionStatus
});
});

View file

@ -0,0 +1,26 @@
// 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,12 +1,65 @@
// 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 {
status
hiddenState,
useAnim,
status,
show,
hide
};

View file

@ -1,26 +1,14 @@
// shared
import { ref, Ref } from "vue";
import useScroll from "@/render/composables/useScroll";
const { isScrolledToBottom, adjustScroll } = useScroll("messenger");
export const messages: Ref<Array<Message>> = ref([]);
export const setMessages: IpcListenerCallback<Array<Message>> = (payload) => {
messages.value = payload as Array<Message>;
setTimeout(() => {
adjustScroll();
}, 1000);
messages.value = payload;
}
export const addMessage: IpcListenerCallback<Message> = (payload) => {
const bottom = isScrolledToBottom();
messages.value.push(payload as Message);
if (bottom) {
setTimeout(() => {
adjustScroll();
}, 10);
}
}
export const updateMessage: IpcListenerCallback<Annotation> = (payload) => {
@ -31,11 +19,15 @@ export const updateMessage: IpcListenerCallback<Annotation> = (payload) => {
if (messages.value[i].uid == annotation.uid) {
if (annotation.name == "content") {
messages.value[i].content = annotation.data as string[];
messages.value[i].content = annotation.data as Content[];
}
if (annotation.name == "context") {
messages.value[i].context = annotation.data as string;
messages.value[i].context = annotation.data as Context;
}
if (annotation.name == "seen") {
messages.value[i].seen = annotation.data as boolean;
}
}
@ -56,6 +48,10 @@ export const deleteMessage: IpcListenerCallback<string> = (payload) => {
}
export const resetMessages = () => {
messages.value = [];
}
export default {
messages,
setMessages,

View file

@ -1,27 +1,19 @@
// shared
import { ref } from "vue";
export const profile = ref();
class Profile implements Profile{
crimata_id = "";
name = "";
photo = "";
}
export const authComplete = ref(false);
export const profile = ref(new Profile());
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 const setProfile: IpcListenerCallback<Profile> = (payload) => {
profile.value = payload;
};
export default {
setProfile,
clearProfile,
profile,
showRender,
authComplete,
};
setProfile
};

View file

@ -0,0 +1,12 @@
// 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,37 +1,28 @@
// import useAudio from "@/audio";
import Messages from "./messages";
import { getProfile } from "./store";
import UIState from "@/uiState";
import { config } from "@/config";
import useWebsockets from "./composables/useWebsockets";
import {config} from "@/config";
import { ipcEmit } from "@/composables/useEmitter";
import { backgroundMitt, ipcEmit } from "@/composables/useEmitter";
/* start and stop audio functionality */
// const { initAudio, closeAudio } = useAudio();
// UI state
let uiState = new UIState()
let messages: Messages;
// let audio = new Audio();
const onMessageCallback = (payload: string) => {
const message = JSON.parse(payload);
if (message === "CLOSE_AUTH_FAIL") {
console.log("deauthenticating");
if (payload === "CLOSE_AUTH_FAIL") {
backgroundMitt.emit("close-auth-fail");
return
}
else if (message.header === "init") {
messages = new Messages(message.body);
}
const message = JSON.parse(payload) as ClientProtocol;
else {
if (messages) {
messages.update(message.body);
}
}
if (message.header === "init") {
uiState.set(message.body as Init);
} else uiState.update(message.body as Update);
}
@ -39,17 +30,6 @@ const connectionStatusCallback = (status: string) => {
ipcEmit('set-connection-status', status);
}
export const updateAppState = (): void => {
const profile = getProfile();
ipcEmit("set-profile", profile);
if (messages)
messages.emit();
}
const statusOptions = {
openMessage: "Connected",
pongMessage: "Nominal",
@ -63,14 +43,13 @@ const { connect, send, close } = useWebsockets(
statusOptions
);
/* send a message to the platform */
export function sendMessage<Message>(message: Message): void {
/* socket send */
send(message);
export const updateAppUI = (): void => {
uiState.emit();
}
export function sendMessage<Message>(message: Raw | Request): void {
send(message);
}
/* launch a new session (the main process for authenticated users) */
export function launchSession(platformKey: string) {
@ -78,16 +57,15 @@ export function launchSession(platformKey: string) {
/* connect to the platform */
connect(config.PLATFORM_URL, platformKey);
/* initialize the audio streams */
// initAudio();
// initialize the audio streams
// audio.record();
}
export function endSession() {
// closeAudio();
close();
uiState.reset();
}

View file

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

View file

@ -1,27 +1,76 @@
/**
* 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: Message[] | Message | Annotation | string;
data: Profile | Message[] | Message | Annotation | string;
}
/**
* A standard message in Messenger
*/
interface Message {
modifier: string;
content: string[];
context: boolean | string;
content: Content[];
context: Context;
seen: boolean;
uid: string;
}
interface Content {
text: 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: string | 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 {
width: number;
height: number;
@ -30,19 +79,9 @@ interface WindowState {
}
interface Profile {
crimataId: string;
alias: string;
initials: string;
}
interface LoginPayload {
email: string;
password: string;
}
interface AuthState {
profile: Profile | null;
token: string | null;
crimata_id: string;
name: string;
photo: string;
}
interface AccountCredentials {

View file

@ -1,25 +1,43 @@
import { ipcEmit } from "@/composables/useEmitter";
export default class Messages {
class Profile implements Profile{
crimata_id = "";
name = "";
photo = "";
}
export default class UIState {
profile: Profile;
messages: Message[];
constructor(messages: Message[]) {
console.log(messages);
this.messages = messages;
this.emit();
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 === "add") {
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);
@ -33,18 +51,28 @@ export default class Messages {
}
reset() {
this.profile = new Profile();
this.messages = [];
this.emit();
}
_annotate(annotation: Annotation) {
for (let i in this.messages) {
for (const i in this.messages) {
if (this.messages[i].uid == annotation.uid) {
if (annotation.name == "content") {
this.messages[i].content = annotation.data as string[];
this.messages[i].content = annotation.data as Content[];
}
if (annotation.name == "context") {
this.messages[i].context = annotation.data as string;
this.messages[i].context = annotation.data as Context;
}
if (annotation.name == "seen") {
this.messages[i].seen = annotation.data as boolean;
}
ipcEmit("update-message", annotation);
@ -56,7 +84,7 @@ export default class Messages {
}
_delete(uid: string) {
for (var i = 0; i < this.messages.length; i++) {
for (let i = 0; i < this.messages.length; i++) {
if (this.messages[i].uid === uid) {
this.messages.splice(i, 1);
break;