Merge remote-tracking branch 'origin/main' into main

This commit is contained in:
Enrique Hernandez 2021-02-17 13:25:37 -06:00
commit 69a9dacb90
34 changed files with 874 additions and 799 deletions

View file

@ -1,5 +1,9 @@
"use strict";
/* eslint @typescript-eslint/no-var-requires: "off" */
import { sendAudio } from './session'
const portAudio = require('naudiodon');
let ia: typeof portAudio.AudioIO;
let audioInput: string[] = [];
function processAudio(audioInput: Array<string>): Buffer {
@ -8,42 +12,39 @@ function processAudio(audioInput: Array<string>): Buffer {
return Buffer.from(audio, "base64");
}
const updateRecorder = async (Event: any, record: boolean): void => {
export const updateRecorder = (_event, record: boolean): void => {
if (record) {
ia.resume();
} else {
ia.pause();
const audio = processAudio(audioInput);
// emmit ws event with audio
// sendAudio(audio);
sendAudio(audio);
audioInput = [];
}
}
export const initRecorder = () => {
const ia = new portAudio.AudioIO({
inOptions: {
channelCount: 1,
sampleFormat: 16,
sampleRate: 16000,
deviceId: -1,
closeOnError: false,
}
});
ia.setEncoding('base64');
ia.start();
ia.pause();
ia.on('error', (e: Error) => {
console.log('Error recording audio', + e);
});
ia.on('data', (chunk: string) => {
audioInput.push(chunk);
});
export function useIA() {
return {
ia,
processAudio
if (!ia) {
ia = new portAudio.AudioIO({
inOptions: {
channelCount: 1,
sampleFormat: 16,
sampleRate: 16000,
deviceId: -1,
closeOnError: false,
}
});
ia.setEncoding('base64');
ia.start();
ia.pause();
ia.on('error', (e: Error) => {
console.log('Error recording audio', + e);
});
ia.on('data', (chunk: string) => {
audioInput.push(chunk);
});
}
}

View file

@ -1,40 +1,10 @@
"use strict";
import { createWindow } from './window';
import { ipcMain, BrowserWindow } from "electron";
import { initSession } from './session';
import { backgroundMitt } from './emitter';
// const portAudio = require('naudiodon');
import { initRecorder } from './audio';
interface RenderMessage {
content: string;
context: string;
subContext: string;
modifiers: string;
time: string;
id: string;
}
interface IpcRendererPayload {
endpoint: string;
message: RenderMessage | null;
}
let win: BrowserWindow | null;
let activeSession = false;
backgroundMitt.on('ipc-renderer', (payload: IpcRendererPayload) => {
if (win)
win.webContents.send(payload.endpoint, {
message: payload.message
});
});
const windowDismount = (): void => {
win = null;
backgroundMitt.emit('window-active', false);
ipcMain.removeAllListeners('send-message');
}
let socket = false;
/*
* The main function will be run after electron app is ready.
@ -42,55 +12,17 @@ const windowDismount = (): void => {
export async function main() {
// create main window.
if (!win) win = await createWindow({
await createWindow({
width: 350,
height: 525,
resizable: true
});
// Instantiate socket session with crimata-platorm.
if (!activeSession) initSession();
if (!socket) initSession();
socket = true;
activeSession = true;
// Begin audio stream.
initRecorder();
// Handle window close.
win.on("closed", windowDismount);
// TODO: FIX recorder restart bug
// let audioInput: string[] = [];
// const updateRecorder = async (Event: any, record: boolean): void => {
// if (record) {
// ia.resume();
// } else {
// ia.pause();
// const audio = processAudio(audioInput);
// sendAudio(audio);
// audioInput = [];
// }
// }
// function processAudio(audioInput: Array<string>): Buffer {
// let audio = '';
// audioInput.forEach(chunk => audio += chunk);
// return Buffer.from(audio, "base64");
// }
// const ia = new portAudio.AudioIO({
// inOptions: {
// channelCount: 1,
// sampleFormat: 16,
// sampleRate: 16000,
// deviceId: -1,
// closeOnError: false,
// }
// });
// ia.setEncoding('base64');
// ia.start();
// ia.pause();
// ia.on('error', (e: Error) => {
// console.log('Error recording audio', + e);
// });
// ia.on('data', (chunk: string) => {
// audioInput.push(chunk);
// });
}

View file

@ -8,7 +8,7 @@ interface RenderMessage {
content: string;
context: string;
subContext: string;
modifiers: string;
modifier: string;
time: string;
id: string;
}
@ -32,22 +32,29 @@ let auth = false;
const authSession = async (_event, payload: string | UserCreds | null): Promise<string> => {
return new Promise((resolve, reject) => {
console.log('authenticating...');
backgroundMitt.on('auth-res', (res: string) => {
backgroundMitt.once('auth-res', (res: string) => {
if (res === 'locked') {
reject(res);
} else {
console.log('Success!');
auth = true;
resolve(res);
}
});
if (!payload) {
socket.send('no-token');
} else if (typeof payload === 'string' || payload instanceof String) {
} else if (payload instanceof String || typeof payload === 'string') {
socket.send(payload);
} else {
socket.send(JSON.stringify(payload));
}
});
};
@ -59,22 +66,52 @@ const receiveMessage = (message: string): void => {
}
const parsed: RenderMessage = JSON.parse(message);
if (parsed.modifier === "sf") {
backgroundMitt.emit('message-res', parsed);
} else if (parsed.modifier === "ai") {
backgroundMitt.emit('ipc-renderer', {
endpoint: 'render-message',
message: parsed
});
}
};
const asyncSend = (_event?, payload: TextMessage | Buffer): Promise<RenderMessage> => {
return new Promise((resolve, reject) => {
backgroundMitt.once('message-res', (res: RenderMessage) => {
if (res.content) {
resolve(res);
} else {
reject('ERROR: NO MESSAGE');
}
});
if (payload instanceof Buffer) {
socket.send(payload);
} else if (payload) {
socket.send(JSON.stringify(payload));
}
});
}
export const sendAudio = async (content: Buffer) => {
const res = await asyncSend(null, content);
backgroundMitt.emit('ipc-renderer', {
endpoint: 'render-message',
message: parsed
message: res
});
};
export const sendMessage = (content: TextMessage | Buffer) => {
if (content instanceof Buffer) {
socket.send(content);
} else if (content) {
socket.send(JSON.stringify(content));
}
}
// Run every time we want to connect to backend.
export const initSession = () => {
// Close existing sockets.
if (socket) {
socket.removeAllListeners();
socket.terminate();
@ -82,25 +119,33 @@ export const initSession = () => {
success = false;
}
// Init new socket.
socket = new WebSocket(`${ip}:${port}`);
socket.binaryType = 'arraybuffer';
// handle renderer auth-token event
ipcMain.removeHandler('auth-user'); // avoid setting duplicate handlers
ipcMain.handle('auth-user', authSession);
ipcMain.removeHandler('auth-session'); // avoid setting duplicate handlers
ipcMain.handle('auth-session', authSession);
// handle renderer send-message event
ipcMain.removeHandler('message'); // avoid setting duplicate handlers
ipcMain.handle('message', asyncSend);
// Connect to the backend.
socket.on('open', () => {
console.log('Success! Connected to Crimata Servers.');
console.log('Connected to Crimata Servers.');
// fetch token from renderer
// call auth renderer event on connection
backgroundMitt.emit('ipc-renderer', {
endpoint: 'fetch-token',
endpoint: 'auth',
message: null
});
success = true;
});
// Handle for failed connect, try again.
socket.on('error', (_e) => {
console.log('ERROR: Failed to connect.');
socket.removeAllListeners();

View file

@ -3,7 +3,7 @@
import { BrowserWindow, ipcMain } from "electron";
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
import { backgroundMitt } from './emitter';
import { sendMessage } from './session';
import { updateRecorder } from './audio';
import * as path from "path";
interface WindowSettings {
@ -12,23 +12,46 @@ interface WindowSettings {
resizable: boolean;
}
interface TextMessage {
audio: 0;
content: string;
interface RenderMessage {
content: string;
context: string;
subContext: string;
modifiers: string;
time: string;
id: string;
}
const handleMessage = (_event, arg: TextMessage | Buffer ) => {
sendMessage(arg);
interface IpcRendererPayload {
endpoint: string;
message: RenderMessage | null;
}
let win: BrowserWindow | null;
const windowMount = (): void => {
backgroundMitt.emit('window-active', true);
ipcMain.on('send-message', handleMessage);
ipcMain.on('update-recorder', updateRecorder);
}
export const createWindow = async (options: WindowSettings): Promise<BrowserWindow> => {
const windowDismount = (): void => {
win = null;
backgroundMitt.emit('window-active', false);
ipcMain.removeAllListeners('update-recorder');
}
backgroundMitt.on('ipc-renderer', (payload: IpcRendererPayload) => {
if (win)
win.webContents.send(payload.endpoint, {
message: payload.message
});
});
export const createWindow = async (options: WindowSettings): Promise<void> => {
return new Promise((resolve, _reject) => {
const win: BrowserWindow = new BrowserWindow({
if (win) resolve();
win = new BrowserWindow({
width: options.width,
height: options.height,
resizable: options.resizable,
@ -50,9 +73,12 @@ export const createWindow = async (options: WindowSettings): Promise<BrowserWind
win.loadURL("app://./index.html");
}
// Handle window close.
win.on("closed", windowDismount);
win.webContents.on('did-finish-load', () => {
windowMount();
resolve(win);
resolve();
});
});

View file

@ -1,60 +0,0 @@
<template>
<svg
id="messenger"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
@click="emittMessage"
>
<MessageVisualizer />
<TextAudioInput />
</svg>
</template>
<script lang="ts">
// child components
import MessageVisualizer from "./messageVisualizer/index.vue";
import TextAudioInput from "@/components/taInput/index.vue";
// event emitter
import { Mitt } from "@/types/mitt/index";
import { defineComponent, ref, inject } from "vue";
export default defineComponent({
name: "Messenger",
components: { MessageVisualizer, TextAudioInput },
setup() {
const count = ref(0);
// imports mitt Emitter safely
let emitter: Mitt;
const emitterInject: Mitt | undefined = inject("mitt");
if (emitterInject) {
emitter = emitterInject;
}
// Render message when render-message event called.
window.ipcRenderer.on("render-message", (event, payload) => {
console.log(payload.message);
emitter.emit("renderMessage", payload.message);
});
},
});
</script>
<style lang="scss" scoped>
#messenger {
width: 350px;
height: 500px;
background-color: #e6e6e6;
box-shadow: 0px 3px 10px 0px rgba(0, 0, 0, 0.3);
// border-radius: 20px;
}
</style>

View file

@ -1,77 +0,0 @@
<template>
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
:viewBox="scrollView"
>
<g id="messageVisualizer" transform="translate(0, 5)">
<g
v-for="(item, index) in renderArr"
:key="index"
class="messageList"
transform="translate(0, 25)"
preserverAspectRatio="none"
>
<transition
appear
v-on:before-appear="beforeEnter"
v-on:appear="enter"
v-on:after-enter="afterEnter"
>
<MessageItem :item="item" :key="index" />
</transition>
</g>
</g>
</svg>
</template>
<script lang="ts">
import useMessageVisualizer from "./viewDetails/visualizer";
import useScrollView from "@/modules/scrollView";
import MessageItem from "../messageBubble/index.vue";
import { Message } from "@/types/message/index";
import { Mitt } from "@/types/mitt/index";
import { defineComponent, reactive, inject } from "vue";
export default defineComponent({
name: "MessageVisualizer",
components: { MessageItem },
setup() {
const renderArr: Message[] = reactive([]);
// imports mitt Emitter safely
let emitter: Mitt;
const emitterInject: Mitt | undefined = inject("mitt");
if (emitterInject) {
emitter = emitterInject;
emitter.on("renderMessage", (payload: Message | undefined) => {
if (payload) renderArr.push(payload);
});
}
const { beforeEnter, enter, afterEnter } = useMessageVisualizer();
const { scrollView } = useScrollView({
targetId: "messageVisualizer",
});
return {
renderArr,
beforeEnter,
enter,
afterEnter,
scrollView,
};
},
});
</script>
<style lang="scss" scoped>
svg {
height: 1%;
overflow: hidden;
}
#messageRenderer {
margin: 0;
}
</style>

View file

@ -1,80 +0,0 @@
import { inject } from "vue";
import AnimeFunc from "@/types/animejs/index";
import { MessageAnimeFunc } from '@/types/message/index';
/**
* anime js animations used by message renderer
*/
export default function useMessageViewAnims(): {
shiftAnimate: MessageAnimeFunc;
stackAnimate: MessageAnimeFunc;
} {
// imports animejs safely
let anime: AnimeFunc;
const animeInject: AnimeFunc | undefined = inject("animejs");
if (animeInject) anime = animeInject;
// shifts message list by a given value
function shiftList({ targetList, sv }: { targetList: NodeList | undefined; sv: number | undefined }) {
if (targetList) {
for (const element of targetList) {
const el = element.parentNode as HTMLElement;
anime({
targets: el,
transform: `translate(0 ${sv})`,
easing: "easeInOutQuad",
duration: 500,
});
}
}
}
// adds new message at the bottom of the list and shifts existing message list
const shiftAnimate: MessageAnimeFunc = (el, transform, shiftValue, targetList) => {
anime({
targets: el,
transform: transform.initialTranslate,
easing: "easeInOutQuad",
duration: 500,
begin: function() {
shiftList({
targetList,
sv: shiftValue
});
},
complete: function() {
anime({
targets: el,
transform: transform.finalTranslate,
easing: "easeInOutQuad",
duration: 500,
offset: 1000
});
}
});
}
// stacks message from top to bottom
const stackAnimate: MessageAnimeFunc = (el, transform) => {
anime({
targets: el,
transform: transform.initialTranslate,
easing: "easeInOutQuad",
duration: 1000,
complete: function() {
anime({
targets: el,
transform: transform.finalTranslate,
easing: "easeInOutQuad",
duration: 500,
offset: 1000
});
}
});
}
return {
shiftAnimate,
stackAnimate
};
}

View file

@ -1,72 +0,0 @@
import { VueTransitionCallback } from '@/types/vue3/vueTransitionCallback/index';
import useMessageViewAnims from "./anime";
import useOffsetCalculator from './offsetCalculator';
/**
* handles visualization of view messages through
* vue transition callbacks and anime-js animations
*/
export default function useMessageVisualizer(): {
beforeEnter: VueTransitionCallback;
enter: VueTransitionCallback;
afterEnter: VueTransitionCallback;
} {
// uses offsetCalculator logic and renderer animations
const { calculateMessageOffsets } = useOffsetCalculator();
const { shiftAnimate, stackAnimate } = useMessageViewAnims();
const messagePadding = 15;
let messageList: NodeList;
let message: HTMLElement | null;
let messageOffset = { x: 0, y: 0 };
let shift = false;
let shiftOffset = 0;
// animates message item to 'enter' renderView
const animateMessage = (el: SVGGElement): void => {
const transform = {
initialTranslate: `translate(${messageOffset.x} ${messageOffset.y})`,
finalTranslate: `translate(${messageOffset.x} ${messageOffset.y})`
}
shift ? shiftAnimate(el, transform, shiftOffset, messageList) : stackAnimate(el, transform);
}
const beforeEnter: VueTransitionCallback = () => {
const queryResult = document.querySelectorAll(".messageList");
if (queryResult.length) messageList = queryResult;
}
const enter: VueTransitionCallback = (el) => {
// query DOM for latest message as soon as it enters
if (el) {
message = document.getElementById(el.id);
}
}
const afterEnter: VueTransitionCallback = (el) => {
// get message positioning & shift state before animating
if (message) {
messageOffset = calculateMessageOffsets(message);
shift = messageOffset.y > 400 ? true : false;
}
if (el) {
if (shift) {
console.log(messageOffset.y)
if (messageOffset.y < 445) {
shiftOffset -= 15;
} else {
shiftOffset -= el.getBBox().height + messagePadding;
}
}
animateMessage(el);
}
}
return {
beforeEnter,
enter,
afterEnter
};
}

View file

@ -1,95 +0,0 @@
<template>
<div id="login">
<!-- login title -->
<div id="loginTitle">Login</div>
<!-- username and password forms -->
<div id="loginBox">
<!-- username -->
<input
class="input"
type="text"
v-model="login.username"
placeholder="username"
/>
<!-- password -->
<input
class="input"
type="password"
v-model="login.password"
placeholder="Password"
/>
</div>
<!-- submit button -->
<button class="button" v-on:click="submitCreds">Submit</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "Login",
data: function() {
return {
login: {
username: "",
password: ""
}
};
}
});
</script>
<style lang="scss" scoped>
#login {
width: 350px;
height: 500px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #e6e6e6;
}
#loginBox {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.input {
background-color: #B9B9B9;
border: none;
color: white;
padding: 16px;
text-decoration: none;
margin: 4px 2px;
cursor: pointer;
border-radius: 10px;
}
.button {
background-color: #58C4FD;
border: none;
color: white;
padding: 10px 20px;
text-decoration: none;
border-radius: 20px;
font-weight: bold;
}
</style>

