]> Repos - mime-chat/commitdiff
WIP:playback queue and tray icon states
authorAndrew Gundersen <gundersena@xavier.edu>
Tue, 24 Aug 2021 20:04:21 +0000 (15:04 -0500)
committerAndrew Gundersen <gundersena@xavier.edu>
Tue, 24 Aug 2021 20:04:21 +0000 (15:04 -0500)
37 files changed:
build/icon_128x128.png [deleted file]
build/icon_128x128@2x.png [deleted file]
build/icon_16x16.png [deleted file]
build/icon_16x16@2x.png [deleted file]
build/icon_256x256.png [deleted file]
build/icon_256x256@2x.png [deleted file]
build/icon_32x32.png [deleted file]
build/icon_32x32@2x.png [deleted file]
build/icon_512x512.png [deleted file]
build/icon_512x512@2x.png [deleted file]
build/tray/000_16x16@2x.png [new file with mode: 0644]
build/tray/001_16x16@2x.png [new file with mode: 0644]
build/tray/010_16x16@2x.png [new file with mode: 0644]
build/tray/011_16x16@2x.png [new file with mode: 0644]
build/tray/100_16x16@2x.png [new file with mode: 0644]
build/tray/101_16x16@2x.png [new file with mode: 0644]
build/tray/110_16x16@2x.png [new file with mode: 0644]
build/tray/111_16x16@2x.png [new file with mode: 0644]
package.json
src/account.ts
src/audio.ts
src/init.ts
src/ipc/listeners.ts
src/main.ts
src/render/audioVisualizer.js [new file with mode: 0644]
src/render/components/context.vue
src/render/components/controllers/helpers.ts
src/render/components/controllers/inputItem.control.audio.ts [deleted file]
src/render/components/inputItem.vue
src/render/ipc.ts
src/render/listeners.ts
src/render/shared/audio.ts [new file with mode: 0644]
src/session.ts
src/tray.ts
src/types.ts
src/uiState.ts
yarn.lock

