stream path animation v4

update input items styling

add ipc event handler for message

add send-message ipc handler

add audio to send-message ipc

remove extra JSON stringify statement for audio message

add render bug fix for add contacts
This commit is contained in:
Enrique Hernandez 2020-12-17 11:50:03 -06:00 committed by riqo
commit 1627e07964
23 changed files with 450 additions and 295 deletions

View file

@ -8,22 +8,15 @@ import * as path from "path";
import { SpeechRecorder } from "speech-recorder"; import { SpeechRecorder } from "speech-recorder";
// TODO: connect To Crimata-BE // TODO: connect To Crimata-BE
// import WebSocket from "ws"; import WebSocket from "ws";
// const ws = new WebSocket("ws://localhost:8080");
// ws.on("open", function open() {
// ws.send("hello fro client");
// });
// ws.on("message", (message: any) => {
// console.log("received message", message);
// });
//
// const recorder = new SpeechRecorder({ sampleRate: 16000, framesPerBuffer: 320 }); // const recorder = new SpeechRecorder({ sampleRate: 16000, framesPerBuffer: 320 });
// Keep a global reference of the window object, if you don't, the window will // Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected. // be closed automatically when the JavaScript object is garbage collected.
let win: BrowserWindow | null; let win: BrowserWindow | null;
let ws: any;
// Scheme must be registered before the app is ready // Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([ protocol.registerSchemesAsPrivileged([
@ -56,44 +49,35 @@ function createWindow() {
win.loadURL("app://./index.html"); win.loadURL("app://./index.html");
} }
win.webContents.on('did-finish-load', () => { win.webContents.on('did-finish-load', () => {
// ipcMain.on('audio-stream', (Event: any, Payload: any) => {
// if (Payload.stream) {
// console.log('begin streaming');
// recorder.start({
// onAudio: (audio: object, speech: boolean) => {
// console.log(audio)
// }
// });
// } else {
// recorder.stop();
// console.log('stop streaming')
// }
// })
// if (win) { ws = new WebSocket("ws://localhost:8080");
// recorder.start({ ws.on("open", function open() {
// onAudio: (audio: object, speech: boolean) => { ws.send("hernandeze2@xavier.edu");
// if (win) { });
// win.setTitle("Listening..."); ws.on("message", (message: any) => {
// // win.webContents.send('render-audio', { const parsed = JSON.parse(message);
// // audio: audio, console.log('new message', message)
// // speech: speech if (win) {
// // }) win.webContents.send('render-message', {
// if (speech) { message: parsed
// win.setTitle("Recording..."); })
// // TODO: send audio to crimata BE }
// // ws.send(audio); });
// }
// } ipcMain.on('send-message', (Event: any, payload: any) => {
// } const stringMessage = JSON.stringify(payload.message)
// }); ws.send(stringMessage);
// } })
ipcMain.on('update-menu-bar', (Event: any, payload: any) => {
// win.setTitle("Listening...");
})
}) })
win.on("closed", () => { win.on("closed", () => {
win = null; win = null;
ws = null;
}); });
} }

View file

