more changes

This commit is contained in:
Andrew Gundersen 2021-06-12 09:49:41 -05:00
commit 9e8641b4de
11 changed files with 167 additions and 285 deletions

View file

@ -4,20 +4,33 @@ import axios from "axios";
const { post } = useHttp(); const { post } = useHttp();
export const submit = async (email: string, password: string) => ( export const usrPwdAuth = async (email: string, password: string) => {
await post('/account/login', { email, password })
)
export const fetchAccount = async (email: string, token: string) => ( try {
await axios({ return await post('/account/login', { email, password })
} catch (e) {
return null;
}
}
export const tokenAuth = async (cid: string, token: string) => {
try {
return await axios({
url: "http://127.0.0.1:3000/api/account/profile", url: "http://127.0.0.1:3000/api/account/profile",
headers: { headers: {
Cookie: `jwt=${token}` Cookie: `jwt=${token}`
}, },
method: 'GET', method: 'GET',
data: { data: {
email, cid,
} }
}) })
)
} catch (e) {
return null;
}
}

36
src/composables/canvas.ts Normal file
View file

@ -0,0 +1,36 @@
export default class Canvas {
messages: Message[];
/* seed canvas with messages on init */
constructor(messages: Message[]) {
this.messages = messages;
ipcEmit("seed-view", this.messages);
}
/* add a new message to the canvas */
add(message: Message) {
this.messages.push(message);
ipcEmit("update-view", message);
}
/* update an existing message */
update(message: Message) {
/* get the target message */
let target_message = this.messages.filter((m: Message) => {
return m.uid = message.uid;
})[0];
/* replace the target message */
if (target_message) {
target_message = message;
ipcEmit("update-view", message);
}
}
}

View file

@ -1,16 +0,0 @@
const Store = require('electron-store');
const schema = {
key: {
type: 'string',
},
crimataId: {
type: 'string'
}
};
export const store = new Store({
schema,
encryptionKey: "super user test"
});

View file

@ -3,7 +3,7 @@
import WebSocket from 'ws'; import WebSocket from 'ws';
export default function useWebSockets(receiveCallback: (s: string) => void, openCallback?: () => void) { export default function useWebSockets(onMessageCallback: (s: string) => void) {
let socket: WebSocket | null = null; let socket: WebSocket | null = null;
@ -19,36 +19,25 @@ export default function useWebSockets(receiveCallback: (s: string) => void, open
}); });
} }
const onOpen = (_event: WebSocket.OpenEvent) => { const connect = (socketUrl: string, secret: string) => {
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) => {
/* create a new socket */
socket = new WebSocket(socketUrl) socket = new WebSocket(socketUrl)
// Add listeners. /* add event listeners */
socket.addEventListener("open", onOpen);
socket.addEventListener("message", onServerMessage); socket.on("open", () => {
socket.addEventListener("close", onClose); if (socket)
socket.addEventListener("error", onError); socket.send(secret);
});
socket.on("message", (event: WebSocket.MessageEvent) => {
onMessageCallback(event.data.toString())
});
socket.on("close", () => {
return
});
} }
@ -59,15 +48,10 @@ export default function useWebSockets(receiveCallback: (s: string) => void, open
} }
} }
const checkConnection = () => {
return true;
}
return { return {
createSocket, connect,
send, send,
close, close
checkConnection
}; };
} }

View file