diff --git a/build/icon_128x128.png b/build/icon_128x128.png
deleted file mode 100644 (file)
index bc835c0..0000000
Binary files a/build/icon_128x128.png and /dev/null differ
diff --git a/build/icon_128x128@2x.png b/build/icon_128x128@2x.png
deleted file mode 100644 (file)
index 0e47443..0000000
Binary files a/build/icon_128x128@2x.png and /dev/null differ
diff --git a/build/icon_16x16.png b/build/icon_16x16.png
deleted file mode 100644 (file)
index b925c06..0000000
Binary files a/build/icon_16x16.png and /dev/null differ
diff --git a/build/icon_16x16@2x.png b/build/icon_16x16@2x.png
deleted file mode 100644 (file)
index 7bf880c..0000000
Binary files a/build/icon_16x16@2x.png and /dev/null differ
diff --git a/build/icon_256x256.png b/build/icon_256x256.png
deleted file mode 100644 (file)
index 0e47443..0000000
Binary files a/build/icon_256x256.png and /dev/null differ
diff --git a/build/icon_256x256@2x.png b/build/icon_256x256@2x.png
deleted file mode 100644 (file)
index 3126e1d..0000000
Binary files a/build/icon_256x256@2x.png and /dev/null differ
diff --git a/build/icon_32x32.png b/build/icon_32x32.png
deleted file mode 100644 (file)
index 7bf880c..0000000
Binary files a/build/icon_32x32.png and /dev/null differ
diff --git a/build/icon_32x32@2x.png b/build/icon_32x32@2x.png
deleted file mode 100644 (file)
index 2a9f925..0000000
Binary files a/build/icon_32x32@2x.png and /dev/null differ
diff --git a/build/icon_512x512.png b/build/icon_512x512.png
deleted file mode 100644 (file)
index 3126e1d..0000000
Binary files a/build/icon_512x512.png and /dev/null differ
diff --git a/build/icon_512x512@2x.png b/build/icon_512x512@2x.png
deleted file mode 100644 (file)
index 7568cb1..0000000
Binary files a/build/icon_512x512@2x.png and /dev/null differ
diff --git a/build/tray/000_16x16@2x.png b/build/tray/000_16x16@2x.png
new file mode 100644 (file)
index 0000000..6d61d7f
Binary files /dev/null and b/build/tray/000_16x16@2x.png differ
diff --git a/build/tray/001_16x16@2x.png b/build/tray/001_16x16@2x.png
new file mode 100644 (file)
index 0000000..b7f0e2c
Binary files /dev/null and b/build/tray/001_16x16@2x.png differ
diff --git a/build/tray/010_16x16@2x.png b/build/tray/010_16x16@2x.png
new file mode 100644 (file)
index 0000000..6b97193
Binary files /dev/null and b/build/tray/010_16x16@2x.png differ
diff --git a/build/tray/011_16x16@2x.png b/build/tray/011_16x16@2x.png
new file mode 100644 (file)
index 0000000..befcd5f
Binary files /dev/null and b/build/tray/011_16x16@2x.png differ
diff --git a/build/tray/100_16x16@2x.png b/build/tray/100_16x16@2x.png
new file mode 100644 (file)
index 0000000..2d26144
Binary files /dev/null and b/build/tray/100_16x16@2x.png differ
diff --git a/build/tray/101_16x16@2x.png b/build/tray/101_16x16@2x.png
new file mode 100644 (file)
index 0000000..91a15be
Binary files /dev/null and b/build/tray/101_16x16@2x.png differ
diff --git a/build/tray/110_16x16@2x.png b/build/tray/110_16x16@2x.png
new file mode 100644 (file)
index 0000000..fd073dd
Binary files /dev/null and b/build/tray/110_16x16@2x.png differ
diff --git a/build/tray/111_16x16@2x.png b/build/tray/111_16x16@2x.png
new file mode 100644 (file)
index 0000000..2bc77fd
Binary files /dev/null and b/build/tray/111_16x16@2x.png differ
index e7c7cfe578536cc05bb0b04b61b27c95c1513849..2cad1d53abc41e6ac41c9630da995a04c2e33cb3 100644 (file)
@@ -28,7 +28,6 @@
     "electron-is-dev": "^2.0.0",
     "electron-store": "^8.0.0",
     "electron-updater": "^4.3.8",
-    "iohook": "^0.9.3",
     "mitt": "^2.1.0",
     "update-electron-app": "^2.0.1",
     "vue": "^3.0.0-0",
index 6ea1ac4a71cb8ff0921ce77071f5dc97cdb7e518..7324e6f0a850024391e2fcf5722d9bbbbc71e7fb 100644 (file)
@@ -114,8 +114,6 @@ export const updateAppState = () => {
 
     ipcEmit("set-version", app.getVersion());
 
-    updateAppUI();
-
 }
 
 