View file

@ -0,0 +1,92 @@
<template>
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
:viewBox="scrollView"
>
<g id="messageView" transform="translate(0, 20)">
<g v-for="(item, index) in renderArr" :key="index" preserverAspectRatio="none">
<transition
appear
v-on:before-appear="beforeEnter"
v-on:appear="enter"
v-on:after-enter="afterEnter"
>
<TextBubble :item="item" :key="index" />
</transition>
</g>
</g>
</svg>
</template>
<script lang="ts">
import TextBubble from "@/components/textBubble/index.vue";
import { Message } from "@/types/message/index";
import useTransitions from "./viewDetails/transitions";
import useScrollView from "@/modules/scrollView";
import useMitt from "@/modules/mitt";
import { useIpc } from "@/modules/ipc";
import { Queue } from '@/modules/queue';
import { defineComponent, reactive, onMounted, onUnmounted, watch } from "vue";
export default defineComponent({
name: "MessageView",
components: { TextBubble },
setup() {
const { emitter } = useMitt();
const renderArr: Message[] = reactive([]);
const renderQ = new Queue();
const { invoke } = useIpc("message");
const { beforeEnter, enter, afterEnter, rendering } = useTransitions();
const { scrollView } = useScrollView({
targetId: "messageView",
});
const render = (m: Message) => {
if (rendering.value) {
renderQ.enqueue(m);
} else {
renderArr.push(m);
}
}
watch(rendering, (rendering, _prevRendering) => {
if (!rendering) {
const m = renderQ.dequeue() as Message | null;
if (m) renderArr.push(m);
}
})
onMounted(() => {
window.ipcRenderer.on("render-message", (event, payload) => {
const m = payload.message;
if (m.content) render(m);
});
emitter.on("user-message", async (message) => {
try {
const res = await invoke(message);
render(res)
} catch (e) {
console.log("No response. Do not render.");
}
});
});
onUnmounted(() => {
window.ipcRenderer.removeAllListeners("render-message");
emitter.all.clear();
});
return {
renderArr,
beforeEnter,
enter,
afterEnter,
scrollView,
};
},
});
</script>

