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">
|
||||
import { defineComponent, onMounted, ref, reactive } from "vue";
|
||||
import useMessageItemComp from "@/composables/messageItem/useMessageItemComp";
|
||||
import useMessageItemAnims from "@/composables/messageItem/useMessageItemAnims";
|
||||
import useTextWrap from "@/composables/textWrap/useTextWrap";
|
||||
import useMessageItemComp from "@/core/messenger/message/messageDetails/useComputed";
|
||||
import useMessageItemAnims from "@/core/messenger/message/messageDetails/useAnimations";
|
||||
import useTextWrap from "@/utils/textWrap/useTextWrap";
|
||||
export default defineComponent({
|
||||
name: "MessageItem",
|
||||
props: {
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import useMessageRenderer from "@/composables/messageRenderer/useMessageRenderer";
|
||||
import useScrollView from "@/composables/scrollView/useScrollView";
|
||||
import useRenderView from '@/core/messenger/renderView/useRenderView';
|
||||
import useScrollView from "@/core/UI/scrollView/useScrollView";
|
||||
import {
|
||||
defineComponent,
|
||||
reactive,
|
||||
|
|
@ -54,8 +54,10 @@ export default defineComponent({
|
|||
});
|
||||
}
|
||||
|
||||
const { beforeEnter, enter, afterEnter } = useMessageRenderer();
|
||||
const {scrollView} = useScrollView({ targetId: "messageRenderer"});
|
||||
const { beforeEnter, enter, afterEnter } = useRenderView();
|
||||
const { scrollView } = useScrollView({
|
||||
targetId: "messageRenderer"
|
||||
});
|
||||
|
||||
return {
|
||||
renderArr,
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from 'vue'
|
||||
import { defineComponent } from 'vue'
|
||||
import TextInput from './inputDetails/textInput.vue';
|
||||
import AudioInput from './inputDetails/audioInput.vue';
|
||||
import useInputRenderer from "@/composables/inputItem/useInputRenderer";
|
||||
import useInputController from "@/core/UI/input/useInputController";
|
||||
export default defineComponent({
|
||||
name: "InputItem",
|
||||
components: {TextInput, AudioInput},
|
||||
|
|
@ -26,7 +26,7 @@ export default defineComponent({
|
|||
renderAudio,
|
||||
reactiveWidth,
|
||||
textInputXoffset,
|
||||
paths } = useInputRenderer();
|
||||
paths } = useInputController();
|
||||
|
||||
return {
|
||||
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 'vue';
|
||||
export default function useStreamRecorder(render: Ref<boolean>) {
|
||||
let streamRecorder: MediaRecorder;
|
||||
const chunks: Blob[] = [];
|
||||
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);
|
||||
|
||||
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 {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -41,8 +27,6 @@ export default function useStreamRecorder(render: Ref<boolean>) {
|
|||
return path;
|
||||
}
|
||||
|
||||
const xInitial = 24;
|
||||
const xFinal = 640;
|
||||
|
||||
function visualize(w: Ref<number>, p: Ref<number>, paths: Ref<any>) {
|
||||
|
||||
|
|
@ -92,7 +76,6 @@ export default function useStreamRecorder(render: Ref<boolean>) {
|
|||
}
|
||||
|
||||
return {
|
||||
initRecorder,
|
||||
renderStream
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
import AnimeFunc from "@/types/animejs/index";
|
||||
import {inject} from "vue";
|
||||
import {Ref} from "@/types/vueRef/index";
|
||||
export default function useInputAnims() {
|
||||
/*
|
||||
* Anime js animations used by input textController
|
||||
*/
|
||||
export default function useTextInputAnims() {
|
||||
|
||||
// imports animejs safely
|
||||
let anime: AnimeFunc;
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
import {ref, computed, watch, inject} from 'vue';
|
||||
import useInputAnims from "./useInputAnims";
|
||||
import useInputRenderer from './useInputRenderer';
|
||||
export default function useTextInput(input: any, audioRender: any) {
|
||||
import {ref, computed, inject} from 'vue';
|
||||
import useTextInputAnims from "./textDetails/useAnimations";
|
||||
import { Ref } from "@/types/vueRef/index";
|
||||
/*
|
||||
* text rendering logic used by inputController
|
||||
*/
|
||||
export default function useTextRender(input: Ref<string>) {
|
||||
|
||||
const emitter: any = inject("mitt");
|
||||
const {animateSend, verticalShiftInput, inputAppear} = useInputAnims();
|
||||
const {animateSend, verticalShiftInput, inputAppear} = useTextInputAnims();
|
||||
|
||||
const inputHeight = ref(42);
|
||||
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";
|
||||
export default function useScrollView({ targetId }: {targetId: string}) {
|
||||
|
||||
const scroll = ref(0);
|
||||
let scrollTarget: HTMLElement | SVGElement | null;
|
||||
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 AnimeFunc from "@/types/animejs/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
|
||||
let anime: AnimeFunc;
|
||||
const animeInject: AnimeFunc | undefined = inject("animejs");
|
||||
if (animeInject) anime = animeInject;
|
||||
|
||||
// shifts message list by a given value
|
||||
function shiftList({ targetList, sv }: { targetList: NodeList | undefined; sv: number | undefined }) {
|
||||
if (targetList) {
|
||||
for (const element of targetList) {
|
||||
|
|
@ -21,9 +29,10 @@ 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) => {
|
||||
anime({
|
||||
targets: el,
|
||||
targets: el,
|
||||
transform: transform.initialTranslate,
|
||||
easing: "easeInOutQuad",
|
||||
duration: 500,
|
||||
|
|
@ -45,6 +54,7 @@ export default function useRendererAnims() {
|
|||
});
|
||||
}
|
||||
|
||||
// stacks message from top to bottom
|
||||
const stackAnimate: MessageAnimeFunc = (el, transform) => {
|
||||
anime({
|
||||
targets: el,
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
import { ref } from "vue";
|
||||
/**
|
||||
* Calculates vertical and horizontal message offsets
|
||||
*/
|
||||
export default function useOffsetCalculator() {
|
||||
const xOffset = ref(0);
|
||||
const yOffset = ref(0);
|
||||
|
|
@ -6,13 +9,13 @@ export default function useOffsetCalculator() {
|
|||
let previousMessageHeight = 0;
|
||||
const messagePadding = 15;
|
||||
|
||||
function setXoffset(message: HTMLElement): void {
|
||||
const computedQuery = window.getComputedStyle(message)
|
||||
function calculateX(message: HTMLElement): void {
|
||||
const computedQuery = window.getComputedStyle(message);
|
||||
const matrix = new WebKitCSSMatrix(computedQuery.webkitTransform);
|
||||
xOffset.value = matrix.m41;
|
||||
}
|
||||
|
||||
function setYoffset(messageHeight: number): void {
|
||||
function calculateY(messageHeight: number): void {
|
||||
if (previousMessageHeight === 0) {
|
||||
yOffset.value = firstChildMarginTop;
|
||||
} else {
|
||||
|
|
@ -21,10 +24,10 @@ export default function useOffsetCalculator() {
|
|||
previousMessageHeight = messageHeight;
|
||||
}
|
||||
|
||||
const calculateMessageOffsets = (msg: HTMLElement) => {
|
||||
const calculateMessageOffsets = (msg: HTMLElement): { x: number; y: number } => {
|
||||
const msgRect = msg.getBoundingClientRect();
|
||||
setXoffset(msg);
|
||||
setYoffset(msgRect.height);
|
||||
calculateX(msg);
|
||||
calculateY(msgRect.height);
|
||||
const offsets = {
|
||||
x: xOffset.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 keyboardNameMap from "./keyboardMaps/keyboardNameMap";
|
||||
import keyboardCharMap from "./keyboardMaps/keyboardCharMap";
|
||||
import keyboardNameMap from "./keyBoardMaps/keyboardNameMap";
|
||||
import keyboardCharMap from "./keyBoardMaps/keyboardCharMap";
|
||||
// This catches keys from a physical keyboard, a soft keyboard on a tablet, or a
|
||||
// scanner attached via USB
|
||||
export default function useKeyDownHandler() {
|
||||
|
|
@ -65,4 +65,4 @@ export default function useKeyDownHandler() {
|
|||
keyDownHandler,
|
||||
input
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
export default async function useMediaStream() {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
return {
|
||||
stream
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
Loading…
Reference in a new issue