index b1a6297af02079aa07b7ec2ca08b6251bdeb4a74..2dcc9cd8762307991b6cd6ca2f4352e30f922de8 100644 (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
-//     }
-// });
-
-// const setStreams = () => {
-
-//     const defaultInput = nodeAudio.GetDefaultInputDevice();
-//     const defaultOutput = nodeAudio.GetDefaultOutputDevice();
-
-//     if (inputDevice !== defaultInput) {
-//         inputDevice = defaultInput;
-//         nodeAudio.CloseInputStream(inputDevice);
-//         nodeAudio.OpenInputStream(inputDevice);
-//     }
-
-//     if (outputDevice !== defaultOutput) {
-//         outputDevice = defaultOutput;
-//         nodeAudio.CloseOutputStream(outputDevice);
-//         nodeAudio.OpenOutputStream(outputDevice);
-//     }
-// }
-
-// const startRecording = () => {
-//     recording = true;
-// }
-
-// const stopRecording = () => {
-//     recording = false;
-//     const audio: Int16Array = nodeAudio.MergeChunks(chunks);
-//     backgroundMitt.emit("audio", audio); // emit audio event
-//     chunks.length = 0;
-// }
+backgroundMitt.on("data", (int16Arr: Int16Array) => {
+    if (recording) {
+        chunks.push(int16Arr);
+        ipcEmit("recording", int16Arr);
+    }
+});
 
-export const initAudio = () => {
-    // nodeAudio.Initialize(backgroundMitt.emit.bind(backgroundMitt));
-    // setStreamsId = setInterval(setStreams, 2000); // sense device 
+backgroundMitt.on("playback", (uid: string, aplitude: number) => {
+    ipcEmit("animate-playback", { uid: uid, aplitude: aplitude });
+});
 
-    const ret = globalShortcut.register('CommandOrControl+R', () => {
-        console.log('Toggle record...');
-    });
+backgroundMitt.on("write", (status: boolean) => {
+    writeToOBuffer = status;
+});
+
+const setStreams = () => {
 
-    if (!ret) {
-        console.log('registration failed');
+    const defaultInput = nodeAudio.core.GetDefaultInputDevice();
+    const defaultOutput = nodeAudio.core.GetDefaultOutputDevice();
+
+    if (inputDevice !== defaultInput) {
+        inputDevice = defaultInput;
+        nodeAudio.core.CloseInputStream(inputDevice);
+        nodeAudio.core.OpenInputStream(inputDevice);
+    }
+
+    if (outputDevice !== defaultOutput) {
+        outputDevice = defaultOutput;
+        nodeAudio.core.CloseOutputStream(outputDevice);
+        nodeAudio.core.OpenOutputStream(outputDevice);
     }
+}
 
-    // Check whether a shortcut is registered.
-    console.log(globalShortcut.isRegistered('CommandOrControl+R'));
+const startRecording = () => {
 
+    /* If playback is happening, we kill it */
+    if (playback) nodeAudio.core.ClearOutputBuffer();
+
+    recording = true;
+    updateTray("recording", recording);
 }
 
-// export const playback = (audio: Int16Array) => {
-//     console.log("Playing audio");
-//     // nodeAudio.WriteToOutputStream(audio);
-// }
+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 terminateAudio = () => {
-//     console.log("Terminating audio");
-//     // clearInterval(setStreamsId);
-// }
\ No newline at end of file
+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, source: string) => {
+
+    /* 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;
+}
index 3dddc734841ecd3cb74055daad31820163274dfb..5e3cbad1df6c8a021bd69e3d8318d9daf8cf0dd1 100644 (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();
 });
 
index ef0f9657643cd686b2a272042e3d21fac6a2ecff..5f6383883c67881e479f570c7414efd855ead76b 100644 (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
+});
index 2d9e5fe42648e54f21be2215c4baea6d6af69188..8ae759b66cc51bd57cd2ab0b8ce2fe0beaf70760 100644 (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() {
 
diff --git a/src/render/audioVisualizer.js b/src/render/audioVisualizer.js
new file mode 100644 (file)
index 0000000..8f1fe13
--- /dev/null
@@ -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 );
+};
\ No newline at end of file
index 1092eb505119f2c6b6ade4a3e69f51e898bf6cb9..d351462d52b11a4ddf61414faa3f4619236ff5e8 100644 (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>
 
index 2fb11611ad70edb94a785372cfccdeb8302e9884..97480f3e764edf307f25c4dcbfeefe9411f241ec 100644 (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) {
diff --git a/src/render/components/controllers/inputItem.control.audio.ts b/src/render/components/controllers/inputItem.control.audio.ts
deleted file mode 100644 (file)
index 9852e53..0000000
+++ /dev/null
@@ -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
-    }
-
-}
index 5b9c4030d1ede161068733705d99bdd41b4fb337..7efa6a5333f03d26c4f72c6360a277a191ffe8a2 100644 (file)
       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,
index e6e994c1f78eff29159b8a726c60c80b3eb31fb6..3d9ce0625200f88077508030bb823b8e7bde7c6e 100644 (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)
 );
 
 /**
index 9e03afb18b2ae7945e7baa0a4122f1fa7e00cd84..7415ecda51c978a2cb7077731c2de73d09e7bd21 100644 (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
 });
\ No newline at end of file
diff --git a/src/render/shared/audio.ts b/src/render/shared/audio.ts
new file mode 100644 (file)
index 0000000..8dd4050
--- /dev/null
@@ -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
+}
\ No newline at end of file
index b2a34842adcf902e97a7a6e12d04493a21917f0a..3bb22b2e5d8396611c82f75b2b083fc48f909de7 100644 (file)
+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) => {
-    // 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 connectionStatusCallback = (status: string) => 
+{
+    /* Simplify connectionStatus for tray state */
+    let connectionStatusSimple = false;
+    if (status == statusOptions.pingErrorMessage || statusOptions.closeMessage)
+    {
+        connectionStatusSimple = true;
+    }
 
-const { connect, send, close } = useWebsockets(
-    onMessageCallback,
-    connectionStatusCallback,
-    statusOptions
-);
+    updateTray("disconnect", connectionStatusSimple);
 
-export const updateAppUI = (): void => {
-    uiState.emit();
+    ipcEmit('set-connection-status', status);
 }
 
-// Listen for audio recordings and send them to backend.
-backgroundMitt.on("audio", (audio: Int16Array) => {
-    sendMessage({
-        text: false,
-        audio: audio
-    } as Raw);
-});
+const { connect, send, close } = useWebsockets(onMessageCallback, 
+    connectionStatusCallback, statusOptions);
 
-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();
 }
 
index c62eb2934c12c65e192a894d46827ff4e58a08aa..07c6e46f6b0e824f33acf3fa51859ef8baaa78ee 100644 (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;
-
-interface Icons {
-    [name: string]: NativeImage;
-}
 
-let tray: Tray | null = null;
-const TRAY_ICON_DIR = 'trayIcon';
+let tray: Tray;
+const ICON_PATH = "./build/tray";
 
-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',
+/* Current state of recording, playback, and ws connection */
+const state = {
+    recording: false,
+    playback: false,
+    disconnect: false // whether we're experiencing connection issues
 }
 
-const TRAY_STATUS = {
-    playback: 'playback',
-    recording: 'recording',
-    ...CONNECTION_STATUS
-};
+export function updateTray(attribute: "recording" | "playback" | "disconnect", newState: boolean) {
 
+    state[attribute] = newState;
 
-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),
-};
+    // convert boolean to number
+    const bi = (b: boolean) => b ? 1 : 0;
 
-backgroundMitt.on('update-icon-status', (payload: string) => {
-    updateTray(payload);
-});
+    // 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 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");
+}
\ No newline at end of file
index 0ae39f8e11b969b15f30bd07e548e0d85625efa0..05ce6c34141b2ea173c6387de08b7f38095362de 100644 (file)
@@ -36,7 +36,7 @@ interface Message {
 
 interface Content {
     text: string;
-    audio: string | boolean;
+    audio: Int16Array | false;
 }
 
 interface Context {
index 9e3e769779c7f9c2ae64eb5c25e39c09e886742a..6b3ae3ffb4d92c2d7557b41144f71f1d88d5c882 100644 (file)
 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;
             }
         }
     }
-    
 }