View file

@ -0,0 +1,60 @@
import { inject } from "vue";
import AnimeFunc from "@/types/animejs/index";
/**
* anime js animations used by message view transitions
*/
export default function useMessageViewAnims() {
// imports animejs safely
let anime: AnimeFunc;
const animeInject: AnimeFunc | undefined = inject("animejs");
if (animeInject) anime = animeInject;
// update message View bottom bound
const shiftView = (el: SVGGElement, transform: string) => {
anime({
targets: el,
transform: transform,
easing: "easeInOutQuad",
duration: 500,
});
}
// stacks message from top to bottom
const stackAnimate = (el: Element, transform: string) => {
return new Promise((resolve, _reject) => {
anime({
targets: el,
transform: transform,
easing: "easeInOutQuad",
duration: 1000,
complete: function() {
resolve();
}
});
})
}
// adds new message at the bottom of the list and shifts existing message list
const shiftAnimate = (el: Element, transform: string, view: SVGGElement, viewTransform: string) => {
return new Promise((resolve, _reject) => {
anime({
targets: el,
transform: transform,
easing: "easeInOutQuad",
duration: 800,
begin: function() {
shiftView(view, viewTransform);
},
complete: function() {
resolve();
}
});
})
}
return {
shiftAnimate,
stackAnimate,
};
}

