auth enhancements amongst other things

This commit is contained in:
Andrew Gundersen 2021-03-29 10:15:54 -05:00
commit 85c3799fca
21 changed files with 336 additions and 363 deletions

View file

@ -1,5 +1,6 @@
<template>
<div id="app" v-if="(windowReady && sessionReady)">
<!-- Only render when profile has been set -->
<div id="app" v-if="(ready)">
<button class="titlebar" />
@ -7,21 +8,25 @@
<span class="menu">
<button
class="menuButton exitButton"
@click.prevent="callNavbar('close')"
@click.prevent="post('nav-bar', 'close')"
/>
<button
class="menuButton minimizeButton"
@click.prevent="callNavbar('min')"
@click.prevent="post('nav-bar', 'min')"
/>
</span>
<!-- Main Pages -->
<Messenger v-if="auth"/>
<Messenger
v-if="profile"
:profile="profile"
:newMessages="newMessages"
/>
<Login v-else />
</div>
<!-- Show spash screen if not ready -->
<!-- Show spash screen if ready is false -->
<Splash v-else />
</template>
@ -31,6 +36,7 @@ import { defineComponent, onMounted, onUnmounted, ref } from "vue";
import { IpcRendererEvent } from "electron";
import { authRequest } from '@/modules/message';
import { useIpc } from "@/modules/ipc";
import { Profile } from "@/types/message";
import Splash from "@/components/splash.vue";
import Messenger from "@/components/messenger.vue";
@ -44,74 +50,52 @@ export default defineComponent({
},
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
// 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 (payload.message.profile) {
console.log(`Logged-in, showing Messenger View.`)
} else {
console.log(`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.
const onAuthResponse = (_event: IpcRendererEvent, payload: any) => {
const key = payload.message.key
const usr = payload.message.usr
console.log(`Received auth response: ${usr}, ${key}`)
ready.value = true;
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(() => {
window.ipcRenderer.on("on-connect", onOpen)
window.ipcRenderer.on("auth-response", onAuthResponse)
window.ipcRenderer.on("window-ready", () => windowReady.value = true);
console.log("APP:mounted.")
window.ipcRenderer.on("update-state", updateState)
post("app-mounted", "")
});
onUnmounted(() => {
window.ipcRenderer.removeAllListeners("on-connect")
window.ipcRenderer.removeAllListeners("window-ready")
window.ipcRenderer.removeAllListeners("auth-response")
window.ipcRenderer.removeAllListeners("update-state")
})
return {
callNavbar,
sessionReady,
windowReady,
auth
ready,
profile,
post,
newMessages
}
}
})

View file

@ -3,13 +3,13 @@
"use strict";
import { ipcMain } from "electron";
import { backgroundMitt } from './emitter';
import { backgroundMitt } from '@/modules/emitter';
const portAudio = require('naudiodon');
// Audio in and out stream objects.
let ai: typeof portAudio.AudioIO | null = null;
let ao: typeof portAudio.AudioIO | null = null;
let ai: typeof portAudio.AudioIO | Boolean = false;
let ao: typeof portAudio.AudioIO | Boolean = false;
// Whether activly recording.
let record = false;
@ -26,9 +26,15 @@ const audioOptions = {
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.
const onRecordingEnd = async (_event: any, payload: any) => {
console.log("AUDIO:Sending audio to browser.")
return new Promise((resolve, reject) => {
@ -59,7 +65,7 @@ export function initAudioIO(): void {
// If recording, we capture the data.
if (record) {
console.log('Recording...')
console.log('AUDIO:Recording...')
audioContainer.input += chunk;
}
@ -84,11 +90,11 @@ export function initAudioIO(): void {
// Listen to record.
console.log("AUDIO:Adding recording listeners.")
ipcMain.removeAllListeners('start-recording');
ipcMain.on('start-recording', () => record = true);
ipcMain.removeAllListeners("start-recording");
ipcMain.on("start-recording", onRecordingStart);
ipcMain.removeHandler('stop-recording');
ipcMain.handle('stop-recording', onRecordingEnd);
ipcMain.removeHandler("stop-recording");
ipcMain.handle("stop-recording", onRecordingEnd);
}
@ -110,10 +116,13 @@ function bufSplit(buf: Buffer, len: number): Array<Buffer> {
}
// Audio playback.
export function play(input: Buffer): void {
let i = 0;
export function play(input: string): void {
const audio = bufSplit(input, 8192);
// Format the audio.
const audio = bufSplit(
Buffer.from(input as string, 'hex'),
8192
);
// Called on end of write.
const callback = () => {
@ -131,6 +140,7 @@ export function play(input: Buffer): void {
function write() {
let chunk: Buffer;
let ok = true;
let i = 0;
do {
chunk = audio[i];
@ -157,42 +167,13 @@ export function play(input: Buffer): void {
// Get's called on window close.
export function stopStream() {
console.log("AUDIO:Stopping audio stream.")
if(ai != null) {
ai.quit();
ai = null;
if (ai) {
ai.quit()
}
if (ao != null) {
ao.quit();
ao = null;
if (ao) {
ao.quit()
}
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);
// });
// };

View file

@ -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
});
}

37
src/background/helpers.ts Normal file
View file

@ -0,0 +1,37 @@
import fs from 'fs';
import { backgroundMitt } from "@/modules/emitter";
import { SessionState } 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 saveState = (fileName: string, state: SessionState) => {
fs.writeFile(fileName, JSON.stringify(state), (err) => {
if (err) {
console.log("Error when saving state.")
}
});
}

View file

@ -37,15 +37,10 @@ export function initApp(dev: boolean): void {
main()
});
// Quit app on window closed.
app.on("window-all-closed", () => {
console.log("MAIN:Quitting app.")
app.quit()
app.on("before-quit", () => {
});
// Shutdown audio streams peacefully.
app.on("before-quit", () => {
stopStream()
app.on("window-all-closed", () => {
});
// When user clicks app icon (re-open)

View file

@ -1,62 +1,131 @@
/*
* 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 { 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;
// 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.")
saveState("session.json", state)
}
}
// Send state on new window.
const onNewBrowserWindow = (_event: IpcMainInvokeEvent, _payload: any) => {
console.log("SESS:New window, sending profile.")
if (profile) {
ipcEmit("update-state", {
profile: profile,
newMessages: state.newMessages
})
}
}
// Calls appropriate endpoint for a server message.
const onMessage = (data: string) => {
const message = JSON.parse(data)
let message = JSON.parse(data)
// AuthProtocol message.
if (message.hasOwnProperty("key")) {
handleAuthMessage(message)
updateState(message)
}
// Standard message.
else if (message.content) {
handleStandardMessage(message)
}
else if (message.first) {
handleProfileMessage(message)
// 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 {
handleAnnotation(message)
if (win) {
ipcEmit("render-message", message)
}
}
};
// Attempt token authentication onOpen.
// When socket connects, we update state.
const onOpen = () => {
if (key) {
console.log(`SESS:Sending key: ${state.key}`)
if (state) {
sendMessage({
"key": key,
"key": state.key,
"usr": false,
"pwd": false
})
}
}
// Websockets module.
const { createSocket, sendMessage } = useWebSockets(onMessage, onOpen);
// Handle messages from window/client.
const onMessageFromWindow = (_event: IpcMainEvent, payload: any) => {
const onClientMessage = (_event: IpcMainEvent, payload: any) => {
console.log("New client message")
const success = sendMessage(payload)
@ -68,17 +137,30 @@ const onMessageFromWindow = (_event: IpcMainEvent, payload: any) => {
}
// 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.
key = key;
// Load Json or createState.
state = loadState("session.json")
console.log("SESS:State loaded: \n" +
` key: ${state.key}\n` +
` new: ${state.newMessages}`)
// Open socket connection.
createSocket()
// Attack browser window init listener.
ipcMain.removeAllListeners("app-mounted")
ipcMain.on("app-mounted", onNewBrowserWindow);
// Attach listeners for frontend.
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;
});
}

View file

@ -2,7 +2,7 @@
import { BrowserWindow, ipcMain } from "electron";
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 * as path from "path";
import fs from 'fs';
@ -50,8 +50,9 @@ const saveWindowState = () => {
);
fs.writeFile('windowState.json', state, (err) => {
if (err) throw err;
return;
if (err) {
console.log("BW:Error saving window state.")
}
});
}
}
@ -64,12 +65,12 @@ const onWindowMount = (): void => {
backgroundMitt.emit('window-active', true);
// Handle win nav-bar event.
ipcMain.removeHandler("nav-bar") // avoid setting duplicate handlers
ipcMain.handle("nav-bar", onNavBar);
ipcMain.removeAllListeners("nav-bar") // avoid setting duplicate handlers
ipcMain.on("nav-bar", onNavBar);
// Gateway for messages to the frontend.
backgroundMitt.removeAllListeners('ipc-renderer')
backgroundMitt.on('ipc-renderer', renderMessage);
backgroundMitt.removeAllListeners("ipc-renderer")
backgroundMitt.on("ipc-renderer", renderMessage);
console.log("BW:Listeners created.")
}

View file

@ -49,11 +49,11 @@ export default function useAudioInputController (typing: Ref) {
// Start recording on space bar.
if (cmd == "SPACE" && !recording.value && !typing.value) {
post('start-recording', "");
console.log("INPT:Starting record.")
post("start-recording", "");
showRecIcon()
recording.value = true;
console.log("Recording...")
}
@ -70,7 +70,8 @@ export default function useAudioInputController (typing: Ref) {
emitter.emit("self-message", message);
// 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.
const clientM = clientMessage("", audio, message.uid);
@ -78,7 +79,6 @@ export default function useAudioInputController (typing: Ref) {
hideRecIcon()
recording.value = false;
console.log("Stopping record...")
}

View file

@ -26,26 +26,24 @@
import { defineComponent, ref, onMounted, onUnmounted } 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",
props: ["initials"],
components: {
TextInput
},
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.
const xStart = 15;
const yStart = window.innerHeight - 200;
@ -57,25 +55,10 @@ export default defineComponent({
const { typing } = useTextInputController(elementX)
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 {
elementX,
elementY,
recording,
initials
recording
};
},
});

View file

@ -29,13 +29,12 @@
</form>
<!-- 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>
<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';
@ -46,11 +45,9 @@ export default defineComponent({
setup() {
const { post } = useIpc();
const { setToken } = useAuth();
const { resetMessages } = useMessages();
let usr = ref("");
let pwd = ref("");
const usr = ref("");
const pwd = ref("");
// Submit login credentials to the backend.
const submitForm = () => {

View file

@ -1,51 +1,66 @@
<template>
<InputItem />
<InputItem :initials="profile.initials"/>
<Settings />
<div id="recIcon" />
<!-- 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]"
/>
</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",
props: ["profile", "newMessages"],
components: {
MessageItem,
Message,
InputItem,
Settings
},
setup() {
setup(props) {
// Listen for self-messages from inputItem.
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;
// Front end listener.
// Load the message history.
if (crimata_id === 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)
});
@ -58,11 +73,14 @@ export default defineComponent({
return {
messages
};
},
});
</script>
<style lang="scss" scoped>
#messenger {
width: 100vw;
height: 100vh;

View file

@ -26,7 +26,6 @@
<script lang="ts">
import { defineComponent, ref } from "vue";
import { useAuth } from "@/modules/auth";
import { useIpc } from "@/modules/ipc";
import { logoutRequest } from '@/modules/message';

View file

@ -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
}
}

View file

@ -1,5 +1,6 @@
export const useIpc = () => {
const invoke = async (endpoint: string, payload: any) => {
try {
const res = await window.ipcRenderer.invoke(endpoint, payload);
@ -10,10 +11,7 @@ export const useIpc = () => {
}
const post = (endpoint: string, payload: any) => {
window.postMessage({
endpoint: endpoint,
content: payload
}, '*')
window.ipcRenderer.send(endpoint, payload);
};
return {

View file

@ -14,7 +14,7 @@ const updateMessage = (annotation: Annotation) => {
message.content.text = annotation.text
}
const loadMessages = () => {
const loadSavedMessages = () => {
const rawData = window.localStorage.getItem("crimata_messages");
if (rawData) {
const messageData = JSON.parse(rawData)
@ -58,51 +58,61 @@ const updateGrouping = () => {
}
}
const { setScroll, updateScrollRef, adjustScroll } = useScroll("messenger");
loadMessages()
setTimeout(setScroll, 1000);
export default function useMessages() {
// Main function for updating the message view.
const updateMessageView = (message: RenderMessage | Annotation) => {
// Scroll controller.
const { setScroll, updateScrollRef, adjustScroll } = useScroll("messenger");
// Step 1: See if user is scrolled down.
updateScrollRef()
// Main function for updating the message view.
const updateMessageView = (message: RenderMessage | Annotation) => {
// 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()
// 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)
}
const resetMessages = () => {
messages.value.clear()
// 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);
}
return {
messages,
updateMessageView,
resetMessages
// 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 => addMessage(message))
setTimeout(setScroll.bind(true), 1000);
}
}
return {
messages,
prepMessageView,
updateMessageView
}
}

View file

@ -5,13 +5,13 @@ export default function useScroll(element: string) {
let isScrolledToBottom: boolean;
// Set the initial scroll position.
const setScroll = () => {
const setScroll = (smooth: boolean) => {
const view = document.getElementById(element)
if (view) {
view.scrollTo({
top: view.scrollHeight - view.clientHeight,
behavior: 'smooth'
behavior: (smooth) ? 'smooth' : 'auto'
});
}
}

View file

@ -16,27 +16,30 @@ export interface ClientMessage {
uid: string;
}
export interface SessionState {
key: string | boolean;
newMessages: RenderMessage[];
}
export interface AuthRequest {
key: boolean | string;
usr: boolean | string;
pwd: boolean | string;
}
export interface LogoutRequest {
logout: boolean;
}
// Possible messages from server:
export interface Profile {
crimata_id: string;
title: string;
first: string;
middle: string;
last: string;
suffix: string | number;
nickname: string;
full: string;
alias: string;
initials: string;
}
export interface AuthProtocol {
key: boolean | string;
profile: boolean | Profile;
}
export interface LogoutRequest {
logout: boolean;
}
export interface Annotation {