\ No newline at end of file
index b1b064aea84d2133ef00560432e40c04facc8fd5..f3da64a03a280c8dd91d4856669bea369d63271a 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -3478,7 +3478,7 @@ chokidar@^3.0.2, chokidar@^3.3.0, chokidar@^3.4.1:
   optionalDependencies:
     fsevents "~2.1.2"
 
-chownr@^1.0.1, chownr@^1.1.1, chownr@^1.1.2:
+chownr@^1.1.1, chownr@^1.1.2:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
   integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
@@ -4364,7 +4364,7 @@ debounce-fn@^4.0.0:
   dependencies:
     mimic-fn "^3.0.0"
 
-debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
+debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@@ -6807,16 +6807,6 @@ invariant@^2.2.2, invariant@^2.2.4:
   dependencies:
     loose-envify "^1.0.0"
 
-iohook@^0.9.3:
-  version "0.9.3"
-  resolved "https://registry.yarnpkg.com/iohook/-/iohook-0.9.3.tgz#3beae68daeac09bad3a7fde290f595ac7172b6e7"
-  integrity sha512-O6xHAyRfJvVUYu87sZH5CgBPLo/f70NaZSIbm3o6U4L/I9PdntfjlNYrJ7hbv5nPUwfE3k5TGDH1R1bFVdDBZg==
-  dependencies:
-    nugget "^2.0.1"
-    pump "^1.0.3"
-    rc "^1.2.8"
-    tar-fs "^1.16.3"
-
 ip-regex@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