@ -12,7 +12,6 @@
class="messageList" class="messageList"
transform="translate(0, 25)" transform="translate(0, 25)"
preserverAspectRatio="none" preserverAspectRatio="none"
> >
<transition <transition
appear appear
@ -28,13 +27,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import useRenderView from '@/core/messenger/renderView/useRenderView'; import useMessageView from "@/core/messenger/messageView/useMessageView";
import useScrollView from "@/core/UI/scrollView/useScrollView"; import useScrollView from "@/core/UI/scrollView/useScrollView";
import { import { defineComponent, reactive, inject } from "vue";
defineComponent,
reactive,
inject,
} from "vue";
import MessageItem from "./messageItem.vue"; import MessageItem from "./messageItem.vue";
import { Message } from "@/types/message/index"; import { Message } from "@/types/message/index";
import { Mitt } from "@/types/mitt/index"; import { Mitt } from "@/types/mitt/index";
@ -54,9 +49,9 @@ export default defineComponent({
}); });
} }
const { beforeEnter, enter, afterEnter } = useRenderView(); const { beforeEnter, enter, afterEnter } = useMessageView();
const { scrollView } = useScrollView({ const { scrollView } = useScrollView({
targetId: "messageRenderer" targetId: "messageRenderer",
}); });
return { return {

View file

@ -2,35 +2,35 @@ import { Message } from "@/types/message/index";
const Messages: Message[] = [ const Messages: Message[] = [
{ {
content: "Sounds good, see you Thursday.", content: "Welcome",
context: "Message from Josie Wright", context: "ai",
subContext: "Subject: Chick File", subContext: "",
modifier: "ai", modifier: "ai",
id: "2", id: "2",
time: "" time: ""
}, },
{ {
content: content:
"I can be there at 5 pm tomorrow to greet you at a Chick file with my almond and chive salad.", "add friend",
context: "You to Josie Wright", context: "launch",
subContext: "josie@gmail.com", subContext: "",
modifier: "sf", modifier: "sf",
id: "1", id: "1",
time: "" time: ""
}, },
{ {
content: "Sweet, thanks!", content: "Did you mean to addcontact?",
context: "Message from Tommy McConville", context: "addcontact",
subContext: "", subContext: "",
modifier: "ai", modifier: "ai",
id: "5", id: "5",
time: "" time: ""
}, },
{ {
content: "Hey do you know Enrique Hernandez?", content: "yes",
context: "Message from Crimata AI", context: "fulfill",
subContext: "", subContext: "",
modifier: "ai", modifier: "sf",
id: "6", id: "6",
time: "" time: ""
}, },

View file

@ -41,6 +41,11 @@ export default defineComponent({
} }
} }
window.ipcRenderer.on("render-message", (event, payload) => {
console.log(payload.message)
emitter.emit("renderMessage", payload.message);
})
return { return {
emittMessage, emittMessage,
}; };

View file

@ -1,53 +1,68 @@
<template> <template>
<svg xmlns="http://www.w3.org/2000/svg" :width="bubbleWidth" height="34" :x="center" y="450" > <svg
<g id="Group_126" data-name="Group 126" transform="translate(0 0)"> xmlns="http://www.w3.org/2000/svg"
<rect id="Rectangle_478" data-name="Rectangle 478" :width="bubbleWidth" height="34" rx="10" transform="translate(0 0)" fill="#b9b9b9"/> :width="bubbleWidth"
<g id="Group_125" data-name="Group 125"> height="34"
:x="center"
<path v-for="(path, index) in paths" y="440"
:key="index" rx="10"
:id="path.id" class="audioInput"
:data-name="path.name" >
:d="path.d" <g
:transform="path.transform" id="Group_126"
fill="none" data-name="Group 126"
stroke="#000" transform="translate(0 0)"
stroke-linecap="round" rx="10"
stroke-width="1.5" >
<rect
id="Rectangle_478"
data-name="Rectangle 478"
:width="bubbleWidth"
height="34"
rx="10"
transform="translate(0 0)"
fill="#b9b9b9"
/> />
<svg>
<!-- <path id="Path_328" data-name="Path 328" d="M0,25v-10" transform="translate(10 0)" fill="none" stroke="#000" stroke-linecap="round" stroke-width="1.5"/> <g rx="20" id="Group_125" data-name="Group 125" v-for="(path, index) in paths">
<path id="Path_328" data-name="Path 328" d="M0,25v-15" transform="translate(15 0)" fill="none" stroke="#000" stroke-linecap="round" stroke-width="1.5"/> <path
<path id="Path_328" data-name="Path 328" d="M0,25v-20" transform="translate(20 0)" fill="none" stroke="#000" stroke-linecap="round" stroke-width="1.5"/> :key="index"
id="path"
<path id="Path_328" data-name="Path 328" d="M0,25v-15" transform="translate(25 0)" fill="none" stroke="#000" stroke-linecap="round" stroke-width="1.5"/> :data-name="path.identifier"
<path id="Path_328" data-name="Path 328" d="M0,25v-10" transform="translate(30 0)" fill="none" stroke="#000" stroke-linecap="round" stroke-width="1.5"/> :d="path.d"
:transform="`translate(${path.offset} 0)`"
<path id="Path_328" data-name="Path 328" d="M0,25v-5" transform="translate(120 0)" fill="none" stroke="#000" stroke-linecap="round" stroke-width="1.5"/> fill="none"
--> </g> :stroke="path.color"
</g> stroke-linecap="round"
</svg> stroke-width="1.5"
/>
</g>
</svg>
</g>
</svg>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, computed, ref } from 'vue' import { defineComponent, computed, inject } from "vue";
export default defineComponent ({ export default defineComponent({
name: 'AudioInput', name: "AudioInput",
props: { props: {
bubbleWidth: Number, bubbleWidth: Number,
paths: Array paths: Array,
}, },
setup(props){ setup(props) {
// need audioPath type/interface const emitter: any = inject("mitt");
function audioSend() {
}
const center = computed(() => { const center = computed(() => {
if (props.bubbleWidth) { if (props.bubbleWidth) {
return 175 - props.bubbleWidth / 2; return 175 - props.bubbleWidth / 2;
} }
}) });
return { center } return { center };
} },
}) });
</script> </script>

View file

@ -1,5 +1,11 @@
<template> <template>
<foreignObject class="inputContainer" :x="textXoffset" y="450" width="55%" height="50%"> <foreignObject
class="inputContainer"
:x="textXoffset"
y="450"
width="55%"
height="50%"
>
<div xmlns="http://www.w3.org/1999/xhtml"> <div xmlns="http://www.w3.org/1999/xhtml">
<div class="inputBubble" contenteditable v-show="input.length > 0"> <div class="inputBubble" contenteditable v-show="input.length > 0">
<p>{{ input }}</p> <p>{{ input }}</p>
@ -28,15 +34,15 @@ div[contenteditable] {
display: table; display: table;
justify-items: center; justify-items: center;
border: 0; border: 0;
border-radius: 20px; border-radius: 10px;
color: #fff; color: #000;
background-color: #585858; background-color: #b9b9b9;
font-size: 14; font-size: 14;
text-align: left; text-align: left;
p { p {
max-width: 150px; max-width: 150px;
overflow-wrap: break-word; overflow-wrap: break-word;
padding: 10px 15px 10px 15px; padding: 7px 10px 5px 10px;
margin: 0; margin: 0;
} }
} }

