--- /dev/null
-{"key":"0c116d29-8cfe-491f-a84d-1d021297cc69","newMessages":[]}
++{"key":"854fb3e1-9207-473d-82b8-b6385f47e895","newMessages":[]}
<template>
- <div id="app" v-if="(windowReady && sessionReady)">
+ <!-- Only render when profile has been set -->
+ <div id="app" v-if="(ready)">
- <button class="titlebar" />
+ <button id="titlebar" />
<!-- Minimize and exit buttons. -->
<span class="menu">
},
setup() {
- const { post, invoke } = useIpc();
+ const { post } = useIpc();
- const auth = ref(false);
- const windowReady = ref(false);
- const sessionReady = ref(false);
+ // Whether browser has received user info yet.
+ const ready = ref(false);
- // When connection is established, we send key over.
- const onOpen = (_event: IpcRendererEvent, payload: any) => {
- console.log(`Connected to Crimata.`)
- const key = window.localStorage.getItem("key");
+ // Information about current user.
+ const profile = ref(false);
- if (key) {
- console.log(`Sending key: ${key}`)
- post("client-message", authRequest(key, false, false))
- }
+ // New messages that browser missed while closed.
+ const newMessages = ref([])
- else {
- console.log("No key, session ready.")
- sessionReady.value = true
- }
-
- }
-
- // Update authenticate state on auth message from server.
- const onAuthResponse = (_event: IpcRendererEvent, payload: any) => {
- const key = payload.message.key
- const usr = payload.message.usr
- console.log(`Received auth response: ${usr}, ${key}`)
+ // Receive updated information about the session.
+ const updateState = (_event: IpcRendererEvent, payload: any) => {
+ console.log("APP:Received updated profile and new messages: \n" +
+ ` profile: ${payload.message.profile}\n` +
+ ` new: ${payload.message.newMessages}`)
- if (key) {
- console.log(`Auth success, saving key: ${key}.`)
- window.localStorage.setItem("key", payload.message.key)
- auth.value = true;
+ if (payload.message.profile) {
- console.log(`Logged-in, showing Messenger View.`)
++ console.log(`APP:Logged-in, showing Messenger View.`)
+ } else {
- console.log(`Logged-out, showing Login View.`)
++ console.log(`APP:Logged-out, showing Login View.`)
}
- else {
- console.log(`Auth failed, clearing local storage.`)
- window.localStorage.clear()
- auth.value = false;
- }
-
- console.log("Session is ready.")
- sessionReady.value = true
+ // Set profile and newMessages.
+ profile.value = payload.message.profile;
+ newMessages.value = payload.message.newMessages;
- }
+ ready.value = true;
- // Post navbar action to backend.
- const callNavbar = (action: string) => {
- invoke("nav-bar", action);
}
onMounted(() => {
--- /dev/null
-import { SessionState } from "@/types/message";
+ import fs from 'fs';
+ import { backgroundMitt } from "@/modules/emitter";
-export const saveState = (fileName: string, state: SessionState) => {
- fs.writeFile(fileName, JSON.stringify(state), (err) => {
++import { SessionState, WindowState } from "@/types/message";
+
+
+ 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(fileName).toString());
+ }
+
+ catch (error) {
+ state = {
+ key: false,
+ newMessages: []
+ }
+ }
+
+ return state
+
+ }
+
- console.log("Error when saving state.")
++export const loadWinState = (fileName: string): WindowState => {
++ let state: WindowState;
++
++ try {
++ state = JSON.parse(fs.readFileSync(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(fileName, JSON.stringify(data), (err) => {
+ if (err) {
- });
++ console.log("Error when saving to json.")
+ }
++ })
++
+ }
/*
* Creates a websocket session with Crimata Servers.
*
- * Connects to Servers and attempts token authentication. Will send the result
- * of the authentication to the window. It will then serve as a communication
- * interface between the window and the servers. It will automatically try to
- * reconnect on websocket disconnect.
+ * 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 './emitter';
- import { ipcMain, IpcMainEvent } from "electron";
+ import { backgroundMitt } from "@/modules/emitter";
-import { ipcEmit, loadState, saveState } from './helpers';
++import { ipcEmit, loadState, saveToJson } from './helpers';
+ import { ipcMain, IpcMainEvent, IpcMainInvokeEvent } from "electron";
- import useWebSockets from "@/modules/ws";
+ import useWebSockets from "@/modules/websockets";
- import { handleAuthMessage, handleStandardMessage, handleProfileMessage, handleAnnotation } from "./handlers";
+ import { play } from "./audio";
+ import { renderMessage } from "@/modules/message";
+ import { AuthProtocol, SessionState, Profile } from "@/types/message";
- // Key for key-based auth.
- let key: string;
+ let win = true;
+ // Info saved to json on quit (key, newMessages).
+ let state: SessionState;
+ // Profile of current user.
+ let profile: Profile | boolean;
- saveState("session.json", state)
+ // Called when server sends auth message.
+ const updateState = (res: AuthProtocol) => {
+ console.log("SESS:Auth message received: \n" +
+ ` key: ${res.key}\n` +
+ ` alias: ${res.profile}`)
+
+ if (state) {
+
+ // Update key.
+ state.key = res.key;
+
+ // Update the user profile.
+ profile = res.profile;
+
+ // Send upated profile to frontend.
+ console.log("SESS:Sending updated user profile to browser.")
+ ipcEmit("update-state", {
+ profile: profile,
+ newMessages: state.newMessages
+ })
+
+ // Save the updated state to json.
+ console.log("SESS:Saving session state.")
- console.log("SESS:New window, sending profile.")
- if (profile) {
++ saveToJson("session.json", state)
+
+ }
+
+ }
+
+ // Send state on new window.
+ const onNewBrowserWindow = (_event: IpcMainInvokeEvent, _payload: any) => {
++ if (typeof profile !== 'undefined') {
++ console.log("SESS:Sending user profile to browser.")
+ ipcEmit("update-state", {
+ profile: profile,
+ newMessages: state.newMessages
+ })
+ }
+ }
// Calls appropriate endpoint for a server message.
const onMessage = (data: string) => {
import { BrowserWindow, ipcMain } from "electron";
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
- import { backgroundMitt } from './emitter';
- import { RenderMessage } from "@/types/message/index";
+ import { backgroundMitt } from '@/modules/emitter';
-import { RenderMessage } from "@/types/message/index";
++import { RenderMessage, WindowState } from "@/types/message/index";
++import { loadWinState, saveToJson } from "./helpers";
import * as path from "path";
import fs from 'fs';
}
let win: BrowserWindow | null;
++let winState: WindowState;
// Called when a NavBar button is pressed.
const onNavBar = (_event: any, action: string): void => {
// Write a json with position and size of window.
const saveWindowState = () => {
-- if (win) {
-- const bounds = win.getBounds();
-- const position = win.getPosition();
--
-- const state = JSON.stringify(
-- {
-- w: bounds.width,
-- h: bounds.height,
-- x: position[0],
-- y: position[1]
-- }
-- );
--
-- fs.writeFile('windowState.json', state, (err) => {
- if (err) throw err;
- return;
- if (err) {
- console.log("BW:Error saving window state.")
- }
-- });
++ if (win) {
++ const bounds = win.getBounds();
++ const position = win.getPosition();
++
++ if (winState) {
++
++ winState.width = bounds.width;
++ winState.height = bounds.height;
++ winState.x = position[0];
++ winState.y = position[1]
++
++ saveToJson("window.json", winState)
++
}
++ }
}
// Do this on window mount.
// function used by run.ts to create the main window.
export async function createWindow(): Promise<void> {
return new Promise((resolve, _reject) => {
-- console.log("BW:Creating browser window. ")
// avoid creating duplicate windows.
if (win) resolve();
// Load the saved window state.
-- const state = JSON.parse(fs.readFileSync('windowState.json').toString());
++ winState = loadWinState("window.json")
++ console.log(`BW:Creating window [${winState.width}, ${winState.height}].`)
// Define the browser window.
win = new BrowserWindow({
-- width: state.w,
-- height: state.h,
-- x: state.x,
-- y: state.y,
++ width: winState.width,
++ height: winState.height,
++ x: winState.x,
++ y: winState.y,
resizable: true,
backgroundColor: '#EBEBEB',
frame: false,
<script lang="ts">
--import { defineComponent, ref, onMounted, onUnmounted } from "vue";
++import { defineComponent } from "vue";
import draggify from "@/modules/draggify";
- import TextInput from "@/components/inputItem/textInput.vue";
+ import TextInput from "@/components/textInput.vue";
+
import useTextInputController from
- "@/components/inputItem/controllers/textCtrl";
+ "@/components/controllers/textCtrl";
import useAudioInputController from
- "@/components/inputItem/controllers/audioCtrl";
+ "@/components/controllers/audioCtrl";
export default defineComponent({
name: "InputItem",
<script lang="ts">
import { defineComponent, ref } from "vue";
- import { useAuth } from "@/modules/auth";
import { useIpc } from "@/modules/ipc";
--import useMessages from "@/modules/messages";
--import { authRequest } from '@/modules/message';
++import { authRequest } from '@/modules/message';
export default defineComponent({
name: "Login",
<!-- 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 === 'launch'"
++ class="divider"
++ >
++ <div>new session</div>
++ </div>
++
<!-- Includes bubble and context. -->
<div
:id="`${message.modifier}MessageBox`"
: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 === ''"
<script lang='ts'>
import {defineComponent, ref, onMounted} from 'vue';
++ import { RenderMessage } from "@/types/message/index";
export default defineComponent({
name: "MessageItem",
-- props: {
-- message: {
-- type: Object,
-- required: true
-- }
-- },
++ props: ["message"],
setup(props) {
++
++ let addListener = false;
++
++ const notify = ref(false)
const isPlaying = ref(false);
++ if (!props.message.seen) {
++
++ // 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 = (e: 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);
})
return {
++ notify,
isPlaying
}
}
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;
}
}
.bubble {
++ position: relative;
++
max-width: 66vw;
display: flex;
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 {
display: flex;
align-items: center;
<!-- List of message bubbles. -->
<div id="messenger">
- <MessageItem v-for="message in messages" :message="message[1]" :key="message[0]" />
+ <Message
+ v-for="message in messages"
+ :message="message[1]"
- :key="message[0]"
++ :key="message[0]"
+ />
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted } from "vue";
- import MessageItem from "@/components/messageItem.vue";
- import InputItem from "@/components/inputItem/inputItem.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';
+ import useMessages from "@/modules/messages";
-import { RenderMessage } from "@/types/message/index"
export default defineComponent({
name: "Messenger",
const { emitter } = useMitt();
// Handle messages in view.
- const { messages, updateMessageView } = useMessages();
-
- // Update message view on new content.
- const onNewContent = (_event: any, payload: any) => {
- updateMessageView(payload.message)
- }
+ const { messages, prepMessageView, updateMessageView } = useMessages();
onMounted(() => {
- const crimata_id = props.profile.crimata_id;
++ const crimataId = props.profile.crimataId;
++ console.log(`MSGR:Initializing messenger for ${crimataId}.`)
- // Front end listener.
+ // Load the message history.
- if (crimata_id === window.localStorage.getItem("last_usr")) {
++ 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)
+ });
- // Back end listener.
- window.ipcRenderer.on("render-message", onNewContent);
+ // Save the usr for next time.
- window.localStorage.setItem("last_usr", crimata_id)
++ window.localStorage.setItem("last_usr", crimataId)
});
modifier: modifier,
time: getTimeStamp(),
uid: uuidv4(),
-- isChild: "none"
++ isChild: "none",
++ seen: false,
++ newMessage: 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 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.
}
}
- const { setScroll, updateScrollRef, adjustScroll } = useScroll("messenger");
- loadMessages()
- setTimeout(setScroll, 1000);
+ export default function useMessages() {
+ // Scroll controller.
+ const { setScroll, updateScrollRef, adjustScroll } = useScroll("messenger");
- export default function useMessages() {
+ // Main function for updating the message view.
+ const updateMessageView = (message: RenderMessage | Annotation) => {
- // Main function for updating the message view.
- const updateMessageView = (message: RenderMessage | Annotation) => {
+ // Step 1: See if user is scrolled down.
+ updateScrollRef()
- // 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 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 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()
- // Step 4: Update grouping.
- updateGrouping()
+ // Setp 5: Scroll the view (if scrolled down).
+ setTimeout(adjustScroll, 20);
- // Setp 5: Scroll the view (if scrolled down).
- setTimeout(adjustScroll, 20);
+ // Step 6: Save the view data.
+ saveMessages()
- // Step 6: Save the view data.
- saveMessages()
+ }
- }
+ // Seed message view with message history.
+ const prepMessageView = (newMessages: RenderMessage[]) => {
+ console.log("MSGR:Prepping messenger view.")
- const resetMessages = () => {
- messages.value.clear()
- }
+ // 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 => addMessage(message))
++
++ newMessages.forEach(message => {
++ message.newMessage = true;
++ addMessage(message)
++ })
++
+ setTimeout(setScroll.bind(true), 1000);
+
- return {
- messages,
- updateMessageView,
- resetMessages
}
+ }
+
+ return {
+ messages,
+ prepMessageView,
+ updateMessageView
+ }
}
time: number;
uid: string;
isChild: string;
++ seen: boolean;
++ newMessage: boolean;
}
export interface ClientMessage {
uid: string;
}
+ export interface SessionState {
+ key: string | boolean;
+ newMessages: RenderMessage[];
+ }
+
++export 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 LogoutRequest {
- logout: boolean;
+ export interface Profile {
- crimata_id: string;
++ crimataId: string;
+ alias: string;
+ initials: string;
}
- // Possible messages from server:
+ export interface AuthProtocol {
+ key: boolean | string;
+ profile: boolean | Profile;
+ }
- export interface Profile {
- crimata_id: string;
- title: string;
- first: string;
- middle: string;
- last: string;
- suffix: string | number;
- nickname: string;
- full: string;
+ export interface LogoutRequest {
+ logout: boolean;
}
export interface Annotation {
--- /dev/null
--- /dev/null
++{"width":386,"height":815,"x":1720,"y":495}
+++ /dev/null
- <<<<<<< HEAD
- {"w":629,"h":500,"x":1423,"y":657}
- =======
- {"w":622,"h":616,"x":1163,"y":499}
- >>>>>>> logout
-{"w":627,"h":801,"x":1462,"y":450}