@@ -7204,11 +7194,6 @@ is-yarn-global@^0.3.0:
   resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"
   integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
 
-isarray@0.0.1:
-  version "0.0.1"
-  resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
-  integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
-
 isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -8444,7 +8429,7 @@ memory-fs@^0.5.0:
     errno "^0.1.3"
     readable-stream "^2.0.1"
 
-meow@^3.1.0, meow@^3.7.0:
+meow@^3.7.0:
   version "3.7.0"
   resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
   integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=
@@ -8601,7 +8586,7 @@ minimatch@^3.0.4, minimatch@~3.0.2:
   dependencies:
     brace-expansion "^1.1.7"
 
-minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
+minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
   integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
@@ -9041,19 +9026,6 @@ nth-check@^1.0.2, nth-check@~1.0.1:
   dependencies:
     boolbase "~1.0.0"
 
-nugget@^2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/nugget/-/nugget-2.0.1.tgz#201095a487e1ad36081b3432fa3cada4f8d071b0"
-  integrity sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA=
-  dependencies:
-    debug "^2.1.3"
-    minimist "^1.1.0"
-    pretty-bytes "^1.0.2"
-    progress-stream "^1.1.0"
-    request "^2.45.0"
-    single-line-log "^1.1.2"
-    throttleit "0.0.2"
-
 num2fraction@^1.2.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
@@ -9111,11 +9083,6 @@ object-keys@^1.0.12, object-keys@^1.1.1:
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
   integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
 
-object-keys@~0.4.0:
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336"
-  integrity sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=
-
 object-visit@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
@@ -10047,14 +10014,6 @@ prettier@^1.18.2:
   resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
   integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
 
-pretty-bytes@^1.0.2:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-1.0.4.tgz#0a22e8210609ad35542f8c8d5d2159aff0751c84"
-  integrity sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=
-  dependencies:
-    get-stdin "^4.0.1"
-    meow "^3.1.0"
-
 pretty-error@^2.0.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6"
@@ -10092,14 +10051,6 @@ process@^0.11.10:
   resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
   integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
 
-progress-stream@^1.1.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-1.2.0.tgz#2cd3cfea33ba3a89c9c121ec3347abe9ab125f77"
-  integrity sha1-LNPP6jO6OonJwSHsM0er6asSX3c=
-  dependencies:
-    speedometer "~0.1.2"
-    through2 "~0.2.3"
-
 progress@^2.0.0, progress@^2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
@@ -10158,14 +10109,6 @@ public-encrypt@^4.0.0:
     randombytes "^2.0.1"
     safe-buffer "^5.1.2"
 
-pump@^1.0.0, pump@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954"
-  integrity sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==
-  dependencies:
-    end-of-stream "^1.1.0"
-    once "^1.3.1"
-
 pump@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
