+++ /dev/null
-<template>
- <!-- Only render when profile has been set -->
- <div id="app" v-if="(ready)">
-
- <button id="titlebar" />
-
- <!-- Minimize and exit buttons. -->
- <span class="menu">
- <button
- class="menuButton exitButton"
- @click.prevent="post('nav-bar', 'close')"
- />
- <button
- class="menuButton minimizeButton"
- @click.prevent="post('nav-bar', 'min')"
- />
- </span>
-
- <!-- Main Pages -->
- <Messenger
- v-if="profile"
- :profile="profile"
- :newMessages="newMessages"
- />
- <Login v-else />
-
- </div>
-
- <!-- Show spash screen if ready is false -->
- <Splash v-else />
-
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, onUnmounted, ref } from "vue";
-import { IpcRendererEvent } from "electron";
-import { useIpc } from "@/modules/ipc";
-import { useProfile } from "@/modules/auth"
-import { invokeProfile } from "@/ipcRend/account";
-import { postInitSession, postMount } from "@/ipcRend/session";
-
-import Splash from "@/components/splash.vue";
-import Messenger from "@/components/messenger.vue";
-import Login from "@/components/login.vue";
-
-import { Profile } from "@/types";
-
-
-export default defineComponent({
-
- components: {
- Splash,
- Messenger,
- Login
- },
-
- setup() {
-
- const { post, invoke } = useIpc();
-
- const { profile, setProfile, clearProfile } = useProfile();
-
- // Whether browser has received user info yet.
- const ready = ref(false);
-
- // New messages that browser missed while closed.
- const newMessages = ref([]);
-
- // Receive updated information about the session.
- const updateState = (_event: IpcRendererEvent, payload: any) => {
- console.log('YAYAYAYAYA');
- console.log("APP:Received updated profile and new messages: \n" +
- ` profile: ${payload.message.profile}\n` +
- ` new: ${payload.message.newMessages}`);
-
- if (payload.message.profile) {
- console.log(`APP:Logged-in, showing Messenger View.`);
- } else {
- console.log(`APP:Logged-out, showing Login View.`);
- }
-
- newMessages.value = payload.message.newMessages;
-
- ready.value = true;
-
- }
-
- const onSessionAuthFail = (_event: IpcRendererEvent, _payload: null) => {
- clearProfile();
- }
-
- onMounted(async () => {
- console.log("APP:mounted.");
-
- window.ipcRenderer.on("session-auth-fail", onSessionAuthFail)
- window.ipcRenderer.on("update-state", updateState)
- window.ipcRenderer.on("update_available", () => {
- console.log('testing auto update');
- })
-
- window.ipcRenderer.on("update_downloaded", () => {
- console.log('testing update download');
- })
-
- postMount();
-
- try {
-
- const profile = await invokeProfile() as Profile;
- setProfile(profile);
- } catch(e) {
- console.log('[AUTH]', e);
- clearProfile();
- } finally {
- ready.value = true;
- if (profile.value.crimataId) {
- // start session
- postInitSession(profile.value.crimataId);
- }
- }
-
- });
-
- onUnmounted(() => {
- window.ipcRenderer.removeAllListeners("update-state");
- window.ipcRenderer.removeAllListeners("session-auth-fail");
- })
-
- return {
- ready,
- post,
- newMessages,
- profile
- }
- }
-})
-
-</script>
-
-<style lang="scss">
-
-html, body {
- margin: 0;
- padding: 0;
- // Background color set in window.ts
-}
-
-#app {
- font-family: "SF Pro Text";
-
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-
- height: 100vh;
- width: 100vw;
-
- border-radius: 15px;
- }
-
-#titlebar {
- position: fixed;
-
- width: 100vw;
- height: 54px;
-
- opacity: 0.75;
-
- background-color: #EBEBEB;
-
- -webkit-app-region: drag;
-
- border: none;
- outline: none;
-
- z-index: 1;
-
-}
-
-.menu {
- position: fixed;
- margin-left: 20px;
- margin-top: 20px;
-
- z-index: 2;
-
-}
-
-.menuButton {
- border: none;
- outline:none;
- min-width: 14px;
- min-height: 14px;
- border-radius: 7px;
-}
-
-.exitButton {
- background-color: #FF6157;
-}
-
-.exitButton:active {
- background: #c14645;
-}
-
-.minimizeButton {
- background-color: #FFC12F;
- margin-left: 8px;
-}
-
-.minimizeButton:active {
- background-color: #c08e38;
-}
-
-</style>
-import { useHttp } from "@/modules/http";
+import { useHttp } from "@/composables/http";
import axios from "axios";
const { post } = useHttp();
-export const submit =
- async (email: string, password: string) => (
-
- await post('/account/login', {
- email,
- password
- })
-
+export const submit = async (email: string, password: string) => (
+ await post('/account/login', { email, password })
)
-
-export const logout =
- async (): Promise<null | Error> => (await post('/account/logout'));
-
-
-
-export const fetchProfile = async(email: string, token: string) => (
-
+export const fetchAccount = async (email: string, token: string) => (
await axios({
url: "http://127.0.0.1:3000/api/account/profile",
headers: {
+++ /dev/null
-<svg xmlns="http://www.w3.org/2000/svg" width="13.795" height="12.759" viewBox="0 0 13.795 12.759">
- <g id="Group_544" data-name="Group 544" 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 6.627)">
- <circle id="Ellipse_50" data-name="Ellipse 50" cx="3.066" cy="3.066" r="3.066" transform="translate(0 0)" fill="#383838"/>
- <circle id="Ellipse_51" data-name="Ellipse 51" cx="3.066" cy="3.066" r="3.066" transform="translate(7.664 0)" fill="#383838"/>
- </g>
- <circle id="Ellipse_52" data-name="Ellipse 52" cx="3.066" cy="3.066" r="3.066" transform="translate(3.826 0)" fill="#383838"/>
- <path id="Path_150" data-name="Path 150" d="M36.27,83.67" transform="translate(-33.199 -73.977)" fill="#ff0"/>
- </g>
- </g>
- </g>
-</svg>
+++ /dev/null
-/*
- * Entry point for Crimata electron app.
- * "Look on my Works, ye Mighty, and despair!"
- */
-
-"use strict";
-
-import { initApp } from './background/init';
-import { protocol } from "electron";
-require('dotenv').config()
-
-// Scheme must be registered before the app is ready
-protocol.registerSchemesAsPrivileged([
- { scheme: "app", privileges: { secure: true, standard: true } }
-]);
-
-// Load environment variable
-const isDev = require('electron-is-dev');
-
-// NOTE Program Begins Here
-(() => {
-
- console.log('Starting Crimata electron app.');
- initApp(isDev);
-
-})();
+++ /dev/null
-/* eslint @typescript-eslint/no-var-requires: "off" */
-
-"use strict";
-
-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;
- }
- }
-}
-
-
+++ /dev/null
-import fs from 'fs';
-import { backgroundMitt } from "@/modules/emitter";
-import { SessionState, WindowState } from "@/types";
-import { app } from "electron";
-
-const configPath = app.getPath("userData");
-
-export const ipcEmit = (channel: string, payload: any) => {
- backgroundMitt.emit('ipc-renderer', {
- endpoint: channel,
- message: payload
- });
-}
-
-export const loadState = (fileName: string): SessionState => {
- let state: SessionState;
-
- try {
- state = JSON.parse(fs.readFileSync(configPath + fileName).toString());
- }
-
- catch (error) {
- state = {
- key: false,
- newMessages: []
- }
- }
-
- return state
-
-}
-
-export const loadWinState = (fileName: string): WindowState => {
- let state: WindowState;
-
- try {
- state = JSON.parse(fs.readFileSync(configPath + fileName).toString());
- }
-
- catch (error) {
- state = {
- width: 600,
- height: 500,
- x: null,
- y: null,
- }
- }
-
- return state
-
-}
-
-// Save session or window state.
-export const saveToJson = (fileName: string, data: any) => {
-
- fs.writeFile(configPath + fileName, JSON.stringify(data), (err) => {
- if (err) {
- console.log("Error when saving to json.")
- }
- })
-
-}
\ No newline at end of file
+++ /dev/null
-
-"use strict";
-
-import { app, dialog } from "electron";
-import { createWindow } from './window';
-import { stopStream } from './audio';
-import { backgroundMitt } from '@/modules/emitter';
-import useIpc from "@/background/ipc/index";
-const { autoUpdater } = require('electron-updater');
-
-let win: boolean;
-
-// Listen for window creation.
-backgroundMitt.on('window-active', (state: boolean) => {
- win = state;
-});
-
-// Auto updating.
-autoUpdater.requestHeaders = { 'PRIVATE-TOKEN': 'mvvgWYwWnot4bisiQMh_' }
-
-autoUpdater.on('update-available', (info: any) => {
- console.log(`Update available: ${info.version}`)
-})
-
-autoUpdater.on('update-downloaded', (info: any) => {
-
- const updateDialog = {
- type: 'info',
- buttons: ['Restart', 'Later'],
- title: 'Application Update',
- message: info.version,
- detail: 'A new version has been downloaded. Restart the application to apply the updates.'
- }
-
- dialog.showMessageBox(updateDialog).then((returnValue) => {
- if (returnValue.response === 0) autoUpdater.quitAndInstall()
- })
-
-})
-
-
-
-// Run when electron app is initialized.
-async function main(): Promise<void> {
-
- console.log("MAIN:Initializing Electron App.");
-
- useIpc();
-
- // Must wait til window is created.
- await createWindow();
-
-}
-
-// Root function of app.
-export function initApp(dev: boolean): void {
-
- // On initial startup.
- app.on("ready", () => {
- // autoUpdater.checkForUpdates()
- main();
- });
-
- // Must keep to ensure app doesn't quit on close.
- app.on("before-quit", async () => {
- await stopStream();
- });
-
- // Must keep to ensure app doesn't quit on close.
- app.on("window-all-closed", () => {
- });
-
- // When user clicks app icon (re-open)
- app.on("activate", () => {
-
- if (!win) {
- createWindow();
- }
-
- });
-
- // Exit cleanly on request from parent process in development mode.
- if (dev) {
- process.on("SIGTERM", () => {
- app.quit();
- });
- }
-}
-
+++ /dev/null
-
-"use strict";
-
-import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron";
-import { fetchAudioInput, toggleRecord } from "@/background/audio";
-
-
-// Toggles record to true to begin capturing chunks.
-const onRecordingStart = (
- _event: IpcMainEvent,
- _payload: null
-): void => {
-
- console.log('[IPC]: start-recording');
-
- toggleRecord();
-};
-
-
-// Returns recorded audio to frontend and sets record to false.
-const onRecordingStop = async (
- _event: IpcMainInvokeEvent,
- _payload: null
-): Promise<Error | string> => {
-
- console.log('[IPC]: stop-recording');
-
- return await fetchAudioInput()
-};
-
-
-export default function useAudioListeners(): void {
-
- ipcMain.removeAllListeners("start-recording");
- ipcMain.on("start-recording", onRecordingStart);
-
- ipcMain.removeHandler("stop-recording");
- ipcMain.handle("stop-recording", onRecordingStop);
-
-}
+++ /dev/null
-
-"use strict";
-
-import { initSession, emitNewMessages, sendMessage } from '@/background/session';
-import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron";
-import { initAudioIO } from "@/background/audio";
-import { ClientMessage } from "@/types";
-import { store } from "@/background/store";
-
-
-// Instantiate socket session with crimata-platorm.
-const onSessionInit = (
- _event: IpcMainInvokeEvent,
- cid: string
-): void => {
-
- console.log('[IPC]: init-session');
-
- const token = store.get('key');
- const crimataId = store.get('crimataId');
-
-
- initSession({
- token,
- crimataId
- });
-
- initAudioIO();
-}
-
-
-const onAppMounted = (
- _event: IpcMainInvokeEvent,
- _payload: null
-): void => {
-
- console.log('[IPC]: app-mounted');
-
- emitNewMessages()
-};
-
-
-// Handle messages from window/client.
-const onClientMessage = (
- _event: IpcMainEvent,
- payload: ClientMessage
-): void => {
-
- console.log('[IPC]: client-message');
-
- sendMessage(payload);
-}
-
-
-export default function useSessionListeners(): void {
-
- console.log('[IPC]: Init session listeners.');
-
- // Attach listeners for frontend.
- ipcMain.removeAllListeners("client-message");
- ipcMain.on("client-message", onClientMessage);
-
- // Attack browser window init listener.
- ipcMain.removeAllListeners("app-mounted");
- ipcMain.on("app-mounted", onAppMounted);
-
- ipcMain.removeAllListeners("init-session");
- ipcMain.on("init-session", onSessionInit);
-
-}
+++ /dev/null
-/*
- * Creates a websocket session with Crimata Servers.
- *
- * Connects to Servers and attempts key authentication. Server will respond
- * with key and user profile. We send the profile to the browser. We also
- * resend this information on new broser window. We then serve as a
- * communication interface between the window and the servers. It will
- * automatically try to reconnect on websocket disconnect.
- */
-
-import { backgroundMitt } from "@/modules/emitter";
-import { ipcEmit, loadState } from './helpers';
-import WebSocket from 'ws';
-
-import useWebSockets from "./websockets";
-
-import { play } from "./audio";
-import { renderMessage } from "@/modules/message";
-import { SessionState } from "@/types";
-
-let win = true;
-
-// Info saved to json on quit (key, newMessages).
-let state: SessionState;
-
-let socket: WebSocket | null = null;
-
-
-// Calls appropriate endpoint for a server message.
-const onMessage = (data: string): void => {
- let message = JSON.parse(data);
- if (message === "CLOSE_AUTH_FAIL") {
- ipcEmit("session-auth-fail", null)
- return;
- }
-
- // Standard message.
- if (message.content) {
-
- // Convert to render message
- message = renderMessage(
- message.content.text,
- message.content.audio,
- message.context,
- message.modifier
- );
-
- if (win) {
- console.log("SESS:Emitting standard message.")
- if (message.audio) {
- play(message.audio)
- }
- ipcEmit("render-message", message)
- }
-
- else {
- console.log("SESS:No window: saving message.")
- state.newMessages.push(message);
- }
- }
-
- else {
- if (win) {
- ipcEmit("render-message", message)
- }
- }
-
-};
-
-
-// Websockets module.
-const { createSocket, send } = useWebSockets(onMessage);
-
-
-export const emitNewMessages = (): void => {
- if (state) {
- ipcEmit("update-state", {
- newMessages: state.newMessages,
- });
- }
-
-}
-
-
-export const sendMessage = (payload: Record<string, any>): void => {
- try {
- send(payload)
- } catch(e) {
- console.log("Unable to send message: ", payload);
- }
-
-}
-
-export const endSession = (): void => {
- if (socket) {
- socket.close();
- socket = null;
- }
-}
-
-
-// Call this to initialize session with Crimata servers.
-export const initSession = (authPayload: {
- token: string;
- crimataId: string;
-}): void => {
- console.log("SESS:Creating new session.")
-
- // Load Json or createState.
- state = loadState("session.json");
-
- // Open socket connection.
- if (!socket)
- socket = createSocket(authPayload);
-
- // Keep win up-to-date.
- backgroundMitt.on('window-active', (state: boolean) => {
- win = state;
- });
-
-}
+++ /dev/null
-
-"use strict";
-
-import WebSocket from 'ws';
-
-let socket: WebSocket;
-
-const socketUrl = "ws://127.0.0.1:8760"
-
-// Run every time we want to connect to backend.
-export default function useWebSockets(
- receiveCallback: (s: string) => void,
- openCallback?: () => void
-) {
-
- // Returns bool (sucess or fail).
- const sendMessage = (data: any) => {
- console.log("WS:Sending message: ", data)
-
- if (socket.readyState !== 1) {
- return false
- }
-
- else {
- socket.send(JSON.stringify(data))
- return true
- }
-
- }
-
- const send = async (data: Record<string, any>): Promise<boolean> => (
- new Promise((resolve, reject) => {
- if (socket.readyState !== 1) {
- reject(false);
- }
- socket.send(JSON.stringify(data))
- resolve(true);
-
- }))
-
-
- const onOpen = (_event: WebSocket.OpenEvent) => {
-
- console.log("WS:Connected to WS Server!");
- if (openCallback) openCallback();
-
- }
-
- const onServerMessage = (event: WebSocket.MessageEvent) => {
-
- console.log("WS:Message received: ", event.data);
-
- receiveCallback(event.data.toString())
-
- }
-
- const onClose = (event: WebSocket.CloseEvent) => {
- console.log("WS:Socket closed normally.")
- }
-
- // Reconnect automatically on error.
- const onError = (event: WebSocket.ErrorEvent) => {
- console.log("WS:WebSocket error: ", event.message);
-
- console.log("Attempting reconnect in 1s.")
- setTimeout(createSocket, 1000)
- }
-
- const createSocket = (authPayload: {
- token: string;
- crimataId: string;
- }):WebSocket => {
- socket = new WebSocket(socketUrl)
-
- // Add listeners.
- socket.addEventListener("open", (_event: WebSocket.OpenEvent) => {
- socket.send(JSON.stringify({
- key: authPayload.token,
- crimata_id: authPayload.crimataId
- }));
- })
- socket.addEventListener("message", onServerMessage)
- socket.addEventListener("close", onClose)
- socket.addEventListener("error", onError)
-
- return socket;
-
- }
-
- return {
- createSocket,
- sendMessage,
- send
- }
-
-}
+++ /dev/null
-import anime from "animejs";
-import useMitt from "@/modules/mitt";
-import { useIpc } from '@/modules/ipc';
-import { onMounted, onUnmounted, ref, Ref } from "vue";
-import keyboardNameMap from "../keyBoardMaps/keyboardNameMap";
-import { renderMessage, clientMessage } from '@/modules/message';
-import { postMessage } from "@/ipcRend/session";
-import { invokeStopRecord } from "@/ipcRend/audio";
-
-
-function showRecIcon () {
-
- anime({
- targets: '#recIcon',
- opacity: [0, 0.75],
- scale: [0.0, 1],
- duration: 250,
- easing: 'linear',
- })
-
-}
-
-function hideRecIcon () {
-
- anime({
- targets: '#recIcon',
- opacity: [0.75, 0],
- scale: [1, 0],
- duration: 250,
- easing: 'linear',
- })
-
-}
-
-
-export default function useAudioInputController (typing: Ref) {
-
- // For sending messages.
- const { post, invoke } = useIpc();
- const { emitter } = useMitt();
-
- // Keepp track of when we are recording.
- const recording = ref(false);
-
- //---Callbacks-----------------------------------------------
-
- const onKeyDown = (e: KeyboardEvent) => {
- const cmd = keyboardNameMap[e.keyCode];
- // console.log(cmd)
-
- // Start recording on space bar.
- if (cmd == "SPACE" && !recording.value && !typing.value) {
-
- console.log("INPT:Starting record.")
- post("start-recording", null);
-
- showRecIcon()
- recording.value = true;
-
- }
-
- }
-
- const onKeyUp = async (e: KeyboardEvent) => {
- const cmd = keyboardNameMap[e.keyCode];
-
- // Stop recording on space up.
- if (cmd == "SPACE" && recording.value) {
-
- // Create a message.
- const message = renderMessage(
- "",
- "",
- "",
- "sf"
- )
-
- // Render it immediately.
- emitter.emit("self-message", message);
-
- // Stop recording and get audio from recorder.
- console.log("INPT:Stopping record.")
- try {
- const audio = await invokeStopRecord() as string;
- // Send message to the backend for processing.
- const clientM = clientMessage("", audio, message.uid);
- postMessage(clientM);
- } catch(e) {
- console.log('Failed to fetch audio.')
- } finally {
- hideRecIcon();
- recording.value = false;
- }
-
-
-
- }
-
- }
-
- //-----------------------------------------------------------
-
- onMounted(() => {
- window.addEventListener("keydown", onKeyDown);
- window.addEventListener("keyup", onKeyUp);
- })
-
- onUnmounted(() => {
- window.removeEventListener("keydown", onKeyDown);
- window.removeEventListener("keyup", onKeyUp);
- });
-
-
- return {
- recording
- }
-
-}
+++ /dev/null
-// This has the UnShifted and Shifted characters that each key maps to
-// Ones that are to be ignored for character input are empty.
-const keyboardCharMap = [
- ["", ""], // [0]
- ["", ""], // [1]
- ["", ""], // [2]
- ["", ""], // [3]
- ["", ""], // [4]
- ["", ""], // [5]
- ["", ""], // [6]
- ["", ""], // [7]
- ["", ""], // [8]
- ["", ""], // [9]
- ["", ""], // [10]
- ["", ""], // [11]
- ["", ""], // [12]
- ["\r", "\r"], // [13] - MOST control characters are ignored. This one (Carriage Return, or "Enter") is significant!
- ["", ""], // [14]
- ["", ""], // [15]
- ["", ""], // [16]
- ["", ""], // [17]
- ["", ""], // [18]
- ["", ""], // [19]
- ["", ""], // [20]
- ["", ""], // [21]
- ["", ""], // [22]
- ["", ""], // [23]
- ["", ""], // [24]
- ["", ""], // [25]
- ["", ""], // [26]
- ["", ""], // [27]
- ["", ""], // [28]
- ["", ""], // [29]
- ["", ""], // [30]
- ["", ""], // [31]
- [" ", " "], // [32] // SPACE! Don't "clean it up" and remove the space!
- ["", ""], // [33]
- ["", ""], // [34]
- ["", ""], // [35]
- ["", ""], // [36]
- ["", ""], // [37]
- ["", ""], // [38]
- ["", ""], // [39]
- ["", ""], // [40]
- ["", ""], // [41]
- ["", ""], // [42]
- ["", ""], // [43]
- ["", ""], // [44]
- ["", ""], // [45]
- ["", ""], // [46]
- ["", ""], // [47]
- ["0", ")"], // [48]
- ["1", "!"], // [49]
- ["2", "@"], // [50]
- ["3", "#"], // [51]
- ["4", "$"], // [52]
- ["5", "%"], // [53]
- ["6", "^"], // [54]
- ["7", "&"], // [55]
- ["8", "*"], // [56]
- ["9", "("], // [57]
- ["", ""], // [58]
- [";", ":"], // [59]
- ["<", ""], // [60]
- ["=", ""], // [61]
- [">", ""], // [62]
- ["?", ""], // [63] shifted; else "/"
- ["", ""], // [64]
- ["a", "A"], // [65]
- ["b", "B"], // [66]
- ["c", "C"], // [67]
- ["d", "D"], // [68]
- ["e", "E"], // [69]
- ["f", "F"], // [70]
- ["g", "G"], // [71]
- ["h", "H"], // [72]
- ["i", "I"], // [73]
- ["j", "J"], // [74]
- ["k", "K"], // [75]
- ["l", "L"], // [76]
- ["m", "M"], // [77]
- ["n", "N"], // [78]
- ["o", "O"], // [79]
- ["p", "P"], // [80]
- ["q", "Q"], // [81]
- ["r", "R"], // [82]
- ["s", "S"], // [83]
- ["t", "T"], // [84]
- ["u", "U"], // [85]
- ["v", "V"], // [86]
- ["w", "W"], // [87]
- ["x", "X"], // [88]
- ["y", "Y"], // [89]
- ["z", "Z"], // [90]
- ["", ""], // [91] Windows Key (Windows) or Command Key (Mac)
- ["", ""], // [92]
- ["", ""], // [93]
- ["", ""], // [94]
- ["", ""], // [95]
- // Number Keypad Entries...
- ["0", ""], // [96]
- ["1", ""], // [97]
- ["2", ""], // [98]
- ["3", ""], // [99]
- ["4", ""], // [100]
- ["5", ""], // [101]
- ["6", ""], // [102]
- ["7", ""], // [103]
- ["8", ""], // [104]
- ["9", ""], // [105]
- ["*", ""], // [106]
- ["+", ""], // [107]
- ["", ""], // [108]
- ["-", ""], // [109]
- [".", ""], // [110]
- ["/", ""], // [111]
-
- ["", ""], // [112]
- ["", ""], // [113]
- ["", ""], // [114]
- ["", ""], // [115]
- ["", ""], // [116]
- ["", ""], // [117]
- ["", ""], // [118]
- ["", ""], // [119]
- ["", ""], // [120]
- ["", ""], // [121]
- ["", ""], // [122]
- ["", ""], // [123]
- ["", ""], // [124]
- ["", ""], // [125]
- ["", ""], // [126]
- ["", ""], // [127]
- ["", ""], // [128]
- ["", ""], // [129]
- ["", ""], // [130]
- ["", ""], // [131]
- ["", ""], // [132]
- ["", ""], // [133]
- ["", ""], // [134]
- ["", ""], // [135]
- ["", ""], // [136]
- ["", ""], // [137]
- ["", ""], // [138]
- ["", ""], // [139]
- ["", ""], // [140]
- ["", ""], // [141]
- ["", ""], // [142]
- ["", ""], // [143]
- ["", ""], // [144]
- ["", ""], // [145]
- ["", ""], // [146]
- ["", ""], // [147]
- ["", ""], // [148]
- ["", ""], // [149]
- ["", ""], // [150]
- ["", ""], // [151]
- ["", ""], // [152]
- ["", ""], // [153]
- ["", ""], // [154]
- ["", ""], // [155]
- ["", ""], // [156]
- ["", ""], // [157]
- ["", ""], // [158]
- ["", ""], // [159]
- ["", ""], // [160]
- ["", ""], // [161]
- ["", ""], // [162]
- ["", ""], // [163]
- ["", ""], // [164]
- ["", ""], // [165]
- ["", ""], // [166]
- ["", ""], // [167]
- ["", ""], // [168]
- ["", ""], // [169]
- ["", ""], // [170]
- ["", ""], // [171]
- ["", ""], // [172]
- ["", ""], // [173]
- ["", ""], // [174]
- ["", ""], // [175]
- ["", ""], // [176]
- ["", ""], // [177]
- ["", ""], // [178]
- ["", ""], // [179]
- ["", ""], // [180]
- ["", ""], // [181]
- ["", ""], // [182]
- ["", ""], // [183]
- ["", ""], // [184]
- ["", ""], // [185]
- [";", ":"], // [186]
- ["=", "+"], // [187]
- [",", "<"], // [188]
- ["-", "_"], // [189]
- [".", ">"], // [190]
- ["/", "?"], // [191]
- ["`", "~"], // [192]
- ["", ""], // [193]
- ["", ""], // [194]
- ["", ""], // [195]
- ["", ""], // [196]
- ["", ""], // [197]
- ["", ""], // [198]
- ["", ""], // [199]
- ["", ""], // [200]
- ["", ""], // [201]
- ["", ""], // [202]
- ["", ""], // [203]
- ["", ""], // [204]
- ["", ""], // [205]
- ["", ""], // [206]
- ["", ""], // [207]
- ["", ""], // [208]
- ["", ""], // [209]
- ["", ""], // [210]
- ["", ""], // [211]
- ["", ""], // [212]
- ["", ""], // [213]
- ["", ""], // [214]
- ["", ""], // [215]
- ["", ""], // [216]
- ["", ""], // [217]
- ["", ""], // [218]
- ["[", "{"], // [219]
- ["\\", "|"], // [220]
- ["]", "}"], // [221]
- ["'", '"'], // [222]
- ["", ""], // [223]
- ["", ""], // [224]
- ["", ""], // [225]
- ["", ""], // [226]
- ["", ""], // [227]
- ["", ""], // [228]
- ["", ""], // [229]
- ["", ""], // [230]
- ["", ""], // [231]
- ["", ""], // [232]
- ["", ""], // [233]
- ["", ""], // [234]
- ["", ""], // [235]
- ["", ""], // [236]
- ["", ""], // [237]
- ["", ""], // [238]
- ["", ""], // [239]
- ["", ""], // [240]
- ["", ""], // [241]
- ["", ""], // [242]
- ["", ""], // [243]
- ["", ""], // [244]
- ["", ""], // [245]
- ["", ""], // [246]
- ["", ""], // [247]
- ["", ""], // [248]
- ["", ""], // [249]
- ["", ""], // [250]
- ["", ""], // [251]
- ["", ""], // [252]
- ["", ""], // [253]
- ["", ""], // [254]
- ["", ""] // [255]
-];
-export default keyboardCharMap;
+++ /dev/null
-// names of known key codes (0-255)
-const keyboardNameMap = [
- "", // [0]
- "", // [1]
- "", // [2]
- "CANCEL", // [3]
- "", // [4]
- "", // [5]
- "HELP", // [6]
- "", // [7]
- "BACK_SPACE", // [8]
- "TAB", // [9]
- "", // [10]
- "", // [11]
- "CLEAR", // [12]
- "ENTER", // [13]
- "ENTER_SPECIAL", // [14]
- "", // [15]
- "SHIFT", // [16]
- "CONTROL", // [17]
- "ALT", // [18]
- "PAUSE", // [19]
- "CAPS_LOCK", // [20]
- "KANA", // [21]
- "EISU", // [22]
- "JUNJA", // [23]
- "FINAL", // [24]
- "HANJA", // [25]
- "", // [26]
- "ESCAPE", // [27]
- "CONVERT", // [28]
- "NONCONVERT", // [29]
- "ACCEPT", // [30]
- "MODECHANGE", // [31]
- "SPACE", // [32]
- "PAGE_UP", // [33]
- "PAGE_DOWN", // [34]
- "END", // [35]
- "HOME", // [36]
- "LEFT", // [37]
- "UP", // [38]
- "RIGHT", // [39]
- "DOWN", // [40]
- "SELECT", // [41]
- "PRINT", // [42]
- "EXECUTE", // [43]
- "PRINTSCREEN", // [44]
- "INSERT", // [45]
- "DELETE", // [46]
- "", // [47]
- "0", // [48]
- "1", // [49]
- "2", // [50]
- "3", // [51]
- "4", // [52]
- "5", // [53]
- "6", // [54]
- "7", // [55]
- "8", // [56]
- "9", // [57]
- "COLON", // [58]
- "SEMICOLON", // [59]
- "LESS_THAN", // [60]
- "EQUALS", // [61]
- "GREATER_THAN", // [62]
- "QUESTION_MARK", // [63]
- "AT", // [64]
- "A", // [65]
- "B", // [66]
- "C", // [67]
- "D", // [68]
- "E", // [69]
- "F", // [70]
- "G", // [71]
- "H", // [72]
- "I", // [73]
- "J", // [74]
- "K", // [75]
- "L", // [76]
- "M", // [77]
- "N", // [78]
- "O", // [79]
- "P", // [80]
- "Q", // [81]
- "R", // [82]
- "S", // [83]
- "T", // [84]
- "U", // [85]
- "V", // [86]
- "W", // [87]
- "X", // [88]
- "Y", // [89]
- "Z", // [90]
- "OS_KEY", // [91] Windows Key (Windows) or Command Key (Mac)
- "", // [92]
- "CONTEXT_MENU", // [93]
- "", // [94]
- "SLEEP", // [95]
- "NUMPAD0", // [96]
- "NUMPAD1", // [97]
- "NUMPAD2", // [98]
- "NUMPAD3", // [99]
- "NUMPAD4", // [100]
- "NUMPAD5", // [101]
- "NUMPAD6", // [102]
- "NUMPAD7", // [103]
- "NUMPAD8", // [104]
- "NUMPAD9", // [105]
- "MULTIPLY", // [106]
- "ADD", // [107]
- "SEPARATOR", // [108]
- "SUBTRACT", // [109]
- "DECIMAL", // [110]
- "DIVIDE", // [111]
- "F1", // [112]
- "F2", // [113]
- "F3", // [114]
- "F4", // [115]
- "F5", // [116]
- "F6", // [117]
- "F7", // [118]
- "F8", // [119]
- "F9", // [120]
- "F10", // [121]
- "F11", // [122]
- "F12", // [123]
- "F13", // [124]
- "F14", // [125]
- "F15", // [126]
- "F16", // [127]
- "F17", // [128]
- "F18", // [129]
- "F19", // [130]
- "F20", // [131]
- "F21", // [132]
- "F22", // [133]
- "F23", // [134]
- "F24", // [135]
- "", // [136]
- "", // [137]
- "", // [138]
- "", // [139]
- "", // [140]
- "", // [141]
- "", // [142]
- "", // [143]
- "NUM_LOCK", // [144]
- "SCROLL_LOCK", // [145]
- "WIN_OEM_FJ_JISHO", // [146]
- "WIN_OEM_FJ_MASSHOU", // [147]
- "WIN_OEM_FJ_TOUROKU", // [148]
- "WIN_OEM_FJ_LOYA", // [149]
- "WIN_OEM_FJ_ROYA", // [150]
- "", // [151]
- "", // [152]
- "", // [153]
- "", // [154]
- "", // [155]
- "", // [156]
- "", // [157]
- "", // [158]
- "", // [159]
- "CIRCUMFLEX", // [160]
- "EXCLAMATION", // [161]
- "DOUBLE_QUOTE", // [162]
- "HASH", // [163]
- "DOLLAR", // [164]
- "PERCENT", // [165]
- "AMPERSAND", // [166]
- "UNDERSCORE", // [167]
- "OPEN_PAREN", // [168]
- "CLOSE_PAREN", // [169]
- "ASTERISK", // [170]
- "PLUS", // [171]
- "PIPE", // [172]
- "HYPHEN_MINUS", // [173]
- "OPEN_CURLY_BRACKET", // [174]
- "CLOSE_CURLY_BRACKET", // [175]
- "TILDE", // [176]
- "", // [177]
- "", // [178]
- "", // [179]
- "", // [180]
- "VOLUME_MUTE", // [181]
- "VOLUME_DOWN", // [182]
- "VOLUME_UP", // [183]
- "", // [184]
- "", // [185]
- "SEMICOLON", // [186]
- "EQUALS", // [187]
- "COMMA", // [188]
- "MINUS", // [189]
- "PERIOD", // [190]
- "SLASH", // [191]
- "BACK_QUOTE", // [192]
- "", // [193]
- "", // [194]
- "", // [195]
- "", // [196]
- "", // [197]
- "", // [198]
- "", // [199]
- "", // [200]
- "", // [201]
- "", // [202]
- "", // [203]
- "", // [204]
- "", // [205]
- "", // [206]
- "", // [207]
- "", // [208]
- "", // [209]
- "", // [210]
- "", // [211]
- "", // [212]
- "", // [213]
- "", // [214]
- "", // [215]
- "", // [216]
- "", // [217]
- "", // [218]
- "OPEN_BRACKET", // [219]
- "BACK_SLASH", // [220]
- "CLOSE_BRACKET", // [221]
- "QUOTE", // [222]
- "", // [223]
- "META", // [224]
- "ALTGR", // [225]
- "", // [226]
- "WIN_ICO_HELP", // [227]
- "WIN_ICO_00", // [228]
- "", // [229]
- "WIN_ICO_CLEAR", // [230]
- "", // [231]
- "", // [232]
- "WIN_OEM_RESET", // [233]
- "WIN_OEM_JUMP", // [234]
- "WIN_OEM_PA1", // [235]
- "WIN_OEM_PA2", // [236]
- "WIN_OEM_PA3", // [237]
- "WIN_OEM_WSCTRL", // [238]
- "WIN_OEM_CUSEL", // [239]
- "WIN_OEM_ATTN", // [240]
- "WIN_OEM_FINISH", // [241]
- "WIN_OEM_COPY", // [242]
- "WIN_OEM_AUTO", // [243]
- "WIN_OEM_ENLW", // [244]
- "WIN_OEM_BACKTAB", // [245]
- "ATTN", // [246]
- "CRSEL", // [247]
- "EXSEL", // [248]
- "EREOF", // [249]
- "PLAY", // [250]
- "ZOOM", // [251]
- "", // [252]
- "PA1", // [253]
- "WIN_OEM_CLEAR", // [254]
- "" // [255]
-];
-
-export default keyboardNameMap;
+++ /dev/null
-<template>
- <div
- class="message"
- :class="`${message.isChild}ChildMessage`"
- :id="`${message.modifier}Message`"
- :key="message.id"
- >
-
- <!-- Three types of messages: self (sf), friend (fr), and crimata (ai) -->
- <!-- !denoted by the modifier attribute -->
-
- <!-- Shows when a new session is created with Crimata. -->
- <div
- v-if="message.context === 'Crimata'"
- class="divider"
- >
- <div>new session</div>
- </div>
-
- <!-- Includes bubble and context. -->
- <div
- :id="`${message.modifier}MessageBox`"
- class="messageBox"
- >
-
- <!-- message bubble -->
- <div
- class="bubble"
- :class="`${message.modifier}-${message.isChild}ChildBubble`"
- :id="`${message.modifier}Bubble`"
- >
-
- <!-- Notification dot for new messages. -->
- <div v-if="notify === true"
- class="notify"
- />
-
- <!-- Show loader when audio is being transcribed. -->
- <div
- v-if="message.content.text === ''"
- class="contentLoader"
- >
- <div class="contentLoaderDot"></div>
- <div class="contentLoaderDot"></div>
- <div class="contentLoaderDot"></div>
- </div>
-
- <!-- Show question mark if transcription comes back as unknown. -->
- <div
- v-else-if="message.content.text === false"
- class="questionMark"
- >
- ?
- </div>
-
- <!-- Else, show text. -->
- <div v-else>
- {{ message.content.text }}
- </div>
-
- </div>
-
- <!-- message context -->
- <span
- v-if="message.isChild === 'last' || message.isChild === 'none'"
- class="context"
- >
-
- <!-- Render Crimata icon or friend's initials. -->
- <div v-if="message.modifier == 'ai'" class="photo">
- <img src="@/assets/crimata.svg"/>
- </div>
-
- <!-- Render Crimata icon or friend's initials. -->
- <div v-if="message.modifier == 'fr'" class="photo"
- :class="{ playing: isPlaying }"
- >
- {{ message.context[0] }}
- </div>
-
- {{ message.context }}
-
- </span>
-
- </div>
-
- </div>
-</template>
-
-<script lang='ts'>
-
- import { defineComponent, ref, onMounted } from 'vue';
-
- export default defineComponent({
- name: "MessageItem",
-
- props: ["message"],
-
- setup(props) {
-
- let addListener = false;
-
- const notify = ref(false)
- const isPlaying = ref(false);
-
- if (!props.message.seen) {
- console.log("MSG:Checking...")
-
- // newMessages always render with blue dot.
- if (props.message.newMessage) {
- console.log("MSG:New message, notifying.")
- props.message.seen = true;
- notify.value = true;
- }
-
- else {
-
- // Do nothing if window is visible on setup.
- if (document.visibilityState === "visible") {
- props.message.seen = true;
- }
-
- // We must listen for window visible event.
- else {
- console.log("MSG:Unseen, adding listener.")
- addListener = true;
- }
-
- }
-
- }
-
- // Callback for window visibilityState.
- const onVisibilityChange = (_event: any) => {
- if (document.visibilityState === "visible") {
- console.log("MSG:Window visible, notifying.")
-
- props.message.seen = true;
- notify.value = true; // triggers blue dot anim.
-
- // Once complete we can remove.
- document.removeEventListener("visibilitychange", onVisibilityChange)
- }
- }
-
- const onStopPlayback = (_event: any) => {
- window.ipcRenderer.removeAllListeners("stop-playback-anim");
- isPlaying.value = false
- }
-
- onMounted(() => {
-
- // Listen for window focus event.
- if (addListener) {
- document.addEventListener("visibilitychange", onVisibilityChange)
- }
-
- // Turn on audio playback animation if audio.
- if (props.message.content.audio === true) {
- window.ipcRenderer.on("stop-playback-anim", onStopPlayback);
- isPlaying.value = true
- }
-
- })
-
- return {
- notify,
- isPlaying
- }
- }
-
- })
-
-</script>
-
-<style lang="scss" scoped>
-
- .message {
- width: 100vw;
-
- display: flex;
- flex-direction: column;
-
- padding-top: 9px;
- padding-bottom: 9px;
-
- }
-
- .message:first-child {
- margin-top: 55px;
- }
-
- .message:last-child {
- margin-bottom: 18px;
- }
-
- .divider {
- width: 100vw;
- display: flex;
- justify-content: center;
- align-items: center;
-
- font-family: "SF Compact Display";
- font-size: 12px;
- font-weight: bold;
- color: #9B9B9B;
-
- margin-bottom: 18px;
- }
-
- .firstChildMessage {
- padding-bottom: 2px;
- }
-
- .middleChildMessage {
- padding-top: 2px;
- padding-bottom: 2px;
- }
-
- .lastChildMessage {
- padding-top: 2px;
- }
-
- #aiMessage {
- align-items: flex-start;
- }
-
- #sfMessage {
- align-items: flex-end;
- }
-
- #frMessage {
- align-items: flex-start;
- }
-
- .messageBox {
- position: relative;
-
- display: flex;
- flex-direction: column;
-
- // Animate on render.
- animation-name: appear;
- animation-duration: 0.25s;
- }
-
- #aiMessageBox {
- margin-left: 20px;
- align-items: flex-start;
- }
-
- #sfMessageBox {
- margin-right: 20px;
- align-items: flex-end;
- }
-
- #frMessageBox {
- margin-left: 20px;
- align-items: flex-start;
- }
-
- .bubble {
- position: relative;
-
- max-width: 66vw;
-
- display: flex;
- flex-direction: column;
-
- font-family: "SF Pro Text";
- font-size: 14px;
-
- padding: 10px;
-
- border-radius: 18px;
- }
-
- @keyframes appear {
- 10% {
- transform: scale(0.3);
- }
- 100% {
- transform: scale(1);
- }
- }
-
- #aiBubble {
- background-color: white;
- }
-
- #sfBubble {
- background-color: #58c4fd;
- color: white;
- }
-
- #frBubble {
- background-color: white;
- }
-
- .sf-firstChildBubble {
- border-bottom-right-radius: 9px;
- }
-
- .sf-middleChildBubble {
- border-top-right-radius: 9px;
- border-bottom-right-radius: 9px;
- }
-
- .sf-lastChildBubble {
- border-top-right-radius: 9px;
- }
-
- .ai-firstChildBubble, .fr-firstChildBubble {
- border-bottom-left-radius: 9px;
- }
-
- .ai-middleChildBubble, .fr-middleChildBubble {
- border-top-left-radius: 9px;
- border-bottom-left-radius: 9px;
- }
-
- .ai-lastChildBubble, .fr-lastChildBubble {
- border-top-left-radius: 9px;
- }
-
- .notify {
- position: absolute;
- width: 12px;
- height: 12px;
- border-radius: 50%;
- background-color: #58D9FF;
- top: -5px;
- left: -5px;
- border: 2px solid #EBEBEB;
- transform: scale(0);
-
- animation-name: notify-anim;
- animation-duration: 5s;
- }
-
- @keyframes notify-anim {
- 0%, 90% {
- transform: scale(1);
- }
- 100% {
- transform: scale(0);
- }
- }
-
- .context {
- position: relative;
-
- display: flex;
- align-items: center;
-
- font-family: "SF Compact Display";
- font-size: 12px;
- font-weight: bold;
- margin-top: 5px;
- }
-
- .photo {
- display: flex;
- justify-content: center;
- align-items: center;
-
- margin-right: 5px;
-
- width: 30px;
- height: 30px;
-
- font-size: 14px;
-
- background-color: white;
- border-radius: 15px;
- }
-
- // Apply for audio message playback.
- .playing {
- animation-name: circle1;
- animation-duration: 2s;
- animation-iteration-count: infinite;
- }
-
- @keyframes circle1 {
- 0%,
- 100% {
- box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.4), 0 0 0 0.25em rgba(195, 195, 195, 0.15);
- }
- 25% {
- box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.15), 0 0 0 0.4em rgba(195, 195, 195, 0.3);
- }
- 50% {
- box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.55), 0 0 0 0.15em rgba(195, 195, 195, 0.05);
- }
- 75% {
- box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.25), 0 0 0 0.55em rgba(195, 195, 195, 0.45);
- }
- }
-
- .contentLoader {
- display: flex;
- }
-
- .contentLoaderDot {
- width: 5px;
- height: 5px;
-
- margin: 2px;
- border-radius: 2.5px;
- background-color: #357CA2;
- }
-
- .questionMark {
- font-weight: 900;
- color: #357CA2;
- }
-
-</style>
+++ /dev/null
-<template>
- <InputItem :initials="profile.initials"/>
- <Settings />
-
- <div id="recIcon" />
-
- <!-- List of message bubbles. -->
- <div id="messenger">
- <Message
- v-for="message in messages"
- :message="message[1]"
- :key="message[0]"
- />
- </div>
-
-</template>
-
-<script lang="ts">
-import { defineComponent, onMounted, onUnmounted } from "vue";
-import Message from "@/components/message.vue";
-import InputItem from "@/components/inputItem.vue";
-import Settings from "@/components/settings.vue";
-import useMitt from "@/modules/mitt";
-import useMessages from "@/modules/messages";
-
-export default defineComponent({
- name: "Messenger",
-
- props: ["profile", "newMessages"],
-
- components: {
- Message,
- InputItem,
- Settings
- },
-
- setup(props) {
-
- // Listen for self-messages from inputItem.
- const { emitter } = useMitt();
-
- // Handle messages in view.
- const { messages, prepMessageView, updateMessageView } = useMessages();
-
- onMounted(() => {
- const crimataId = props.profile.crimataId;
- console.log(`MSGR:Initializing messenger for ${crimataId}.`)
-
- // Load the message history.
- if (crimataId === window.localStorage.getItem("last_usr")) {
- prepMessageView(props.newMessages);
- } else {
- messages.value.clear();
- }
-
- // New content listeners.
- emitter.on('self-message', (message) => updateMessageView(message));
- window.ipcRenderer.on("render-message", (_e: any, payload: any) => {
- updateMessageView(payload.message);
- });
-
- // Save the usr for next time.
- window.localStorage.setItem("last_usr", crimataId);
-
- });
-
- onUnmounted(() => {
- window.ipcRenderer.removeAllListeners("render-message");
- emitter.all.clear();
- });
-
- return {
- messages
- };
-
- },
-
-});
-</script>
-
-<style lang="scss" scoped>
-
-#messenger {
- width: 100vw;
- height: 100vh;
- overflow: auto;
-}
-
-#messenger::-webkit-scrollbar {
- display: none;
-}
-
-#recIcon {
- position: fixed;
- right: 0;
-
- opacity: 0;
- transform: scale(0.0);
-
- margin-top: 24px;
- margin-right: 66px;
-
-
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background-color: #FF3B3B;
-
- z-index: 2;
-
-}
-
-
-</style>
+++ /dev/null
-<template>
-
- <input
- id="textInput"
- type="text"
- />
-
-</template>
-
-<script lang="ts">
-import { defineComponent } from "vue";
-
-export default defineComponent({
- name: "TextInput",
-
-})
-</script>
-
-<style lang="scss" scoped>
-
-#textInput {
- position: absolute;
-
- opacity: 0;
-
- min-width: 150px;
- height: 16px;
-
- border-radius: 18px;
-
- padding: 10px;
- margin-right: 10px;
- margin-left: 10px;
-
- outline: none;
- border: none;
- pointer-events: none;
-
- background-color: white;
-
- z-index: -1;
-
- box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
-
- transform: translateX(50px) scale(0.3);
-
-}
-
-</style>
\ No newline at end of file
--- /dev/null
+/* eslint @typescript-eslint/no-var-requires: "off" */
+
+"use strict";
+
+// where the audio goes
+let buffer: ArrayBuffer[] = [];
+
+// place audio data in buffer
+export function collect (chunk: ArrayBuffer) {
+ buffer.push(chunk);
+}
+
+// return audio and clear buffer
+export function flush () {
+ 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;
+// }
+// }
+// }
+
+
--- /dev/null
+const { autoUpdater } = require('electron-updater');
+
+let win: boolean;
+
+// Listen for window creation.
+backgroundMitt.on('window-active', (state: boolean) => {
+ win = state;
+});
+
+// Auto updating.
+autoUpdater.requestHeaders = { 'PRIVATE-TOKEN': 'mvvgWYwWnot4bisiQMh_' }
+
+autoUpdater.on('update-available', (info: any) => {
+ console.log(`Update available: ${info.version}`)
+})
+
+autoUpdater.on('update-downloaded', (info: any) => {
+
+ const updateDialog = {
+ type: 'info',
+ buttons: ['Restart', 'Later'],
+ title: 'Application Update',
+ message: info.version,
+ detail: 'A new version has been downloaded. Restart the application to apply the updates.'
+ }
+
+ dialog.showMessageBox(updateDialog).then((returnValue) => {
+ if (returnValue.response === 0) autoUpdater.quitAndInstall()
+ })
+
+})
\ No newline at end of file
/* eslint-disable */
-import { Emitter } from "mitt";
-
// Backend emitter
-type Mitt = Emitter;
-
const EventEmitter = require('events');
-
class BackgroundMitt extends EventEmitter { }
export const backgroundMitt = new BackgroundMitt();
+
+export default function ipcEmit (channel: string, payload: any) {
+ backgroundMitt.emit('ipc-renderer', {
+ endpoint: channel,
+ message: payload
+ });
+}
--- /dev/null
+export const saveToJson = (fileName: string, data: any) => {
+
+ fs.writeFile(configPath + fileName, JSON.stringify(data), (err) => {
+ if (err) {
+ console.log("Error when saving to json.")
+ }
+ })
+
+}
\ No newline at end of file
--- /dev/null
+
+"use strict";
+
+import WebSocket from 'ws';
+
+export default function useWebSockets(receiveCallback: (s: string) => void, openCallback?: () => void) {
+
+ let socket: WebSocket | null = null;
+
+ const send = async (data: Record<string, any>): Promise<boolean> => {
+ return new Promise((resolve, reject) => {
+ if (socket) {
+ if (socket.readyState === WebSocket.OPEN) {
+ socket.send(JSON.stringify(data));
+ resolve(true);
+ }
+ }
+ reject(false);
+ });
+ }
+
+ const onOpen = (_event: WebSocket.OpenEvent) => {
+ console.log("WS:Connected to WS Server!");
+ if (openCallback) openCallback();
+ }
+
+ const onServerMessage = (event: WebSocket.MessageEvent) => {
+ console.log("WS:Message received: ", event.data);
+ receiveCallback(event.data.toString())
+ }
+
+ const onClose = (event: WebSocket.CloseEvent) => {
+ console.log("WS:Socket closed normally.")
+ }
+
+ // Reconnect automatically on error.
+ const onError = (event: WebSocket.ErrorEvent) => {
+ console.log("WS:WebSocket error: ", event.message);
+ console.log("Attempting reconnect in 1s.")
+ setTimeout(createSocket, 1000);
+ }
+
+ const createSocket = (socketUrl: string) => {
+
+ socket = new WebSocket(socketUrl)
+
+ // Add listeners.
+ socket.addEventListener("open", onOpen);
+ socket.addEventListener("message", onServerMessage);
+ socket.addEventListener("close", onClose);
+ socket.addEventListener("error", onError);
+
+ }
+
+ const close = () => {
+ if (socket) {
+ socket.close();
+ socket = null;
+ }
+ }
+
+ const checkConnection = () => {
+ return true;
+ }
+
+ return {
+ createSocket,
+ send,
+ close,
+ checkConnection
+ };
+
+}
--- /dev/null
+/**
+ * Entry point for Crimata electron app.
+ * "Look on my Works, ye Mighty, and despair!"
+ */
+
+"use strict";
+
+import { app, protocol } from "electron";
+import createWindow from "./window";
+import main from "./main";
+
+require('dotenv').config();
+
+console.log('Starting Crimata electron app.');
+
+// Scheme must be registered before the app is ready
+protocol.registerSchemesAsPrivileged([
+ { scheme: "app", privileges: { secure: true, standard: true } }
+]);
+
+const isDev = require('electron-is-dev');
+
+/* Start main process on ready */
+app.on("ready", async () => {
+ await main();
+});
+
+// Must keep to ensure app doesn't quit on close.
+app.on("before-quit", async () => {
+});
+
+// Must keep to ensure app doesn't quit on close.
+app.on("window-all-closed", () => {
+});
+
+// When user clicks app icon (re-open)
+app.on("activate", () => {
+ if (!win) createWindow();
+});
+
+// Exit cleanly on request from parent process in development mode.
+if (isDev) {
+ process.on("SIGTERM", () => {
+ app.quit();
+ });
+}
\ No newline at end of file
"use strict";
-import { Profile } from "@/types";
-import { submit, fetchProfile, logout } from "@/api/account";
+import { submit, fetchProfile, logout } from "../api/account";
import { ipcMain, IpcMainInvokeEvent } from "electron";
-import { store } from "@/background/store";
-import { endSession } from "@/background/session";
+import { store } from "@/composables/store";
const parseAuthRes = (authRes: any) => {
};
-const onProfile = async (
+
+
+
+/**
+ * Get user profile from store and try to login with it.
+ */
+const onTokenLogin = async (
_event: IpcMainInvokeEvent,
_payload: null
): Promise<Profile | Error> => (
// authenticate and fetch profile
try {
- const res = await fetchProfile(
- crimataId,
- token
- );
-
+ // attempt login with email token
+ const res = await fetchProfile(crimataId, token);
const parsed = parseAuthRes(res);
+
+ // return profile to renderer
resolve(parsed.profile);
} catch(e) {
if ( account.password && account.email ) {
try {
+
+ // attempt login with email password
const res = await submit(account.email, account.password);
const parsed = parseAuthRes(res);
store.set('key', parsed.token);
store.set('crimataId', parsed.profile.crimataId);
+ // init session
+
// return profile to renderer
resolve(parsed.profile);
store.delete('crimataId');
// TODO: kill crimata platform session
- endSession();
+ // endSession();
resolve();
} catch(e) {
--- /dev/null
+
+"use strict";
+
+import { ipcMain, IpcMainInvokeEvent, IpcMainEvent } from "electron";
+import { collect, flush } from "@/composbales/audio";
+
+// handle the new audio data
+const onAudioChunk = (
+ _e: IpcMainEvent,
+ payload: ArrayBuffer
+) => {
+ console.log('[IPC]: audio-buffer');
+ collect(payload);
+}
+
+// Returns recorded audio to frontend and sets record to false.
+const onGetAudio = async (
+ _event: IpcMainInvokeEvent,
+ _payload: null
+): Promise<Error | ArrayBuffer[]> => {
+ console.log('[IPC]: stop-recording');
+ return await flush()
+};
+
+
+export default function useAudioListeners(): void {
+
+ ipcMain.removeAllListeners("audio-chunk");
+ ipcMain.on("audio-chunk", onAudioChunk);
+
+ ipcMain.removeHandler("get-audio");
+ ipcMain.handle("get-audio", onGetAudio);
+
+}
import useAccountListeners from "./account";
import useSessionListeners from "./session";
-import useAudioListeners from "./audio";
+// import useAudioListeners from "./audio";
export default function useIpc(): void {
useSessionListeners();
- useAudioListeners();
+ // useAudioListeners();
}
--- /dev/null
+
+"use strict";
+
+import { ipcMain, IpcMainEvent, IpcMainInvokeEvent } from "electron";
+import { sendMessage } from '@/session';
+
+// Handle messages from window/client.
+function onSendMessage(_event: IpcMainEvent, payload: Message): void {
+ sendMessage(payload);
+}
+
+// Login attempt, returns success or not.
+function onLogin(_event: IpcMainEvent, payload: LoginPayload) => {
+ authenticate(payload.email, payload.password);
+}
+
+export default function useSessionListeners(): void {
+ ipcMain.removeAllListeners("client-message");
+ ipcMain.on("client-message", onSendMessage);
+}
\ No newline at end of file
+++ /dev/null
-
-import { useIpc } from "@/modules/ipc";
-import { Profile } from "@/types";
-
-const { invoke } = useIpc();
-
-interface LoginPayload {
- email: string;
- password: string;
-}
-
-
-export const invokeProfile = async (): Promise<Profile | Error> => (
- await invoke('user-profile', null)
-);
-
-
-export const invokeLogin = async (
- payload: LoginPayload
-): Promise<Profile | Error> => (
- await invoke('user-login', JSON.stringify(payload))
-);
-
-
-export const invokeLogout = async (): Promise<void> => (
- await invoke("user-logout", null)
-);
+++ /dev/null
-
-import { useIpc } from "@/modules/ipc";
-
-
-const { post, invoke } = useIpc();
-
-
-export const postStartRecord = (): void => (
- post("start-recording", null)
-);
-
-
-export const invokeStopRecord = async (): Promise<string | Error> => (
- await invoke("stop-recording", null)
-);
+++ /dev/null
-
-import { useIpc } from "@/modules/ipc";
-
-import { ClientMessage } from "@/types";
-
-const { post } = useIpc();
-
-
-export const postMount = (): void => (
- post("app-mounted", null)
-);
-
-
-export const postInitSession = (cid: string): void => (
- post("init-session", cid)
-);
-
-
-export const postMessage = (payload: ClientMessage): void => (
- post('client-message', payload)
-);
-
-
-// src/main.ts
+/**
+ * Where the background logic really begins, gets called by app.onReady().
+ *
+ * Handles authentication. If profile is set, we launch a session, which consis
+ * of opening a connection with the platform, initializing the audio streams.
+ *
+ * The session is primarily an interface between the frontend and the platform,
+ * relaying messages from one to the other.
+ *
+ */
-import App from "./App.vue";
+import { fetchAccount, submit } from "@/api/account";
+import { launchSession, endSession } from "@/session";
+import useIpc from "@/ipc/index";
+import store from "@/composables/store";
-import mitt from "mitt";
-import { createApp } from "vue";
-require('dotenv').config()
+/* user profile, signals whether user is logged in */
+let auth: Profile | null = null;
+/* authenticate the user */
+export async function authenticate(email: string, password: string) {
-// Handle events.
-const emitter = mitt();
+ /* attempt normal login */
+ try {
+ auth = await submit(email, password);
+ } catch (e) {
+ console.log(e);
+ }
-const app = createApp(App)
+ /* launch if profile */
+ if (auth) {
+ launchSession(auth);
+ }
-app.provide("mitt", emitter)
-app.mount("#app");
+}
+
+/* logout the user, end the session */
+export function deauthenticate() {
+
+ /* set profile back to null */
+ auth = null;
+
+ /* terminate the session */
+ endSession();
+
+}
+
+export default async function main() {
+
+ /* launch browser window */
+ // await createWindow();
+
+ /* attempt key-based authentication with business api */
+ const token = store.get('key', null);
+ const crimataId = store.get('crimataId', null);
+
+ try {
+ const res = await fetchAccount(crimataId, token);
+ auth = parseAuthRes(res);
+ } catch (e) {
+ console.log('[MAIN]', e);
+ }
+
+ /* connect to Crimata, or listen for manual login req */
+ if (auth) {
+ launchSession(auth);
+ }
+
+ /* initiate controls for frontend to use when needed */
+ useIpc();
+
+}
\ No newline at end of file
+++ /dev/null
-import {
- RenderMessage,
- ClientMessage,
- ClientRequest,
- AuthRequest,
- LogoutRequest
-} from "@/types";
-
-import { v4 as uuidv4 } from 'uuid';
-
-function getTimeStamp(): number {
- const currentdate = new Date();
- return currentdate.getTime();
-}
-
-// Create a RenderMessage object.
-export const renderMessage = (text: boolean | string, audio: boolean | string, context: string, modifier: string): RenderMessage => (
- {
- content: {
- text: text,
- audio: audio
- },
- context: context,
- modifier: modifier,
- time: getTimeStamp(),
- uid: uuidv4(),
- isChild: "none",
- seen: false,
- newMessage: false
- }
-)
-
-export const clientMessage = (text: string, audio: string | boolean, uid: string): ClientMessage => (
- {
- audio,
- text,
- uid
- }
-)
-
-export const clientRequest = (intent: string, params: object, epic: string | boolean): ClientRequest => (
- {
- intent: intent,
- params: params,
- epic: epic,
- confidence: 1.0
- }
-)
-
-export const authRequest = (key: boolean | string, usr: boolean | string, pwd: boolean | string): AuthRequest => (
- {
- key,
- usr,
- pwd
- }
-)
-
-export const logoutRequest = (): LogoutRequest => (
- {
- logout: true
- }
-)
\ No newline at end of file
+++ /dev/null
-import { ref } from 'vue';
-import useScroll from "@/modules/scroll";
-import { RenderMessage, Annotation } from "@/types";
-
-const messages = ref(new Map());
-
-const addMessage = (message: RenderMessage) => {
- messages.value.set(message.uid, message)
-}
-
-const updateMessage = (annotation: Annotation) => {
- const message = messages.value.get(annotation.uid)
- message.context = annotation.context
- message.content.text = annotation.text
-}
-
-const loadSavedMessages = () => {
- const rawData = window.localStorage.getItem("crimata_messages");
- if (rawData) {
- const messageData = JSON.parse(rawData)
- messages.value = new Map(Object.entries(messageData));
- }
-}
-
-const saveMessages = () => {
- const messageData = Object.fromEntries(messages.value);
- window.localStorage.setItem("crimata_messages", JSON.stringify(messageData));
-}
-
-const isSimmilar = (messageA: RenderMessage, messageB: RenderMessage) => {
- if ((Math.abs(messageA.time - messageB.time) < 20000) && (messageA.modifier == messageB.modifier) && (messageA.context == messageB.context)) {
- return true
- }
- return false
-}
-
-const updateGrouping = () => {
- console.log("updating grouping")
- const refs = Array.from(messages.value.keys())
-
- // Get the last three messages.
- const first = messages.value.get(refs[refs.length - 1])
- const second = messages.value.get(refs[refs.length - 2])
- const third = messages.value.get(refs[refs.length - 3])
-
- // If messages are simmilar, update the classes.
- if ((first) && (second)) {
- if (isSimmilar(first, second)) {
- first.isChild = "last" // i.e. last in group.
- second.isChild = "first"
-
- if (third) {
- if ((third.isChild == "first") || (third.isChild == "middle")) {
- second.isChild = "middle"
- }
- }
- }
- }
-}
-
-
-export default function useMessages() {
-
- // Scroll controller.
- const { setScroll, updateScrollRef, adjustScroll } = useScroll("messenger");
-
- // Main function for updating the message view.
- const updateMessageView = (message: RenderMessage | Annotation) => {
-
- // Step 1: See if user is scrolled down.
- updateScrollRef()
-
- // Step 2: Add the new content to the view.
- if ("content" in message) {
- addMessage(message)
- } else {
- updateMessage(message)
- }
-
- // Step 3: Pop off oldest message (if > 200).
- if (messages.value.size >= 200) {
- const oldest = Array.from(messages.value.keys()).shift();
- messages.value.delete(oldest);
- }
-
- // Step 4: Update grouping.
- updateGrouping()
-
- // Setp 5: Scroll the view (if scrolled down).
- setTimeout(adjustScroll, 20);
-
- // Step 6: Save the view data.
- saveMessages()
-
- }
-
- // Seed message view with message history.
- const prepMessageView = (newMessages: RenderMessage[]) => {
- console.log("MSGR:Prepping messenger view.")
-
- // Load and render saved messages and immediately scroll to bottom.
- loadSavedMessages()
- setTimeout(setScroll.bind(false), 10);
-
- // Render new messages, then wait 1s to scroll.
- if (newMessages.length) {
-
- console.log("MSGR:Adding new messages")
-
- newMessages.forEach(message => {
- message.newMessage = true;
- addMessage(message)
- })
-
- setTimeout(setScroll.bind(true), 1000);
-
- }
- }
-
- return {
- messages,
- prepMessageView,
- updateMessageView
- }
-}
\ No newline at end of file
+++ /dev/null
-import { inject } from "vue";
-import { Emitter } from "mitt";
-
-// Frontend emitter
-
-type Mitt = Emitter;
-
-let emitter: Mitt;
-
-export default function useMitt() {
-
- const emitterInject: Mitt | undefined = inject("mitt");
- if (emitterInject) {
- emitter = emitterInject;
- }
- return {
- emitter
- }
-}
\ No newline at end of file
+++ /dev/null
-
-
-export default function useScroll(element: string) {
-
- let isScrolledToBottom: boolean;
-
- // Set the initial scroll position.
- const setScroll = (smooth: boolean) => {
- const view = document.getElementById(element)
-
- if (view) {
- view.scrollTo({
- top: view.scrollHeight - view.clientHeight,
- behavior: (smooth) ? 'smooth' : 'auto'
- });
- }
- }
-
- // Update isScrolledToBottom
- const updateScrollRef = () => {
- const view = document.getElementById(element)
-
- if (view) {
- isScrolledToBottom = view.scrollHeight - view.clientHeight <= view.scrollTop + 1
- }
-
- }
-
- // Adjust scroll after we add content to the messenger.
- const adjustScroll = () => {
- const view = document.getElementById(element)
-
- if (view) {
- if (isScrolledToBottom) {
- view.scrollTo({
- top: view.scrollHeight - view.clientHeight,
- behavior: 'smooth'
- });
- }
- }
-
- }
-
- return {
- updateScrollRef,
- adjustScroll,
- setScroll,
- }
-
-}
--- /dev/null
+<template>
+ <!-- Only render when profile has been set -->
+ <main id="app" v-if="typeof profile !== 'undefined'">
+
+ <Header />
+
+ <!-- Main Components -->
+ <Messenger
+ v-if="profile"
+ :profile="profile"
+ />
+ <Login v-else />
+
+ </main>
+
+ <!-- Show spash screen if ready is false -->
+ <Splash v-else />
+
+</template>
+
+<script lang="ts">
+
+import { defineComponent, onMounted, onUnmounted, Ref } from "vue";
+import { invokeProfile } from "@/ipc/account";
+
+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";
+
+export default defineComponent({
+
+ components: {
+ Splash,
+ Messenger,
+ Login,
+ Header
+ },
+
+ setup() {
+
+ const state: Ref;
+
+ onMounted(async () => {
+ console.log("[APP]:mounted.");
+
+ /* listen for auth related messages */
+ window.addEventListener("update-state", (event: any) => {
+ state.value = event.data;
+ });
+
+ });
+
+ onUnmounted(() => {
+ window.ipcRenderer.removeAllListeners("update-state");
+ });
+
+ return {
+ profile
+ }
+ }
+})
+
+</script>
+
+<style lang="scss">
+
+html, body {
+ margin: 0;
+ padding: 0;
+ // Background color set in window.ts
+}
+
+#app {
+ font-family: "SF Pro Text";
+
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ height: 100vh;
+ width: 100vw;
+
+ border-radius: 15px;
+ }
+
+</style>
--- /dev/null
+<template>
+ <div :class="`${type}-message`">
+
+ <!-- message bubble -->
+ <div :class="`${type}-${child}-bubble`">
+ <div class="notification-dot"/>
+ <div class="error-dot"/>
+ {{ text }}
+ </div>
+
+ <!-- message context -->
+ <span class="context">
+ <div :class="`${type}-icon`">
+ {{ context }}
+ </span>
+
+ </div>
+</template>
+
+<script lang='ts'>
+
+ import { defineComponent, ref, onMounted } from 'vue';
+
+ export default defineComponent({
+ name: "MessageItem",
+
+ props: ["text", "context", "type", "position"],
+
+ setup() {
+
+ /* initialize seen state */
+ const seen = ref(() => {
+ if (document.visibilityState === "visible") {
+ return true;
+ } else return false;
+ });
+
+ onMounted(() => {
+
+ if (!seen)
+ document.addEventListener("visibilitychange", () => seen = true);
+
+ });
+
+ return {
+ seen
+ };
+
+ }
+
+ })
+
+</script>
+
+<style lang="scss" scoped>
+
+ .message {
+ width: 100vw;
+ display: flex;
+ flex-direction: column;
+ padding-top: 9px;
+ padding-bottom: 9px;
+ }
+
+ .message:first-child {
+ margin-top: 55px;
+ }
+
+ .message:last-child {
+ margin-bottom: 18px;
+ }
+
+ .sf-message {
+ @extend .message;
+ justify-content: flex-end;
+ }
+
+ .ai-message, .fr-message {
+ @extend .message;
+ justify-content: flex-start;
+ }
+
+ .bubble {
+ position: relative;
+ max-width: 66vw;
+ font-family: "SF Pro Text";
+ font-size: 14px;
+ padding: 10px;
+ border-radius: 18px;
+ }
+
+ .sf-bubble {
+ @extend .bubble;
+ background-color: #58c4fd;
+ color: white;
+ }
+
+
+
+
+
+
+
+
+ .firstChildMessage {
+ padding-bottom: 2px;
+ }
+
+ .middleChildMessage {
+ padding-top: 2px;
+ padding-bottom: 2px;
+ }
+
+ .lastChildMessage {
+ padding-top: 2px;
+ }
+
+ #aiMessage {
+ align-items: flex-start;
+ }
+
+ #sfMessage {
+ align-items: flex-end;
+ }
+
+ #frMessage {
+ align-items: flex-start;
+ }
+
+ .messageBox {
+ position: relative;
+
+ display: flex;
+ flex-direction: column;
+
+ // Animate on render.
+ animation-name: appear;
+ animation-duration: 0.25s;
+ }
+
+ #aiMessageBox {
+ margin-left: 20px;
+ align-items: flex-start;
+ }
+
+ #sfMessageBox {
+ margin-right: 20px;
+ align-items: flex-end;
+ }
+
+ #frMessageBox {
+ margin-left: 20px;
+ align-items: flex-start;
+ }
+
+ .bubble {
+ position: relative;
+
+ max-width: 66vw;
+
+ display: flex;
+ flex-direction: column;
+
+ font-family: "SF Pro Text";
+ font-size: 14px;
+
+ padding: 10px;
+
+ border-radius: 18px;
+ }
+
+ @keyframes appear {
+ 10% {
+ transform: scale(0.3);
+ }
+ 100% {
+ transform: scale(1);
+ }
+ }
+
+ #aiBubble {
+ background-color: white;
+ }
+
+ #sfBubble {
+ background-color: #58c4fd;
+ color: white;
+ }
+
+ #frBubble {
+ background-color: white;
+ }
+
+ .sf-firstChildBubble {
+ border-bottom-right-radius: 9px;
+ }
+
+ .sf-middleChildBubble {
+ border-top-right-radius: 9px;
+ border-bottom-right-radius: 9px;
+ }
+
+ .sf-lastChildBubble {
+ border-top-right-radius: 9px;
+ }
+
+ .ai-firstChildBubble, .fr-firstChildBubble {
+ border-bottom-left-radius: 9px;
+ }
+
+ .ai-middleChildBubble, .fr-middleChildBubble {
+ border-top-left-radius: 9px;
+ border-bottom-left-radius: 9px;
+ }
+
+ .ai-lastChildBubble, .fr-lastChildBubble {
+ border-top-left-radius: 9px;
+ }
+
+ .notify {
+ position: absolute;
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background-color: #58D9FF;
+ top: -5px;
+ left: -5px;
+ border: 2px solid #EBEBEB;
+ transform: scale(0);
+
+ animation-name: notify-anim;
+ animation-duration: 5s;
+ }
+
+ @keyframes notify-anim {
+ 0%, 90% {
+ transform: scale(1);
+ }
+ 100% {
+ transform: scale(0);
+ }
+ }
+
+ .context {
+ position: relative;
+
+ display: flex;
+ align-items: center;
+
+ font-family: "SF Compact Display";
+ font-size: 12px;
+ font-weight: bold;
+ margin-top: 5px;
+ }
+
+ .photo {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ margin-right: 5px;
+
+ width: 30px;
+ height: 30px;
+
+ font-size: 14px;
+
+ background-color: white;
+ border-radius: 15px;
+ }
+
+ // Apply for audio message playback.
+ .playing {
+ animation-name: circle1;
+ animation-duration: 2s;
+ animation-iteration-count: infinite;
+ }
+
+ @keyframes circle1 {
+ 0%,
+ 100% {
+ box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.4), 0 0 0 0.25em rgba(195, 195, 195, 0.15);
+ }
+ 25% {
+ box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.15), 0 0 0 0.4em rgba(195, 195, 195, 0.3);
+ }
+ 50% {
+ box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.55), 0 0 0 0.15em rgba(195, 195, 195, 0.05);
+ }
+ 75% {
+ box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.25), 0 0 0 0.55em rgba(195, 195, 195, 0.45);
+ }
+ }
+
+ .contentLoader {
+ display: flex;
+ }
+
+ .contentLoaderDot {
+ width: 5px;
+ height: 5px;
+
+ margin: 2px;
+ border-radius: 2.5px;
+ background-color: #357CA2;
+ }
+
+ .questionMark {
+ font-weight: 900;
+ color: #357CA2;
+ }
+
+ // .divider {
+ // width: 100vw;
+ // display: flex;
+ // justify-content: center;
+ // align-items: center;
+
+ // font-family: "SF Compact Display";
+ // font-size: 12px;
+ // font-weight: bold;
+ // color: #9B9B9B;
+
+ // margin-bottom: 18px;
+ // }
+
+
+</style>
+
+
--- /dev/null
+import { ref } from "vue";
+import anime from "animejs";
+import { v4 as uuidv4 } from 'uuid';
+
+
+export function animateTextInput () {
+
+ const side = ref("right");
+
+ function show () {
+ const t1 = (side.value === "right") ? 50 : -70;
+ const t2 = (side.value === "right") ? 110 : -130;
+
+ anime({
+ targets: '#textInput',
+ opacity: [0, 1],
+ translateX: [t1, t2],
+ scale: [0.3, 1],
+ duration: 500,
+ easing: 'easeOutExpo',
+ })
+
+ }
+
+ function hide () {
+ const t = (side.value === "right") ? 50 : -80;
+
+ anime({
+ targets: '#textInput',
+ opacity: [1, 0],
+ translateX: t,
+ scale: 0.3,
+ duration: 500,
+ easing: 'easeOutExpo',
+ })
+
+ }
+
+ function switchSide () {
+ const t = (side.value === "right") ? -130 : 110;
+
+ anime({
+ targets: '#textInput',
+ translateX: t,
+ duration: 500,
+ easing: 'easeOutExpo',
+ })
+
+ }
+
+ return {
+ side,
+ show,
+ hide,
+ switchSide
+ };
+
+}
+
+export function animateAudioInput () {
+
+ function show () {
+ anime({
+ targets: '#recIcon',
+ opacity: [0, 0.75],
+ scale: [0.0, 1],
+ duration: 250,
+ easing: 'linear',
+ })
+ }
+
+ function hide () {
+ anime({
+ targets: '#recIcon',
+ opacity: [0.75, 0],
+ scale: [1, 0],
+ duration: 250,
+ easing: 'linear',
+ })
+ }
+
+ return {
+ show,
+ hide
+ };
+
+}
+
+
+export function newMessage ({
+ text=false,
+ audio=false,
+ context=false,
+ uid=uuidv4()
+}): Message {
+ return {
+ text: text,
+ audio: audio,
+ context: context,
+ uid: uid
+ };
+}
--- /dev/null
+import anime from "animejs";
+import { useIpc } from '@/modules/ipc';
+import { onMounted, onUnmounted, ref, Ref } from "vue";
+import { postMessage } from "@/ipc/session";
+import { newMessage, animateAudioInput } from "./helpers";
+import { invokeStopRecord, postAudioChunk } from "@/ipc/audio";
+
+export default function useAudioInputController (typing: Ref) {
+
+ const recording = ref(false);
+ let mediaRecorder: MediaRecorder;
+
+ const { show, hide } = animateAudioInput();
+
+ // initialize audio
+ const conf = {audio: true, video: false}
+ navigator.mediaDevices.getUserMedia(conf).then((stream: MediaStream) => {
+
+ const options = {mimeType: 'audio/webm'};
+ mediaRecorder = new MediaRecorder(stream, options);
+
+ // post any mew audio to backend
+ mediaRecorder.addEventListener('dataavailable', (e: BlobEvent) => {
+ e.data.arrayBuffer().then((buff: ArrayBuffer) => {
+ postAudioChunk(buff);
+ });
+ });
+
+ // get audio and post new message to backend
+ mediaRecorder.addEventListener('stop', (_e: Event) => {
+ invokeStopRecord().then((audio: ArrayBuffer[] | Error) => {
+ console.log(audio);
+ // postMessage(newMessage({audio: audio}));
+ });
+ });
+
+ });
+
+ // start recording on space bar
+ const record = () => {
+ console.log("INPT:Capturing audio...")
+ mediaRecorder.start();
+ recording.value = true;
+ show()
+ }
+
+ // stop recording and send on release
+ const stop = () => {
+ console.log("INPT:Stopping record.")
+ mediaRecorder.stop();
+ recording.value = false;
+ hide();
+ }
+
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (e.keyCode == 32 && !typing.value) record();
+ }
+
+ const onKeyUp = (e: KeyboardEvent) => {
+ if (e.keyCode == 32 && recording.value) stop();
+ }
+
+ //-----------------------------------------------------------
+
+ onMounted(() => {
+ window.addEventListener("keydown", onKeyDown);
+ window.addEventListener("keyup", onKeyUp);
+ });
+
+ onUnmounted(() => {
+ window.removeEventListener("keydown", onKeyDown);
+ window.removeEventListener("keyup", onKeyUp);
+ })
+
+ return {
+ recording
+ }
+
+}
-import anime from "animejs";
-import useMitt from "@/modules/mitt";
-import { useIpc } from '@/modules/ipc';
import { Ref, ref, watch, onMounted, onUnmounted } from "vue";
-import keyboardNameMap from "../keyBoardMaps/keyboardNameMap";
-import { clientMessage, renderMessage } from '@/modules/message';
-import { postMessage } from "@/ipcRend/session";
-
-
-//---Animations-----------------------------------------------
-
-let side = "right"; // Side of parent we are on.
-
-function showTextInput () {
- const t1 = (side === "right") ? 50 : -70;
- const t2 = (side === "right") ? 110 : -130;
-
- anime({
- targets: '#textInput',
- opacity: [0, 1],
- translateX: [t1, t2],
- scale: [0.3, 1],
- duration: 500,
- easing: 'easeOutExpo',
- })
-
-}
-
-function hideTextInput() {
- const t = (side === "right") ? 50 : -80;
-
- anime({
- targets: '#textInput',
- opacity: [1, 0],
- translateX: t,
- scale: 0.3,
- duration: 500,
- easing: 'easeOutExpo',
- })
-
-}
-
-function switchSide(currentSide: string) {
- const t = (currentSide === "right") ? -130 : 110;
-
- anime({
- targets: '#textInput',
- translateX: t,
- duration: 500,
- easing: 'easeOutExpo',
- })
-
-}
-
-//------------------------------------------------------------
+import { postMessage } from "@/ipc/session";
+import { newMessage, animateTextInput } from "./helpers";
export default function useTextInputController(elementX: Ref) {
+
let textInput: HTMLInputElement | null;
- const { post } = useIpc();
- const { emitter } = useMitt();
+ const { side, show, hide, switchSide } = animateTextInput();
let firstKey = true;
const typing = ref(false);
// Prep inputItem for typing.
const prepInput = () => {
- showTextInput()
+ show()
typing.value = true
}
textInput.blur();
}
- hideTextInput()
+ hide()
firstKey = true;
typing.value = false;
}
const sendMessage = () => {
if (textInput) {
- // Create the message.
- const message = renderMessage(
- textInput.value,
- false,
- "",
- "sf"
- )
-
- emitter.emit("self-message", message);
-
// Send it to the backend for processing.
- const clientM = clientMessage(textInput.value, false, message.uid);
- postMessage(clientM);
+ const message = newMessage({
+ text: textInput.value
+ });
+
+ postMessage(message);
clearInput()
}
}
// Keys that are capable of opening the text input (numbers and letters).
- const hotKeyRange = keyboardNameMap.slice(47, 91)
- const isHotKey = (key: string) => {
- if (hotKeyRange.includes(key)) {
+ const isHotKey = (key: number) => {
+ if (key >= 47 && key <= 91) { // a letter
return true
}
}
//---Callbacks-----------------------------------------------
const onKeyDown = (e: KeyboardEvent) => {
- const key = keyboardNameMap[e.keyCode]
+ const key = e.keyCode;
if (textInput) {
textInput.focus();
// Close input when no text or on ESC.
- if ((textInput.value == "") && (!firstKey) && (key === "BACK_SPACE")) {
+ if ((textInput.value == "") && (!firstKey) && (key === 8)) { // backspace
clearInput()
return
}
- if (key === "ESCAPE") {
+ if (key === 27) { // escape
clearInput()
return
}
// Close and send on enter.
- if (key === "ENTER") {
+ if (key === 13) {
if (textInput.value) {
sendMessage()
return
const winW = window.innerWidth
// Logic depends on the side we are on.
- if (side === "right") {
+ if (side.value === "right") {
if (winW - elementX < 230) {
- switchSide(side)
- side = "left"
+ switchSide()
+ side.value = "left"
}
}
else {
if (winW - elementX > 230) {
- switchSide(side)
- side = "right"
+ switchSide()
+ side.value = "right"
}
}
--- /dev/null
+import { ref } from 'vue';
+import invokeSavedMessages from "@/render/ipc";
+import useScroll from "@/render/composables/scroll";
+
+const messages = ref(new Map());
+
+function getTimeStamp(): number {
+ const currentdate = new Date();
+ return currentdate.getTime();
+}
+
+const newViewMessage = (message: Message): ViewMessage => {
+ return {
+ text: message.text,
+ context: message.context,
+ audio: message.audio,
+ from: message.from,
+ uid: message.uid,
+ time: getTimeStamp(),
+ isChild: "none",
+ seen: false,
+ newMessage: false
+ };
+}
+
+const addMessage = (message: Message, newMessage=false) => {
+ const viewMessage = newViewMessage(message);
+ if (newMessage) viewMessage.newMessage = true;
+ messages.value.set(viewMessage.uid, viewMessage);
+}
+
+const updateMessage = (message: Message) => {
+ const viewMessage = messages.value.get(message.uid);
+ viewMessage.context = message.context;
+ viewMessage.text = message.text;
+}
+
+const loadSavedMessages = async () => {
+ const messageData = await invokeSavedMessages();
+ messages.value = new Map(Object.entries(messageData));
+}
+
+const saveMessages = () => {
+ const messageData = Object.fromEntries(messages.value);
+ // must save to json.
+}
+
+const pruneMessages = (limit=200) => {
+ if (messages.value.size >= limit) {
+ const oldest = Array.from(messages.value.keys()).shift();
+ messages.value.delete(oldest);
+ }
+}
+
+const updateGrouping = () => {
+
+ const isSimmilar = (messageA: ViewMessage, messageB: ViewMessage) => {
+ if ((Math.abs(messageA.time - messageB.time) < 20000) && (messageA.from == messageB.from) && (messageA.context == messageB.context)) {
+ return true
+ }
+ return false
+ }
+
+ const refs = Array.from(messages.value.keys())
+
+ // Get the last three messages.
+ const first = messages.value.get(refs[refs.length - 1])
+ const second = messages.value.get(refs[refs.length - 2])
+ const third = messages.value.get(refs[refs.length - 3])
+
+ // If messages are simmilar, update the classes.
+ if ((first) && (second)) {
+ if (isSimmilar(first, second)) {
+ first.isChild = "last" // i.e. last in group.
+ second.isChild = "first"
+
+ if (third) {
+ if ((third.isChild == "first") || (third.isChild == "middle")) {
+ second.isChild = "middle"
+ }
+ }
+ }
+ }
+}
+
+export default function useMessages() {
+
+ const { updateScrollRef, adjustScroll } = useScroll("messenger");
+
+ /* Given new message object, update the view accordingly */
+ const updateMessageView = (newMessages: Message[]) => {
+
+ const bottom = updateScrollRef(); // see if the user is scrolled down
+
+ // take each message and apply view
+ newMessages.forEach((message: Message) => {
+
+ // add or update message depending
+ if (messages.value.has(message.uid)) {
+ updateMessage(message);
+ } else addMessage(message);
+
+ pruneMessages(); // pop off old messages from view
+ updateGrouping(); // group like message together
+
+ if (bottom) adjustScroll(); // only scroll if user was at bottom
+
+ saveMessages();
+
+ });
+
+ }
+
+ return {
+ messages,
+ updateMessageView,
+ loadSavedMessages
+ }
+
+}
+
+
+ // // Seed message view with message history.
+ // const prepMessageView = async (newMessages: Message[]) => {
+ // console.log("MSGR:Prepping messenger view.")
+
+ // // Load and render saved messages and immediately scroll to bottom.
+ // await loadSavedMessages();
+ // setTimeout(setScroll.bind(false), 10);
+
+ // // Render new messages, then wait 1s to scroll.
+ // if (newMessages.length) {
+
+ // console.log("MSGR:Adding new messages")
+
+ // newMessages.forEach(message => {
+ // addMessage(message, true);
+ // })
+
+ // setTimeout(setScroll.bind(true), 1000);
+
+ // }
+ // }
\ No newline at end of file
--- /dev/null
+<template>
+ <button id="titlebar" />
+ <span id="menu">
+ <button
+ class="menuButton exitButton"
+ @click.prevent="postNavBarExit"
+ />
+ <button
+ class="menuButton minimizeButton"
+ @click.prevent="postNavBarMin"
+ />
+ </span>
+</template>
+
+<script lang="ts">
+
+ import { postNavBarExit, postNavBarMin } from "@/render/ipc";
+
+ setup() {
+ return {
+ postNavBarExit,
+ postNavBarMin
+ }
+ }
+
+</script>
+
+<style lang="scss">
+#titlebar {
+ position: fixed;
+
+ width: 100vw;
+ height: 54px;
+
+ opacity: 0.75;
+
+ background-color: #EBEBEB;
+
+ -webkit-app-region: drag;
+
+ border: none;
+ outline: none;
+
+ z-index: 1;
+
+}
+
+#menu {
+ position: fixed;
+ margin-left: 20px;
+ margin-top: 20px;
+
+ z-index: 2;
+
+}
+
+.menuButton {
+ border: none;
+ outline:none;
+ min-width: 14px;
+ min-height: 14px;
+ border-radius: 7px;
+}
+
+.exitButton {
+ background-color: #FF6157;
+}
+
+.exitButton:active {
+ background: #c14645;
+}
+
+.minimizeButton {
+ background-color: #FFC12F;
+ margin-left: 8px;
+}
+
+.minimizeButton:active {
+ background-color: #c08e38;
+}
+</style>
\ No newline at end of file
<span v-if="recording" class="pause"></span>
<!-- Show text input on key-down -->
- <TextInput />
+ <input
+ id="textInput"
+ type="text"
+ />
<!-- Show suggestions menu on click -->
<!-- <Menu /> -->
import TextInput from "@/components/textInput.vue";
import useTextInputController from
- "@/components/controllers/textCtrl";
+ "@/components/controllers/inputItem.control.audio";
import useAudioInputController from
- "@/components/controllers/audioCtrl";
+ "@/components/controllers/inputItem.control.text";
export default defineComponent({
name: "InputItem",
}
}
+#textInput {
+ position: absolute;
+
+ opacity: 0;
+
+ min-width: 150px;
+ height: 16px;
+
+ border-radius: 18px;
+
+ padding: 10px;
+ margin-right: 10px;
+ margin-left: 10px;
+
+ outline: none;
+ border: none;
+ pointer-events: none;
+
+ background-color: white;
+
+ z-index: -1;
+
+ box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
+
+ transform: translateX(50px) scale(0.3);
+
+}
+
</style>
import { authRequest } from '@/modules/message';
import { useProfile } from '@/modules/auth';
import { invokeLogin } from "@/ipcRend/account";
-import { Profile } from "@/types";
import { postInitSession } from "@/ipcRend/session";
export default defineComponent({
setup() {
- const { profile, setProfile } = useProfile();
-
const usr = ref("");
const pwd = ref("");
const submitForm = async () => {
try {
+
const profile = await invokeLogin({
email: usr.value,
password: pwd.value
- }) as Profile;
-
- setProfile(profile);
+ });
- } catch(e) {
- console.log('Error login in.')
- } finally{
+ /* emit event to app.vue */
+ window.postMessage(profile);
- if (profile.value.crimataId) {
- // start session
- postInitSession(profile.value.crimataId);
- }
+ } catch (e) console.log(e);
- }
}
return {
--- /dev/null
+<template>
+
+ <!-- Position fixed items -->
+ <InputItem :initials="profile.initials"/>
+ <Settings />
+ <div id="recIcon" />
+
+ <!-- List of message bubbles. -->
+ <div id="messenger">
+ <Bubble
+ v-for="message in messages"
+ :text="message.text"
+ :context="message.context"
+ :key="message[0]"
+ />
+ </div>
+
+</template>
+
+<script lang="ts">
+
+import { defineComponent, onMounted, onUnmounted } from "vue";
+import Message from "@/render/components/message.vue";
+import InputItem from "@/render/components/inputItem.vue";
+import Settings from "@/render/components/settings.vue";
+import useMessages from "@/render/composables/messages";
+
+export default defineComponent({
+ name: "Messenger",
+
+ props: ["state"],
+
+ components: {
+ Message,
+ InputItem,
+ Settings
+ },
+
+ setup(props) {
+
+ // Handle messages in view.
+ const { messages, updateMessageView } = useMessages();
+
+ onMounted(() => {
+
+ /* populate the message view with existing messages */
+ updateMessageView(state.savedMessages, state.newMessages);
+
+ /* wait and listen for new messages to come in */
+ window.ipcRenderer.on("new-message", (_e: any, payload: any) => {
+ updateMessageView(payload.message);
+ });
+
+ });
+
+ onUnmounted(() => {
+ window.ipcRenderer.removeAllListeners("new-message");
+ });
+
+ return {
+ messages
+ };
+
+ },
+
+});
+</script>
+
+<style lang="scss" scoped>
+
+#messenger {
+ width: 100vw;
+ height: 100vh;
+ overflow: auto;
+}
+
+#messenger::-webkit-scrollbar {
+ display: none;
+}
+
+#recIcon {
+ position: fixed;
+ right: 0;
+
+ opacity: 0;
+ transform: scale(0.0);
+
+ margin-top: 24px;
+ margin-right: 66px;
+
+
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: #FF3B3B;
+
+ z-index: 2;
+
+}
+
+</style>
import { ref } from "vue";
-import { Profile } from "@/types";
-
const profile = ref();
-export const useIpc = () => {
+export default function useIpc () {
const invoke = async (endpoint: string, payload: any) => {
try {
--- /dev/null
+
+
+export default function useScroll(element: string) {
+
+ let isScrolledToBottom: boolean;
+ const view = document.getElementById(element)
+
+ // Update isScrolledToBottom
+ const updateScrollRef = () => {
+ if (view) isScrolledToBottom = view.scrollHeight - view.clientHeight <= view.scrollTop + 1;
+ return isScrolledToBottom;
+ }
+
+ // Adjust scroll after we add content to the messenger.
+ const adjustScroll = () => {
+ if (view) {
+ view.scrollTo({
+ top: view.scrollHeight - view.clientHeight,
+ behavior: 'smooth'
+ });
+ }
+ }
+
+ return {
+ updateScrollRef,
+ adjustScroll,
+ };
+
+}
\ No newline at end of file
--- /dev/null
+
+import useIpc from "@/render/composables/ipc";
+
+const { post, invoke } = useIpc();
+
+/**
+ *
+ * Account and auth related endpoints
+ *
+ */
+
+export const invokeProfile = async (): Promise<Profile | Error> => (
+ await invoke('user-profile', null)
+);
+
+export const invokeLogin = async (
+ payload: LoginPayload
+): Promise<Profile | Error> => (
+ await invoke('user-login', JSON.stringify(payload))
+);
+
+export const invokeLogout = async (): Promise<void> => (
+ await invoke("user-logout", null)
+);
+
+/**
+ *
+ * Audio endpoints
+ *
+ */
+
+export const postAudioChunk = (chunk: ArrayBuffer): void => (
+ post("audio-chunk", chunk)
+);
+
+export const invokeReturnAudio = async (): Promise<ArrayBuffer[] | Error> => (
+ await invoke("get-audio", null)
+);
+
+/**
+ *
+ * Crimata Platform (session) endpoints
+ *
+ */
+
+export const invokeSession = async (cid: string): Promise<Profile | Error> => (
+ await invoke("messenger-init", cid)
+);
+
+export const postMessage = (payload: Message): void => (
+ post('client-message', payload)
+);
\ No newline at end of file
--- /dev/null
+
+
+
+
+import useAudio from "@/audio";
+import { loadState, saveState, emitState } from "@/state";
+
+/* start and stop audio functionality */
+const { initAudio, closeAudio } = useAudio();
+
+/**
+ * Controls for interfacing with the platform.
+ * Takes an onMessage callback which we define below.
+ */
+const { connect, send, close } = usePlatform((message: Message) => {
+
+ /* add the message to the state */
+ addMessage(message);
+
+ /* push the message to the browser */
+ if (win) emit("new-message", message);
+
+});
+
+/* send a message to the platform */
+export function sendMessage(message: Message) {
+
+ /* add the message to the state */
+ addMessage(message);
+
+ /* push the message to the browser */
+ if (win) emit("new-message", message);
+
+ /* socket send */
+ send(message);
+
+}
+
+/* launch a new session (the main process for authenticated users) */
+export function launchSession(profile: Profile) {
+
+ /* load any previously saved state for that user */
+ loadState(profile);
+
+ /* connect to the platform */
+ connect(profile);
+
+ /* initialize the audio streams */
+ // initAudio();
+
+ /* finally we can push state to browser */
+ if (win) emitState();
+
+}
+
+export function endSession() {
+
+ closeAudioStreams();
+
+ closeSocket();
+
+ state.clear();
+
+}
\ No newline at end of file
--- /dev/null
+const Store = require('electron-store');
+
+/* simple data persistance */
+const store = new Store;
+
+/* state of the session (e.g. profile and messages for now) */
+let state: State | null = null;
+
+/* load saved state in electron store for given user */
+export function loadState(profile: Profile) {
+ state = store.get("state", null);
+}
+
+/* add a message to state.messages */
+export function addMessage(message: Message) {
+ if (state) {
+ state.messages.push(message);
+ saveState();
+ }
+}
+
+export function saveState() {
+ store.set("state", state);
+}
+
+export function emitState() {
+ emit("update-state", state);
+}
+
-export interface RenderMessage {
- content: {
- text: boolean | string;
- audio: boolean | string;
- };
- context: string;
- modifier: string;
- time: number;
- uid: string;
- isChild: string;
- seen: boolean;
- newMessage: boolean;
-}
-
-export interface ClientMessage {
- text: string;
+interface Message {
+ text: boolean | string;
+ context: boolean | string;
audio: boolean | string;
+ type: 1 | 2 | 3;
+ time: number;
uid: string;
}
-export interface ClientRequest {
- intent: string;
- params: object;
- epic: string | boolean;
- confidence: number;
-}
-
-export interface SessionState {
- key: string | boolean;
- newMessages: RenderMessage[];
+interface ViewMessage extends Message {
+ child: string;
+ seen: boolean;
+ newMessage: boolean;
}
-export interface WindowState {
+interface WindowState {
width: number;
height: number;
x: number | null;
y: number | null;
}
-export interface AuthRequest {
- key: boolean | string;
- usr: boolean | string;
- pwd: boolean | string;
-}
-
-export interface Profile {
+interface Profile {
crimataId: string;
alias: string;
initials: string;
}
-export interface AuthProtocol {
- token: null | string;
- profile: null | Profile;
- password?: string;
- email?: string;
-}
-
-export interface LogoutRequest {
- logout: boolean;
-}
-
-export interface Annotation {
- text: string;
- context: string;
- uid: string;
+interface State {
+ profile: Profile | null;
+ messages
}
-export interface StandardMessage {
- content: {
- text: boolean | string;
- audio: boolean | string;
- };
- context: string;
- modifier: string;
+interface LoginPayload {
+ email: string;
+ password: string;
}
import { BrowserWindow, ipcMain } from "electron";
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
-import { backgroundMitt } from '@/modules/emitter';
-import { RenderMessage, WindowState } from "@/types";
-import { loadWinState, saveToJson } from "./helpers";
+import { backgroundMitt } from './coposables/emitter';
+import { saveToJson } from "./coposables/json";
import * as path from "path";
const { autoUpdater } = require('electron-updater');
interface IpcRendererPayload {
endpoint: string;
- message: RenderMessage | null;
+ message: Message | null;
}
let win: BrowserWindow | null;
let winState: WindowState;
+const loadWinState = (fileName: string): WindowState => {
+ let state: WindowState;
+
+ try {
+ state = JSON.parse(fs.readFileSync(configPath + fileName).toString());
+ }
+
+ catch (error) {
+ state = {
+ width: 600,
+ height: 500,
+ x: null,
+ y: null,
+ }
+ }
+
+ return state
+
+}
+
// Called when a NavBar button is pressed.
const onNavBar = (_event: any, action: string): void => {
if (win) {
}
// function used by run.ts to create the main window.
-export async function createWindow(): Promise<void> {
+export default async function createWindow(): Promise<void> {
return new Promise((resolve, _reject) => {
// avoid creating duplicate windows.
]
},
"include": [
+ "**/*.ts",
"src/*.ts",
"src/**/*.ts",
"src/**/*.tsx",