View file

@ -9,7 +9,7 @@ export default function useOffsetCalculator() {
let previousMessageHeight = 0;
const messagePadding = 15;
function calculateX(message: HTMLElement): void {
function calculateX(message: HTMLElement| SVGGElement): void {
const computedQuery = window.getComputedStyle(message);
const matrix = new WebKitCSSMatrix(computedQuery.webkitTransform);
xOffset.value = matrix.m41;

View file

@ -0,0 +1,85 @@
/**
* Do stuff before, on, and after message enters the view.
* Rendering Process consists of the execution of the above
* mentioned callbacks followed by the shift or stack animation.
* The animation called depends on the height of the message view
* relative to the window height. Additionally, the animation will
* always be called on the after enter callback, after all the neccesary
* calculations have been completed. While all of this is hapenning,
* the rendering state is updated throughout the process.
*/
import { VueTransitionCallback } from '@/types/vue3/vueTransitionCallback/index';
import useMessageViewAnims from "./anime";
import useOffsetCalculator from './offsetCalculator';
import { ref } from 'vue';
const wH = 500; // window Height
const vP = 10; // view padding
const mP = 15; // message padding
let mH: number; // message Height
let vH: number; // view Height
let vB: number; // view Bottom Bound
let message: HTMLElement | null;
let messageOffset = { x: 0, y: 0 };
let shift = false;
let beforeEnterVH: number;
let messageView: SVGGElement;
export default function useTransitions() {
const { calculateMessageOffsets } = useOffsetCalculator();
const { shiftAnimate, stackAnimate } = useMessageViewAnims();
const rendering = ref(false);
const animateMessage = async (el: SVGGElement) => {
const elTransform = `translate(${messageOffset.x} ${messageOffset.y})`;
const viewTransform = `translate(0 ${-vB})`;
if (shift) {
await shiftAnimate(el, elTransform, messageView, viewTransform);
} else {
await stackAnimate(el, elTransform)
}
rendering.value = false;
}
const beforeEnter: VueTransitionCallback = () => {
rendering.value = true;
// query DOM for messageView element
const messageViewQuery = document.querySelectorAll("#messageView");
messageView = messageViewQuery[0] as SVGGElement;
if (messageViewQuery) {
beforeEnterVH = messageView.getBBox().height;
}
}
const enter: VueTransitionCallback = (el) => {
// query DOM for latest message
if (el) {
message = document.getElementById(el.id);
}
}
const afterEnter: VueTransitionCallback = (el) => {
if (message) messageOffset = calculateMessageOffsets(message);
if (el) {
// calculate message & view heights, as well as view bottom bound
mH = el.getBoundingClientRect().height;
vH = mH + beforeEnterVH + mP;
vB = Math.abs(vH - wH) + vP;
shift = vH > wH ? true : false;
animateMessage(el);
}
}
return {
beforeEnter,
enter,
afterEnter,
rendering
};
}

View file

@ -1,73 +0,0 @@
<template>
<div id="splash">
<svg
xmlns="http://www.w3.org/2000/svg"
width="57.046"
height="52.759"
viewBox="0 0 57.046 52.759"
>
<g transform="translate(-4127 -507)">
<g transform="translate(3949 332.206)">
<g transform="translate(178 174.794)">
<g transform="translate(0 27.405)">
<circle
cx="12.677"
cy="12.677"
r="12.677"
transform="translate(0 0)"
fill="#383838"
/>
<circle
cx="12.677"
cy="12.677"
r="12.677"
transform="translate(31.692 0)"
fill="#383838"
/>
</g>
<circle
cx="12.677"
cy="12.677"
r="12.677"
transform="translate(15.822 0)"
fill="#383838"
/>
<path
d="M36.27,83.67"
transform="translate(-23.569 -43.588)"
fill="#ff0"
/>
</g>
</g>
</g>
</svg>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "Splash",
});
</script>
<style lang="scss" scoped>
#splash {
width: 350px;
height: 500px;
display: flex;
align-items: center;
justify-content: center;
background-color: #EBEBEB;
box-shadow: 0px 3px 10px 0px rgba(0, 0, 0, 0.3);
// border-radius: 20px;
}
</style>

View file

@ -1,37 +1,46 @@
import { ref, watch, onUnmounted, onMounted } from "vue";
import useKeyDownHandler from "@/modules/keyDownHandler/useKeyDownHandler";
import useMediaStream from "@/modules/mediaStream";
import useAudioVisualizer from './audioDetails/audioVisualizer';
import useTextVisualizer from './textDetails/textVisualizer';
/*
* Controls which input to visualize
* Will visualize text input by default as user types or
* if user holds space bar down, input audio stream
* will be visualized & recorded
*/
// NOTE: input params should probably be input and MediaStream
// TODO: visualize audio stream from nodeJS background process
import { ref, watch, onUnmounted, onMounted } from "vue";
import useKeyDownHandler from "@/modules/keyDownHandler/useKeyDownHandler";
import useMediaStream from "@/modules/mediaStream";
import useAudioVisualizer from './audioDetails/audioVisualizer';
import useTextVisualizer from './textDetails/textVisualizer';
import useMitt from "@/modules/mitt";
import { useIpc } from '@/modules/ipc';
let stream: MediaStream;
export default function useInputController() {
const visualizeStream = ref(false);
const paths = ref([]);
const audioBubbleWidth = ref(10);
let stream: MediaStream;
const { emitter } = useMitt();
const { post } = useIpc('update-recorder');
// send message on ENTER
const enterCallback = (input: string) => {
const message = {
audio: 0,
text: input
};
emitter.emit("user-message", message);
}
const { keyDownHandler, input } = useKeyDownHandler();
const { textInputXoffset, renderTextInput } = useTextVisualizer(input);
const { visualizeStreamAsPaths, expandBubble } = useAudioVisualizer(visualizeStream);
const { keyDownHandler, input } = useKeyDownHandler(enterCallback);
const { textInputXoffset, renderTextInput } = useTextVisualizer(input);
// stops stream recording on space key up
function keyupHandler(e: any) {
if (e.key === " " && visualizeStream.value) {
console.log("stop recording");
window.postMessage({
myTypeField: 'update-recorder',
record: false
}, '*')
post(false);
visualizeStream.value = false;
paths.value = []
audioBubbleWidth.value = 10;
@ -39,15 +48,18 @@ export default function useInputController() {
}
}
// add window event keyboard listeners
window.addEventListener("keydown", keyDownHandler);
window.addEventListener('keyup', keyupHandler);
// get stream & initialize recorder object on mount
onMounted(async () => {
stream = await useMediaStream({ audio: true });
// add window event keyboard listeners
window.addEventListener("keydown", keyDownHandler);
window.addEventListener('keyup', keyupHandler);
})
onUnmounted(() => {
window.removeEventListener("keydown", keyDownHandler);
window.removeEventListener("keyup", keyupHandler);
});
// determine whether to render text or audio based one the keyboard input
watch(input, (input, prevInput) => {
if (prevInput !== "" || prevInput.length > 0) {
@ -57,14 +69,8 @@ export default function useInputController() {
return;
}
if (input === " " && visualizeStream.value === false) {
console.log("record MediaStream");
visualizeStream.value = true;
window.postMessage({
myTypeField: 'update-recorder',
record: true
}, '*')
post(true);
expandBubble(audioBubbleWidth);
visualizeStreamAsPaths(stream, paths, audioBubbleWidth);
return;
@ -72,11 +78,6 @@ export default function useInputController() {
renderTextInput();
});
// remove Event Listeners on component unMount
onUnmounted(() => {
window.removeEventListener("keydown", keyDownHandler);
window.removeEventListener("keyup", keyupHandler);
});
return {
input,

View file

@ -1,14 +1,16 @@
import { ref, computed, inject } from 'vue';
import useTextInputAnims from "./textAnime";
import { Ref } from "@/types/vueRef/index";
/*
* text rendering logic used by inputController
*/
import { ref, computed, onMounted, onUnmounted } from 'vue';
import useTextInputAnims from "./textAnime";
import { Ref } from "@/types/vueRef/index";
import useMitt from "@/modules/mitt";
export default function useTextVisualizer(input: Ref<string>) {
const inputHeight = ref(42);
const inputWidth = ref(0);
const emitter: any = inject("mitt");
const { emitter } = useMitt();
const { animateSend, verticalShiftInput, inputAppear } = useTextInputAnims();
async function renderTextInput() {
@ -32,10 +34,16 @@ export default function useTextVisualizer(input: Ref<string>) {
}
}
emitter.on("newSelfMessage", async (payload: any) => {
const inputEl = document.getElementsByClassName("inputContainer");
const foreignEl = inputEl[0] as HTMLElement;
animateSend(foreignEl, inputHeight.value, input);
onMounted(() => {
emitter.on("user-message", () => {
const inputEl = document.getElementsByClassName("inputContainer");
const foreignEl = inputEl[0] as HTMLElement;
animateSend(foreignEl, inputHeight.value, input);
});
});
onUnmounted(() => {
emitter.all.clear();
});
const textInputXoffset = computed(() => {

View file

@ -49,7 +49,7 @@ export default function useComputedBubble(m: string, id: string) {
}
const startingOffset = computed(() => {
const parentView = document.getElementById('messageVisualizer');
const parentView = document.getElementById('messageView');
if (parentView) {
const rect = parentView.getBoundingClientRect();
let Y = 600;

View file

@ -29,13 +29,11 @@
<script lang="ts">
import useComputedBubble from "./bubbleDetails/computed";
import useBubbleDrawer from "./bubbleDetails/drawer";
// text wrap composition
import useTextWrap from "@/modules/textWrap";
import { defineComponent, onMounted, ref, reactive } from "vue";
export default defineComponent({
name: "MessageItem",
name: "TextBubble",
props: {
item: {
type: Object,

View file

@ -2,12 +2,12 @@ import { reactive, toRefs } from 'vue';
import { useIpc } from './ipc';
interface AuthState {
accessToken?: string | null;
accessToken: string | null;
error?: Error;
}
const state = reactive<AuthState>({
accessToken: undefined,
accessToken: null,
error: undefined,
});
@ -16,7 +16,7 @@ const AUTH_KEY = 'crimata_token';
const token = window.localStorage.getItem(AUTH_KEY);
const authToken = async () => {
const { data, invoke } = useIpc('auth-user');
const { data, invoke } = useIpc('auth-session');
try {
await invoke(token);
state.accessToken = data.value;
@ -28,29 +28,26 @@ const authToken = async () => {
}
}
// authenticate on socket connect
window.ipcRenderer.on("fetch-token", async (_event, _arg) => {
authToken();
// authenticate on auth event
window.ipcRenderer.on("auth", async (_event, _arg) => {
await authToken();
});
if (token) {
authToken();
state.accessToken = token;
}
export const useAuth = () => {
const setToken = (token: string, remember: boolean) => {
if (remember) {
// Save
window.localStorage.setItem(AUTH_KEY, token);
}
const setToken = (token: string) => {
window.localStorage.setItem(AUTH_KEY, token);
state.accessToken = token;
state.error = undefined;
}
const logout = (): Promise<void> => {
const logout = (): Promise<null> => {
window.localStorage.removeItem(AUTH_KEY);
return Promise.resolve(state.accessToken = undefined);
return Promise.resolve(state.accessToken = null);
}
return {

View file

@ -2,9 +2,7 @@ import { ref, computed } from 'vue'
export const useIpc = (endpoint: string) => {
const data = ref();
const loading = ref(false);
const error = ref();
const listener = ref(false);
const errorMessage = computed(() => {
console.log('ERROR', error.value);
@ -13,54 +11,29 @@ export const useIpc = (endpoint: string) => {
}
})
const invoke = (payload: any) => {
loading.value = true;
const invoke = async (payload: any) => {
error.value = undefined;
return window.ipcRenderer.invoke(endpoint, payload).then(res => {
data.value = res;
}).catch(e => {
try {
const res = await window.ipcRenderer.invoke(endpoint, payload);
return data.value = res;
} catch (e) {
error.value = e;
throw e;
}).finally(() => {
loading.value = false;
})
}
}
const send = (payload?: Record<string, any>, callback?: (arg: any) => void) => {
loading.value = true;
error.value = undefined;
window.ipcRenderer.on(endpoint + '-reply', (event: any, arg: any | Error) => {
if (arg instanceof Error) {
error.value = arg;
throw arg;
} else {
data.value = arg;
loading.value = false;
}
});
window.ipcRenderer.send(endpoint, payload);
};
const onEvent = (callback: (event: any, arg: any) => void) => {
listener.value = true;
window.ipcRenderer.on(endpoint, callback);
};
const post = (payload?: Record<string, any>) => {
const post = (payload: any) => {
window.postMessage({
myTypeField: endpoint,
message: payload
endpoint: endpoint,
content: payload
}, '*')
};
return {
loading,
data,
error,
errorMessage,
send,
invoke
invoke,
post
}
}

View file

@ -1,36 +1,18 @@
import { ref, inject } from "vue";
import { ref } from "vue";
import keyboardNameMap from "./keyBoardMaps/keyboardNameMap";
import keyboardCharMap from "./keyBoardMaps/keyboardCharMap";
// This catches keys from a physical keyboard, a soft keyboard on a tablet, or a
// scanner attached via USB
export default function useKeyDownHandler() {
interface KeyDownEvent {
interface KeyDownEvent {
keyCode: number;
shiftKey: boolean;
}
}
// This catches keys from a physical keyboard, a soft keyboard on a tablet, or a
// scanner attached via USB
export default function useKeyDownHandler(enterCallback: (input: string) => void | null) {
const emitter: any = inject("mitt");
const input = ref("");
// submits newSelfMessage event
function handleEnterKey() {
// does nothing if input value is empty
if (input.value === "") {
return;
}
const message = {
text: input.value,
audio: 0
};
// fire event with message payload
emitter.emit("newSelfMessage", message);
window.postMessage({
myTypeField: 'send-message',
message: message
}, '*')
}
function keyDownHandler(e: KeyDownEvent) {
// keyboardCharMap is an array of arrays, with each inner
// array having 2 columns - un-shifted, and shifted values.
@ -49,7 +31,7 @@ export default function useKeyDownHandler() {
const cmd = keyboardNameMap[e.keyCode];
switch (cmd) {
case "ENTER":
handleEnterKey();
enterCallback(input.value);
break;
case "BACK_SPACE":
input.value = input.value.slice(0, -1);
@ -66,4 +48,4 @@ export default function useKeyDownHandler() {
keyDownHandler,
input
};
}
}

17
src/modules/mitt.ts Normal file
View file

@ -0,0 +1,17 @@
import { inject } from "vue";
import { Mitt } from "@/types/mitt/index";
let emitter: Mitt;
export default function useMitt() {
const emitterInject: Mitt | undefined = inject("mitt");
if (emitterInject) {
emitter = emitterInject;
}
return {
emitter
}
}

78
src/modules/queue.ts Normal file
View file

@ -0,0 +1,78 @@
export interface DataNode<T> {
data: T;
next: DataNode<T> | null;
}
export interface QueueI<T> {
enqueue(data: T): void;
dequeue(): T | null;
peek(): T | null;
size(): number;
}
export class Queue<T> implements QueueI<T> {
private _size: number;
private head: DataNode<T> | null;
private tail: DataNode<T> | null;
constructor() {
this._size = 0;
this.head = this.tail = null;
}
enqueue(data: T) {
const node: DataNode<T> = {
data,
next: null
}
if (this.head === null) {
this.head = this.tail = node;
} else {
if (this.tail) {
this.tail.next = node;
this.tail = this.tail.next;
}
}
++this._size;
}
dequeue(): T | null {
let item: T | null = null;
if (this._size === 0) {
return null;
}
if (this.head) {
item = this.head.data;
this.head = this.head.next;
--this._size;
if (!this._size) {
this.tail = null;
}
}
return item;
}
peek(): T | null {
let item: T | null = null;
if (this._size === 0) {
return null;
}
if (this.head) {
item = this.head.data;
}
return item;
}
size(): number {
return this._size;
}
}

View file

@ -1,7 +1,7 @@
import { ref, computed, onMounted, onUnmounted } from "vue";
/**
* handles visualization of view messages through
* handles visualization of view messages through
* vue transition callbacks and anime-js animations
*/
export default function useScrollView({ targetId }: { targetId: string }) {

View file

@ -1,17 +0,0 @@
interface SocketState {
connection: WebSocket | null;
auth: boolean;
}
// Create WebSocket connection.
const socket = new WebSocket('ws://localhost:8080');
// Connection opened
socket.onopen = function (event) {
socket.send('Hello Server!');
}
socket.onmessage = function(event) {
console.debug("WebSocket message received:", event);
}

View file

@ -17,22 +17,14 @@ process.once("loaded", () => {
// do something with custom event
const message = event.data;
if (message.myTypeField === "send-message") {
ipcRenderer.send("send-message", message.message);
if (message.endpoint === "update-menu-bar") {
ipcRenderer.send("update-menu-bar", message.content);
}
if (message.myTypeField === "update-menu-bar") {
ipcRenderer.send("update-menu-bar", message);
}
if (message.myTypeField === "update-recorder") {
ipcRenderer.send("update-recorder", message.record);
}
if (message.myTypeField === "submit-creds") {
ipcRenderer.send("submit-creds", message)
if (message.endpoint === "update-recorder") {
ipcRenderer.send("update-recorder", message.content);
}
});
});
});

View file

@ -5,6 +5,7 @@ import {
RouteRecordRaw
} from "vue-router";
import Home from "@/views/home.vue";
import Splash from "@/views/splash.vue";
import { useAuth } from '@/modules/auth';
// Define the routes (/*) for the app here.
@ -15,6 +16,14 @@ const routes: Array<RouteRecordRaw> = [
component: Home,
meta: { requiresAuth: true },
},
{
path: "/splash",
name: "splash",
component: Splash,
meta: {
requiresAuth: false
},
},
{
path: "/login",
name: "login",
@ -34,14 +43,19 @@ const router = createRouter({
routes
});
// route auth check
router.beforeEach((to, from, next) => {
const { accessToken } = useAuth();
// Not logged into a guarded route?
if (to.meta.requiresAuth && !accessToken?.value) next({ name: 'login' });
if (to.meta.requiresAuth && !accessToken.value) {
next({ name: 'login' })
}
// Logged in for an auth route
else if ((to.name == 'login' || to.name == 'register') && accessToken!.value) next({ name: 'home' });
else if ((to.name == 'login' || to.name == 'register') && accessToken.value){
next({ name: 'home' });
}
// Carry On...
else next();

View file

@ -1,17 +1,32 @@
<template>
<HomeComponent />
<svg
id="messenger"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<MessageView />
<TextAudioInput />
</svg>
</template>
<script lang="ts">
import MessageView from "@/components/messageView/index.vue";
import TextAudioInput from "@/components/taInput/index.vue";
import { defineComponent } from "vue";
import HomeComponent from "@/components/home/index.vue";
export default defineComponent({
name: "Home",
components: { HomeComponent },
name: "Messenger",
components: { MessageView, TextAudioInput },
});
</script>
<style lang="scss" scoped>
#messenger {
width: 350px;
height: 500px;
background-color: #e6e6e6;
box-shadow: 0px 3px 10px 0px rgba(0, 0, 0, 0.3);
// border-radius: 20px;
}
</style>

View file

@ -18,10 +18,9 @@
<label for="checkbox">Remember Me</label>
<!-- submit button; position: fixed -->
<button class="button" @click="submit">Submit</button>
<button class="button" @click.prevent="submit">Submit</button>
<!-- back to login button: position: fixed -->
<button class="button2" @click="switchView">Create an account</button>
</form>
</template>
@ -45,9 +44,9 @@ export default defineComponent({
const email = ref("");
const password = ref("");
const rememberMe = ref(false);
const rememberMe = ref(true);
const { data, invoke } = useIpc('auth-user');
const { data, invoke } = useIpc('auth-session');
const submit = async () => {
const payload = {
@ -57,8 +56,7 @@ export default defineComponent({
};
try {
await invoke(payload);
console.log('testing incoke', data.value)
setToken(data.value, rememberMe.value);
setToken(data.value);
router.push({ name: "home" });
} catch(e){
console.log('Error loging in.')

View file

@ -6,42 +6,40 @@
</div>
<!-- username and password forms -->
<div id="registerBox">
<form id="registerBox" @submit.prevent="submit">
<!-- username -->
<input class="input" type="text" v-model="email" placeholder="email" />
<input class="input" type="text" v-model="email" placeholder="email" required/>
<!-- password -->
<input class="input" type="password" v-model="password" placeholder="Password" />
<input class="input" type="password" v-model="password" placeholder="Password" required/>
<!-- firstName -->
<input class="input" type="text" v-model="password2" placeholder="Re-enter password" />
</div>
<!-- re-enter passoword -->
<input class="input" type="password" v-model="password2" placeholder="Re-enter password" required/>
<!-- submit button; position: fixed -->
<button class="button" @click="submit">Submit</button>
<!-- submit button; position: fixed -->
<button class="button" type="submit">Submit</button>
</form>
<!-- back to login button: position: fixed -->
<button class="button2" @click="switchView">Back to login</button>
<button class="button2" @click.prevent="switchView">Back to login</button>
</div>
</template>
<script lang="ts">
import {
computed,
defineComponent,
onErrorCaptured,
reactive,
ref,
toRefs,
} from "vue";
import { useRouter } from "vue-router";
import { useIpc } from "@/modules/ipc";
import { useAuth } from "@/modules/auth";
interface RegisterPayload {
request?: string;
email?: string;
password?: string;
request: string;
email: string;
password: string;
}
export default defineComponent({
@ -49,25 +47,46 @@ export default defineComponent({
setup() {
const email = ref("");
const password = ref("");
const rememberMe = ref("");
const password2 = ref("");
const { invoke, data,} = useIpc("auth-user");
const { invoke, data,} = useIpc("auth-session");
const { setToken } = useAuth();
const router = useRouter();
const payload: RegisterPayload = {
request: "register",
email: email.value,
password: password.value,
};
const re = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
const passwordMatch = () => {
let match = false;
if (password.value == password2.value) {
match = true;
}
return match;
}
const validate = () => {
let valid = false;
if (re.test(email.value) && passwordMatch()) {
valid = true;
}
return valid;
}
const submit = async () => {
try {
await invoke(payload);
setToken(data.value, true);
router.push({ name: "home" });
} catch(e) {
console.log('Failed to register.')
const payload: RegisterPayload = {
request: "register",
email: email.value,
password: password.value,
};
if (validate()) {
try {
await invoke(payload);
setToken(data.value);
router.push({ name: "home" });
} catch(e) {
console.log('Failed to register.')
}
}
};
@ -78,6 +97,7 @@ export default defineComponent({
return {
email,
password,
password2,
switchView,
submit,
};

View file

@ -1,17 +1,72 @@
<template>
<SplashComponent />
<div id="splash">
<svg
xmlns="http://www.w3.org/2000/svg"
width="57.046"
height="52.759"
viewBox="0 0 57.046 52.759"
>
<g transform="translate(-4127 -507)">
<g transform="translate(3949 332.206)">
<g transform="translate(178 174.794)">
<g transform="translate(0 27.405)">
<circle
cx="12.677"
cy="12.677"
r="12.677"
transform="translate(0 0)"
fill="#383838"
/>
<circle
cx="12.677"
cy="12.677"
r="12.677"
transform="translate(31.692 0)"
fill="#383838"
/>
</g>
<circle
cx="12.677"
cy="12.677"
r="12.677"
transform="translate(15.822 0)"
fill="#383838"
/>
<path
d="M36.27,83.67"
transform="translate(-23.569 -43.588)"
fill="#ff0"
/>
</g>
</g>
</g>
</svg>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import SplashComponent from "@/components/splash/index.vue"
export default defineComponent({
name: "Splash",
components: { SplashComponent },
});
</script>
</script>
<style lang="scss" scoped>
#splash {
width: 350px;
height: 500px;
display: flex;
align-items: center;
justify-content: center;
background-color: #EBEBEB;
box-shadow: 0px 3px 10px 0px rgba(0, 0, 0, 0.3);
// border-radius: 20px;
}
</style>

160
tests/mockMessages.ts Normal file
View file

@ -0,0 +1,160 @@
import { Message } from "@/types/message/index";
export const Messages: Message[] = [
{
content: "Good morning Dave",
context: "launch",
subContext: "",
modifier: "ai",
id: "2",
time: ""
},
{
content:
"add contact",
context: "addcontact",
subContext: "",
modifier: "sf",
id: "1",
time: ""
},
{
content: "I'm looking for name, crimata_idto addcontact. Feel free toenter only a subset of thisinformation, but then let meknow when you are done",
context: "addcontact",
subContext: "",
modifier: "ai",
id: "5",
time: ""
},
{
content: "andrew gundersen",
context: "addcontact",
subContext: "",
modifier: "sf",
id: "6",
time: ""
},
{
content: "gundersena@xavier.edu",
context: "addcontact",
subContext: "",
modifier: "sf",
id: "3",
time: ""
},
{
content: "I already have a contact forandrew gundersen. Are you sureyou want this?",
context: "addcontact",
subContext: "",
modifier: "ai",
id: "7",
time: ""
},
{
content: "Yes posting it online soon.",
context: "You to Tommy McConville",
subContext: "",
modifier: "sf",
id: "4",
time: ""
},
{
content: "Hey do you know Enrique Hernandez?",
context: "Message from Crimata",
subContext: "",
modifier: "ai",
id: "8",
time: ""
},
{
content: "Hey Anna, did you get the paper done?",
context: "Message from Tommy McConville",
subContext: "",
modifier: "ai",
id: "9",
time: ""
},
{
content: "Hey Anna, did you get the paper done?",
context: "Message from Tommy McConville",
subContext: "",
modifier: "ai",
id: "10",
time: ""
},
{
content: "Yes posting it online soon.",
context: "Message from Tommy McConville",
subContext: "",
modifier: "sf",
id: "11",
time: ""
},
{
content:
"I can be there at 5 pm tomorrow to greet you at a Chick file with my almond and chive salad.",
context: "You to Tommy McConville",
subContext: "",
modifier: "sf",
id: "12",
time: ""
},
{
content: "Hey Anna, did you get the paper done?",
context: "Message from Tommy McConville",
subContext: "",
modifier: "ai",
id: "13",
time: ""
},
{
content: "Hey Anna, did you get the paper done?",
context: "Message from Tommy McConville",
subContext: "",
modifier: "ai",
id: "14",
time: ""
},
{
content: "Yes posting it online soon.",
context: "You to Tommy McConville",
subContext: "",
modifier: "sf",
id: "15",
time: ""
},
{
content: "Sweet, thanks!",
context: "Message from Tommy McConville",
subContext: "",
modifier: "ai",
id: "16",
time: ""
},
{
content: "Hey do you know Enrique Hernandez?",
context: "Message from Crimata AI",
subContext: "",
modifier: "ai",
id: "17",
time: ""
},
{
content: "Yes posting it online soon.",
context: "You to Tommy McConville",
subContext: "",
modifier: "sf",
id: "18",
time: ""
},
{
content: "Sweet, thanks!",
context: "Message from Tommy McConville",
subContext: "",
modifier: "ai",
id: "19",
time: ""
},
];

View file

@ -8,10 +8,10 @@ wss.on("connection", function connection(ws, req) {
ws.on("message", function incoming(message) {
console.log(message)
if (message === 'no-token' || message === 'undefined') {
console.log('no token to authenticate');
ws.send('locked');
}
if (message === 'no-token' || message === 'undefined') {
console.log('no token to authenticate');
ws.send('success');
}
});