+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 {
- const res = await postAuth(token);
+ 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
+
+ // launch session
+ launchSession(parsed.token);
- return {
- profile: parsed.profile,
- token: parsed.token
- };
+ } else throw(new Error('Failed to authenticate (no token).'));
- } catch(e) {
- console.log('[ACCOUNT]', e);
- clearStore();
- throw(new Error('Failed to authenticate.'));
+ } 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> => {
// 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();
}
+// 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();
+
+}
+
+
+
+++ /dev/null
-/* 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;
-// }
-// }
-// }
-
-
-
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}
+};
\ No newline at end of file
+++ /dev/null
-
-
-
-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);
- }
-
- }
-
-}
setTimeout(() => {
connect(socketUrl, secret), _reconnectTimeout
});
-
}
});
"use strict";
-import { accountLogin, accountLogout, accountProfile } from "@/account";
+import { accountLogin, accountLogout } from "@/account";
import { IpcHandler } from "@/composables/useIpcMain";
import { flush } from "@/audio";
channel: GET_AUDIO_CHANNEL,
handlerCallback: flush
});
-
-
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
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
+});
\ No newline at end of file
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() {
/* 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();
}
--- /dev/null
+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()
+ }
+
+}
\ No newline at end of file
<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>
<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({
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
}
}
})
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
<template>
<div :class="`bubble ${modifier}-bubble ${modifier}-${child}`">
- {{ content }}
+ <div v-show="!seen" class="notify"></div>
+ {{ text }}
</div>
</template>
export default defineComponent({
name: "Bubble",
- props: ["modifier", "content", "child"],
+ props: ["modifier", "text", "child", "seen"],
})
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>
\ No newline at end of file
<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
}
</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>
<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>
}
.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 {
}
-// 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) {
class="menuButton minimizeButton"
@click.prevent="postNavBarMin"
/>
- <connection-status />
</span>
</template>
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');
: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>
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";
export default defineComponent({
name: "InputItem",
- props: ["initials"],
-
setup() {
// Default values for position.
const recording = false;
return {
+ profile,
elementX,
elementY,
recording
<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" />
</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({
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,
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;
margin-left: 15px;
}
-.submitButton {
+.submit {
font-family: "SF Compact Display";
position: fixed;
margin-top: 141px;
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;
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>
<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"
/>
<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";
export default defineComponent({
name: "Message",
- props: ["modifier", "content", "context", "uid"],
+ props: ["modifier", "content", "context", "seen", "uid"],
components: {
Bubble,
setup() {
+ onMounted(() => {
+
+ const messengerEl = document.getElementById("messenger");
+
+ if (messengerEl)
+ messengerEl.dispatchEvent(new CustomEvent('adjust-scroll'))
+
+ });
+
return {
calcChild
};
padding-top: 9px;
padding-bottom: 9px;
animation-name: message-init-anim;
- animation-duration: 0.25s;
+ animation-duration: 0.25s;
}
@keyframes message-init-anim {
<template>
<!-- Position fixed items -->
- <InputItem :initials="profile.initials"/>
<Settings />
+ <ConnectionStatus />
<div id="recIcon" />
+ <InputItem />
<!-- List of message bubbles. -->
<div id="messenger">
:modifier="message.modifier"
:content="message.content"
:context="message.context"
+ :seen="message.seen"
:uid="message.uid"
/>
</div>
<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
<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",
try {
await invokeLogout();
- clearProfile();
} catch(e) {
console.log('error')
}
}
return {
+ account,
+ version,
onActive,
toggleSettings,
onLogout
#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;
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>
-
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);
-}
\ No newline at end of file
+}
export const invokeLogin = async (
- payload: LoginPayload
+ payload: AccountCredentials
): Promise<Profile | Error> => (
await invoke('invoke-account-login', payload)
);
*
*/
-export const postMessage = (payload: Raw): void => (
+export const postMessage = (payload: Raw | PlatformRequest): void => (
post('post-session-send', payload)
);
/*
* 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";
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
export const connectionStatusListener = new IpcRendererListener({
channel: CONNECTION_STATUS_CHANNEL,
listenerCallback: setConnectionStatus
-});
+});
\ No newline at end of file
--- /dev/null
+// 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
+};
// 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
};
+
+
// 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) => {
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;
}
}
}
+export const resetMessages = () => {
+ messages.value = [];
+}
+
export default {
messages,
setMessages,
// 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
+};
\ No newline at end of file
--- /dev/null
+// shared
+import { ref } from "vue";
+
+export const version = ref();
+
+export const setVersion: IpcListenerCallback<Profile | null> = (payload) => {
+ version.value = payload;
+};
+
+export default {
+ setVersion
+}
\ No newline at end of file
-
-
-
// 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);
}
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",
statusOptions
);
-/* send a message to the platform */
-export function sendMessage<Message>(message: Message): void {
+export const updateAppUI = (): void => {
+ uiState.emit();
+}
- /* socket send */
+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) {
/* 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();
+
}
const schema = {
token: {
type: 'string',
- },
- profile: {}
+ }
};
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");
+}
\ No newline at end of file
+/**
+ * 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;
}
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 {
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);
}
+ 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);
}
_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;