@ -9,37 +9,29 @@
* *
*/ */
import { fetchAccount, submit } from "@/api/account"; import { tokenAuth, usrPwdAuth } from "@/api/account";
import { launchSession, endSession } from "@/session"; import { launchSession, endSession } from "@/session";
import useIpc from "@/ipc/index"; import useIpc from "@/ipc/index";
import store from "@/composables/store"; import store from "@/composables/store";
/* user profile, signals whether user is logged in */
let auth: Profile | null = null;
/* authenticate the user */ /* authenticate the user */
export async function authenticate(email: string, password: string) { export async function authenticate(email: string, password: string) {
/* attempt normal login */ /* attempt normal login */
try { const platformKey, token, crimataId = await usrPwdAuth(email, password);
auth = await submit(email, password);
} catch (e) {
console.log(e);
}
/* launch if profile */ /* launch if successful */
if (auth) { if (platformKey)
launchSession(auth); launchSession(platformKey, crimataId);
}
/* save the token */
store.set("token", token);
} }
/* logout the user, end the session */ /* logout the user, end the session */
export function deauthenticate() { export function deauthenticate() {
/* set profile back to null */
auth = null;
/* terminate the session */ /* terminate the session */
endSession(); endSession();
@ -47,26 +39,24 @@ export function deauthenticate() {
export default async function main() { 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 */ /* initiate controls for frontend to use when needed */
useIpc(); useIpc();
/* launch browser window */
await createWindow();
/* attempt to get a login token from the store */
const token = store.get("token");
/* try to login with it, returns platform secret and new token on success */
if (token)
const newToken, profile = await tokenAuth(token);
/* if secret, we launch a session */
if (newToken)
launchSession(newToken, profile);
/* finally, save the most recent token */
store.set("token", newToken);
} }

View file

@ -6,8 +6,9 @@
<!-- Main Components --> <!-- Main Components -->
<Messenger <Messenger
v-if="profile" v-if="state.profile"
:profile="profile" :profile="state.profile"
:messages="state.messages"
/> />
<Login v-else /> <Login v-else />
@ -39,13 +40,13 @@ export default defineComponent({
setup() { setup() {
const state: Ref; const crimataId = ref(false);
onMounted(async () => { onMounted(async () => {
console.log("[APP]:mounted."); console.log("[APP]:mounted.");
/* listen for auth related messages */ /* listen for auth related messages */
window.addEventListener("update-state", (event: any) => { window.addEventListener("update-auth", (event: any) => {
state.value = event.data; state.value = event.data;
}); });
@ -56,7 +57,7 @@ export default defineComponent({
}); });
return { return {
profile crimataId
} }
} }
}) })
@ -68,7 +69,6 @@ export default defineComponent({
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
// Background color set in window.ts
} }
#app { #app {

View file

@ -1,143 +1,38 @@
import { ref } from 'vue'; import { ref } from 'vue';
import invokeSavedMessages from "@/render/ipc";
import useScroll from "@/render/composables/scroll"; import useScroll from "@/render/composables/scroll";
const messages = ref(new Map()); const canvas = ref();
function getTimeStamp(): number { /* seed the canvas with messages */
const currentdate = new Date(); const seedCanvas = (messages: Message[]) => {
return currentdate.getTime(); canvas.value = messages;
} }
const newViewMessage = (message: Message): ViewMessage => { const addMessage = (message: Message) => {
return { canvas.value.push(message);
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 updateMessage = (message: Message) => {
const viewMessage = messages.value.get(message.uid);
viewMessage.context = message.context;
viewMessage.text = message.text;
}
const loadSavedMessages = async () => { let target_message = canvas.value.filter((m: Message) => {
const messageData = await invokeSavedMessages(); return m.uid = message.uid;
messages.value = new Map(Object.entries(messageData)); })[0];
}
const saveMessages = () => { if (target_message) {
const messageData = Object.fromEntries(messages.value); target_message = message;
// 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() { export default function useMessages() {
const { updateScrollRef, adjustScroll } = useScroll("messenger"); 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 { return {
messages, canvas,
updateMessageView, seedCanvas,
loadSavedMessages addMessage,
} updateMessage
};
} }
// // 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);
// }
// }

View file

@ -11,6 +11,7 @@
v-for="message in messages" v-for="message in messages"
:text="message.text" :text="message.text"
:context="message.context" :context="message.context"
:child
:key="message[0]" :key="message[0]"
/> />
</div> </div>
@ -28,7 +29,7 @@ import useMessages from "@/render/composables/messages";
export default defineComponent({ export default defineComponent({
name: "Messenger", name: "Messenger",
props: ["state"], props: ["profile", "messages"],
components: { components: {
Message, Message,
@ -43,12 +44,19 @@ export default defineComponent({
onMounted(() => { onMounted(() => {
/* populate the message view with existing messages */ /* seed messages */
updateMessageView(state.savedMessages, state.newMessages); window.ipcRenderer.on("init-messages", (e_: any, payload: any) => {
seedMessages(payload.messages);
});
/* wait and listen for new messages to come in */ /* add a new message */
window.ipcRenderer.on("new-message", (_e: any, payload: any) => { window.ipcRenderer.on("add-message", (_e: any, payload: any) => {
updateMessageView(payload.message); addMessage(payload.message);
});
/* update an existing message */
window.ipcRenderer.on("update-message", (_e: any, payload: any) => {
updateMessage(payload.message);
}); });
}); });

View file

@ -3,7 +3,11 @@
import useAudio from "@/audio"; import useAudio from "@/audio";
import { loadState, saveState, emitState } from "@/state"; import Canvas from "@/composables/convas";
import ipcEmit from "@/composables/emitter";
/* data structure of messages that's tied to the UI */
let msgrState: UIState | null = null;
/* start and stop audio functionality */ /* start and stop audio functionality */
const { initAudio, closeAudio } = useAudio(); const { initAudio, closeAudio } = useAudio();
@ -12,53 +16,57 @@ const { initAudio, closeAudio } = useAudio();
* Controls for interfacing with the platform. * Controls for interfacing with the platform.
* Takes an onMessage callback which we define below. * Takes an onMessage callback which we define below.
*/ */
const { connect, send, close } = usePlatform((message: Message) => { const { connect, send, close } = useWebsockets((content: any) => {
/* add the message to the state */ /* if the platform fails to authenticate, we must back down */
addMessage(message); if (content === "auth_error") {
deauthenticate();
return;
}
/* push the message to the browser */ /* on init, platform sends state, used to init canvas */
if (win) emit("new-message", message); if (isInitMessage(content)) {
uiState.set(content);
}
else if (isAddMessage(content)) {
uiState.add(content);
}
else {
uiState.update(content);
}
}); });
/* send a message to the platform */ /* send a message to the platform */
export function sendMessage(message: Message) { 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 */ /* socket send */
send(message); send(message);
} }
/* launch a new session (the main process for authenticated users) */ /* launch a new session (the main process for authenticated users) */
export function launchSession(profile: Profile) { export function launchSession(platformKey: string, crimata_id: string) {
/* load any previously saved state for that user */
loadState(profile);
/* connect to the platform */ /* connect to the platform */
connect(profile); connect(PLATFORM_URL, platformKey);
/* initialize the audio streams */ /* initialize the audio streams */
// initAudio(); initAudio();
/* finally we can push state to browser */ /* push profile to window */
if (win) emitState(); if (win)
ipcEmit("update-auth", crimata_id);
} }
export function endSession() { export function endSession() {
closeAudioStreams(); closeAudio();
closeSocket(); close();
state.clear(); }
}

View file

@ -1,29 +0,0 @@
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);
}

View file

@ -9,8 +9,6 @@ interface Message {
interface ViewMessage extends Message { interface ViewMessage extends Message {
child: string; child: string;
seen: boolean;
newMessage: boolean;
} }
interface WindowState { interface WindowState {
@ -26,11 +24,6 @@ interface Profile {
initials: string; initials: string;
} }
interface State {
profile: Profile | null;
messages
}
interface LoginPayload { interface LoginPayload {
email: string; email: string;
password: string; password: string;