Merge branch 'core' into 'master'
refactor composables into core directory See merge request crimata-ai/crimata-electron!16
This commit is contained in:
commit
52f6893c50
25 changed files with 353 additions and 332 deletions
|
|
@ -28,9 +28,9 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, ref, reactive } from "vue";
|
import { defineComponent, onMounted, ref, reactive } from "vue";
|
||||||
import useMessageItemComp from "@/composables/messageItem/useMessageItemComp";
|
import useMessageItemComp from "@/core/messenger/message/messageDetails/useComputed";
|
||||||
import useMessageItemAnims from "@/composables/messageItem/useMessageItemAnims";
|
import useMessageItemAnims from "@/core/messenger/message/messageDetails/useAnimations";
|
||||||
import useTextWrap from "@/composables/textWrap/useTextWrap";
|
import useTextWrap from "@/utils/textWrap/useTextWrap";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "MessageItem",
|
name: "MessageItem",
|
||||||
props: {
|
props: {
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import useMessageRenderer from "@/composables/messageRenderer/useMessageRenderer";
|
import useRenderView from '@/core/messenger/renderView/useRenderView';
|
||||||
import useScrollView from "@/composables/scrollView/useScrollView";
|
import useScrollView from "@/core/UI/scrollView/useScrollView";
|
||||||
import {
|
import {
|
||||||
defineComponent,
|
defineComponent,
|
||||||
reactive,
|
reactive,
|
||||||
|
|
@ -54,8 +54,10 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { beforeEnter, enter, afterEnter } = useMessageRenderer();
|
const { beforeEnter, enter, afterEnter } = useRenderView();
|
||||||
const {scrollView} = useScrollView({ targetId: "messageRenderer"});
|
const { scrollView } = useScrollView({
|
||||||
|
targetId: "messageRenderer"
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
renderArr,
|
renderArr,
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@
|
||||||
</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 useInputRenderer from "@/composables/inputItem/useInputRenderer";
|
import useInputController from "@/core/UI/input/useInputController";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "InputItem",
|
name: "InputItem",
|
||||||
components: {TextInput, AudioInput},
|
components: {TextInput, AudioInput},
|
||||||
|
|
@ -26,7 +26,7 @@ export default defineComponent({
|
||||||
renderAudio,
|
renderAudio,
|
||||||
reactiveWidth,
|
reactiveWidth,
|
||||||
textInputXoffset,
|
textInputXoffset,
|
||||||
paths } = useInputRenderer();
|
paths } = useInputController();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
input,
|
input,
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
export default function useAudioRenderer(stream: MediaStream) {
|
|
||||||
const audioCtx = new (window.AudioContext)();
|
|
||||||
const analyzer = audioCtx.createAnalyser();
|
|
||||||
const source = audioCtx.createMediaStreamSource(stream);
|
|
||||||
source.connect(analyzer);
|
|
||||||
|
|
||||||
analyzer.fftSize = 2048;
|
|
||||||
const xInitial = 24;
|
|
||||||
const xFinal = 640;
|
|
||||||
analyzer.minDecibels = -90;
|
|
||||||
analyzer.smoothingTimeConstant = 0.85;
|
|
||||||
const dataArray = new Uint8Array(analyzer.frequencyBinCount);
|
|
||||||
// const sampleRate = 16000;
|
|
||||||
|
|
||||||
const renderAudio = (audio: Float32Array, sampleRate: number) => {
|
|
||||||
|
|
||||||
// step 2: source audio frequency {ffft} data
|
|
||||||
const buffer = audioCtx.createBuffer(1, audio.length, sampleRate);
|
|
||||||
buffer.copyToChannel(audio, 0, 0);
|
|
||||||
const source = audioCtx.createBufferSource();
|
|
||||||
source.buffer = buffer;
|
|
||||||
source.connect(analyzer);
|
|
||||||
source.start();
|
|
||||||
analyzer.getByteFrequencyData(dataArray);
|
|
||||||
|
|
||||||
// step 3: process audio frequency data
|
|
||||||
const dataView = dataArray.slice(xInitial, xFinal);
|
|
||||||
const sum = dataView.reduce((a, b) => a + b);
|
|
||||||
const average = sum / dataView.length;
|
|
||||||
const scaled = average / 255;
|
|
||||||
const zeroed = scaled - 0.6;
|
|
||||||
return average;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
renderAudio
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
import {ref, watch, onUnmounted, onMounted } from "vue";
|
|
||||||
import useKeyDownHandler from "@/composables/keyDownHandler/useKeyDownHandler";
|
|
||||||
import useMediaStream from "@/composables/mediaStream/useMediaStream";
|
|
||||||
import useStreamRecorder from "@/composables/streamRecorder/useStreamRecorder";
|
|
||||||
import useTextInput from '@/composables/inputItem/useTextInput';
|
|
||||||
/*
|
|
||||||
* Renders input to the user
|
|
||||||
*/
|
|
||||||
export default function useInputRenderer() {
|
|
||||||
// program ref varaiables
|
|
||||||
const renderAudio = ref(false);
|
|
||||||
const reactiveWidth = ref(10);
|
|
||||||
const pathOffset = ref(10);
|
|
||||||
const paths = ref([]);
|
|
||||||
|
|
||||||
// get input feedback & handler function
|
|
||||||
const { keyDownHandler, input } = useKeyDownHandler();
|
|
||||||
// get text Offset and & textRender function
|
|
||||||
const { textInputXoffset, renderTextInput } = useTextInput(input, renderAudio);
|
|
||||||
|
|
||||||
// why do we have audio stuff here ?
|
|
||||||
let stream: MediaStream;
|
|
||||||
const {renderStream} = useStreamRecorder(renderAudio)
|
|
||||||
onMounted(async () => {
|
|
||||||
const streamRes = await useMediaStream()
|
|
||||||
stream = streamRes.stream;
|
|
||||||
})
|
|
||||||
|
|
||||||
// stop stream recording on space keyup event
|
|
||||||
function keyupHandler(e: any) {
|
|
||||||
if (e.key === " " && renderAudio.value) {
|
|
||||||
console.log("stop recording");
|
|
||||||
renderAudio.value = false;
|
|
||||||
input.value = "";
|
|
||||||
reactiveWidth.value = 10;
|
|
||||||
pathOffset.value = 10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add window event listener
|
|
||||||
window.addEventListener("keydown", keyDownHandler);
|
|
||||||
window.addEventListener('keyup', keyupHandler);
|
|
||||||
|
|
||||||
// remove Event Listeners on component unMount
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener("keydown", keyDownHandler);
|
|
||||||
window.removeEventListener("keyup", keyupHandler);
|
|
||||||
});
|
|
||||||
|
|
||||||
// determine whether to render text or audio based one the keyboard input
|
|
||||||
watch(input, async (input, prevInput) => {
|
|
||||||
if (prevInput !== "" || prevInput.length > 0) {
|
|
||||||
if (!renderAudio.value) {
|
|
||||||
renderTextInput();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (input === " " && renderAudio.value === false) {
|
|
||||||
console.log("record");
|
|
||||||
renderAudio.value = true;
|
|
||||||
renderStream(stream, reactiveWidth, pathOffset, paths);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
renderTextInput();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
input,
|
|
||||||
renderAudio,
|
|
||||||
reactiveWidth,
|
|
||||||
textInputXoffset,
|
|
||||||
paths
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import { ref, onMounted } from "vue";
|
|
||||||
import {Ref} from "@/types/vueRef/index";
|
|
||||||
export default function useMessageItemAnims({
|
|
||||||
mr,
|
|
||||||
tr,
|
|
||||||
}: {
|
|
||||||
mr: Ref<string>;
|
|
||||||
tr: Ref<string>;
|
|
||||||
}) {
|
|
||||||
const signatureTranslate = ref("");
|
|
||||||
let messageRect: SVGSVGElement & SVGRectElement;
|
|
||||||
let messageText: SVGSVGElement & SVGTextElement;
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
messageRect = (mr.value as unknown) as SVGSVGElement & SVGRectElement;
|
|
||||||
messageText = (tr.value as unknown) as SVGSVGElement & SVGTextElement;
|
|
||||||
});
|
|
||||||
|
|
||||||
function adjustMessageRect(): void {
|
|
||||||
// must get dimensions of text box to fit bubble around
|
|
||||||
const padding = 12.5;
|
|
||||||
const textBox = messageText.getBBox();
|
|
||||||
messageRect.setAttribute("x", String(textBox.x - padding));
|
|
||||||
messageRect.setAttribute("y", String(textBox.y - padding));
|
|
||||||
messageRect.setAttribute("width", String(textBox.width + 2 * padding));
|
|
||||||
messageRect.setAttribute("height", String(textBox.height + 2 * padding));
|
|
||||||
}
|
|
||||||
|
|
||||||
function translateMsgSignature(): void {
|
|
||||||
if (messageRect.getAttribute("height") !== null) {
|
|
||||||
const height = Number(messageRect.getAttribute("height"));
|
|
||||||
signatureTranslate.value = `translate(-10 ${height})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function drawMessage() {
|
|
||||||
// ignore lint error: await needed for proper render
|
|
||||||
await adjustMessageRect();
|
|
||||||
translateMsgSignature();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
drawMessage,
|
|
||||||
signatureTranslate,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
import { computed, onMounted, ref } from "vue";
|
|
||||||
export default function useMessageItemComp(m: string, id: string) {
|
|
||||||
const x = ref(0)
|
|
||||||
const paddingLeft = 30;
|
|
||||||
const winW = 350;
|
|
||||||
const messageWidth = ref(0);
|
|
||||||
const messageMarginTop = 75;
|
|
||||||
const test = ref("");
|
|
||||||
|
|
||||||
const textFill = computed(() => {
|
|
||||||
switch (m) {
|
|
||||||
case "sf":
|
|
||||||
return "#fff";
|
|
||||||
default:
|
|
||||||
return "#000";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const bubbleFill = computed(() => {
|
|
||||||
switch (m) {
|
|
||||||
case "sf":
|
|
||||||
return "#61b4f4";
|
|
||||||
default:
|
|
||||||
return "#fff";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const setMessageWidthRef = async () => {
|
|
||||||
const msg = await document.getElementById(id);
|
|
||||||
if (msg) {
|
|
||||||
messageWidth.value = msg.getBoundingClientRect().width;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selfMessageMarginRight = 2.5;
|
|
||||||
const calculateXoffset = async () => {
|
|
||||||
switch (m) {
|
|
||||||
case "sf":
|
|
||||||
x.value = winW - messageWidth.value - selfMessageMarginRight;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
x.value = paddingLeft;
|
|
||||||
// to center a message
|
|
||||||
// x = messageMargin + (winW -messageWidth) / 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const startingOffset = computed(() => {
|
|
||||||
const messageRenderer = document.getElementById('messageRenderer');
|
|
||||||
if (messageRenderer) {
|
|
||||||
const rect = messageRenderer.getBoundingClientRect();
|
|
||||||
let Y = 600;
|
|
||||||
if (rect.height > 500) {
|
|
||||||
Y = rect.height + messageMarginTop;
|
|
||||||
}
|
|
||||||
return `translate(${x.value} ${Y})`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
// to calculate X offset:
|
|
||||||
// First: set message width ref
|
|
||||||
await setMessageWidthRef();
|
|
||||||
// Then: calculate message X offset
|
|
||||||
calculateXoffset();
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
textFill,
|
|
||||||
bubbleFill,
|
|
||||||
startingOffset
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import useRendererAnims from "./useRendererAnims";
|
|
||||||
import useOffsetCalculator from './useOffsetCalculator';
|
|
||||||
export default function useMessageRenderer() {
|
|
||||||
|
|
||||||
const { calculateMessageOffsets } = useOffsetCalculator();
|
|
||||||
const { shiftAnimate, stackAnimate } = useRendererAnims();
|
|
||||||
|
|
||||||
let messageList: NodeList;
|
|
||||||
let message: HTMLElement | null;
|
|
||||||
let messageOffset = { x: 0, y: 0 };
|
|
||||||
let shift = false;
|
|
||||||
let shiftValue = 0;
|
|
||||||
|
|
||||||
function animateMessage(el: SVGGElement): void {
|
|
||||||
const transform = {
|
|
||||||
initialTranslate: `translate(${messageOffset.x} ${messageOffset.y})`,
|
|
||||||
finalTranslate: `translate(${messageOffset.x} ${messageOffset.y})`
|
|
||||||
}
|
|
||||||
shift ? shiftAnimate(el, transform, shiftValue, messageList) : stackAnimate(el, transform);
|
|
||||||
}
|
|
||||||
|
|
||||||
function beforeEnter(el: SVGGElement): void {
|
|
||||||
const queryResult = document.querySelectorAll(".messageList");
|
|
||||||
if (queryResult.length) messageList = queryResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
function enter(el: SVGGElement): void {
|
|
||||||
message = document.getElementById(el.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function afterEnter(el: SVGGElement) {
|
|
||||||
const messagePadding = 15;
|
|
||||||
|
|
||||||
if (message) {
|
|
||||||
messageOffset = calculateMessageOffsets(message);
|
|
||||||
shift = messageOffset.y > 445 ? true : false;
|
|
||||||
}
|
|
||||||
if (shift) {
|
|
||||||
shiftValue -= el.getBBox().height + messagePadding;
|
|
||||||
}
|
|
||||||
animateMessage(el);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
beforeEnter,
|
|
||||||
enter,
|
|
||||||
afterEnter
|
|
||||||
};
|
|
||||||
}
|
|
||||||
20
src/core/UI/input/audio/useStreamRecord.ts
Normal file
20
src/core/UI/input/audio/useStreamRecord.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
export default function useStreamRecord(): {
|
||||||
|
initRecorder: (stream: MediaStream) => MediaRecorder;
|
||||||
|
} {
|
||||||
|
let streamRecorder: MediaRecorder;
|
||||||
|
const chunks: Blob[] = [];
|
||||||
|
|
||||||
|
const initRecorder = (stream: MediaStream) => {
|
||||||
|
streamRecorder = new MediaRecorder(stream);
|
||||||
|
streamRecorder.ondataavailable = (e: any) => { chunks.push(e.data) }
|
||||||
|
streamRecorder.onstop = (e: any) => {
|
||||||
|
// send audio to BE
|
||||||
|
const audioBlob = new Blob(chunks, { 'type': 'audio/ogg; codecs=opus' });
|
||||||
|
}
|
||||||
|
return streamRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
initRecorder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,28 +1,14 @@
|
||||||
import {Ref} from '@/types/vueRef/index';
|
import {Ref} from '@/types/vueRef/index';
|
||||||
import { ref } from 'vue';
|
export default function useStreamRender(render: Ref<boolean>) {
|
||||||
export default function useStreamRecorder(render: Ref<boolean>) {
|
|
||||||
let streamRecorder: MediaRecorder;
|
|
||||||
const chunks: Blob[] = [];
|
|
||||||
|
|
||||||
// window.AudioContext = window.AudioContext;
|
// window.AudioContext = window.AudioContext;
|
||||||
const audioContext = new AudioContext();
|
const audioContext = new AudioContext();
|
||||||
const analyzer = audioContext.createAnalyser();
|
const analyzer = audioContext.createAnalyser();
|
||||||
analyzer.fftSize = 2048;
|
analyzer.fftSize = 2048;
|
||||||
|
const xInitial = 24;
|
||||||
|
const xFinal = 640;
|
||||||
const dataArray = new Uint8Array(analyzer.frequencyBinCount);
|
const dataArray = new Uint8Array(analyzer.frequencyBinCount);
|
||||||
|
|
||||||
const initRecorder = async () => {
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
||||||
streamRecorder = new MediaRecorder(stream);
|
|
||||||
streamRecorder.ondataavailable = (e: any) => { chunks.push(e.data) }
|
|
||||||
streamRecorder.onstop = (e: any) => {
|
|
||||||
// send audio to BE
|
|
||||||
const audioBlob = new Blob(chunks, { 'type': 'audio/ogg; codecs=opus' });
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
streamRecorder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AmplitudePath {
|
interface AmplitudePath {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -41,8 +27,6 @@ export default function useStreamRecorder(render: Ref<boolean>) {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
const xInitial = 24;
|
|
||||||
const xFinal = 640;
|
|
||||||
|
|
||||||
function visualize(w: Ref<number>, p: Ref<number>, paths: Ref<any>) {
|
function visualize(w: Ref<number>, p: Ref<number>, paths: Ref<any>) {
|
||||||
|
|
||||||
|
|
@ -92,7 +76,6 @@ export default function useStreamRecorder(render: Ref<boolean>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initRecorder,
|
|
||||||
renderStream
|
renderStream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import AnimeFunc from "@/types/animejs/index";
|
import AnimeFunc from "@/types/animejs/index";
|
||||||
import {inject} from "vue";
|
import {inject} from "vue";
|
||||||
import {Ref} from "@/types/vueRef/index";
|
import {Ref} from "@/types/vueRef/index";
|
||||||
export default function useInputAnims() {
|
/*
|
||||||
|
* Anime js animations used by input textController
|
||||||
|
*/
|
||||||
|
export default function useTextInputAnims() {
|
||||||
|
|
||||||
// imports animejs safely
|
// imports animejs safely
|
||||||
let anime: AnimeFunc;
|
let anime: AnimeFunc;
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import {ref, computed, watch, inject} from 'vue';
|
import {ref, computed, inject} from 'vue';
|
||||||
import useInputAnims from "./useInputAnims";
|
import useTextInputAnims from "./textDetails/useAnimations";
|
||||||
import useInputRenderer from './useInputRenderer';
|
import { Ref } from "@/types/vueRef/index";
|
||||||
export default function useTextInput(input: any, audioRender: any) {
|
/*
|
||||||
|
* text rendering logic used by inputController
|
||||||
|
*/
|
||||||
|
export default function useTextRender(input: Ref<string>) {
|
||||||
|
|
||||||
const emitter: any = inject("mitt");
|
const emitter: any = inject("mitt");
|
||||||
const {animateSend, verticalShiftInput, inputAppear} = useInputAnims();
|
const {animateSend, verticalShiftInput, inputAppear} = useTextInputAnims();
|
||||||
|
|
||||||
const inputHeight = ref(42);
|
const inputHeight = ref(42);
|
||||||
const inputWidth = ref(0);
|
const inputWidth = ref(0);
|
||||||
88
src/core/UI/input/useInputController.ts
Normal file
88
src/core/UI/input/useInputController.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import {ref, watch, onUnmounted, onMounted } from "vue";
|
||||||
|
import useKeyDownHandler from "@/utils/keyDownHandler/useKeyDownHandler";
|
||||||
|
import useMediaStream from "@/utils/mediaStream/useMediaStream";
|
||||||
|
import useStreamRecord from "./audio/useStreamRecord";
|
||||||
|
import useStreamRender from './audio/useStreamRender';
|
||||||
|
import useTextRender from './text/useTextRender';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Input controller logic
|
||||||
|
* Will render text input by default as user types or
|
||||||
|
* if user holds space bar down, audio media-stream
|
||||||
|
* will be rendered & recorded
|
||||||
|
*/
|
||||||
|
export default function useInputController() {
|
||||||
|
// program ref varaiables
|
||||||
|
const renderAudio = ref(false);
|
||||||
|
const reactiveWidth = ref(10);
|
||||||
|
const pathOffset = ref(10);
|
||||||
|
const paths = ref([]);
|
||||||
|
// audioStream variables
|
||||||
|
let stream: MediaStream;
|
||||||
|
let recorder: MediaRecorder;
|
||||||
|
|
||||||
|
// use input ref & keydown handler function
|
||||||
|
const { keyDownHandler, input } = useKeyDownHandler();
|
||||||
|
// use text Offset computed prop and & textRender function
|
||||||
|
const { textInputXoffset, renderTextInput } = useTextRender(input);
|
||||||
|
// use stream render and record logic
|
||||||
|
const { renderStream } = useStreamRender(renderAudio);
|
||||||
|
const { initRecorder } = useStreamRecord()
|
||||||
|
|
||||||
|
// get stream & initialize recorder object on mount
|
||||||
|
onMounted(async () => {
|
||||||
|
stream = await useMediaStream();
|
||||||
|
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
|
||||||
|
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) => {
|
||||||
|
if (prevInput !== "" || prevInput.length > 0) {
|
||||||
|
if (!renderAudio.value) {
|
||||||
|
renderTextInput();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (input === " " && renderAudio.value === false) {
|
||||||
|
console.log("record");
|
||||||
|
renderAudio.value = true;
|
||||||
|
recorder.start();
|
||||||
|
renderStream(stream, reactiveWidth, pathOffset, paths);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderTextInput();
|
||||||
|
});
|
||||||
|
|
||||||
|
// remove Event Listeners on component unMount
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener("keydown", keyDownHandler);
|
||||||
|
window.removeEventListener("keyup", keyupHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
input,
|
||||||
|
renderAudio,
|
||||||
|
reactiveWidth,
|
||||||
|
textInputXoffset,
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import {ref, computed, onMounted} from "vue";
|
import {ref, computed, onMounted} from "vue";
|
||||||
export default function useScrollView({ targetId }: {targetId: string}) {
|
export default function useScrollView({ targetId }: {targetId: string}) {
|
||||||
|
|
||||||
const scroll = ref(0);
|
const scroll = ref(0);
|
||||||
let scrollTarget: HTMLElement | SVGElement | null;
|
let scrollTarget: HTMLElement | SVGElement | null;
|
||||||
const targetHeight = ref(0);
|
const targetHeight = ref(0);
|
||||||
46
src/core/messenger/message/messageDetails/useAnimations.ts
Normal file
46
src/core/messenger/message/messageDetails/useAnimations.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { Ref } from "@/types/vueRef/index";
|
||||||
|
export default function useMessageItemAnims({
|
||||||
|
mr,
|
||||||
|
tr,
|
||||||
|
}: {
|
||||||
|
mr: Ref<string>;
|
||||||
|
tr: Ref<string>;
|
||||||
|
}) {
|
||||||
|
const signatureTranslate = ref("");
|
||||||
|
let messageRect: SVGSVGElement & SVGRectElement;
|
||||||
|
let messageText: SVGSVGElement & SVGTextElement;
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
messageRect = (mr.value as unknown) as SVGSVGElement & SVGRectElement;
|
||||||
|
messageText = (tr.value as unknown) as SVGSVGElement & SVGTextElement;
|
||||||
|
});
|
||||||
|
|
||||||
|
function adjustMessageRect(): void {
|
||||||
|
// must get dimensions of text box to fit bubble around
|
||||||
|
const padding = 12.5;
|
||||||
|
const textBox = messageText.getBBox();
|
||||||
|
messageRect.setAttribute("x", String(textBox.x - padding));
|
||||||
|
messageRect.setAttribute("y", String(textBox.y - padding));
|
||||||
|
messageRect.setAttribute("width", String(textBox.width + 2 * padding));
|
||||||
|
messageRect.setAttribute("height", String(textBox.height + 2 * padding));
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateMsgSignature(): void {
|
||||||
|
if (messageRect.getAttribute("height") !== null) {
|
||||||
|
const height = Number(messageRect.getAttribute("height"));
|
||||||
|
signatureTranslate.value = `translate(-10 ${height})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function drawMessage() {
|
||||||
|
// ignore lint error: await needed for proper render
|
||||||
|
await adjustMessageRect();
|
||||||
|
translateMsgSignature();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
drawMessage,
|
||||||
|
signatureTranslate,
|
||||||
|
};
|
||||||
|
}
|
||||||
72
src/core/messenger/message/messageDetails/useComputed.ts
Normal file
72
src/core/messenger/message/messageDetails/useComputed.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
export default function useMessageItemComp(m: string, id: string) {
|
||||||
|
const x = ref(0)
|
||||||
|
const paddingLeft = 30;
|
||||||
|
const winW = 350;
|
||||||
|
const messageWidth = ref(0);
|
||||||
|
const messageMarginTop = 75;
|
||||||
|
|
||||||
|
const textFill = computed(() => {
|
||||||
|
switch (m) {
|
||||||
|
case "sf":
|
||||||
|
return "#fff";
|
||||||
|
default:
|
||||||
|
return "#000";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const bubbleFill = computed(() => {
|
||||||
|
switch (m) {
|
||||||
|
case "sf":
|
||||||
|
return "#61b4f4";
|
||||||
|
default:
|
||||||
|
return "#fff";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setMessageWidthRef = async () => {
|
||||||
|
const msg = await document.getElementById(id);
|
||||||
|
if (msg) {
|
||||||
|
messageWidth.value = msg.getBoundingClientRect().width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selfMessageMarginRight = 2.5;
|
||||||
|
const calculateXoffset = async () => {
|
||||||
|
switch (m) {
|
||||||
|
case "sf":
|
||||||
|
x.value = winW - messageWidth.value - selfMessageMarginRight;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
x.value = paddingLeft;
|
||||||
|
// to center a message
|
||||||
|
// x = messageMargin + (winW -messageWidth) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startingOffset = computed(() => {
|
||||||
|
const messageRenderer = document.getElementById('messageRenderer');
|
||||||
|
if (messageRenderer) {
|
||||||
|
const rect = messageRenderer.getBoundingClientRect();
|
||||||
|
let Y = 600;
|
||||||
|
if (rect.height > 500) {
|
||||||
|
Y = rect.height + messageMarginTop;
|
||||||
|
}
|
||||||
|
return `translate(${x.value} ${Y})`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// to calculate X offset:
|
||||||
|
// First: set message width ref
|
||||||
|
await setMessageWidthRef();
|
||||||
|
// Then: calculate message X offset
|
||||||
|
calculateXoffset();
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
textFill,
|
||||||
|
bubbleFill,
|
||||||
|
startingOffset
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
import { inject } from "vue";
|
import { inject } from "vue";
|
||||||
import AnimeFunc from "@/types/animejs/index";
|
import AnimeFunc from "@/types/animejs/index";
|
||||||
import { MessageAnimeFunc } from '@/types/message/index';
|
import { MessageAnimeFunc } from '@/types/message/index';
|
||||||
export default function useRendererAnims() {
|
|
||||||
|
/**
|
||||||
|
* anime js animations used by message renderer
|
||||||
|
*/
|
||||||
|
export default function useRendererAnims(): {
|
||||||
|
shiftAnimate: MessageAnimeFunc;
|
||||||
|
stackAnimate: MessageAnimeFunc;
|
||||||
|
} {
|
||||||
// imports animejs safely
|
// imports animejs safely
|
||||||
let anime: AnimeFunc;
|
let anime: AnimeFunc;
|
||||||
const animeInject: AnimeFunc | undefined = inject("animejs");
|
const animeInject: AnimeFunc | undefined = inject("animejs");
|
||||||
if (animeInject) anime = animeInject;
|
if (animeInject) anime = animeInject;
|
||||||
|
|
||||||
|
// shifts message list by a given value
|
||||||
function shiftList({ targetList, sv }: { targetList: NodeList | undefined; sv: number | undefined }) {
|
function shiftList({ targetList, sv }: { targetList: NodeList | undefined; sv: number | undefined }) {
|
||||||
if (targetList) {
|
if (targetList) {
|
||||||
for (const element of targetList) {
|
for (const element of targetList) {
|
||||||
|
|
@ -21,6 +29,7 @@ export default function useRendererAnims() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// adds new message at the bottom of the list and shifts existing message list
|
||||||
const shiftAnimate: MessageAnimeFunc = (el, transform, shiftValue, targetList) => {
|
const shiftAnimate: MessageAnimeFunc = (el, transform, shiftValue, targetList) => {
|
||||||
anime({
|
anime({
|
||||||
targets: el,
|
targets: el,
|
||||||
|
|
@ -45,6 +54,7 @@ export default function useRendererAnims() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stacks message from top to bottom
|
||||||
const stackAnimate: MessageAnimeFunc = (el, transform) => {
|
const stackAnimate: MessageAnimeFunc = (el, transform) => {
|
||||||
anime({
|
anime({
|
||||||
targets: el,
|
targets: el,
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
/**
|
||||||
|
* Calculates vertical and horizontal message offsets
|
||||||
|
*/
|
||||||
export default function useOffsetCalculator() {
|
export default function useOffsetCalculator() {
|
||||||
const xOffset = ref(0);
|
const xOffset = ref(0);
|
||||||
const yOffset = ref(0);
|
const yOffset = ref(0);
|
||||||
|
|
@ -6,13 +9,13 @@ export default function useOffsetCalculator() {
|
||||||
let previousMessageHeight = 0;
|
let previousMessageHeight = 0;
|
||||||
const messagePadding = 15;
|
const messagePadding = 15;
|
||||||
|
|
||||||
function setXoffset(message: HTMLElement): void {
|
function calculateX(message: HTMLElement): void {
|
||||||
const computedQuery = window.getComputedStyle(message)
|
const computedQuery = window.getComputedStyle(message);
|
||||||
const matrix = new WebKitCSSMatrix(computedQuery.webkitTransform);
|
const matrix = new WebKitCSSMatrix(computedQuery.webkitTransform);
|
||||||
xOffset.value = matrix.m41;
|
xOffset.value = matrix.m41;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setYoffset(messageHeight: number): void {
|
function calculateY(messageHeight: number): void {
|
||||||
if (previousMessageHeight === 0) {
|
if (previousMessageHeight === 0) {
|
||||||
yOffset.value = firstChildMarginTop;
|
yOffset.value = firstChildMarginTop;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -21,10 +24,10 @@ export default function useOffsetCalculator() {
|
||||||
previousMessageHeight = messageHeight;
|
previousMessageHeight = messageHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculateMessageOffsets = (msg: HTMLElement) => {
|
const calculateMessageOffsets = (msg: HTMLElement): { x: number; y: number } => {
|
||||||
const msgRect = msg.getBoundingClientRect();
|
const msgRect = msg.getBoundingClientRect();
|
||||||
setXoffset(msg);
|
calculateX(msg);
|
||||||
setYoffset(msgRect.height);
|
calculateY(msgRect.height);
|
||||||
const offsets = {
|
const offsets = {
|
||||||
x: xOffset.value as number,
|
x: xOffset.value as number,
|
||||||
y: yOffset.value as number
|
y: yOffset.value as number
|
||||||
72
src/core/messenger/renderView/useRenderView.ts
Normal file
72
src/core/messenger/renderView/useRenderView.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import useRendererAnims from "./renderDetails/useAnimations";
|
||||||
|
import useOffsetCalculator from './renderDetails/useOffsetCalculator';
|
||||||
|
import { VueTransitionCallback } from '@/types/vue3/vueTransitionCallback/index';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* renderView component logic
|
||||||
|
* handles rendering of exisiting user messages
|
||||||
|
* as well as incoming messages
|
||||||
|
*/
|
||||||
|
export default function useRenderView(): {
|
||||||
|
beforeEnter: VueTransitionCallback;
|
||||||
|
enter: VueTransitionCallback;
|
||||||
|
afterEnter: VueTransitionCallback;
|
||||||
|
} {
|
||||||
|
// uses offsetCalculator logic and renderer animations
|
||||||
|
const { calculateMessageOffsets } = useOffsetCalculator();
|
||||||
|
const { shiftAnimate, stackAnimate } = useRendererAnims();
|
||||||
|
|
||||||
|
const messagePadding = 15;
|
||||||
|
let messageList: NodeList;
|
||||||
|
let message: HTMLElement | null;
|
||||||
|
let messageOffset = { x: 0, y: 0 };
|
||||||
|
let shift = false;
|
||||||
|
let shiftOffset = 0;
|
||||||
|
|
||||||
|
|
||||||
|
// animates message item to 'enter' renderView
|
||||||
|
const animateMessage = (el: SVGGElement) => {
|
||||||
|
|
||||||
|
const transform = {
|
||||||
|
initialTranslate: `translate(${messageOffset.x} ${messageOffset.y})`,
|
||||||
|
finalTranslate: `translate(${messageOffset.x} ${messageOffset.y})`
|
||||||
|
}
|
||||||
|
shift ? shiftAnimate(el, transform, shiftOffset, messageList) : stackAnimate(el, transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
// callback called before message enter
|
||||||
|
const beforeEnter: VueTransitionCallback = () => {
|
||||||
|
const queryResult = document.querySelectorAll(".messageList");
|
||||||
|
if (queryResult.length) messageList = queryResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// callback called on message enter
|
||||||
|
const enter: VueTransitionCallback = (el) => {
|
||||||
|
// query DOM for latest message as soon as it enters
|
||||||
|
if (el) {
|
||||||
|
message = document.getElementById(el.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// callback called after message enter
|
||||||
|
const afterEnter: VueTransitionCallback = (el) => {
|
||||||
|
// get message positioning & shift state before animating
|
||||||
|
if (message) {
|
||||||
|
messageOffset = calculateMessageOffsets(message);
|
||||||
|
shift = messageOffset.y > 445 ? true : false;
|
||||||
|
}
|
||||||
|
if (el) {
|
||||||
|
if (shift) {
|
||||||
|
shiftOffset -= el.getBBox().height + messagePadding;
|
||||||
|
}
|
||||||
|
animateMessage(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
beforeEnter,
|
||||||
|
enter,
|
||||||
|
afterEnter
|
||||||
|
};
|
||||||
|
}
|
||||||
3
src/types/vue3/vueTransitionCallback/index.ts
Normal file
3
src/types/vue3/vueTransitionCallback/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export type VueTransitionCallback = {
|
||||||
|
(el?: SVGGElement | null): void;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { ref, inject } from "vue";
|
import { ref, inject } from "vue";
|
||||||
import keyboardNameMap from "./keyboardMaps/keyboardNameMap";
|
import keyboardNameMap from "./keyBoardMaps/keyboardNameMap";
|
||||||
import keyboardCharMap from "./keyboardMaps/keyboardCharMap";
|
import keyboardCharMap from "./keyBoardMaps/keyboardCharMap";
|
||||||
// This catches keys from a physical keyboard, a soft keyboard on a tablet, or a
|
// This catches keys from a physical keyboard, a soft keyboard on a tablet, or a
|
||||||
// scanner attached via USB
|
// scanner attached via USB
|
||||||
export default function useKeyDownHandler() {
|
export default function useKeyDownHandler() {
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
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 });
|
||||||
return {
|
return stream;
|
||||||
stream
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue