"mitt": "^2.1.0",
"naudiodon": "^2.3.2",
"node-record-lpcm16": "^1.0.1",
+ "uuid": "^8.3.2",
"vue": "^3.0.0-0",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0",
const audioContainer = {
input: '',
}
-const encoding = "base64";
+const encoding = "hex";
const audioOptions = {
channelCount: 1,
sampleFormat: 16,
}
// callback run on space bar key up and down.
-const updateRecorder = (_event, payload: boolean): void => {
- record = payload;
+const updateRecorder = (_event, payload: {
+ isRecording: boolean;
+ uid: string | null;
+}): void => {
+ record = payload.isRecording;
if (!record) {
- sendAudio(Buffer.from(audioContainer.input, 'base64'));
+ if (payload.uid) sendAudio(audioContainer.input, payload.uid);
}
};
import { backgroundMitt } from './emitter';
import { ipcMain } from "electron";
import { play } from './audio';
-
-interface RenderMessage {
- content: {
- text: string;
- audio: string;
- };
- context: string;
- modifier: string;
- id: string;
-}
-
-interface UserCreds {
- email: string;
- password: string;
-}
-
-interface TextMessage {
- audio: 0;
- content: string;
-}
+import { UserCreds, ClientMessage } from "@/types/message/index";
+import { clientMessage } from '@/modules/message';
+import { v4 as uuidv4 } from 'uuid';
const ip = 'ws://127.0.0.1';
const port = 8760;
let success = false;
let auth = false;
-
-const authSession = async (_event, payload: string | UserCreds | null): Promise<string> => {
+const onAuthSession = async (_event, payload: string | UserCreds | null): Promise<string> => {
return new Promise((resolve, reject) => {
console.log('authenticating...');
});
};
-// function parseHexString(str: string) {
-// var result = [];
-// while (str.length >= 8) {
-// result.push(parseInt(str.substring(0, 8), 16));
-
-// str = str.substring(8, str.length);
-// }
-
-// return result;
-// }
-
-const receiveMessage = (message: string): void => {
+const onMessage = (message: string): void => {
while (!auth) {
backgroundMitt.emit('auth-res', message);
}
// Parse the message.
- const parsed: RenderMessage = JSON.parse(message);
- console.log('message received', parsed.content.text);
+ const m = JSON.parse(message);
+
+ // check to see if this is a Render Message.
+ if (m.content) {
+ // If there is audio, play it.
+ if (m.content.audio) {
+ const audioBytes = Buffer.from(m.content.audio as string, 'hex');
+ play(audioBytes);
+ }
- // If there is audio, play it.
- if (parsed.content.audio) {
- const audioBytes = Buffer.from(parsed.content.audio, 'hex');
- play(audioBytes);
+ // if there is no unique id, add it.
+ if(!m.uid) m.uid = uuidv4();
}
backgroundMitt.emit('ipc-renderer', {
endpoint: 'render-message',
- message: parsed
+ message: m
});
};
-const sendMessage = (_event, payload: TextMessage): void => {
- console.log('sending message')
+const onClientMessage = (_event, payload: ClientMessage): void => {
+ console.log('sending message');
socket.send(JSON.stringify(payload));
}
-export const sendAudio = (content: Buffer) => {
- console.log('sending audio')
- socket.send(content);
-}
-
// Run every time we want to connect to backend.
-export const initSession = () => {
+export function initSession(): void {
// Close existing sockets.
if (socket) {
// handle renderer auth-token event
ipcMain.removeHandler('auth-session'); // avoid setting duplicate handlers
- ipcMain.handle('auth-session', authSession);
+ ipcMain.handle('auth-session', onAuthSession);
// handle user message event
- ipcMain.removeAllListeners('message');
- ipcMain.on('message', sendMessage);
+ ipcMain.removeAllListeners('client-message');
+ ipcMain.on('client-message', onClientMessage);
// Connect to the backend.
socket.on('open', () => {
initSession();
});
- socket.on("message", receiveMessage);
-};
+ socket.on("message", onMessage);
+}
+export function sendAudio(content: string, uid: string): void {
+ const m = clientMessage("", content, uid);
+ socket.send(JSON.stringify(m));
+}
/*
-* 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
+* Main logic used by inputItem component.
+* Will visualize text input by default as user types.
+* If user holds space bar down, input audio stream will
+* be recorded and sent to the backend for analysis.
*/
import { ref, watch, onUnmounted, onMounted } from "vue";
import { useIpc } from '@/modules/ipc';
import keyboardNameMap from "./keyBoardMaps/keyboardNameMap";
import keyboardCharMap from "./keyBoardMaps/keyboardCharMap";
+import { RenderMessage, ClientMessage } from "@/types/message/index";
+import { v4 as uuidv4 } from 'uuid';
+import {clientMessage, renderMessage} from '@/modules/message';
let textInput: HTMLInputElement | null;
+let m: RenderMessage | ClientMessage;
+let uid: string;
export default function useInputController() {
const isTyping = ref(false);
const input = ref("");
- // send message on ENTER
+ // send message on ENTER.
const onEnter = () => {
+ if (input.value) {
+ // remove first character of input.
+ const text = input.value.substring(1);
- const message = {
- audio: 0,
- text: input.value
- };
+ uid = uuidv4();
- // trigger send animation
- emitter.emit("animate-send");
+ m = renderMessage(text, false, "sf", uid);
- // send message to backend
- if (input.value) post('message', message);
+ // render user message.
+ emitter.emit("self-message", m);
+
+ m = clientMessage(m.content.text, m.content.audio, uid);
+
+ // send message to backend.
+ post('client-message', m);
+
+ // reset text input.
+ if (textInput) textInput.value = '';
+ input.value = " ";
+ }
}
// update input on keydown event & listen for special commands.
switch (cmd) {
case "ENTER":
onEnter();
- if (textInput) textInput.value = ''
- input.value = " ";
break;
case "BACK_SPACE":
input.value = input.value.slice(0, -1);
}
}
- // stops stream recording on space key up event.
+ // stop stream recording and render audio message on space key up event.
const onKeyUp = (e: KeyboardEvent) => {
if (e.key === " " && isRecording.value) {
isRecording.value = false;
- post('update-recorder', isRecording.value);
+
+ uid = uuidv4();
+
+ // render audio message.
+ m = renderMessage("", false, "sf", uid);
+ emitter.emit("self-message", m);
+
+ // stop stream recording.
+ post('update-recorder', {
+ isRecording: isRecording.value,
+ uid
+ });
+
input.value = ''
}
}
if (input === " " && isRecording.value === false) {
isTyping.value = false;
isRecording.value = true;
- post('update-recorder', isRecording.value);
+ post('update-recorder', {
+ isRecording: isRecording.value,
+ uid: null
+ });
return;
}
isTyping.value = true;
--- /dev/null
+import { RenderMessage, ClientMessage } from "@/types/message/index";
+
+export const renderMessage = (text: string, audio: string | boolean, modifier: string, uid: string): RenderMessage => (
+ {
+ content: {
+ audio,
+ text
+ },
+ context: "",
+ modifier,
+ uid
+ }
+)
+
+export const clientMessage = (text: string, audio: string | boolean, uid: string): ClientMessage => (
+ {
+ audio,
+ text,
+ uid
+ }
+)
\ No newline at end of file
ipcRenderer.send("update-recorder", message.content);
}
- if (message.endpoint === "message") {
- ipcRenderer.send("message", message.content);
+ if (message.endpoint === "client-message") {
+ ipcRenderer.send("client-message", message.content);
}
});
});
// 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' })
-// }
-//
-// // Logged in for an auth route
-// else if ((to.name == 'login' || to.name == 'register') && accessToken.value){
-// next({ name: 'home' });
-// }
-//
-// // Carry On...
-// else next();
-// })
+router.beforeEach((to, from, next) => {
+ const { accessToken } = useAuth();
+
+ // Not logged into a guarded route?
+ 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' });
+ }
+
+ // Carry On...
+ else next();
+})
export default router;
-export interface Message {
+export interface UserCreds {
+ email: string;
+ password: string;
+}
+
+export interface RenderMessage {
content: {
text: string;
- audio: boolean | Buffer;
+ audio: boolean | string;
};
context: string;
modifier: string;
- id: string;
+ uid: string;
}
-interface MessageTransform {
- initialTranslate: string;
- finalTranslate: string;
+export interface ClientMessage {
+ text: string;
+ audio: boolean | string;
+ uid: string;
}
-export interface MessageAnimeFunc {
- (message: SVGElement, transform: MessageTransform, scrollHeight?: number | undefined, targetList?: NodeList | undefined): void;
-}
\ No newline at end of file
+export interface Annotation {
+ text: string;
+ context: string;
+ uid: string;
+}
<!-- List of message bubbles. -->
<div id="messenger">
- <MessageItem v-for="message in messages" :message="message" :key="message.id" />
+ <MessageItem v-for="message in messages" :message="message[1]" :key="message[0]" />
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, onMounted, onUnmounted } from "vue";
-import { Message } from "@/types/message/index";
+import { RenderMessage , Annotation } from "@/types/message/index";
import MessageItem from "@/components/messageItem.vue";
import InputItem from "@/components/inputItem/inputItem.vue";
+import useMitt from "@/modules/mitt";
export default defineComponent({
name: "Messenger",
},
setup() {
+ const { emitter } = useMitt();
// List of messages in the view.
// Oldest message first.
- const messages: Message[] = reactive([]);
- const messages_ref: any = {};
-
- // Render a message.
- // Called on "enter" instead of on "recv message"
- const render = (message: Message) => {
- messages.push(message);
- messages_ref[message.id] = message;
- };
+ const messages = reactive(new Map());
- // Annotate/update an existing message.
- const annotate = (message: Message) => {
- 1 + 1;
- };
+ let m: RenderMessage | Annotation;
+
+ const onRenderMessage = (event: any, payload: any) => {
+
+ // render message if there is content.
+ if (payload.message.content) {
+ m = payload.message as RenderMessage;
+
+ messages.set(m.uid, m);
+
+ } else {
+ // must be an annotation, update messages.
+ m = payload.message as Annotation;
+
+ if (messages.has(m.uid)) {
+ messages.get(m.uid).context = m.context;
+ messages.get(m.uid).content.text = m.text;
+ }
+
+ }
+ }
onMounted(() => {
+ emitter.on('self-message', (message) => messages.set(message.uid, message));
// Listen for new messages to render.
- window.ipcRenderer.on("render-message", (event, payload) => {
- if (payload.message.content) render(payload.message);
- });
+ window.ipcRenderer.on("render-message", onRenderMessage);
});
onUnmounted(() => {
window.ipcRenderer.removeAllListeners("render-message");
+ emitter.all.clear();
});
return {
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31"
integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==
+uuid@^8.3.2:
+ version "8.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
+ integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
+
v8-compile-cache@^2.0.3:
version "2.2.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132"