merging
This commit is contained in:
commit
81662942a2
24 changed files with 506 additions and 402 deletions
1
session.json
Normal file
1
session.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"key":"854fb3e1-9207-473d-82b8-b6385f47e895","newMessages":[]}
|
||||||
96
src/App.vue
96
src/App.vue
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app" v-if="(windowReady && sessionReady)">
|
<!-- Only render when profile has been set -->
|
||||||
|
<div id="app" v-if="(ready)">
|
||||||
|
|
||||||
<button id="titlebar" />
|
<button id="titlebar" />
|
||||||
|
|
||||||
|
|
@ -7,21 +8,25 @@
|
||||||
<span class="menu">
|
<span class="menu">
|
||||||
<button
|
<button
|
||||||
class="menuButton exitButton"
|
class="menuButton exitButton"
|
||||||
@click.prevent="callNavbar('close')"
|
@click.prevent="post('nav-bar', 'close')"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="menuButton minimizeButton"
|
class="menuButton minimizeButton"
|
||||||
@click.prevent="callNavbar('min')"
|
@click.prevent="post('nav-bar', 'min')"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Main Pages -->
|
<!-- Main Pages -->
|
||||||
<Messenger v-if="auth"/>
|
<Messenger
|
||||||
|
v-if="profile"
|
||||||
|
:profile="profile"
|
||||||
|
:newMessages="newMessages"
|
||||||
|
/>
|
||||||
<Login v-else />
|
<Login v-else />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Show spash screen if not ready -->
|
<!-- Show spash screen if ready is false -->
|
||||||
<Splash v-else />
|
<Splash v-else />
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -29,7 +34,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
|
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
|
||||||
import { IpcRendererEvent } from "electron";
|
import { IpcRendererEvent } from "electron";
|
||||||
import { authRequest } from '@/modules/message';
|
|
||||||
import { useIpc } from "@/modules/ipc";
|
import { useIpc } from "@/modules/ipc";
|
||||||
|
|
||||||
import Splash from "@/components/splash.vue";
|
import Splash from "@/components/splash.vue";
|
||||||
|
|
@ -44,74 +48,52 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
const { post, invoke } = useIpc();
|
const { post } = useIpc();
|
||||||
|
|
||||||
const auth = ref(false);
|
// Whether browser has received user info yet.
|
||||||
const windowReady = ref(false);
|
const ready = ref(false);
|
||||||
const sessionReady = ref(false);
|
|
||||||
|
|
||||||
// When connection is established, we send key over.
|
// Information about current user.
|
||||||
const onOpen = (_event: IpcRendererEvent, payload: any) => {
|
const profile = ref(false);
|
||||||
console.log(`Connected to Crimata.`)
|
|
||||||
const key = window.localStorage.getItem("key");
|
|
||||||
|
|
||||||
if (key) {
|
// New messages that browser missed while closed.
|
||||||
console.log(`Sending key: ${key}`)
|
const newMessages = ref([])
|
||||||
post("client-message", authRequest(key, false, false))
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
// Receive updated information about the session.
|
||||||
console.log("No key, session ready.")
|
const updateState = (_event: IpcRendererEvent, payload: any) => {
|
||||||
sessionReady.value = true
|
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.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
// Set profile and newMessages.
|
||||||
|
profile.value = payload.message.profile;
|
||||||
|
newMessages.value = payload.message.newMessages;
|
||||||
|
|
||||||
// Update authenticate state on auth message from server.
|
ready.value = true;
|
||||||
const onAuthResponse = (_event: IpcRendererEvent, payload: any) => {
|
|
||||||
const key = payload.message.key
|
|
||||||
const usr = payload.message.usr
|
|
||||||
console.log(`Received auth response: ${usr}, ${key}`)
|
|
||||||
|
|
||||||
if (key) {
|
|
||||||
console.log(`Auth success, saving key: ${key}.`)
|
|
||||||
window.localStorage.setItem("key", payload.message.key)
|
|
||||||
auth.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
console.log(`Auth failed, clearing local storage.`)
|
|
||||||
window.localStorage.clear()
|
|
||||||
auth.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Session is ready.")
|
|
||||||
sessionReady.value = true
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post navbar action to backend.
|
|
||||||
const callNavbar = (action: string) => {
|
|
||||||
invoke("nav-bar", action);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.ipcRenderer.on("on-connect", onOpen)
|
console.log("APP:mounted.")
|
||||||
window.ipcRenderer.on("auth-response", onAuthResponse)
|
window.ipcRenderer.on("update-state", updateState)
|
||||||
window.ipcRenderer.on("window-ready", () => windowReady.value = true);
|
post("app-mounted", "")
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.ipcRenderer.removeAllListeners("on-connect")
|
window.ipcRenderer.removeAllListeners("update-state")
|
||||||
window.ipcRenderer.removeAllListeners("window-ready")
|
|
||||||
window.ipcRenderer.removeAllListeners("auth-response")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
callNavbar,
|
ready,
|
||||||
sessionReady,
|
profile,
|
||||||
windowReady,
|
post,
|
||||||
auth
|
newMessages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import { backgroundMitt } from './emitter';
|
import { backgroundMitt } from '@/modules/emitter';
|
||||||
|
|
||||||
const portAudio = require('naudiodon');
|
const portAudio = require('naudiodon');
|
||||||
|
|
||||||
// Audio in and out stream objects.
|
// Audio in and out stream objects.
|
||||||
let ai: typeof portAudio.AudioIO | null = null;
|
let ai: typeof portAudio.AudioIO | boolean = false;
|
||||||
let ao: typeof portAudio.AudioIO | null = null;
|
let ao: typeof portAudio.AudioIO | boolean = false;
|
||||||
|
|
||||||
// Whether activly recording.
|
// Whether activly recording.
|
||||||
let record = false;
|
let record = false;
|
||||||
|
|
@ -26,9 +26,15 @@ const audioOptions = {
|
||||||
closeOnError: false,
|
closeOnError: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Toggles record to true to begin capturing chunks.
|
||||||
|
const onRecordingStart = (_event: any, _payload: any) => {
|
||||||
|
console.log("AUDIO: Beginning audio capture.")
|
||||||
|
record = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Returns recorded audio to frontend and sets record to false.
|
// Returns recorded audio to frontend and sets record to false.
|
||||||
const onRecordingEnd = async (_event: any, payload: any) => {
|
const onRecordingEnd = async (_event: any, payload: any) => {
|
||||||
|
console.log("AUDIO:Sending audio to browser.")
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
|
|
@ -59,7 +65,7 @@ export function initAudioIO(): void {
|
||||||
|
|
||||||
// If recording, we capture the data.
|
// If recording, we capture the data.
|
||||||
if (record) {
|
if (record) {
|
||||||
console.log('Recording...')
|
console.log('AUDIO:Recording...')
|
||||||
audioContainer.input += chunk;
|
audioContainer.input += chunk;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,11 +90,11 @@ export function initAudioIO(): void {
|
||||||
// Listen to record.
|
// Listen to record.
|
||||||
console.log("AUDIO:Adding recording listeners.")
|
console.log("AUDIO:Adding recording listeners.")
|
||||||
|
|
||||||
ipcMain.removeAllListeners('start-recording');
|
ipcMain.removeAllListeners("start-recording");
|
||||||
ipcMain.on('start-recording', () => record = true);
|
ipcMain.on("start-recording", onRecordingStart);
|
||||||
|
|
||||||
ipcMain.removeHandler('stop-recording');
|
ipcMain.removeHandler("stop-recording");
|
||||||
ipcMain.handle('stop-recording', onRecordingEnd);
|
ipcMain.handle("stop-recording", onRecordingEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -110,10 +116,13 @@ function bufSplit(buf: Buffer, len: number): Array<Buffer> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Audio playback.
|
// Audio playback.
|
||||||
export function play(input: Buffer): void {
|
export function play(input: string): void {
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
const audio = bufSplit(input, 8192);
|
// Format the audio.
|
||||||
|
const audio = bufSplit(
|
||||||
|
Buffer.from(input as string, 'hex'),
|
||||||
|
8192
|
||||||
|
);
|
||||||
|
|
||||||
// Called on end of write.
|
// Called on end of write.
|
||||||
const callback = () => {
|
const callback = () => {
|
||||||
|
|
@ -131,6 +140,7 @@ export function play(input: Buffer): void {
|
||||||
function write() {
|
function write() {
|
||||||
let chunk: Buffer;
|
let chunk: Buffer;
|
||||||
let ok = true;
|
let ok = true;
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
chunk = audio[i];
|
chunk = audio[i];
|
||||||
|
|
@ -157,42 +167,13 @@ export function play(input: Buffer): void {
|
||||||
// Get's called on window close.
|
// Get's called on window close.
|
||||||
export function stopStream() {
|
export function stopStream() {
|
||||||
console.log("AUDIO:Stopping audio stream.")
|
console.log("AUDIO:Stopping audio stream.")
|
||||||
if(ai != null) {
|
if (ai) {
|
||||||
ai.quit();
|
ai.quit()
|
||||||
ai = null;
|
|
||||||
}
|
}
|
||||||
if (ao != null) {
|
if (ao) {
|
||||||
ao.quit();
|
ao.quit()
|
||||||
ao = null;
|
|
||||||
}
|
}
|
||||||
|
console.log("AUDIO:Audio closed.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// // Returns recorded audio to frontend.
|
|
||||||
// const getAudio = async (_event: any, payload: any) => {
|
|
||||||
|
|
||||||
// return new Promise((resolve, reject) => {
|
|
||||||
|
|
||||||
// // Handle auth response from server.
|
|
||||||
// backgroundMitt.once('auth-res', (res: string) => {
|
|
||||||
|
|
||||||
// if (res == "locked") {
|
|
||||||
// reject(res);
|
|
||||||
// } else {
|
|
||||||
// console.log('Success!');
|
|
||||||
// auth = true;
|
|
||||||
// resolve(res);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (typeof payload !== "string") {
|
|
||||||
// payload = JSON.stringify(payload)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Send token to backend
|
|
||||||
// socket.send(payload);
|
|
||||||
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import { backgroundMitt } from "./emitter";
|
|
||||||
import { renderMessage } from "@/modules/message";
|
|
||||||
import { Profile, StandardMessage, Annotation } from "@/types/message/index";
|
|
||||||
import { play } from "./audio";
|
|
||||||
|
|
||||||
|
|
||||||
export const handleAuthMessage = (message: string) => {
|
|
||||||
backgroundMitt.emit('ipc-renderer', {
|
|
||||||
endpoint: 'auth-response',
|
|
||||||
message: message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const handleProfileMessage = (message: Profile) => {
|
|
||||||
backgroundMitt.emit('ipc-renderer', {
|
|
||||||
endpoint: 'update-profile',
|
|
||||||
message: message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const handleStandardMessage = (m: StandardMessage) => {
|
|
||||||
|
|
||||||
// Create a render message object.
|
|
||||||
const message = renderMessage(
|
|
||||||
m.content.text, m.content.audio, m.context, m.modifier);
|
|
||||||
|
|
||||||
// Play audio if any.
|
|
||||||
if (message.content.audio) {
|
|
||||||
const audioBytes = Buffer.from(m.content.audio as string, 'hex');
|
|
||||||
m.content.audio = true;
|
|
||||||
play(audioBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
backgroundMitt.emit('ipc-renderer', {
|
|
||||||
endpoint: 'render-message',
|
|
||||||
message: message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const handleAnnotation = (message: Annotation) => {
|
|
||||||
backgroundMitt.emit('ipc-renderer', {
|
|
||||||
endpoint: 'render-message',
|
|
||||||
message: message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
60
src/background/helpers.ts
Normal file
60
src/background/helpers.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import { backgroundMitt } from "@/modules/emitter";
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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.")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -37,15 +37,10 @@ export function initApp(dev: boolean): void {
|
||||||
main()
|
main()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Quit app on window closed.
|
app.on("before-quit", () => {
|
||||||
app.on("window-all-closed", () => {
|
|
||||||
console.log("MAIN:Quitting app.")
|
|
||||||
app.quit()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Shutdown audio streams peacefully.
|
app.on("window-all-closed", () => {
|
||||||
app.on("before-quit", () => {
|
|
||||||
stopStream()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// When user clicks app icon (re-open)
|
// When user clicks app icon (re-open)
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,131 @@
|
||||||
/*
|
/*
|
||||||
* Creates a websocket session with Crimata Servers.
|
* Creates a websocket session with Crimata Servers.
|
||||||
*
|
*
|
||||||
* Connects to Servers and attempts token authentication. Will send the result
|
* Connects to Servers and attempts key authentication. Server will respond
|
||||||
* of the authentication to the window. It will then serve as a communication
|
* with key and user profile. We send the profile to the browser. We also
|
||||||
* interface between the window and the servers. It will automatically try to
|
* resend this information on new broser window. We then serve as a
|
||||||
* reconnect on websocket disconnect.
|
* communication interface between the window and the servers. It will
|
||||||
|
* automatically try to reconnect on websocket disconnect.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { backgroundMitt } from './emitter';
|
import { backgroundMitt } from "@/modules/emitter";
|
||||||
import { ipcMain, IpcMainEvent } from "electron";
|
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 win = true;
|
||||||
let key: string;
|
|
||||||
|
|
||||||
|
// Info saved to json on quit (key, newMessages).
|
||||||
|
let state: SessionState;
|
||||||
|
|
||||||
|
// Profile of current user.
|
||||||
|
let profile: Profile | boolean;
|
||||||
|
|
||||||
|
// 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.")
|
||||||
|
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.
|
// Calls appropriate endpoint for a server message.
|
||||||
const onMessage = (data: string) => {
|
const onMessage = (data: string) => {
|
||||||
const message = JSON.parse(data)
|
let message = JSON.parse(data)
|
||||||
|
|
||||||
|
// AuthProtocol message.
|
||||||
if (message.hasOwnProperty("key")) {
|
if (message.hasOwnProperty("key")) {
|
||||||
handleAuthMessage(message)
|
updateState(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Standard message.
|
||||||
else if (message.content) {
|
else if (message.content) {
|
||||||
handleStandardMessage(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (message.first) {
|
// Convert to render message
|
||||||
handleProfileMessage(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 {
|
else {
|
||||||
handleAnnotation(message)
|
if (win) {
|
||||||
|
ipcEmit("render-message", message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Attempt token authentication onOpen.
|
// When socket connects, we update state.
|
||||||
const onOpen = () => {
|
const onOpen = () => {
|
||||||
if (key) {
|
console.log(`SESS:Sending key: ${state.key}`)
|
||||||
|
|
||||||
|
if (state) {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
"key": key,
|
"key": state.key,
|
||||||
"usr": false,
|
"usr": false,
|
||||||
"pwd": false
|
"pwd": false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Websockets module.
|
||||||
const { createSocket, sendMessage } = useWebSockets(onMessage, onOpen);
|
const { createSocket, sendMessage } = useWebSockets(onMessage, onOpen);
|
||||||
|
|
||||||
// Handle messages from window/client.
|
// Handle messages from window/client.
|
||||||
const onMessageFromWindow = (_event: IpcMainEvent, payload: any) => {
|
const onClientMessage = (_event: IpcMainEvent, payload: any) => {
|
||||||
console.log("New client message")
|
console.log("New client message")
|
||||||
|
|
||||||
const success = sendMessage(payload)
|
const success = sendMessage(payload)
|
||||||
|
|
@ -68,17 +137,30 @@ const onMessageFromWindow = (_event: IpcMainEvent, payload: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call this to initialize session with Crimata servers.
|
// Call this to initialize session with Crimata servers.
|
||||||
export const initSession = (key: string) => {
|
export const initSession = () => {
|
||||||
|
console.log("SESS:Creating new session.")
|
||||||
|
|
||||||
// Set the key.
|
// Load Json or createState.
|
||||||
key = key;
|
state = loadState("session.json")
|
||||||
|
|
||||||
|
console.log("SESS:State loaded: \n" +
|
||||||
|
` key: ${state.key}\n` +
|
||||||
|
` new: ${state.newMessages}`)
|
||||||
|
|
||||||
// Open socket connection.
|
// Open socket connection.
|
||||||
createSocket()
|
createSocket()
|
||||||
|
|
||||||
|
// Attack browser window init listener.
|
||||||
|
ipcMain.removeAllListeners("app-mounted")
|
||||||
|
ipcMain.on("app-mounted", onNewBrowserWindow);
|
||||||
|
|
||||||
// Attach listeners for frontend.
|
// Attach listeners for frontend.
|
||||||
ipcMain.removeAllListeners("client-message")
|
ipcMain.removeAllListeners("client-message")
|
||||||
ipcMain.on("client-message", onMessageFromWindow);
|
ipcMain.on("client-message", onClientMessage);
|
||||||
|
|
||||||
|
// Keep win up-to-date.
|
||||||
}
|
backgroundMitt.on('window-active', (state: boolean) => {
|
||||||
|
win = state;
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
|
|
||||||
import { BrowserWindow, ipcMain } from "electron";
|
import { BrowserWindow, ipcMain } from "electron";
|
||||||
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
|
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
|
||||||
import { backgroundMitt } from './emitter';
|
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 * as path from "path";
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
|
|
@ -13,6 +14,7 @@ interface IpcRendererPayload {
|
||||||
}
|
}
|
||||||
|
|
||||||
let win: BrowserWindow | null;
|
let win: BrowserWindow | null;
|
||||||
|
let winState: WindowState;
|
||||||
|
|
||||||
// Called when a NavBar button is pressed.
|
// Called when a NavBar button is pressed.
|
||||||
const onNavBar = (_event: any, action: string): void => {
|
const onNavBar = (_event: any, action: string): void => {
|
||||||
|
|
@ -36,24 +38,21 @@ const renderMessage = (payload: IpcRendererPayload): void => {
|
||||||
|
|
||||||
// Write a json with position and size of window.
|
// Write a json with position and size of window.
|
||||||
const saveWindowState = () => {
|
const saveWindowState = () => {
|
||||||
if (win) {
|
if (win) {
|
||||||
const bounds = win.getBounds();
|
const bounds = win.getBounds();
|
||||||
const position = win.getPosition();
|
const position = win.getPosition();
|
||||||
|
|
||||||
const state = JSON.stringify(
|
if (winState) {
|
||||||
{
|
|
||||||
w: bounds.width,
|
winState.width = bounds.width;
|
||||||
h: bounds.height,
|
winState.height = bounds.height;
|
||||||
x: position[0],
|
winState.x = position[0];
|
||||||
y: position[1]
|
winState.y = position[1]
|
||||||
}
|
|
||||||
);
|
saveToJson("window.json", winState)
|
||||||
|
|
||||||
fs.writeFile('windowState.json', state, (err) => {
|
|
||||||
if (err) throw err;
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do this on window mount.
|
// Do this on window mount.
|
||||||
|
|
@ -64,12 +63,12 @@ const onWindowMount = (): void => {
|
||||||
backgroundMitt.emit('window-active', true);
|
backgroundMitt.emit('window-active', true);
|
||||||
|
|
||||||
// Handle win nav-bar event.
|
// Handle win nav-bar event.
|
||||||
ipcMain.removeHandler("nav-bar") // avoid setting duplicate handlers
|
ipcMain.removeAllListeners("nav-bar") // avoid setting duplicate handlers
|
||||||
ipcMain.handle("nav-bar", onNavBar);
|
ipcMain.on("nav-bar", onNavBar);
|
||||||
|
|
||||||
// Gateway for messages to the frontend.
|
// Gateway for messages to the frontend.
|
||||||
backgroundMitt.removeAllListeners('ipc-renderer')
|
backgroundMitt.removeAllListeners("ipc-renderer")
|
||||||
backgroundMitt.on('ipc-renderer', renderMessage);
|
backgroundMitt.on("ipc-renderer", renderMessage);
|
||||||
|
|
||||||
console.log("BW:Listeners created.")
|
console.log("BW:Listeners created.")
|
||||||
}
|
}
|
||||||
|
|
@ -84,20 +83,20 @@ const onWindowDismount = (): void => {
|
||||||
// function used by run.ts to create the main window.
|
// function used by run.ts to create the main window.
|
||||||
export async function createWindow(): Promise<void> {
|
export async function createWindow(): Promise<void> {
|
||||||
return new Promise((resolve, _reject) => {
|
return new Promise((resolve, _reject) => {
|
||||||
console.log("BW:Creating browser window. ")
|
|
||||||
|
|
||||||
// avoid creating duplicate windows.
|
// avoid creating duplicate windows.
|
||||||
if (win) resolve();
|
if (win) resolve();
|
||||||
|
|
||||||
// Load the saved window state.
|
// 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.
|
// Define the browser window.
|
||||||
win = new BrowserWindow({
|
win = new BrowserWindow({
|
||||||
width: state.w,
|
width: winState.width,
|
||||||
height: state.h,
|
height: winState.height,
|
||||||
x: state.x,
|
x: winState.x,
|
||||||
y: state.y,
|
y: winState.y,
|
||||||
resizable: true,
|
resizable: true,
|
||||||
backgroundColor: '#EBEBEB',
|
backgroundColor: '#EBEBEB',
|
||||||
frame: false,
|
frame: false,
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,11 @@ export default function useAudioInputController (typing: Ref) {
|
||||||
// Start recording on space bar.
|
// Start recording on space bar.
|
||||||
if (cmd == "SPACE" && !recording.value && !typing.value) {
|
if (cmd == "SPACE" && !recording.value && !typing.value) {
|
||||||
|
|
||||||
post('start-recording', "");
|
console.log("INPT:Starting record.")
|
||||||
|
post("start-recording", "");
|
||||||
|
|
||||||
showRecIcon()
|
showRecIcon()
|
||||||
recording.value = true;
|
recording.value = true;
|
||||||
console.log("Recording...")
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,7 +70,8 @@ export default function useAudioInputController (typing: Ref) {
|
||||||
emitter.emit("self-message", message);
|
emitter.emit("self-message", message);
|
||||||
|
|
||||||
// Stop recording and get audio from recorder.
|
// Stop recording and get audio from recorder.
|
||||||
const audio = await invoke('stop-recording', "");
|
console.log("INPT:Stopping record.")
|
||||||
|
const audio = await invoke("stop-recording", "");
|
||||||
|
|
||||||
// Send message to the backend for processing.
|
// Send message to the backend for processing.
|
||||||
const clientM = clientMessage("", audio, message.uid);
|
const clientM = clientMessage("", audio, message.uid);
|
||||||
|
|
@ -78,7 +79,6 @@ export default function useAudioInputController (typing: Ref) {
|
||||||
|
|
||||||
hideRecIcon()
|
hideRecIcon()
|
||||||
recording.value = false;
|
recording.value = false;
|
||||||
console.log("Stopping record...")
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,29 +23,27 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import { defineComponent, ref, onMounted, onUnmounted } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import draggify from "@/modules/draggify";
|
import draggify from "@/modules/draggify";
|
||||||
|
|
||||||
import TextInput from "@/components/inputItem/textInput.vue";
|
import TextInput from "@/components/textInput.vue";
|
||||||
|
|
||||||
import useTextInputController from
|
import useTextInputController from
|
||||||
"@/components/inputItem/controllers/textCtrl";
|
"@/components/controllers/textCtrl";
|
||||||
import useAudioInputController from
|
import useAudioInputController from
|
||||||
"@/components/inputItem/controllers/audioCtrl";
|
"@/components/controllers/audioCtrl";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "InputItem",
|
name: "InputItem",
|
||||||
|
|
||||||
|
props: ["initials"],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
TextInput
|
TextInput
|
||||||
},
|
},
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
|
|
||||||
// Mark input item with user's initials.
|
|
||||||
const initials = ref("")
|
|
||||||
|
|
||||||
// Set initials.
|
|
||||||
const initialData = window.localStorage.getItem("initials")
|
|
||||||
if (initialData) initials.value = initialData
|
|
||||||
|
|
||||||
// Default values for position.
|
// Default values for position.
|
||||||
const xStart = 15;
|
const xStart = 15;
|
||||||
const yStart = window.innerHeight - 200;
|
const yStart = window.innerHeight - 200;
|
||||||
|
|
@ -57,25 +55,10 @@ export default defineComponent({
|
||||||
const { typing } = useTextInputController(elementX)
|
const { typing } = useTextInputController(elementX)
|
||||||
const { recording } = useAudioInputController(typing)
|
const { recording } = useAudioInputController(typing)
|
||||||
|
|
||||||
// Update profile functionality.
|
|
||||||
const onUpdateProfile = (_event: any, payload: any) => {
|
|
||||||
initials.value = payload.message.first[0] + payload.message.last[0]
|
|
||||||
window.localStorage.setItem("initials", initials.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
window.ipcRenderer.on("update-profile", onUpdateProfile);
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.ipcRenderer.removeAllListeners("update-profile");
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elementX,
|
elementX,
|
||||||
elementY,
|
elementY,
|
||||||
recording,
|
recording
|
||||||
initials
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -29,16 +29,14 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- back to login button: position: fixed -->
|
<!-- back to login button: position: fixed -->
|
||||||
<button class="createAccountButton button" @click.prevent="switchView">Create account</button>
|
<!-- <button class="createAccountButton button" @click.prevent="switchView">Create account</button> -->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import { defineComponent, ref } from "vue";
|
import { defineComponent, ref } from "vue";
|
||||||
import { useAuth } from "@/modules/auth";
|
|
||||||
import { useIpc } from "@/modules/ipc";
|
import { useIpc } from "@/modules/ipc";
|
||||||
import useMessages from "@/modules/messages";
|
import { authRequest } from '@/modules/message';
|
||||||
import { authRequest } from '@/modules/message';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Login",
|
name: "Login",
|
||||||
|
|
@ -46,11 +44,9 @@ export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
|
|
||||||
const { post } = useIpc();
|
const { post } = useIpc();
|
||||||
const { setToken } = useAuth();
|
|
||||||
const { resetMessages } = useMessages();
|
|
||||||
|
|
||||||
let usr = ref("");
|
const usr = ref("");
|
||||||
let pwd = ref("");
|
const pwd = ref("");
|
||||||
|
|
||||||
// Submit login credentials to the backend.
|
// Submit login credentials to the backend.
|
||||||
const submitForm = () => {
|
const submitForm = () => {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,14 @@
|
||||||
<!-- Three types of messages: self (sf), friend (fr), and crimata (ai) -->
|
<!-- Three types of messages: self (sf), friend (fr), and crimata (ai) -->
|
||||||
<!-- !denoted by the modifier attribute -->
|
<!-- !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. -->
|
<!-- Includes bubble and context. -->
|
||||||
<div
|
<div
|
||||||
:id="`${message.modifier}MessageBox`"
|
:id="`${message.modifier}MessageBox`"
|
||||||
|
|
@ -21,6 +29,12 @@
|
||||||
:class="`${message.modifier}-${message.isChild}ChildBubble`"
|
:class="`${message.modifier}-${message.isChild}ChildBubble`"
|
||||||
:id="`${message.modifier}Bubble`"
|
:id="`${message.modifier}Bubble`"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<!-- Notification dot for new messages. -->
|
||||||
|
<div v-if="notify === true"
|
||||||
|
class="notify"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Show loader when audio is being transcribed. -->
|
<!-- Show loader when audio is being transcribed. -->
|
||||||
<div
|
<div
|
||||||
v-if="message.content.text === ''"
|
v-if="message.content.text === ''"
|
||||||
|
|
@ -73,21 +87,60 @@
|
||||||
<script lang='ts'>
|
<script lang='ts'>
|
||||||
|
|
||||||
import {defineComponent, ref, onMounted} from 'vue';
|
import {defineComponent, ref, onMounted} from 'vue';
|
||||||
|
import { RenderMessage } from "@/types/message/index";
|
||||||
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "MessageItem",
|
name: "MessageItem",
|
||||||
|
|
||||||
props: {
|
props: ["message"],
|
||||||
message: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setup(props) {
|
setup(props) {
|
||||||
|
|
||||||
|
let addListener = false;
|
||||||
|
|
||||||
|
const notify = ref(false)
|
||||||
const isPlaying = 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) => {
|
const onStopPlayback = (e: any) => {
|
||||||
window.ipcRenderer.removeAllListeners("stop-playback-anim");
|
window.ipcRenderer.removeAllListeners("stop-playback-anim");
|
||||||
isPlaying.value = false
|
isPlaying.value = false
|
||||||
|
|
@ -95,6 +148,11 @@
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
||||||
|
// Listen for window focus event.
|
||||||
|
if (addListener) {
|
||||||
|
document.addEventListener("visibilitychange", onVisibilityChange)
|
||||||
|
}
|
||||||
|
|
||||||
// Turn on audio playback animation if audio.
|
// Turn on audio playback animation if audio.
|
||||||
if (props.message.content.audio === true) {
|
if (props.message.content.audio === true) {
|
||||||
window.ipcRenderer.on("stop-playback-anim", onStopPlayback);
|
window.ipcRenderer.on("stop-playback-anim", onStopPlayback);
|
||||||
|
|
@ -104,6 +162,7 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
notify,
|
||||||
isPlaying
|
isPlaying
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +192,20 @@
|
||||||
margin-bottom: 18px;
|
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 {
|
.firstChildMessage {
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
@ -183,6 +256,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.bubble {
|
.bubble {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
max-width: 66vw;
|
max-width: 66vw;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -244,6 +319,30 @@
|
||||||
border-top-left-radius: 9px;
|
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 {
|
.context {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,66 @@
|
||||||
<template>
|
<template>
|
||||||
<InputItem />
|
<InputItem :initials="profile.initials"/>
|
||||||
<Settings />
|
<Settings />
|
||||||
|
|
||||||
<div id="recIcon" />
|
<div id="recIcon" />
|
||||||
|
|
||||||
<!-- List of message bubbles. -->
|
<!-- List of message bubbles. -->
|
||||||
<div id="messenger">
|
<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]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, onUnmounted } from "vue";
|
import { defineComponent, onMounted, onUnmounted } from "vue";
|
||||||
import MessageItem from "@/components/messageItem.vue";
|
import Message from "@/components/message.vue";
|
||||||
import InputItem from "@/components/inputItem/inputItem.vue";
|
import InputItem from "@/components/inputItem.vue";
|
||||||
import Settings from "@/components/settings.vue";
|
import Settings from "@/components/settings.vue";
|
||||||
import useMitt from "@/modules/mitt";
|
import useMitt from "@/modules/mitt";
|
||||||
import useMessages from '@/modules/messages';
|
import useMessages from "@/modules/messages";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Messenger",
|
name: "Messenger",
|
||||||
|
|
||||||
|
props: ["profile", "newMessages"],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
MessageItem,
|
Message,
|
||||||
InputItem,
|
InputItem,
|
||||||
Settings
|
Settings
|
||||||
},
|
},
|
||||||
setup() {
|
|
||||||
|
setup(props) {
|
||||||
|
|
||||||
// Listen for self-messages from inputItem.
|
// Listen for self-messages from inputItem.
|
||||||
const { emitter } = useMitt();
|
const { emitter } = useMitt();
|
||||||
|
|
||||||
// Handle messages in view.
|
// Handle messages in view.
|
||||||
const { messages, updateMessageView } = useMessages();
|
const { messages, prepMessageView, updateMessageView } = useMessages();
|
||||||
|
|
||||||
// Update message view on new content.
|
|
||||||
const onNewContent = (_event: any, payload: any) => {
|
|
||||||
updateMessageView(payload.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
const crimataId = props.profile.crimataId;
|
||||||
|
console.log(`MSGR:Initializing messenger for ${crimataId}.`)
|
||||||
|
|
||||||
// Front end listener.
|
// 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));
|
emitter.on('self-message', (message) => updateMessageView(message));
|
||||||
|
window.ipcRenderer.on("render-message", (_e: any, payload: any) => {
|
||||||
|
updateMessageView(payload.message)
|
||||||
|
});
|
||||||
|
|
||||||
// Back end listener.
|
// Save the usr for next time.
|
||||||
window.ipcRenderer.on("render-message", onNewContent);
|
window.localStorage.setItem("last_usr", crimataId)
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -58,11 +73,14 @@ export default defineComponent({
|
||||||
return {
|
return {
|
||||||
messages
|
messages
|
||||||
};
|
};
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
#messenger {
|
#messenger {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import { defineComponent, ref } from "vue";
|
import { defineComponent, ref } from "vue";
|
||||||
import { useAuth } from "@/modules/auth";
|
|
||||||
import { useIpc } from "@/modules/ipc";
|
import { useIpc } from "@/modules/ipc";
|
||||||
import { logoutRequest } from '@/modules/message';
|
import { logoutRequest } from '@/modules/message';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
import { reactive, toRefs } from 'vue';
|
|
||||||
import { useIpc } from './ipc';
|
|
||||||
|
|
||||||
interface AuthState {
|
|
||||||
accessToken: string | null;
|
|
||||||
error?: Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = reactive<AuthState>({
|
|
||||||
accessToken: null,
|
|
||||||
error: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const AUTH_KEY = 'crimata_token';
|
|
||||||
|
|
||||||
let token: string | null;
|
|
||||||
|
|
||||||
token = window.localStorage.getItem(AUTH_KEY);
|
|
||||||
if (token) {
|
|
||||||
state.accessToken = token;
|
|
||||||
window.localStorage.setItem(AUTH_KEY, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Token authentication.
|
|
||||||
const authToken = async () => {
|
|
||||||
console.log("Calling auth token!!")
|
|
||||||
const { invoke } = useIpc();
|
|
||||||
try {
|
|
||||||
token = window.localStorage.getItem(AUTH_KEY);
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
token = "no-token"
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Token ${token}`)
|
|
||||||
const res = await invoke('auth-session', token);
|
|
||||||
window.localStorage.setItem(AUTH_KEY, res);
|
|
||||||
state.accessToken = res;
|
|
||||||
}
|
|
||||||
catch(e) {
|
|
||||||
state.error = e;
|
|
||||||
console.log('ERROR: failed authentication');
|
|
||||||
window.localStorage.removeItem(AUTH_KEY);
|
|
||||||
state.accessToken = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets called on socket open.
|
|
||||||
window.ipcRenderer.on("auth", async (_event, _arg) => {
|
|
||||||
await authToken();
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useAuth = () => {
|
|
||||||
|
|
||||||
const setToken = (token: string) => {
|
|
||||||
window.localStorage.setItem(AUTH_KEY, token);
|
|
||||||
state.accessToken = token;
|
|
||||||
state.error = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logout = (): Promise<null> => {
|
|
||||||
window.localStorage.removeItem(AUTH_KEY);
|
|
||||||
return Promise.resolve(state.accessToken = null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
setToken,
|
|
||||||
logout,
|
|
||||||
...toRefs(state), // accessToken, error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
export const useIpc = () => {
|
export const useIpc = () => {
|
||||||
|
|
||||||
const invoke = async (endpoint: string, payload: any) => {
|
const invoke = async (endpoint: string, payload: any) => {
|
||||||
try {
|
try {
|
||||||
const res = await window.ipcRenderer.invoke(endpoint, payload);
|
const res = await window.ipcRenderer.invoke(endpoint, payload);
|
||||||
|
|
@ -10,10 +11,7 @@ export const useIpc = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const post = (endpoint: string, payload: any) => {
|
const post = (endpoint: string, payload: any) => {
|
||||||
window.postMessage({
|
window.ipcRenderer.send(endpoint, payload);
|
||||||
endpoint: endpoint,
|
|
||||||
content: payload
|
|
||||||
}, '*')
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@ export const renderMessage = (text: boolean | string, audio: boolean | string, c
|
||||||
modifier: modifier,
|
modifier: modifier,
|
||||||
time: getTimeStamp(),
|
time: getTimeStamp(),
|
||||||
uid: uuidv4(),
|
uid: uuidv4(),
|
||||||
isChild: "none"
|
isChild: "none",
|
||||||
|
seen: false,
|
||||||
|
newMessage: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ const updateMessage = (annotation: Annotation) => {
|
||||||
message.content.text = annotation.text
|
message.content.text = annotation.text
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadMessages = () => {
|
const loadSavedMessages = () => {
|
||||||
const rawData = window.localStorage.getItem("crimata_messages");
|
const rawData = window.localStorage.getItem("crimata_messages");
|
||||||
if (rawData) {
|
if (rawData) {
|
||||||
const messageData = JSON.parse(rawData)
|
const messageData = JSON.parse(rawData)
|
||||||
|
|
@ -39,8 +39,8 @@ const updateGrouping = () => {
|
||||||
const refs = Array.from(messages.value.keys())
|
const refs = Array.from(messages.value.keys())
|
||||||
|
|
||||||
// Get the last three messages.
|
// Get the last three messages.
|
||||||
const first = messages.value.get(refs[refs.length - 1])
|
const first = messages.value.get(refs[refs.length - 1])
|
||||||
const second = messages.value.get(refs[refs.length - 2])
|
const second = messages.value.get(refs[refs.length - 2])
|
||||||
const third = messages.value.get(refs[refs.length - 3])
|
const third = messages.value.get(refs[refs.length - 3])
|
||||||
|
|
||||||
// If messages are simmilar, update the classes.
|
// If messages are simmilar, update the classes.
|
||||||
|
|
@ -58,51 +58,68 @@ const updateGrouping = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { setScroll, updateScrollRef, adjustScroll } = useScroll("messenger");
|
|
||||||
|
|
||||||
loadMessages()
|
|
||||||
setTimeout(setScroll, 1000);
|
|
||||||
|
|
||||||
|
|
||||||
export default function useMessages() {
|
export default function useMessages() {
|
||||||
|
|
||||||
// Main function for updating the message view.
|
// Scroll controller.
|
||||||
const updateMessageView = (message: RenderMessage | Annotation) => {
|
const { setScroll, updateScrollRef, adjustScroll } = useScroll("messenger");
|
||||||
|
|
||||||
// Step 1: See if user is scrolled down.
|
// Main function for updating the message view.
|
||||||
updateScrollRef()
|
const updateMessageView = (message: RenderMessage | Annotation) => {
|
||||||
|
|
||||||
// Step 2: Add the new content to the view.
|
// Step 1: See if user is scrolled down.
|
||||||
if ("content" in message) {
|
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)
|
addMessage(message)
|
||||||
} else {
|
})
|
||||||
updateMessage(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 3: Pop off oldest message (if > 200).
|
setTimeout(setScroll.bind(true), 1000);
|
||||||
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()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resetMessages = () => {
|
return {
|
||||||
messages.value.clear()
|
messages,
|
||||||
}
|
prepMessageView,
|
||||||
|
updateMessageView
|
||||||
return {
|
}
|
||||||
messages,
|
|
||||||
updateMessageView,
|
|
||||||
resetMessages
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -5,13 +5,13 @@ export default function useScroll(element: string) {
|
||||||
let isScrolledToBottom: boolean;
|
let isScrolledToBottom: boolean;
|
||||||
|
|
||||||
// Set the initial scroll position.
|
// Set the initial scroll position.
|
||||||
const setScroll = () => {
|
const setScroll = (smooth: boolean) => {
|
||||||
const view = document.getElementById(element)
|
const view = document.getElementById(element)
|
||||||
|
|
||||||
if (view) {
|
if (view) {
|
||||||
view.scrollTo({
|
view.scrollTo({
|
||||||
top: view.scrollHeight - view.clientHeight,
|
top: view.scrollHeight - view.clientHeight,
|
||||||
behavior: 'smooth'
|
behavior: (smooth) ? 'smooth' : 'auto'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ export interface RenderMessage {
|
||||||
time: number;
|
time: number;
|
||||||
uid: string;
|
uid: string;
|
||||||
isChild: string;
|
isChild: string;
|
||||||
|
seen: boolean;
|
||||||
|
newMessage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientMessage {
|
export interface ClientMessage {
|
||||||
|
|
@ -16,27 +18,37 @@ export interface ClientMessage {
|
||||||
uid: string;
|
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 {
|
export interface AuthRequest {
|
||||||
key: boolean | string;
|
key: boolean | string;
|
||||||
usr: boolean | string;
|
usr: boolean | string;
|
||||||
pwd: boolean | string;
|
pwd: boolean | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogoutRequest {
|
export interface Profile {
|
||||||
logout: boolean;
|
crimataId: string;
|
||||||
|
alias: string;
|
||||||
|
initials: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Possible messages from server:
|
export interface AuthProtocol {
|
||||||
|
key: boolean | string;
|
||||||
|
profile: boolean | Profile;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Profile {
|
export interface LogoutRequest {
|
||||||
crimata_id: string;
|
logout: boolean;
|
||||||
title: string;
|
|
||||||
first: string;
|
|
||||||
middle: string;
|
|
||||||
last: string;
|
|
||||||
suffix: string | number;
|
|
||||||
nickname: string;
|
|
||||||
full: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Annotation {
|
export interface Annotation {
|
||||||
|
|
|
||||||
1
window.json
Normal file
1
window.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"width":386,"height":815,"x":1720,"y":495}
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
<<<<<<< HEAD
|
|
||||||
{"w":629,"h":500,"x":1423,"y":657}
|
|
||||||
=======
|
|
||||||
{"w":622,"h":616,"x":1163,"y":499}
|
|
||||||
>>>>>>> logout
|
|
||||||
Loading…
Reference in a new issue