@@ -10382,16 +10325,6 @@ readable-stream@^3.0.0, readable-stream@^3.0.6, readable-stream@^3.1.1, readable
     string_decoder "^1.1.1"
     util-deprecate "^1.0.1"
 
-readable-stream@~1.1.9:
-  version "1.1.14"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
-  integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
-  dependencies:
-    core-util-is "~1.0.0"
-    inherits "~2.0.1"
-    isarray "0.0.1"
-    string_decoder "~0.10.x"
-
 readdirp@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
@@ -10570,7 +10503,7 @@ request-promise-native@^1.0.5, request-promise-native@^1.0.7:
     stealthy-require "^1.1.1"
     tough-cookie "^2.3.3"
 
-request@^2.45.0, request@^2.83.0, request@^2.87.0, request@^2.88.0, request@^2.88.2:
+request@^2.83.0, request@^2.87.0, request@^2.88.0, request@^2.88.2:
   version "2.88.2"
   resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
   integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@@ -11116,13 +11049,6 @@ simple-swizzle@^0.2.2:
   dependencies:
     is-arrayish "^0.3.1"
 
-single-line-log@^1.1.2:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/single-line-log/-/single-line-log-1.1.2.tgz#c2f83f273a3e1a16edb0995661da0ed5ef033364"
-  integrity sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q=
-  dependencies:
-    string-width "^1.0.1"
-
 sisteransi@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
@@ -11339,11 +11265,6 @@ spectron@11.0.0:
     split "^1.0.0"
     webdriverio "^4.13.0"
 
-speedometer@~0.1.2:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-0.1.4.tgz#9876dbd2a169d3115402d48e6ea6329c8816a50d"
-  integrity sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0=
-
 split-string@^3.0.1, split-string@^3.0.2:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
@@ -11578,11 +11499,6 @@ string_decoder@^1.0.0, string_decoder@^1.1.1:
   dependencies:
     safe-buffer "~5.2.0"
 
-string_decoder@~0.10.x:
-  version "0.10.31"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
-  integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
-
 string_decoder@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
@@ -11769,17 +11685,7 @@ tapable@^1.0.0, tapable@^1.1.3:
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
   integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
 
-tar-fs@^1.16.3:
-  version "1.16.3"
-  resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509"
-  integrity sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==
-  dependencies:
-    chownr "^1.0.1"
-    mkdirp "^0.5.1"
-    pump "^1.0.0"
-    tar-stream "^1.1.2"
-
-tar-stream@^1.1.2, tar-stream@^1.5.0:
+tar-stream@^1.5.0:
   version "1.6.2"
   resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"
   integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==
@@ -11923,11 +11829,6 @@ throat@^4.0.0:
   resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
   integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=
 
-throttleit@0.0.2:
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-0.0.2.tgz#cfedf88e60c00dd9697b61fdd2a8343a9b680eaf"
-  integrity sha1-z+34jmDADdlpe2H90qg0OptoDq8=
-
 through2-filter@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254"
@@ -11952,14 +11853,6 @@ through2@^2.0.0, through2@~2.0.0:
     readable-stream "~2.3.6"
     xtend "~4.0.1"
 
-through2@~0.2.3:
-  version "0.2.3"
-  resolved "https://registry.yarnpkg.com/through2/-/through2-0.2.3.tgz#eb3284da4ea311b6cc8ace3653748a52abf25a3f"
-  integrity sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=
-  dependencies:
-    readable-stream "~1.1.9"
-    xtend "~2.1.1"
-
 through@2, through@^2.3.6:
   version "2.3.8"
   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
@@ -13141,13 +13034,6 @@ xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1:
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
   integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
 
-xtend@~2.1.1:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b"
-  integrity sha1-bv7MKk2tjmlixJAbM3znuoe10os=
-  dependencies:
-    object-keys "~0.4.0"
-
 y18n@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"