WIP:playback queue and tray icon states

This commit is contained in:
Andrew Gundersen 2021-08-24 15:04:21 -05:00
commit a4d6a883b0
37 changed files with 365 additions and 470 deletions

View file

@ -114,8 +114,6 @@ export const updateAppState = () => {
ipcEmit("set-version", app.getVersion());
updateAppUI();
}

View file

@ -1,5 +1,8 @@
const nodeAudio = require('@crimata/nodeaudio');
import { globalShortcut } from "electron";
const nodeAudio = require('@crimata/nodeaudio');
import { updateTray } from "@/tray";
import { sendMessage } from "@/session";
import { backgroundMitt, ipcEmit } from '@/composables/useEmitter';
let inputDevice: number;
@ -7,68 +10,100 @@ let outputDevice: number;
let setStreamsId: ReturnType<typeof setTimeout>;
let playbackEl;
let writeToOBuffer = false;
let recording = false;
const chunks: Int16Array[] = [];
// backgroundMitt.on("data", (int16Arr: Int16Array) => {
// if (recording) {
// chunks.push(int16Arr);
// ipcEmit("recording", int16Arr); // for recording animation
// }
// });
backgroundMitt.on("data", (int16Arr: Int16Array) => {
if (recording) {
chunks.push(int16Arr);
ipcEmit("recording", int16Arr);
}
});
// const setStreams = () => {
backgroundMitt.on("playback", (uid: string, aplitude: number) => {
ipcEmit("animate-playback", { uid: uid, aplitude: aplitude });
});
// const defaultInput = nodeAudio.GetDefaultInputDevice();
// const defaultOutput = nodeAudio.GetDefaultOutputDevice();
backgroundMitt.on("write", (status: boolean) => {
writeToOBuffer = status;
});
// if (inputDevice !== defaultInput) {
// inputDevice = defaultInput;
// nodeAudio.CloseInputStream(inputDevice);
// nodeAudio.OpenInputStream(inputDevice);
// }
const setStreams = () => {
// if (outputDevice !== defaultOutput) {
// outputDevice = defaultOutput;
// nodeAudio.CloseOutputStream(outputDevice);
// nodeAudio.OpenOutputStream(outputDevice);
// }
// }
const defaultInput = nodeAudio.core.GetDefaultInputDevice();
const defaultOutput = nodeAudio.core.GetDefaultOutputDevice();
// const startRecording = () => {
// recording = true;
// }
// const stopRecording = () => {
// recording = false;
// const audio: Int16Array = nodeAudio.MergeChunks(chunks);
// backgroundMitt.emit("audio", audio); // emit audio event
// chunks.length = 0;
// }
export const initAudio = () => {
// nodeAudio.Initialize(backgroundMitt.emit.bind(backgroundMitt));
// setStreamsId = setInterval(setStreams, 2000); // sense device
const ret = globalShortcut.register('CommandOrControl+R', () => {
console.log('Toggle record...');
});
if (!ret) {
console.log('registration failed');
if (inputDevice !== defaultInput) {
inputDevice = defaultInput;
nodeAudio.core.CloseInputStream(inputDevice);
nodeAudio.core.OpenInputStream(inputDevice);
}
// Check whether a shortcut is registered.
console.log(globalShortcut.isRegistered('CommandOrControl+R'));
if (outputDevice !== defaultOutput) {
outputDevice = defaultOutput;
nodeAudio.core.CloseOutputStream(outputDevice);
nodeAudio.core.OpenOutputStream(outputDevice);
}
}
const startRecording = () => {
/* If playback is happening, we kill it */
if (playback) nodeAudio.core.ClearOutputBuffer();
recording = true;
updateTray("recording", recording);
}
const stopRecording = () => {
recording = false;
updateTray("recording", recording);
/* Send the data to the platform right away */
sendMessage({
text: false,
audio: nodeAudio.utils.mergeChunks(chunks)
} as Raw);
chunks.length = 0;
}
export const initAudio = () => {
nodeAudio.core.Initialize(backgroundMitt.emit.bind(backgroundMitt));
setStreamsId = setInterval(setStreams, 2000); // sense device
/* Toggle recording switch with global shortcut */
globalShortcut.register('CommandOrControl+R', () => {
if (recording) {
stopRecording();
} else {
startRecording();
};
});
}
// export const playback = (audio: Int16Array) => {
// console.log("Playing audio");
// // nodeAudio.WriteToOutputStream(audio);
// }
export const playback = (audio: Int16Array, source: string) => {
// export const terminateAudio = () => {
// console.log("Terminating audio");
// // clearInterval(setStreamsId);
// }
/* If existing playback is happening, we kill it */
if (playback) nodeAudio.core.ClearOutputBuffer();
const playbackEl = uid;
nodeAudio.core.WriteToOutputStream(audio.buffer);
}
export const terminateAudio = () => {
clearInterval(setStreamsId);
nodeAudio.core.Terminate();
}
export const getStreamState = () => {
let busy = false;
if (playback || recording) {
busy = true;
}
return busy;
}

View file

@ -10,6 +10,7 @@ import createWindow from "./window";
import main from "./main";
import { backgroundMitt } from '@/composables/useEmitter';
import { setWindowOpen, getWindowOpen } from './store';
import { terminateAudio } from './audio';
console.log('Starting Crimata electron app.');
@ -31,6 +32,7 @@ app.on("ready", async () => {
});
app.on("will-quit", () => {
terminateAudio();
globalShortcut.unregisterAll();
});

View file

@ -1,13 +1,15 @@
import { IpcListener } from "@/composables/useIpcMain"
import { sendMessage } from '@/session';
import { updateAppState } from "@/account";
import { playback } from "@/audio";
import { onNavBar } from "@/window";
import { sendMessage } from '@/session';
import { setWindowFocus } from '@/store';
import { updateAppState } from "@/account";
import { IpcListener } from "@/composables/useIpcMain"
const CLIENT_MESSAGE_CHANNEL = "post-session-send"
const APP_MOUNT_CHANNEL = "post-app-mount";
const NAV_BAR_CHANNEL = "post-nav-bar";
const POST_WINDOW_FOCUS = 'post-window-focus';
const PLAYBACK_CHANNEL = "post-playback;"
export const messageListener = new IpcListener<Raw | Request>({
channel: CLIENT_MESSAGE_CHANNEL,
@ -28,3 +30,8 @@ export const windowFocusListener = new IpcListener<FocusPayload>({
channel: POST_WINDOW_FOCUS,
listenerCallback: ({ isFocused }) => setWindowFocus(isFocused)
});
export const playbackListener = new IpcListener({
channel: PLAYBACK_CHANNEL,
listenerCallback: playback
});

View file

@ -1,18 +1,7 @@
/**
* Where the background logic really begins, gets called by app.onReady().
*
* Handles authentication. If profile is set, we launch a session, which consis
* of opening a connection with the platform, initializing the audio streams.
*
* The session is primarily an interface between the frontend and the platform,
* relaying messages from one to the other.
*
*/
import { initTray } from '@/tray';
import createWindow from "@/window";
import initIpcMain from "@/ipc/index";
import { accountAuth } from "./account";
import createWindow from "./window";
import { initTray } from './tray';
import { accountAuth } from "@/account";
export default async function main() {

View file

@ -0,0 +1,81 @@
class AudioVisualizer {
constructor( audioContext, processFrame, processError ) {
this.audioContext = audioContext;
this.processFrame = processFrame;
this.connectStream = this.connectStream.bind( this );
navigator.mediaDevices.getUserMedia( { audio: true, video: false } )
.then( this.connectStream )
.catch( ( error ) => {
if ( processError ) {
processError( error );
}
} );
}
connectStream( stream ) {
this.analyser = this.audioContext.createAnalyser();
const source = this.audioContext.createMediaStreamSource( stream );
source.connect( this.analyser );
this.analyser.smoothingTimeConstant = 0.5;
this.analyser.fftSize = 32;
this.initRenderLoop( this.analyser );
}
initRenderLoop() {
const frequencyData = new Uint8Array( this.analyser.frequencyBinCount );
const processFrame = this.processFrame || ( () => {} );
const renderFrame = () => {
this.analyser.getByteFrequencyData( frequencyData );
processFrame( frequencyData );
requestAnimationFrame( renderFrame );
};
requestAnimationFrame( renderFrame );
}
}
const visualMainElement = document.querySelector( 'main' );
const visualValueCount = 16;
let visualElements;
const createDOMElements = () => {
let i;
for ( i = 0; i < visualValueCount; ++i ) {
const elm = document.createElement( 'div' );
visualMainElement.appendChild( elm );
}
visualElements = document.querySelectorAll( 'main div' );
};
createDOMElements();
const init = () => {
// Creating initial DOM elements
const audioContext = new AudioContext();
const initDOM = () => {
visualMainElement.innerHTML = '';
createDOMElements();
};
initDOM();
// Swapping values around for a better visual effect
const dataMap = { 0: 15, 1: 10, 2: 8, 3: 9, 4: 6, 5: 5, 6: 2, 7: 1, 8: 0, 9: 4, 10: 3, 11: 7, 12: 11, 13: 12, 14: 13, 15: 14 };
const processFrame = ( data ) => {
const values = Object.values( data );
let i;
for ( i = 0; i < visualValueCount; ++i ) {
const value = values[ dataMap[ i ] ] / 255;
const elmStyles = visualElements[ i ].style;
elmStyles.transform = `scaleY( ${ value } )`;
elmStyles.opacity = Math.max( .25, value );
}
};
const processError = () => {
visualMainElement.classList.add( 'error' );
visualMainElement.innerText = 'Please allow access to your microphone in order to see this demo.\nNothing bad is going to happen... hopefully :P';
}
const a = new AudioVisualizer( audioContext, processFrame, processError );
};

View file

@ -2,8 +2,8 @@
<span :class="`context ${modifier}-context`">
<div v-if="context.img" class="avatar">
<img v-if="context.img === 'crimata'" :src="require('@/render/assets/crimataAvatar.svg')">
<div v-if="context.img" class="avatar" @click="clickMethod">
<img v-click="" v-if="context.img === 'crimata'" :src="require('@/render/assets/crimataAvatar.svg')">
<div v-else>{{ context.img }}</div>
</div>

View file

@ -85,6 +85,14 @@ export function animateAudioInput () {
}
/* TODO: animation function that takes in a message ID, and data about the
* playedback audio. We target the element with the UID and animate it according
* to the data. - still very hypothetical...
*/
export function animatePlayback (uid: string, meta: number) {
console.log("animating playback!")
}
// Given index and length of content, return message child status.
export function calcChild (index: number, len: number) {
if (len === 1) {

View file

@ -1,77 +0,0 @@
import { onMounted, onUnmounted, ref, Ref } from "vue";
import { postMessage } from "@/render/ipc";
import { animateAudioInput } from "./helpers";
import { invokeReturnAudio, postAudioChunk } from "@/render/ipc";
export default function useAudioInputController (typing: Ref) {
const recording = ref(false);
let mediaRecorder: MediaRecorder;
const { show, hide } = animateAudioInput();
// initialize audio
const conf = {audio: true, video: false}
navigator.mediaDevices.getUserMedia(conf).then((stream: MediaStream) => {
const options = {mimeType: 'audio/webm'};
mediaRecorder = new MediaRecorder(stream, options);
// post any mew audio to backend
mediaRecorder.addEventListener('dataavailable', (e: BlobEvent) => {
e.data.arrayBuffer().then((buff: ArrayBuffer) => {
postAudioChunk(buff);
});
});
// get audio and post new message to backend
mediaRecorder.addEventListener('stop', (_e: Event) => {
invokeReturnAudio().then((audio: ArrayBuffer[] | Error) => {
console.log(audio);
// postMessage(newMessage({audio: audio}));
});
});
});
// start recording on space bar
const record = () => {
console.log("INPT:Capturing audio...")
mediaRecorder.start();
recording.value = true;
show()
}
// stop recording and send on release
const stop = () => {
console.log("INPT:Stopping record.")
mediaRecorder.stop();
recording.value = false;
hide();
}
const onKeyDown = (e: KeyboardEvent) => {
if (e.keyCode == 32 && !typing.value) record();
}
const onKeyUp = (e: KeyboardEvent) => {
if (e.keyCode == 32 && recording.value) stop();
}
//-----------------------------------------------------------
onMounted(() => {
window.addEventListener("keydown", onKeyDown);
window.addEventListener("keyup", onKeyUp);
});
onUnmounted(() => {
window.removeEventListener("keydown", onKeyDown);
window.removeEventListener("keyup", onKeyUp);
})
return {
recording
}
}

View file

@ -19,21 +19,19 @@
type="text"
/>
<!-- Show suggestions menu on click -->
<!-- <Menu /> -->
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, watch } from "vue";
import draggify from "@/render/composables/useDraggify";
import { profile } from "@/render/shared/profile";
import { recording } from "@/render/shared/audio";
import { animateAudioInput } from "@/render/components/controllers/helpers";
import useTextInputController from
"@/render/components/controllers/inputItem.control.text";
// import useAudioInputController from
"@/render/components/controllers/inputItem.control.audio";
export default defineComponent({
@ -50,8 +48,12 @@ export default defineComponent({
// Controllers for text and audio.
const { typing } = useTextInputController(elementX)
// const { recording } = useAudioInputController(typing)
const recording = false;
// Control rec icon (red dot upper right of UI)
const { show, hide } = animateAudioInput();
watch(recording, (val, _oldval) => {
recording ? show() : hide();
});
return {
profile,

View file

@ -10,7 +10,6 @@ const { post, invoke } = useIpc();
*
*/
export const invokeLogin = async (
payload: AccountCredentials
): Promise<Profile | Error> => (
@ -27,12 +26,8 @@ export const invokeLogout = async (): Promise<void> => (
*
*/
export const postAudioChunk = (chunk: ArrayBuffer): void => (
post("post-audio-collect", chunk)
);
export const invokeReturnAudio = async (): Promise<ArrayBuffer[] | Error> => (
await invoke("invoke-audio-flush", null)
export const postPlayback = (payload: string): void => (
post('post-playback', payload)
);
/**

View file

@ -13,6 +13,7 @@ import { setAccount } from "./shared/account";
import { setProfile } from "./shared/profile";
import { setVersion } from "./shared/version";
import { setConnectionStatus } from "./shared/connectionStatus";
import { setRecording } from "./shared/audio";
const SET_ACCOUNT_CHANNEL = "set-account";
const SET_PROFILE_CHANNEL = "set-profile";
@ -23,6 +24,7 @@ const ADD_MESSAGE_CHANNEL = "add-message";
const UPDATE_MESSAGE_CHANNEL = "update-message";
const DELETE_MESSAGE_CHANNEL = "delete-message";
const CONNECTION_STATUS_CHANNEL = "set-connection-status";
const RECORDING_STATUS_CHANNEL = "recording-status";
export const setAccountListener = new IpcRendererListener({
channel: SET_ACCOUNT_CHANNEL,
@ -63,4 +65,14 @@ export const deleteMessagesListener = new IpcRendererListener({
export const connectionStatusListener = new IpcRendererListener({
channel: CONNECTION_STATUS_CHANNEL,
listenerCallback: setConnectionStatus
});
export const recordingStatusListener = new IpcRendererListener({
channel: RECORDING_STATUS_CHANNEL,
listenerCallback: setRecording
});
export const animatePlaybackListener = new IpcRendererListener({
channel: PLAYBACK_LISTENER,
listenerCallback: animatePlayback
});

View file

@ -0,0 +1,12 @@
// shared
import { ref } from "vue";
export const recording = ref();
export const setRecording: IpcListenerCallback<boolean> = (payload) => {
recording.value = payload;
}
export default {
recording
}

View file

@ -1,117 +1,112 @@
import { Notification } from 'electron';
// import useAudio from "@/audio";
import UIState from "@/uiState";
import { config } from "@/config";
import useWebsockets from "./composables/useWebsockets";
import { updateTray } from "@/tray";
import { getWindowFocus, getWindowOpen } from '@/store';
import useWebsockets from "@/composables/useWebsockets";
import { backgroundMitt, ipcEmit } from "@/composables/useEmitter";
import { Notification } from 'electron';
import { getWindowFocus, getWindowOpen } from './store';
import { initAudio, terminateAudio, playback } from "./audio";
import { initAudio, terminateAudio, playback, getStreamState } from "@/audio";
export const CONNECTION_STATUS = {
connected: 'Connected',
nominal: 'Nominal',
reconnecting: 'Reconnecting',
lost: 'Connection Lost',
};
const contentQueue: Update[] = [];
const uiState = new UIState();
const onMessageCallback = (payload: string) => {
const onMessageCallback = async (payload: string) =>
{
const message = JSON.parse(payload) as ClientProtocol;
if (payload === "CLOSE_AUTH_FAIL") {
backgroundMitt.emit("close-auth-fail");
if (message.header == "init") {
uiState.set(message.body as Init);
return;
}
const message = JSON.parse(payload) as ClientProtocol;
if (message.header === "init")
if (content)
{
uiState.set(message.body as Init);
contentQueue.push(message.body)
}
else
{
const body = message.body as Update;
uiState.update(body);
if (body.name === "add")
{
const data = body.data as Message;
// Show a notification if new content and window is not focused.
if (!getWindowFocus())
{
new Notification({
title: 'New Message',
subtitle: data.context.text as string,
body: data.content[0].text,
})
.show();
}
// Playback audio if message has audio.
// TODO: Should we worry about playback and record at the same time?
// if (data.content.audio)
// {
// playback(data.content.audio);
// }
}
uiState.update(message.body as Update);
}
}
const statusOptions = { openMessage: 'Connected',
pongMessage: 'Nominal',
closeMessage: 'Reconnecting',
pingErrorMessage: 'Connection Lost' };
const connectionStatusCallback = (status: string) =>
{
/* Simplify connectionStatus for tray state */
let connectionStatusSimple = false;
if (status == statusOptions.pingErrorMessage || statusOptions.closeMessage)
{
connectionStatusSimple = true;
}
updateTray("disconnect", connectionStatusSimple);
const connectionStatusCallback = (status: string) => {
// backgroundMitt.emit("update-icon-status", status);
ipcEmit('set-connection-status', status);
}
const statusOptions = {
openMessage: CONNECTION_STATUS.connected,
pongMessage: CONNECTION_STATUS.nominal,
closeMessage: CONNECTION_STATUS.reconnecting,
pingErrorMessage: CONNECTION_STATUS.lost,
}
const { connect, send, close } = useWebsockets(onMessageCallback,
connectionStatusCallback, statusOptions);
const { connect, send, close } = useWebsockets(
onMessageCallback,
connectionStatusCallback,
statusOptions
);
export const updateAppUI = (): void => {
uiState.emit();
}
// Listen for audio recordings and send them to backend.
backgroundMitt.on("audio", (audio: Int16Array) => {
sendMessage({
text: false,
audio: audio
} as Raw);
});
export function sendMessage(message: Raw | Request): void {
export function sendMessage(message: Raw | Request): void
{
send(message);
}
/* launch a new session (the main process for authenticated users) */
export function launchSession(platformKey: string) {
let processContentId: ReturnType<typeof setTimeout>;
/* launch a new session (the main process for authenticated users) */
export function launchSession(platformKey: string)
{
/* connect to the platform */
connect(config.PLATFORM_URL, platformKey);
initAudio();
processContentId = setInterval(() =>
{
if (getStreamState())
{
if (contentQueue.length > 0)
{
const update = contentQueue.shift() as Update;
/* Update the UI */
uiState.update(update);
/* Spawn notification if window not in focus */
if (!getWindowFocus())
{
new Notification({
title: "New Message",
subtitle: data.context.text,
body: data.content[0].text
}).show();
}
/* Play audio if audio */
if (update.data.content[-1].audio)
{
playback(message.audio, message.uid);
}
}
}
}, 100);
}
export function endSession() {
export function endSession()
{
terminateAudio();
close(1000, 'session-logout');
uiState.reset();
clearInterval(processContentId)
uiState.reset();
}

View file

@ -1,90 +1,34 @@
"use strict";
import fs from 'fs'
import path from 'path'
import { Tray } from 'electron';
const nativeImage = require('electron').nativeImage;
import { NativeImage } from 'electron'
import { backgroundMitt } from "@/composables/useEmitter";
import { getIconState, setIconState } from "@/store";
import { CONNECTION_STATUS } from '@/session';
declare const __static: string;
let tray: Tray;
const ICON_PATH = "./build/tray";
interface Icons {
[name: string]: NativeImage;
/* Current state of recording, playback, and ws connection */
const state = {
recording: false,
playback: false,
disconnect: false // whether we're experiencing connection issues
}
let tray: Tray | null = null;
const TRAY_ICON_DIR = 'trayIcon';
export function updateTray(attribute: "recording" | "playback" | "disconnect", newState: boolean) {
const ICON_NAMES = {
default: 'icon.png',
// TODO: need icons for different states
// TODO: need higher density icons @3x, @4x, @5x
// TODO: need to test with 20x20 & 24x24 icon sizes
recording: 'recording.png',
playback: 'playback.png',
connected: 'connected.png',
nominal: 'nominal.png',
reconnecting: 'reconnecting.png',
lost: 'lost.png',
}
state[attribute] = newState;
const TRAY_STATUS = {
playback: 'playback',
recording: 'recording',
...CONNECTION_STATUS
};
// convert boolean to number
const bi = (b: boolean) => b ? 1 : 0;
// Make sure we have the icon we're looking for.
tray.setImage(
ICON_PATH +
`${bi(state.recording)}${bi(state.playback)}${bi(state.disconnect)}` +
".png"
);
const ICONS: Icons = {
default: iconFactory(ICON_NAMES.default),
[TRAY_STATUS.recording]: iconFactory(ICON_NAMES.recording),
[TRAY_STATUS.playback]: iconFactory(ICON_NAMES.playback),
[TRAY_STATUS.connected]: iconFactory(ICON_NAMES.connected),
[TRAY_STATUS.nominal]: iconFactory(ICON_NAMES.nominal),
[TRAY_STATUS.reconnecting]: iconFactory(ICON_NAMES.reconnecting),
[TRAY_STATUS.lost]: iconFactory(ICON_NAMES.lost),
};
backgroundMitt.on('update-icon-status', (payload: string) => {
updateTray(payload);
});
const changeIcon = (icon: NativeImage): void => tray ? tray.setImage(icon): undefined;
function iconFactory(iconName: string): NativeImage {
if (iconName !== 'icon.png') {
console.log('ERROR: Missing Icons: ', iconName);
}
// TODO: remove hard code, need icons for different states
const iconBuffer = fs.readFileSync(path.join(__static, TRAY_ICON_DIR, 'icon.png'));
const icon = nativeImage.createFromBuffer(iconBuffer);
return icon;
}
function updateTray(status: string): void {
// read icon state
const current = getIconState();
if (current !== status) {
setIconState(status);
if (status in ICONS) {
changeIcon(ICONS[status]);
} else {
changeIcon(ICONS.default);
}
}
}
export function initTray(): void {
if (!tray) {
try {
tray = new Tray(ICONS.default);
} catch(e) {
console.log('Error initilizing tray', e);
}
}
}
tray = new Tray(ICON_PATH + "000.png");
}

View file

@ -36,7 +36,7 @@ interface Message {
interface Content {
text: string;
audio: string | boolean;
audio: Int16Array | false;
}
interface Context {

View file

@ -1,95 +1,102 @@
import { ipcEmit } from "@/composables/useEmitter";
class Profile implements Profile{
class Profile implements Profile
{
crimata_id = "";
name = "";
photo = "";
}
export default class UIState {
export default class UIState
{
profile: Profile;
messages: Message[];
constructor() {
constructor()
{
this.profile = new Profile();
this.messages = [];
}
emit() {
emit()
{
ipcEmit("set-profile", this.profile);
ipcEmit("init-messages", this.messages);
}
set (message: Init) {
set (message: Init)
{
this.profile = message.profile;
this.messages = message.messages;
this.emit();
}
update(update: Update) {
if (update.name === "profile") {
update(update: Update)
{
if (update.name === "profile")
{
this.profile = update.data as Profile;
ipcEmit("set-profile", this.profile);
}
else if (update.name === "add") {
else if (update.name === "add")
{
this.messages.push(update.data as Message);
ipcEmit("add-message", update.data);
}
else if (update.name === "annotate") {
else if (update.name === "annotate")
{
this._annotate(update.data as Annotation);
ipcEmit("update-message", update.data);
}
else {
else
{
this._delete(update.data as string);
ipcEmit("delete-message", update.data);
}
}
reset() {
reset()
{
this.profile = new Profile();
this.messages = [];
this.emit();
}
_annotate(annotation: Annotation) {
for (const i in this.messages) {
if (this.messages[i].uid == annotation.uid) {
if (annotation.name == "content") {
_annotate(annotation: Annotation)
{
for (const i in this.messages)
{
if (this.messages[i].uid == annotation.uid)
{
if (annotation.name == "content")
{
this.messages[i].content = annotation.data as Content[];
}
if (annotation.name == "context") {
if (annotation.name == "context")
{
this.messages[i].context = annotation.data as Context;
}
if (annotation.name == "seen") {
if (annotation.name == "seen")
{
this.messages[i].seen = annotation.data as boolean;
}
ipcEmit("update-message", annotation);
}
}
}
_delete(uid: string) {
for (let i = 0; i < this.messages.length; i++) {
if (this.messages[i].uid === uid) {
_delete(uid: string)
{
for (let i = 0; i < this.messages.length; i++)
{
if (this.messages[i].uid === uid)
{
this.messages.splice(i, 1);
break;
}
}
}
}