View file

@ -1,40 +1,32 @@
<template> <template>
<audio-input <audio-input v-if="visualizeStream" :bubbleWidth="audioBubbleWidth" :paths="paths" />
v-if="renderAudio" <text-input v-else :input="input" :textXoffset="textInputXoffset" />
:bubbleWidth="reactiveWidth"
:paths="paths"
/>
<text-input
v-else
:input="input"
:textXoffset="textInputXoffset"
/>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent } from "vue";
import TextInput from './inputDetails/textInput.vue'; import TextInput from "./inputDetails/textInput.vue";
import AudioInput from './inputDetails/audioInput.vue'; import AudioInput from "./inputDetails/audioInput.vue";
import useInputController from "@/core/UI/input/useInputController"; import useInputController from "@/core/UI/input/useInputController";
export default defineComponent({ export default defineComponent({
name: "InputItem", name: "InputItem",
components: {TextInput, AudioInput}, components: { TextInput, AudioInput },
setup() { setup() {
const { const {
input, input,
renderAudio, visualizeStream,
reactiveWidth,
textInputXoffset, textInputXoffset,
paths } = useInputController(); audioBubbleWidth,
paths,
} = useInputController();
return { return {
input, input,
textInputXoffset, textInputXoffset,
renderAudio, visualizeStream,
reactiveWidth, audioBubbleWidth,
paths paths,
} };
} },
}) });
</script> </script>

View file

@ -0,0 +1,28 @@
import AnimeFunc from "@/types/animejs/index";
import {inject} from "vue";
import {Ref} from "@/types/vueRef/index";
export default function useAudioAnimations() {
// imports animejs safely
let anime: AnimeFunc;
const animeInject: AnimeFunc | undefined = inject("animejs");
if (animeInject) anime = animeInject;
function animateSend(el: Element, trigger: Ref<boolean>) {
console.log(el);
anime({
targets: el,
opacity: 0,
translateY: -100,
duration: 0,
complete: function() {
trigger.value = false;
}
})
}
return {
animateSend
}
}

View file

@ -1,8 +1,10 @@
import {inject} from 'vue';
export default function useStreamRecord(): { export default function useStreamRecord(): {
initRecorder: (stream: MediaStream) => MediaRecorder; initRecorder: (stream: MediaStream) => MediaRecorder;
} { } {
let streamRecorder: MediaRecorder; let streamRecorder: MediaRecorder;
const chunks: Blob[] = []; const chunks: Blob[] = [];
const emitter: any = inject("mitt");
const initRecorder = (stream: MediaStream) => { const initRecorder = (stream: MediaStream) => {
streamRecorder = new MediaRecorder(stream); streamRecorder = new MediaRecorder(stream);
@ -10,6 +12,31 @@ export default function useStreamRecord(): {
streamRecorder.onstop = (e: any) => { streamRecorder.onstop = (e: any) => {
// send audio to BE // send audio to BE
const audioBlob = new Blob(chunks, { 'type': 'audio/ogg; codecs=opus' }); const audioBlob = new Blob(chunks, { 'type': 'audio/ogg; codecs=opus' });
// emitter.emit("newSelfMessage", audioBlob);
const blobToBase64 = (blob: any) => {
return new Promise((resolve) => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function () {
resolve(reader.result);
};
});
};
(async () => {
const b64 = await blobToBase64(audioBlob);
const jsonString = JSON.stringify({blob: b64});
const message = {
text: '',
audio: b64
}
window.postMessage({
myTypeField: 'send-message',
message: message
}, '*')
})();
} }
return streamRecorder return streamRecorder
} }

View file

@ -1,81 +0,0 @@
import {Ref} from '@/types/vueRef/index';
export default function useStreamRender(render: Ref<boolean>) {
// window.AudioContext = window.AudioContext;
const audioContext = new AudioContext();
const analyzer = audioContext.createAnalyser();
analyzer.fftSize = 2048;
const xInitial = 24;
const xFinal = 640;
const dataArray = new Uint8Array(analyzer.frequencyBinCount);
interface AmplitudePath {
id: string;
name: string;
d: string;
transform: string;
}
const createAudioPath = (a: number, offset: number) => {
const identifier = `path_${offset}`
const path: AmplitudePath = {
id: identifier,
name: identifier,
d: `M0,25v${-a}`,
transform: `translate(${offset} 0)`
}
return path;
}
function visualize(w: Ref<number>, p: Ref<number>, paths: Ref<any>) {
const amplitude = () => {
const dataView = dataArray.slice(xInitial, xFinal);
const sum = dataView.reduce((a, b) => a + b);
const average = sum / dataView.length;
const scaled = average * 0.45;
return scaled;
}
function draw() {
if (!render.value) {
return;
}
requestAnimationFrame(draw);
analyzer.getByteFrequencyData(dataArray);
// max-width
if (w.value === 300) {
return;
}
// increase width as time passes
// get amplitude
const a = amplitude();
if (a > 1) {
w.value++;
p.value += 5;
console.log(a)
const path = createAudioPath(a, p.value);
paths.value.push(path)
}
// create path item
}
draw();
}
function renderStream(s: MediaStream, w: Ref<number>, pathOffset: Ref<number>, paths: Ref<any>) {
const source = audioContext.createMediaStreamSource(s);
source.connect(analyzer);
visualize(w, pathOffset, paths);
}
return {
renderStream
}
}

View file

@ -0,0 +1,148 @@
import { Ref } from '@/types/vueRef/index';
import AnimeFunc from "@/types/animejs/index";
import { inject, ref } from "vue";
interface AudioPath {
identifier: string;
d: string;
offset: number;
color: string;
draw: boolean;
}
export default function useStreamVisualizer(draw: Ref<boolean>) {
const audioContext = new AudioContext();
console.log('audi sample rate', audioContext.sampleRate)
const analyzer = audioContext.createAnalyser();
analyzer.fftSize = 128;
const bufferLength = analyzer.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
// imports animejs safely
let anime: AnimeFunc;
const animeInject: AnimeFunc | undefined = inject("animejs");
if (animeInject) anime = animeInject;
/*
* Draws MediaStream audioTrack as 'AudioPath' objects
*/
function drawStreamAs(paths: Ref<any>, bubbleWidth: Ref<number>): void {
const xInitial = ref(10);
const maxAmplitude = 15;
const maxBubbleWidth = 300;
function shiftPathStream(pathList: NodeList): void {
for (let i = 0; i < pathList.length; i++) {
const stick = pathList[i] as HTMLElement;
const computedQuery = window.getComputedStyle(stick);
const matrix = new WebKitCSSMatrix(computedQuery.webkitTransform);
anime({
targets: stick,
translateX: [matrix.m41, matrix.m41 - 6],
})
}
}
function filterPathList(pathList: NodeList) {
for (let i = 0; i < pathList.length; i++) {
const stick = pathList[i] as HTMLElement;
const computedQuery = window.getComputedStyle(stick);
const matrix = new WebKitCSSMatrix(computedQuery.webkitTransform);
if (matrix.m41 < 10) {
anime({
targets: stick,
opacity: 0,
duration: 0
})
}
}
}
function drawNewPath(): void {
const calculateAmplitude = (): number => (
Math.min(Math.max(
(dataArray.reduce((a, b) => a + b) / bufferLength) * 0.45,
0), maxAmplitude)
)
const createAudioPath = (a: number, o: number): AudioPath => ({
identifier: `path_${o}`,
d: `M0,25v${-a}`,
offset: o,
color: '#000',
draw: true
})
const amplitude = calculateAmplitude();
const p: AudioPath = createAudioPath(amplitude, xInitial.value);
paths.value.push(p);
}
paths.value = [];
function drawPathStream(): void {
let streamPath;
if (!draw.value) {
window.clearTimeout(streamPath)
return;
}
setTimeout(() => {
requestAnimationFrame(drawPathStream);
analyzer.getByteFrequencyData(dataArray);
}, 1000 / 10)
/*
* To Draw pathStream
*/
if (bubbleWidth.value === 300) {
xInitial.value = bubbleWidth.value - 10;
} else if (bubbleWidth.value > 20) {
xInitial.value = bubbleWidth.value;
} else {
xInitial.value +=3;
}
streamPath = setTimeout(async () => {
const pathList = document.querySelectorAll('path');
filterPathList(pathList);
drawNewPath();
if (bubbleWidth.value === maxBubbleWidth) {
shiftPathStream(pathList);
}
}, 200)
}
drawPathStream();
}
function increaseBubbleWidth(bubbleRef: Ref<number>) {
const increase = () => {
let animation;
if (!draw.value) {
if (animation) {
cancelAnimationFrame(animation)
}
return;
}
animation = requestAnimationFrame(increase);
if (bubbleRef.value < 300) {
bubbleRef.value++;
}
}
increase();
}
function expandBubble(bubbleWidth: Ref<number>) {
increaseBubbleWidth(bubbleWidth);
}
function visualizeStreamAsPaths(s: MediaStream, paths: Ref<any>, bubbleWidth: Ref<number>): void {
const source = audioContext.createMediaStreamSource(s);
source.connect(analyzer);
drawStreamAs(paths, bubbleWidth);
}
return {
visualizeStreamAsPaths,
expandBubble
}
}

View file

@ -35,6 +35,7 @@ export default function useTextInputAnims() {
anime({ anime({
targets: el, targets: el,
translateY: -10, translateY: -10,
duration: 0
}); });
} }

View file

@ -1,33 +1,35 @@
import {ref, computed, inject} from 'vue'; import { ref, computed, inject } from 'vue';
import useTextInputAnims from "./textDetails/useAnimations"; import useTextInputAnims from "./textDetails/useAnimations";
import { Ref } from "@/types/vueRef/index"; import { Ref } from "@/types/vueRef/index";
/* /*
* text rendering logic used by inputController * text rendering logic used by inputController
*/ */
export default function useTextRender(input: Ref<string>) { export default function useTextRender(input: Ref<string>) {
const emitter: any = inject("mitt");
const {animateSend, verticalShiftInput, inputAppear} = useTextInputAnims();
const inputHeight = ref(42); const inputHeight = ref(42);
const inputWidth = ref(0); const inputWidth = ref(0);
const renderTextInput = async() => {
const inputDivs = await document.getElementsByClassName("inputBubble"); const emitter: any = inject("mitt");
const height = inputDivs[0].getBoundingClientRect().height; const { animateSend, verticalShiftInput, inputAppear } = useTextInputAnims();
inputWidth.value = inputDivs[0].getBoundingClientRect().width;
const foreignObjectDiv = document.getElementsByClassName( async function renderTextInput() {
"inputContainer" // first, get input dimensions
); const inputDivs = await document.getElementsByClassName("inputBubble");
if (height !== inputHeight.value) { const height = inputDivs[0].getBoundingClientRect().height;
inputHeight.value = height; inputWidth.value = inputDivs[0].getBoundingClientRect().width;
} // then, query for foreignObject div
const foreignEl = foreignObjectDiv[0] as HTMLElement; const foreignObjectDiv = document.getElementsByClassName(
if (inputHeight.value === 0) { "inputContainer"
inputAppear(foreignEl) );
} else if (inputHeight.value > 36) { const foreignEl = foreignObjectDiv[0] as HTMLElement;
const yTrans = -inputHeight.value + 30; if (height !== inputHeight.value) {
verticalShiftInput(foreignEl, yTrans); inputHeight.value = height;
} }
if (inputHeight.value === 0) {
inputAppear(foreignEl)
} else if (inputHeight.value > 36) {
const yTrans = -inputHeight.value + 30;
verticalShiftInput(foreignEl, yTrans);
}
} }
emitter.on("newSelfMessage", async (payload: any) => { emitter.on("newSelfMessage", async (payload: any) => {

View file

@ -1,71 +1,75 @@
import {ref, watch, onUnmounted, onMounted } from "vue"; import { ref, watch, onUnmounted, onMounted } from "vue";
import useKeyDownHandler from "@/utils/keyDownHandler/useKeyDownHandler"; import useKeyDownHandler from "@/utils/keyDownHandler/useKeyDownHandler";
import useMediaStream from "@/utils/mediaStream/useMediaStream"; import useMediaStream from "@/utils/mediaStream/useMediaStream";
import useStreamRecord from "./audio/useStreamRecord"; import useStreamRecord from "./audio/useStreamRecord";
import useStreamRender from './audio/useStreamRender'; import useStreamVisualizer from './audio/useStreamVisualizer';
import useTextRender from './text/useTextRender'; import useTextRender from './text/useTextRender';
import useAudioAnimations from './audio/audioDetails/useAnimations';
/* /*
* Input controller logic * Input controller logic
* Will render text input by default as user types or * Will visualize text input by default as user types or
* if user holds space bar down, audio media-stream * if user holds space bar down, input audio stream
* will be rendered & recorded * will be visualized & recorded
*/ */
// NOTE: input params should probably be input and MediaStream
export default function useInputController() { export default function useInputController() {
// program ref varaiables const visualizeStream = ref(false);
const renderAudio = ref(false);
const reactiveWidth = ref(10);
const pathOffset = ref(10);
const paths = ref([]); const paths = ref([]);
// audioStream variables const audioBubbleWidth = ref(10);
let stream: MediaStream; let stream: MediaStream;
let recorder: MediaRecorder; let recorder: MediaRecorder;
// use input ref & keydown handler function
const { keyDownHandler, input } = useKeyDownHandler(); const { keyDownHandler, input } = useKeyDownHandler();
// use text Offset computed prop and & textRender function
const { textInputXoffset, renderTextInput } = useTextRender(input); const { textInputXoffset, renderTextInput } = useTextRender(input);
// use stream render and record logic const { visualizeStreamAsPaths, expandBubble } = useStreamVisualizer(visualizeStream);
const { renderStream } = useStreamRender(renderAudio);
const { initRecorder } = useStreamRecord() const { initRecorder } = useStreamRecord()
const { animateSend } = useAudioAnimations();
function audioSend() {
const domQuery = document.getElementsByClassName("audioInput");
const inputEl = domQuery[0];
animateSend(inputEl, visualizeStream);
}
// stops stream recording on space key up
function keyupHandler(e: any) {
if (e.key === " " && visualizeStream.value) {
console.log("stop recording");
recorder.stop();
visualizeStream.value = false;
paths.value = []
audioBubbleWidth.value = 10;
input.value = ''
}
}
// add window event keyboard listeners
window.addEventListener("keydown", keyDownHandler);
window.addEventListener('keyup', keyupHandler);
// get stream & initialize recorder object on mount // get stream & initialize recorder object on mount
onMounted(async () => { onMounted(async () => {
stream = await useMediaStream(); stream = await useMediaStream();
console.log(stream)
recorder = initRecorder(stream); recorder = initRecorder(stream);
}) })
// callback function called on key up event
function keyupHandler(e: any) {
// stop stream recording on space key up
if (e.key === " " && renderAudio.value) {
console.log("stop recording");
recorder.stop();
// reset controller ref variables
renderAudio.value = false;
input.value = "";
reactiveWidth.value = 10;
pathOffset.value = 10;
}
}
// step 1: add window event keyboard listeners // determine whether to render text or audio based one the keyboard input
window.addEventListener("keydown", keyDownHandler);
window.addEventListener('keyup', keyupHandler);
// step 2: determine whether to render text or audio based one the keyboard input
watch(input, (input, prevInput) => { watch(input, (input, prevInput) => {
if (prevInput !== "" || prevInput.length > 0) { if (prevInput !== "" || prevInput.length > 0) {
if (!renderAudio.value) { if (!visualizeStream.value) {
renderTextInput(); renderTextInput();
} }
return; return;
} }
if (input === " " && renderAudio.value === false) { if (input === " " && visualizeStream.value === false) {
console.log("record"); console.log("record MediaStream");
renderAudio.value = true; visualizeStream.value = true;
recorder.start(); recorder.start();
renderStream(stream, reactiveWidth, pathOffset, paths); expandBubble(audioBubbleWidth);
visualizeStreamAsPaths(stream, paths, audioBubbleWidth);
return; return;
} }
renderTextInput(); renderTextInput();
@ -79,9 +83,9 @@ export default function useInputController() {
return { return {
input, input,
renderAudio, visualizeStream,
reactiveWidth,
textInputXoffset, textInputXoffset,
audioBubbleWidth,
paths paths
} }

View file

@ -49,6 +49,7 @@ export default function useMessageItemComp(m: string, id: string) {
if (messageRenderer) { if (messageRenderer) {
const rect = messageRenderer.getBoundingClientRect(); const rect = messageRenderer.getBoundingClientRect();
let Y = 600; let Y = 600;
console.log('renderer height',rect.height)
if (rect.height > 500) { if (rect.height > 500) {
Y = rect.height + messageMarginTop; Y = rect.height + messageMarginTop;
} }

View file

@ -1,21 +1,21 @@
import useRendererAnims from "./renderDetails/useAnimations"; import useMessageViewAnims from "./viewDetails/useAnimations";
import useOffsetCalculator from './renderDetails/useOffsetCalculator'; import useOffsetCalculator from './viewDetails/useOffsetCalculator';
import { VueTransitionCallback } from '@/types/vue3/vueTransitionCallback/index'; import { VueTransitionCallback } from '@/types/vue3/vueTransitionCallback/index';
/** /**
* renderView component logic * messageView component logic
* handles rendering of exisiting user messages * handles visualization of exisiting user messages
* as well as incoming messages * as well as incoming messages
*/ */
export default function useRenderView(): { export default function useMessageView(): {
beforeEnter: VueTransitionCallback; beforeEnter: VueTransitionCallback;
enter: VueTransitionCallback; enter: VueTransitionCallback;
afterEnter: VueTransitionCallback; afterEnter: VueTransitionCallback;
} { } {
// uses offsetCalculator logic and renderer animations // uses offsetCalculator logic and renderer animations
const { calculateMessageOffsets } = useOffsetCalculator(); const { calculateMessageOffsets } = useOffsetCalculator();
const { shiftAnimate, stackAnimate } = useRendererAnims(); const { shiftAnimate, stackAnimate } = useMessageViewAnims();
const messagePadding = 15; const messagePadding = 15;
let messageList: NodeList; let messageList: NodeList;
@ -54,11 +54,18 @@ export default function useRenderView(): {
// get message positioning & shift state before animating // get message positioning & shift state before animating
if (message) { if (message) {
messageOffset = calculateMessageOffsets(message); messageOffset = calculateMessageOffsets(message);
shift = messageOffset.y > 445 ? true : false; console.log(messageOffset.y)
shift = messageOffset.y > 400 ? true : false;
} }
if (el) { if (el) {
if (shift) { if (shift) {
shiftOffset -= el.getBBox().height + messagePadding;
console.log(messageOffset.y)
if (messageOffset.y < 445) {
shiftOffset -= 15;
} else {
shiftOffset -= el.getBBox().height + messagePadding;
}
} }
animateMessage(el); animateMessage(el);
} }

View file

@ -5,7 +5,7 @@ import { MessageAnimeFunc } from '@/types/message/index';
/** /**
* anime js animations used by message renderer * anime js animations used by message renderer
*/ */
export default function useRendererAnims(): { export default function useMessageViewAnims(): {
shiftAnimate: MessageAnimeFunc; shiftAnimate: MessageAnimeFunc;
stackAnimate: MessageAnimeFunc; stackAnimate: MessageAnimeFunc;
} { } {

View file

@ -2,8 +2,10 @@
// It has the same sandbox as a Chrome extension. // It has the same sandbox as a Chrome extension.
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
interface Window { declare global {
ipcRenderer: typeof ipcRenderer; interface Window {
ipcRenderer: typeof ipcRenderer;
}
} }
window.ipcRenderer = ipcRenderer; window.ipcRenderer = ipcRenderer;
@ -12,8 +14,12 @@ process.once("loaded", () => {
// do something with custom event // do something with custom event
const message = event.data; const message = event.data;
if (message.myTypeField === "audio-stream") { if (message.myTypeField === "send-message") {
ipcRenderer.send("audio-stream", message); ipcRenderer.send("send-message", message);
}
if (message.myTypeField === "update-menu-bar") {
ipcRenderer.send("update-menu-bar", message);
} }
}); });
}); });

View file

@ -1,12 +1,16 @@
interface AnimeFuncParams { interface AnimeFuncParams {
targets: Element | HTMLElement | SVGGElement | SVGElement; targets: Element | HTMLElement | SVGGElement | SVGElement;
translateY?: number; translateY?: number;
translateX?: number | any[] | undefined;
transform?: string; transform?: string;
easing?: string; easing?: string;
duration?: number; duration?: number;
offset?: number; offset?: number;
begin?: () => void; begin?: () => void;
complete?: () => void; complete?: () => void;
stroke?: string;
opacity?: number;
} }
export default interface AnimeFunc { export default interface AnimeFunc {

View file

@ -19,15 +19,16 @@ export default function useKeyDownHandler() {
return; return;
} }
const message = { const message = {
content: input.value, text: input.value,
signature: "EnriqueH", audio: 0
outgoing: true,
modifier: "sf",
imgURL: "/test/path",
id: null
}; };
// fire event with message payload // fire event with message payload
emitter.emit("newSelfMessage", message); emitter.emit("newSelfMessage", message);
window.postMessage({
myTypeField: 'send-message',
message: message
}, '*')
} }
function keyDownHandler(e: KeyDownEvent) { function keyDownHandler(e: KeyDownEvent) {

View file

@ -1,4 +1,6 @@
export default async function useMediaStream() { export default async function useMediaStream() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// var audioTrack = stream.getAudioTracks()[0];
// console.log(audioTrack.getSettings())
return stream; return stream;
} }

View file

@ -1,5 +1,13 @@
const WebSocket = require("ws"); const WebSocket = require("ws");
const testMessage = {
content: 'Hello from test server!' ,
context: 'test message',
subContext: '',
modifiers: 'ai',
id: "0",
time: 'test'
}
const wss = new WebSocket.Server({ const wss = new WebSocket.Server({
port: 8080 port: 8080
// perMessageDeflate: { // perMessageDeflate: {
@ -30,5 +38,5 @@ wss.on("connection", function connection(ws, req) {
const ip = req.socket.remoteAddress; const ip = req.socket.remoteAddress;
console.log("received connection from", ip); console.log("received connection from", ip);
ws.send("hello from server!"); ws.send(JSON.stringify(testMessage));
}); });