production refactor
This commit is contained in:
parent
92fb472e3c
commit
41528e7e27
115 changed files with 20719 additions and 22107 deletions
44
src/render/components/app.js
Normal file
44
src/render/components/app.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import Splash from "./splash.js";
|
||||
import Messenger from "./messenger.js";
|
||||
import Login from "./login.js";
|
||||
import Header from "./header.js";
|
||||
|
||||
const account = Vue.ref(null);
|
||||
|
||||
const App =
|
||||
{
|
||||
components: {
|
||||
Splash,
|
||||
Messenger,
|
||||
Login,
|
||||
Header
|
||||
},
|
||||
|
||||
setup()
|
||||
{
|
||||
return { account };
|
||||
},
|
||||
|
||||
template: `
|
||||
<main id="app" v-if="account !== null">
|
||||
|
||||
<Header />
|
||||
|
||||
<Messenger v-if="account"/>
|
||||
<Login v-else />
|
||||
|
||||
</main>
|
||||
|
||||
<Splash v-else />`
|
||||
}
|
||||
|
||||
window.mainApi.on("set-account", (account_) => {
|
||||
console.log("setting-account");
|
||||
account.value = account_;
|
||||
});
|
||||
|
||||
export default App;
|
||||
export { account };
|
||||
|
||||
// Is called when window is about to close or reload
|
||||
window.onbeforeunload = () => console.log("beforeUnload");
|
||||
16
src/render/components/bubble.js
Normal file
16
src/render/components/bubble.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
const Bubble =
|
||||
{
|
||||
props: ["modifier", "text", "child"],
|
||||
|
||||
template: `
|
||||
<div :class="'bubble ' + modifier + '-bubble ' + modifier +'-'+ child">
|
||||
{{ text }}
|
||||
</div> `
|
||||
};
|
||||
|
||||
export default Bubble;
|
||||
|
||||
|
||||
// <div v-show="!seen" class="bubble-notify"></div>
|
||||
|
||||
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
<template>
|
||||
<div :class="`bubble ${modifier}-bubble ${modifier}-${child}`">
|
||||
<div v-show="!seen" class="notify"></div>
|
||||
{{ text }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: "Bubble",
|
||||
|
||||
props: ["modifier", "text", "child", "seen"],
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.bubble {
|
||||
position: relative;
|
||||
max-width: 66vw;
|
||||
font-family: "SF Pro Text";
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
border-radius: 18px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.ai-bubble {
|
||||
background-color: #FFFFFF;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.client-bubble {
|
||||
color: white;
|
||||
background-color: #58C4FD;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.ai-first-child {
|
||||
border-bottom-left-radius: 9px;
|
||||
}
|
||||
|
||||
.ai-middle-child {
|
||||
border-top-left-radius: 9px;
|
||||
border-bottom-left-radius: 9px;
|
||||
}
|
||||
|
||||
.ai-last-child {
|
||||
border-top-left-radius: 9px;
|
||||
}
|
||||
|
||||
.client-first-child {
|
||||
border-bottom-right-radius: 9px;
|
||||
}
|
||||
|
||||
.client-middle-child {
|
||||
border-top-right-radius: 9px;
|
||||
border-bottom-right-radius: 9px;
|
||||
}
|
||||
|
||||
.client-last-child {
|
||||
border-top-right-radius: 9px;
|
||||
}
|
||||
|
||||
.ai-none-child, .client-none-child {
|
||||
border-top-right-radius: 9px;
|
||||
}
|
||||
|
||||
.notify {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: #58D9FF;
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
border: 2px solid #EBEBEB;
|
||||
// transform: scale(0);
|
||||
|
||||
// animation-name: notify-anim;
|
||||
// animation-duration: 5s;
|
||||
}
|
||||
|
||||
@keyframes notify-anim {
|
||||
0%, 90% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
<template>
|
||||
<div id="container">
|
||||
<img id="logo" src="@/render/assets/connectLogo.svg">
|
||||
<div id="connect">
|
||||
<div class="dot"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, onMounted, watch } from 'vue'
|
||||
import { hiddenState, useAnim, status, show, hide } from "@/render/shared/connectionStatus";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ConnectionStatus",
|
||||
|
||||
setup() {
|
||||
|
||||
watch(status, (val, _oldval) => {
|
||||
if (val === "Connected" || val === "Nominal") {
|
||||
if (!hiddenState) hide();
|
||||
} else {
|
||||
if (hiddenState)
|
||||
show();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(useAnim);
|
||||
|
||||
return {
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
#container {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 54px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#connect {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
shape {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: #48E065;
|
||||
}
|
||||
|
||||
.dot {
|
||||
@extend shape;
|
||||
position: relative;
|
||||
transform: translateX(-15px);
|
||||
animation: flashing 1s infinite linear alternate;
|
||||
animation-delay: .25s;
|
||||
}
|
||||
|
||||
.dot::before, .dot::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.dot::before {
|
||||
@extend shape;
|
||||
left: -9px;
|
||||
animation: flashing 1s infinite alternate;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.dot::after {
|
||||
@extend shape;
|
||||
left: 9px;
|
||||
animation: flashing 1s infinite alternate;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
@keyframes flashing {
|
||||
0% {
|
||||
background-color: #48E065;
|
||||
}
|
||||
50%,
|
||||
100% {
|
||||
background-color: #9B9B9B;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
38
src/render/components/context.js
Normal file
38
src/render/components/context.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
|
||||
const Context =
|
||||
{
|
||||
props: ["modifier", "context", "avatar", "audio", "id"],
|
||||
|
||||
setup(props) {
|
||||
const playback = Vue.ref(false);
|
||||
|
||||
Vue.onMounted(() => {
|
||||
window.mainApi.on("playback", (id, status) => {
|
||||
if (id === props.id) {
|
||||
playback.value = status;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return { playback };
|
||||
},
|
||||
|
||||
template: `
|
||||
<span :class="'context ' + modifier + '-context'">
|
||||
|
||||
<div
|
||||
v-if="avatar != 'False'"
|
||||
class="context-avatar"
|
||||
:class="{ audio: playback }"
|
||||
>
|
||||
|
||||
<img :src="avatar">
|
||||
|
||||
</div>
|
||||
|
||||
{{ context }}
|
||||
|
||||
</span> `
|
||||
}
|
||||
|
||||
export default Context;
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
<template>
|
||||
|
||||
<span :class="`context ${modifier}-context`">
|
||||
|
||||
<div v-if="context.img" class="avatar" @click="clickMethod">
|
||||
<img v-click="" v-if="context.img === 'crimata'" :src="require('@/render/assets/crimataAvatar.svg')">
|
||||
<div v-else>{{ context.img }}</div>
|
||||
</div>
|
||||
|
||||
{{ context.text == false ? " " : context.text }}
|
||||
|
||||
</span>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: "Context",
|
||||
|
||||
props: ["modifier", "context", "uid"],
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.context {
|
||||
position: relative;
|
||||
min-height: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: "SF Compact Display";
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.ai-context {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.client-context {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: white;
|
||||
border-radius: 15px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.playing {
|
||||
animation-name: message-playback-anim;
|
||||
animation-duration: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes message-playback-anim {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0.4em rgba(195, 195, 195, 0.4), 0 0 0 0.25em rgba(195, 195, 195, 0.15);
|
||||
}
|
||||
25% {
|
||||
box-shadow: 0 0 0 0.15em rgba(195, 195, 195, 0.15), 0 0 0 0.4em rgba(195, 195, 195, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 0.55em rgba(195, 195, 195, 0.55), 0 0 0 0.15em rgba(195, 195, 195, 0.05);
|
||||
}
|
||||
75% {
|
||||
box-shadow: 0 0 0 0.25em rgba(195, 195, 195, 0.25), 0 0 0 0.55em rgba(195, 195, 195, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
199
src/render/components/control/draggify.js
Normal file
199
src/render/components/control/draggify.js
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
const saveLocation = "input_item_position";
|
||||
const defaultPosition = { x: 15, y: window.innerHeight - 300 };
|
||||
|
||||
//---Dragabble Helper Funcs--------------------------------------
|
||||
|
||||
// Calculate distance to nearest side.
|
||||
function calcSideProximity (elementX, elementLength, winW) {
|
||||
|
||||
// Calc right short.
|
||||
let short = winW - elementX - elementLength;
|
||||
|
||||
// See if it's left short.
|
||||
if (elementX + 20 < winW / 2) {
|
||||
short = elementX
|
||||
}
|
||||
|
||||
return short
|
||||
}
|
||||
|
||||
// Update elementX or elementY value on window resize
|
||||
function calcPosition (elementPosition, elementLength, percent, short, win) {
|
||||
|
||||
// Is it close to the right/bottom side?
|
||||
if (percent > 0.75) {
|
||||
elementPosition = win - short - elementLength;
|
||||
}
|
||||
|
||||
// Is it not close to a side?
|
||||
if (percent < 0.75 && percent > 0.25) {
|
||||
elementPosition = win * percent
|
||||
}
|
||||
|
||||
return elementPosition
|
||||
}
|
||||
|
||||
function draggify(elementId, margin) {
|
||||
|
||||
let element;
|
||||
|
||||
/* only compatible with elements having equal width and height */
|
||||
let elementLength;
|
||||
|
||||
// Cords of inputItem.
|
||||
const elementX = Vue.ref();
|
||||
const elementY = Vue.ref();
|
||||
|
||||
// Position of inputItem on terms of percentage of window.
|
||||
let percentX;
|
||||
let percentY;
|
||||
|
||||
// How close inputItem is to closest X or Y side.
|
||||
let xShort;
|
||||
let yShort;
|
||||
|
||||
// Keep track of window size;
|
||||
let winW = window.innerWidth;
|
||||
let winH = window.innerHeight;
|
||||
|
||||
//---Reposition Anime-----------------------------------------------
|
||||
|
||||
// Move element to target smoothly.
|
||||
const repositionAnime = (xChange, yChange) => {
|
||||
const xStep = xChange / 6000;
|
||||
const yStep = yChange / 6000;
|
||||
|
||||
for (let i = 1; i <= 6000; i++) {
|
||||
|
||||
setTimeout(() => {
|
||||
elementX.value += xStep;
|
||||
elementY.value += yStep;
|
||||
}, 16) // 60fps
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//---Event Handlers-----------------------------------------------
|
||||
|
||||
// Update the position of inputItem on mouse dragging.
|
||||
const onMouseMove = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const inputItemRect = element.getBoundingClientRect();
|
||||
elementX.value = inputItemRect.left + e.movementX;
|
||||
elementY.value = inputItemRect.top + e.movementY;
|
||||
}
|
||||
|
||||
// Add an event listener for dragging.
|
||||
const onMouseDown = (e) => {
|
||||
e.preventDefault();
|
||||
window.addEventListener('mousemove', onMouseMove);
|
||||
window.addEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
|
||||
// Update position references when user is done moving targetEl.
|
||||
const onMouseUp = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
console.log("onMouseUp");
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
|
||||
// See if and calculate reposition.
|
||||
let x = 0; // vector change
|
||||
let y = 0;
|
||||
|
||||
if (elementX.value < 0) {
|
||||
x = (elementX.value - margin)*-1
|
||||
}
|
||||
|
||||
if (elementX.value > winW - elementLength) {
|
||||
const b = winW - elementLength - margin;
|
||||
x = (elementX.value - b)*-1
|
||||
}
|
||||
|
||||
// Reposition y
|
||||
if (elementY.value < 0) {
|
||||
y = (elementY.value - margin)*-1
|
||||
}
|
||||
|
||||
if (elementY.value > winH - elementLength) {
|
||||
const d = winH - elementLength - margin;
|
||||
y = (elementY.value - d)*-1
|
||||
}
|
||||
|
||||
// Reposition if needed.
|
||||
if (x !== 0 || y !== 0) repositionAnime(x, y);
|
||||
|
||||
xShort = calcSideProximity(elementX.value, elementLength, winW);
|
||||
yShort = calcSideProximity(elementY.value, elementLength, winH);
|
||||
|
||||
// Update percentages.
|
||||
percentX = elementX.value / winW;
|
||||
percentY = elementY.value / winH;
|
||||
|
||||
// Save position.
|
||||
savePosition();
|
||||
}
|
||||
|
||||
// Update position of targetEl on windowResize.
|
||||
const onWindowResize = (_e) => {
|
||||
|
||||
// Update window dimensions.
|
||||
winW = window.innerWidth;
|
||||
winH = window.innerHeight;
|
||||
|
||||
elementX.value = calcPosition(elementX.value, elementLength, percentX, xShort, winW);
|
||||
elementY.value = calcPosition(elementY.value, elementLength, percentY, yShort, winH);
|
||||
|
||||
savePosition();
|
||||
}
|
||||
|
||||
const savePosition = () => {
|
||||
window.localStorage.setItem(saveLocation, JSON.stringify({
|
||||
x: elementX.value,
|
||||
y: elementY.value
|
||||
}));
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
|
||||
Vue.onMounted(() => {
|
||||
|
||||
element = document.getElementById(elementId);
|
||||
|
||||
elementLength = element.offsetWidth;
|
||||
|
||||
// Initialize the positional references.
|
||||
percentX = elementX.value / window.innerWidth;
|
||||
percentY = elementY.value / window.innerHeight;
|
||||
|
||||
xShort = calcSideProximity(elementX.value, elementLength, window.innerWidth);
|
||||
yShort = calcSideProximity(elementX.value, elementLength, window.innerHeight);
|
||||
|
||||
// Then, we can listen for window resize (and mousedown).
|
||||
element.addEventListener("mousedown", onMouseDown);
|
||||
window.addEventListener('resize', onWindowResize)
|
||||
});
|
||||
|
||||
// remove event listeners on component dismount.
|
||||
Vue.onUnmounted(() => {
|
||||
element.removeEventListener('mousedown', onMouseDown);
|
||||
window.removeEventListener('resize', onWindowResize)
|
||||
window.removeEventListener('mouseup', onMouseUp)
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
});
|
||||
|
||||
// Try loading initPosition, otherwise set default values
|
||||
let initPosition;
|
||||
const rawData = window.localStorage.getItem("saveLocation")
|
||||
rawData ? initPosition = JSON.parse(rawData) : initPosition = defaultPosition;
|
||||
|
||||
elementX.value = initPosition.x;
|
||||
elementY.value = initPosition.y;
|
||||
|
||||
return { elementX, elementY };
|
||||
|
||||
}
|
||||
|
||||
export default draggify;
|
||||
24
src/render/components/control/scroll.js
Normal file
24
src/render/components/control/scroll.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
let el;
|
||||
|
||||
const initScroll = (elementId) => {
|
||||
el = document.getElementById(elementId);
|
||||
el.addEventListener('adjust-scroll', adjustScroll);
|
||||
window.addEventListener('resize', adjustScroll);
|
||||
}
|
||||
|
||||
const isBottom = () => {
|
||||
if (el) {
|
||||
return el.scrollHeight - el.clientHeight <= el.scrollTop + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const adjustScroll = () => {
|
||||
if (el) {
|
||||
el.scrollTo({
|
||||
top: el.scrollHeight - el.clientHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default initScroll;
|
||||
73
src/render/components/control/text.js
Normal file
73
src/render/components/control/text.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
const inputLength = 230;
|
||||
|
||||
const metaKeys = [
|
||||
"Tab",
|
||||
"CapsLock",
|
||||
"Shift",
|
||||
"Control",
|
||||
"Alt",
|
||||
"Meta",
|
||||
" ",
|
||||
"ArrowLeft",
|
||||
"ArrowRight",
|
||||
"ArrowUp",
|
||||
"ArrowDown",
|
||||
"Enter",
|
||||
"Backspace",
|
||||
"Escape"
|
||||
];
|
||||
|
||||
function useText(elementId, left, typing)
|
||||
{
|
||||
let el;
|
||||
|
||||
const show = Vue.ref(false);
|
||||
const leftSide = Vue.ref(false);
|
||||
|
||||
const calcSide = () => {
|
||||
window.innerWidth - left.value < inputLength ? leftSide.value = true : leftSide.value = false;
|
||||
}
|
||||
|
||||
/* handle user typing */
|
||||
Vue.onMounted(() => {
|
||||
|
||||
el = document.getElementById(elementId);
|
||||
|
||||
window.addEventListener("keydown", (e) => {
|
||||
|
||||
if (!show.value && !metaKeys.includes(e.key))
|
||||
{
|
||||
show.value = true;
|
||||
}
|
||||
else if (show.value && ((el.value.length === 1 && e.key === "Backspace") || e.key === "Escape"))
|
||||
{
|
||||
show.value = false;
|
||||
}
|
||||
|
||||
if (e.key === "Enter" && el.value)
|
||||
{
|
||||
window.mainApi.send("message", {
|
||||
text: el.value, audio: false });
|
||||
show.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Vue.watch(left, calcSide);
|
||||
|
||||
Vue.watch(show, (c, p_) => {
|
||||
if (c) {
|
||||
el.focus();
|
||||
} else {
|
||||
el.value = "";
|
||||
el.blur();
|
||||
}
|
||||
});
|
||||
|
||||
calcSide();
|
||||
|
||||
return { leftSide, show };
|
||||
}
|
||||
|
||||
export default useText;
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
import { ref, Ref } from "vue";
|
||||
import anime from "animejs";
|
||||
|
||||
|
||||
export function animateTextInput () {
|
||||
|
||||
const side = ref("right");
|
||||
|
||||
function show () {
|
||||
const t1 = (side.value === "right") ? 50 : -70;
|
||||
const t2 = (side.value === "right") ? 110 : -130;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
opacity: [0, 1],
|
||||
translateX: [t1, t2],
|
||||
scale: [0.3, 1],
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function hide () {
|
||||
const t = (side.value === "right") ? 50 : -80;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
opacity: [1, 0],
|
||||
translateX: t,
|
||||
scale: 0.3,
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function switchSide () {
|
||||
const t = (side.value === "right") ? -130 : 110;
|
||||
|
||||
anime({
|
||||
targets: '#textInput',
|
||||
translateX: t,
|
||||
duration: 500,
|
||||
easing: 'easeOutExpo',
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
side,
|
||||
show,
|
||||
hide,
|
||||
switchSide
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export function animateAudioInput () {
|
||||
|
||||
function show () {
|
||||
anime({
|
||||
targets: '#recIcon',
|
||||
opacity: [0, 0.75],
|
||||
scale: [0.0, 1],
|
||||
duration: 250,
|
||||
easing: 'linear',
|
||||
})
|
||||
}
|
||||
|
||||
function hide () {
|
||||
anime({
|
||||
targets: '#recIcon',
|
||||
opacity: [0.75, 0],
|
||||
scale: [1, 0],
|
||||
duration: 250,
|
||||
easing: 'linear',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
show,
|
||||
hide
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// Given index and length of content, return message child status.
|
||||
export function calcChild (index: number, len: number)
|
||||
{
|
||||
if (len === 1)
|
||||
{
|
||||
return "none";
|
||||
}
|
||||
else if (index === 0)
|
||||
{
|
||||
return "first-child";
|
||||
}
|
||||
else if (index === len - 1)
|
||||
{
|
||||
return "last-child";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "middle-child";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
import { Ref, ref, watch, onMounted, onUnmounted } from "vue";
|
||||
import { postMessage } from "@/render/ipc";
|
||||
import { animateTextInput } from "./helpers";
|
||||
|
||||
|
||||
export default function useTextInputController(elementX: Ref) {
|
||||
|
||||
let textInput: HTMLInputElement | null;
|
||||
|
||||
const { side, show, hide, switchSide } = animateTextInput();
|
||||
|
||||
let firstKey = true;
|
||||
const typing = ref(false);
|
||||
|
||||
// Prep inputItem for typing.
|
||||
const prepInput = () => {
|
||||
show()
|
||||
typing.value = true
|
||||
}
|
||||
|
||||
// Clear and hide inputItem after done typing.
|
||||
const clearInput = () => {
|
||||
|
||||
if (textInput) {
|
||||
textInput.value = "";
|
||||
textInput.blur();
|
||||
}
|
||||
|
||||
hide()
|
||||
firstKey = true;
|
||||
typing.value = false;
|
||||
}
|
||||
|
||||
// Send a message and clean up after.
|
||||
const sendMessage = () => {
|
||||
if (textInput) {
|
||||
|
||||
// Send it to the backend for processing.
|
||||
const message: Raw = {
|
||||
text: textInput.value,
|
||||
audio: false
|
||||
};
|
||||
|
||||
postMessage(message);
|
||||
|
||||
clearInput()
|
||||
}
|
||||
}
|
||||
|
||||
// Keys that are capable of opening the text input (numbers and letters).
|
||||
const isHotKey = (key: number) => {
|
||||
if (key >= 47 && key <= 91) { // a letter
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
//---Callbacks-----------------------------------------------
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
const key = e.keyCode;
|
||||
|
||||
if (textInput) {
|
||||
|
||||
// Only runs on firstKey.
|
||||
if (firstKey) {
|
||||
|
||||
if (!isHotKey(key)) {
|
||||
return
|
||||
}
|
||||
|
||||
prepInput()
|
||||
}
|
||||
|
||||
textInput.focus();
|
||||
|
||||
// Close input when no text or on ESC.
|
||||
if ((textInput.value == "") && (!firstKey) && (key === 8)) { // backspace
|
||||
clearInput()
|
||||
return
|
||||
}
|
||||
|
||||
if (key === 27) { // escape
|
||||
clearInput()
|
||||
return
|
||||
}
|
||||
|
||||
// Close and send on enter.
|
||||
if (key === 13) {
|
||||
if (textInput.value) {
|
||||
sendMessage()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (firstKey) firstKey = false
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------
|
||||
|
||||
// Watch parent position and update side.
|
||||
watch(elementX, (elementX, _previous) => {
|
||||
const winW = window.innerWidth
|
||||
|
||||
// Logic depends on the side we are on.
|
||||
if (side.value === "right") {
|
||||
if (winW - elementX < 230) {
|
||||
switchSide()
|
||||
side.value = "left"
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
if (winW - elementX > 230) {
|
||||
switchSide()
|
||||
side.value = "right"
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
textInput = document.getElementById("textInput") as HTMLInputElement;
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keydown", onKeyDown);
|
||||
});
|
||||
|
||||
return {
|
||||
typing
|
||||
}
|
||||
|
||||
}
|
||||
23
src/render/components/header.js
Normal file
23
src/render/components/header.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
const Header =
|
||||
{
|
||||
setup() {
|
||||
const onNavBar = (command) => window.mainApi.send('nav-bar', command);
|
||||
return { onNavBar };
|
||||
},
|
||||
|
||||
template: `
|
||||
<button id="header-titlebar" />
|
||||
|
||||
<span id="header-menu">
|
||||
<button
|
||||
class="header-menu-button header-exit-button button"
|
||||
@click.prevent="onNavBar('close')"
|
||||
/>
|
||||
<button
|
||||
class="header-menu-button header-min-button button"
|
||||
@click.prevent="onNavBar('min')"
|
||||
/>
|
||||
</span> `
|
||||
}
|
||||
|
||||
export default Header;
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
<template>
|
||||
<button id="titlebar" />
|
||||
<span id="menu">
|
||||
<button
|
||||
class="menuButton exitButton"
|
||||
@click.prevent="postNavBarExit"
|
||||
/>
|
||||
<button
|
||||
class="menuButton minimizeButton"
|
||||
@click.prevent="postNavBarMin"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
// import { postNavBarExit, postNavBarMin } from "@/render/ipc";
|
||||
import { defineComponent } from "vue";
|
||||
import { postNavBar } from "@/render/ipc";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Header",
|
||||
setup() {
|
||||
const postNavBarExit = () => postNavBar('close');
|
||||
const postNavBarMin = () => postNavBar('min');
|
||||
return {
|
||||
postNavBarExit,
|
||||
postNavBarMin
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#titlebar {
|
||||
position: fixed;
|
||||
|
||||
width: 100vw;
|
||||
height: 54px;
|
||||
|
||||
opacity: 0.75;
|
||||
|
||||
background-color: #EBEBEB;
|
||||
|
||||
-webkit-app-region: drag;
|
||||
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
z-index: 1;
|
||||
|
||||
}
|
||||
|
||||
#menu {
|
||||
position: fixed;
|
||||
margin-left: 20px;
|
||||
margin-top: 20px;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
}
|
||||
|
||||
.menuButton {
|
||||
border: none;
|
||||
outline:none;
|
||||
min-width: 14px;
|
||||
min-height: 14px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.exitButton {
|
||||
background-color: #FF6157;
|
||||
}
|
||||
|
||||
.exitButton:active {
|
||||
background: #c14645;
|
||||
}
|
||||
|
||||
.minimizeButton {
|
||||
background-color: #FFC12F;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.minimizeButton:active {
|
||||
background-color: #c08e38;
|
||||
}
|
||||
</style>
|
||||
39
src/render/components/input.js
Normal file
39
src/render/components/input.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import useText from "./control/text.js";
|
||||
import draggify from "./control/draggify.js";
|
||||
|
||||
const Input =
|
||||
{
|
||||
setup()
|
||||
{
|
||||
const recording = Vue.ref(false);
|
||||
|
||||
const { elementX, elementY } = draggify("input", 15);
|
||||
|
||||
const { leftSide, show } = useText("input-text", elementX);
|
||||
|
||||
window.mainApi.on("record", (status) => recording.value = status);
|
||||
|
||||
return { elementX, elementY, recording, leftSide, show };
|
||||
},
|
||||
|
||||
template: `
|
||||
<div id="input" :class="{ audio: recording }"
|
||||
:style="{ top: elementY + 'px', left: elementX + 'px' }"
|
||||
>
|
||||
|
||||
<div id="input-icon"></div>
|
||||
|
||||
<input
|
||||
id="input-text" type="text"
|
||||
:class="[
|
||||
{ leftSide: leftSide },
|
||||
{ show: show },
|
||||
{ recording: recording }
|
||||
]"
|
||||
/>
|
||||
|
||||
</div> `
|
||||
}
|
||||
|
||||
export default Input;
|
||||
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
id="input-item"
|
||||
:class="{ playing: recording }"
|
||||
:style="{ top: `${elementY}px`, left: `${elementX}px` }"
|
||||
>
|
||||
|
||||
<!-- Show the profile photo in the center of the input item -->
|
||||
<!-- Background shadow animates on record -->
|
||||
<div id="profile">{{ profile.photo }}</div>
|
||||
|
||||
<!-- Show text input on key-down -->
|
||||
<input
|
||||
id="textInput"
|
||||
type="text"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, watch } from "vue";
|
||||
import draggify from "@/render/composables/useDraggify";
|
||||
import { profile } from "@/render/shared/profile";
|
||||
import { recording } from "@/render/shared/audio";
|
||||
import { animateAudioInput } from "@/render/components/controllers/helpers";
|
||||
|
||||
import useTextInputController from "@/render/components/controllers/inputItemControl";
|
||||
|
||||
export default defineComponent({
|
||||
name: "InputItem",
|
||||
|
||||
setup() {
|
||||
|
||||
// Default values for position.
|
||||
const xStart = 15;
|
||||
const yStart = window.innerHeight - 200;
|
||||
|
||||
// Calculate position of inputItem on drag.
|
||||
const { elementX, elementY } = draggify("inputItem", xStart, yStart, 15);
|
||||
|
||||
// Controllers for text and audio.
|
||||
const { typing } = useTextInputController(elementX)
|
||||
|
||||
// Control rec icon (red dot upper right of UI)
|
||||
const { show, hide } = animateAudioInput();
|
||||
watch(recording, (val, _oldval) => {
|
||||
recording ? show() : hide();
|
||||
});
|
||||
|
||||
return {
|
||||
profile,
|
||||
elementX,
|
||||
elementY,
|
||||
recording
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
|
||||
#input-item {
|
||||
position: absolute;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: 0;
|
||||
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
z-index: 3;
|
||||
|
||||
// To prevent window drag when overlapping with titlebar.
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
// border: 4px solid #C6C6C6;
|
||||
border-radius: 50%;
|
||||
|
||||
// Set opacity here to not affect child.
|
||||
background-color: rgba(235, 235, 235, 0.75);
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.rec-icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: red;
|
||||
transform: translate(14px);
|
||||
|
||||
animation-name: rec;
|
||||
animation-duration: 0.5s;
|
||||
}
|
||||
|
||||
@keyframes rec {
|
||||
from {
|
||||
transform: translate(14px) scale(0.3);
|
||||
}
|
||||
to {
|
||||
transform: translate(14px) scale(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
#textInput {
|
||||
position: absolute;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
min-width: 150px;
|
||||
height: 16px;
|
||||
|
||||
border-radius: 18px;
|
||||
|
||||
padding: 10px;
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
|
||||
outline: none;
|
||||
border: none;
|
||||
pointer-events: none;
|
||||
|
||||
background-color: white;
|
||||
|
||||
z-index: -1;
|
||||
|
||||
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
|
||||
|
||||
transform: translateX(50px) scale(0.3);
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
58
src/render/components/login.js
Normal file
58
src/render/components/login.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
const Login =
|
||||
{
|
||||
setup()
|
||||
{
|
||||
const usr = Vue.ref("");
|
||||
const pwd = Vue.ref("");
|
||||
|
||||
const submitForm = async () => {
|
||||
const success = await window.mainApi.invoke("login", usr.value, pwd.value);
|
||||
if (!success) shake("login-form");
|
||||
}
|
||||
|
||||
return { usr, pwd, submitForm };
|
||||
},
|
||||
|
||||
template: `
|
||||
<form id="login" @submit.prevent="submitForm">
|
||||
|
||||
<!-- username and password forms -->
|
||||
<div id="login-form">
|
||||
<!-- username -->
|
||||
<div class="login-input-box">
|
||||
<input class="login-input" type="text" v-model="usr" />
|
||||
<div class="login-input-modifier">Email</div>
|
||||
</div>
|
||||
|
||||
<!-- password -->
|
||||
<div class="login-input-box">
|
||||
<input class="login-input" type="password" v-model="pwd" />
|
||||
<div class="login-input-modifier">Password</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- submit button; position: fixed -->
|
||||
<button
|
||||
class="login-submit-button button"
|
||||
type="submit"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
|
||||
</form> `
|
||||
}
|
||||
|
||||
/** Shake a div to indicate incorrect credentials */
|
||||
const shake = (elementId) =>
|
||||
{
|
||||
const el = document.getElementById(elementId);
|
||||
|
||||
if (el)
|
||||
el.classList.remove("shake");
|
||||
|
||||
setTimeout(() => {
|
||||
if (el) el.classList.add("shake");
|
||||
}, 250);
|
||||
}
|
||||
|
||||
export default Login;
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
<template>
|
||||
<form id="login" @submit.prevent="submitForm">
|
||||
|
||||
<!-- username and password forms -->
|
||||
<div id="form">
|
||||
<!-- username -->
|
||||
<div class="inputBox">
|
||||
<input class="input" type="text" v-model="usr" />
|
||||
<div class="inputModifier">Email</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- password -->
|
||||
<div class="inputBox">
|
||||
<input class="input" type="password" v-model="pwd" />
|
||||
<div class="inputModifier">Password</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- submit button; position: fixed -->
|
||||
<button class="submit button" type="submit">Login</button>
|
||||
|
||||
</form>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { invokeLogin } from "@/render/ipc";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Login",
|
||||
|
||||
setup() {
|
||||
|
||||
const usr = ref("");
|
||||
const pwd = ref("");
|
||||
|
||||
// Submit login credentials to the backend.
|
||||
const submitForm = async () => {
|
||||
|
||||
try {
|
||||
await invokeLogin({email: usr.value, password: pwd.value});
|
||||
} catch(e) {
|
||||
shake("form");
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const shake = (elementId: string) => {
|
||||
const el = document.getElementById(elementId);
|
||||
|
||||
if (el)
|
||||
el.classList.remove("shake");
|
||||
|
||||
setTimeout(() => {
|
||||
if (el) el.classList.add("shake");
|
||||
}, 250);
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
usr,
|
||||
pwd,
|
||||
submitForm
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#login {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#form {
|
||||
font-family: "SF Compact Display";
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.inputBox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.input {
|
||||
background-color: #B9B9B9;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 16px;
|
||||
text-decoration: none;
|
||||
margin: 4px 2px;
|
||||
cursor: pointer;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.inputModifier {
|
||||
min-width: 181px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.submit {
|
||||
font-family: "SF Compact Display";
|
||||
position: fixed;
|
||||
margin-top: 260px;
|
||||
background-color: #58C4FD;
|
||||
color: white;
|
||||
padding: 10px 30px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.submit:active {
|
||||
background-color: #4296C3;
|
||||
}
|
||||
|
||||
.button {
|
||||
border: none;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shake {
|
||||
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
|
||||
transform: translate3d(0, 0, 0);
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
10%, 90% {
|
||||
transform: translate3d(-1px, 0, 0);
|
||||
}
|
||||
|
||||
20%, 80% {
|
||||
transform: translate3d(2px, 0, 0);
|
||||
}
|
||||
|
||||
30%, 50%, 70% {
|
||||
transform: translate3d(-4px, 0, 0);
|
||||
}
|
||||
|
||||
40%, 60% {
|
||||
transform: translate3d(4px, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
69
src/render/components/message.js
Normal file
69
src/render/components/message.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import Bubble from "./bubble.js";
|
||||
import Context from "./context.js";
|
||||
|
||||
const Message =
|
||||
{
|
||||
components: {
|
||||
Bubble,
|
||||
Context
|
||||
},
|
||||
|
||||
props: ["modifier", "content", "context", "avatar", "audio", "id"],
|
||||
|
||||
setup(props)
|
||||
{
|
||||
Vue.onMounted(() => {
|
||||
|
||||
const messengerEl = document.getElementById("messenger");
|
||||
|
||||
if (messengerEl) {
|
||||
messengerEl.dispatchEvent(new CustomEvent('adjust-scroll'));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return { calcChild };
|
||||
},
|
||||
|
||||
template: `
|
||||
<div :id="id" :class="'message ' + modifier + '-message'">
|
||||
|
||||
<Bubble
|
||||
v-for="(text, index) in content"
|
||||
:modifier="modifier"
|
||||
:text="text"
|
||||
:child="calcChild(index, content.length)"
|
||||
/>
|
||||
|
||||
<Context
|
||||
:modifier="modifier"
|
||||
:context="context"
|
||||
:avatar="avatar"
|
||||
:audio="audio"
|
||||
:id="id"
|
||||
/>
|
||||
|
||||
</div> `
|
||||
}
|
||||
|
||||
function calcChild(index, len)
|
||||
{
|
||||
if (len === 1)
|
||||
{
|
||||
return "none-child";
|
||||
}
|
||||
else if (index === 0)
|
||||
{
|
||||
return "first-child";
|
||||
}
|
||||
else if (index === len - 1)
|
||||
{
|
||||
return "last-child";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "middle-child";
|
||||
}
|
||||
}
|
||||
|
||||
export default Message;
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
<template>
|
||||
<div :id="uid" :class="`${modifier}-message`">
|
||||
|
||||
<Bubble
|
||||
v-for="(val, index) in content"
|
||||
:modifier="modifier"
|
||||
:text="val.text"
|
||||
:child="calcChild(index, content.length)"
|
||||
:seen="seen"
|
||||
/>
|
||||
|
||||
<Context
|
||||
:modifier="modifier"
|
||||
:context="context"
|
||||
:uid="uid"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
|
||||
import { defineComponent, onMounted } from 'vue';
|
||||
import Bubble from "@/render/components/bubble.vue";
|
||||
import Context from "@/render/components/context.vue";
|
||||
import { calcChild } from "@/render/components/controllers/helpers";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Message",
|
||||
|
||||
props: ["modifier", "content", "context", "seen", "uid", "playback"],
|
||||
|
||||
components: {
|
||||
Bubble,
|
||||
Context
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
|
||||
watch(props.playback, (val, _oldval) =>
|
||||
{
|
||||
boxShadow = val/200;
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
const messengerEl = document.getElementById("messenger");
|
||||
|
||||
if (messengerEl)
|
||||
messengerEl.dispatchEvent(new CustomEvent('adjust-scroll'))
|
||||
|
||||
});
|
||||
|
||||
return {
|
||||
calcChild
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.message {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 9px;
|
||||
padding-bottom: 9px;
|
||||
animation-name: message-init-anim;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
@keyframes message-init-anim {
|
||||
from {
|
||||
opacity: 0;
|
||||
} to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.message:first-child {
|
||||
margin-top: 55px;
|
||||
}
|
||||
|
||||
.message:last-child {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.client-message {
|
||||
@extend .message;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.ai-message {
|
||||
@extend .message;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
71
src/render/components/messenger.js
Normal file
71
src/render/components/messenger.js
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import initScroll from "./control/scroll.js";
|
||||
|
||||
import WS from "./ws.js";
|
||||
import Input from "./input.js";
|
||||
import Settings from "./settings.js";
|
||||
import Message from "./message.js";
|
||||
|
||||
const Messenger = {
|
||||
|
||||
components: {
|
||||
WS,
|
||||
Settings,
|
||||
Message,
|
||||
Input
|
||||
},
|
||||
|
||||
setup()
|
||||
{
|
||||
const messages = Vue.ref([]);
|
||||
|
||||
Vue.onMounted(() => {
|
||||
|
||||
window.mainApi.on("ui-init", (ui) => {
|
||||
messages.value = ui;
|
||||
});
|
||||
|
||||
window.mainApi.on("ui-update", (message) => {
|
||||
console.log(message)
|
||||
|
||||
let index = messages.value.findIndex(m => m.id===message.id);
|
||||
if (index >= 0)
|
||||
{
|
||||
messages.value[index] = message;
|
||||
}
|
||||
else
|
||||
{
|
||||
messages.value.push(message);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
initScroll("messenger");
|
||||
window.mainApi.send("messenger");
|
||||
});
|
||||
|
||||
Vue.onUnmounted(() => {
|
||||
messages.value = [];
|
||||
});
|
||||
|
||||
return { messages };
|
||||
},
|
||||
|
||||
template: `
|
||||
<Settings />
|
||||
<WS />
|
||||
<Input/>
|
||||
|
||||
<div id="messenger">
|
||||
<Message
|
||||
v-for="message in messages"
|
||||
:modifier="message.modifier"
|
||||
:content="message.content"
|
||||
:context="message.context"
|
||||
:avatar="message.avatar"
|
||||
:audio="message.audio"
|
||||
:id="message.id"
|
||||
/>
|
||||
</div> `
|
||||
}
|
||||
|
||||
export default Messenger;
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
<template>
|
||||
|
||||
<!-- Position fixed items -->
|
||||
<Settings />
|
||||
<ConnectionStatus />
|
||||
<div id="recIcon" />
|
||||
<InputItem />
|
||||
|
||||
<!-- List of message bubbles. -->
|
||||
<div id="messenger">
|
||||
<Message
|
||||
v-for="message in messages"
|
||||
:modifier="message.modifier"
|
||||
:content="message.content"
|
||||
:context="message.context"
|
||||
:seen="message.seen"
|
||||
:uid="message.uid"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, onMounted, onUnmounted } from "vue";
|
||||
import InputItem from "@/render/components/inputItem.vue";
|
||||
import Settings from "@/render/components/settings.vue";
|
||||
import Message from "@/render/components/message.vue";
|
||||
import ConnectionStatus from "./connectionStatus.vue";
|
||||
|
||||
import { profile } from "@/render/shared/profile";
|
||||
import { messages } from "@/render/shared/messages";
|
||||
import useScroll from "@/render/composables/useScroll";
|
||||
import { postMessage, postWindowFocus } from "@/render/ipc";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Messenger",
|
||||
|
||||
components: {
|
||||
ConnectionStatus,
|
||||
InputItem,
|
||||
Settings,
|
||||
Message,
|
||||
},
|
||||
|
||||
setup() {
|
||||
|
||||
const onFocus = () => {
|
||||
postWindowFocus(true);
|
||||
postMessage({
|
||||
intent: "focus",
|
||||
params: { "focus": true },
|
||||
epic: false,
|
||||
confidence: 1.0
|
||||
} as PlatformRequest);
|
||||
}
|
||||
|
||||
const onBlur = () => {
|
||||
postWindowFocus(false);
|
||||
postMessage({
|
||||
intent: "focus",
|
||||
params: { "focus": false },
|
||||
epic: false,
|
||||
confidence: 1.0
|
||||
} as PlatformRequest);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
useScroll("messenger");
|
||||
|
||||
window.addEventListener('blur', onBlur);
|
||||
window.addEventListener('focus', onFocus);
|
||||
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
window.removeEventListener("blur", onBlur);
|
||||
window.removeEventListener("focus", onFocus);
|
||||
|
||||
});
|
||||
|
||||
return {
|
||||
messages,
|
||||
profile
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
#messenger {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#messenger::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#recIcon {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
|
||||
opacity: 0;
|
||||
transform: scale(0.0);
|
||||
|
||||
margin-top: 24px;
|
||||
margin-right: 66px;
|
||||
|
||||
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #FF3B3B;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
40
src/render/components/settings.js
Normal file
40
src/render/components/settings.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
|
||||
const Settings =
|
||||
{
|
||||
setup() {
|
||||
const active = Vue.ref(false);
|
||||
|
||||
function hideSettings(e) {
|
||||
if (e.key == "Escape") {
|
||||
active.value = false;
|
||||
window.removeEventListener("keydown", hideSettings);
|
||||
}
|
||||
}
|
||||
|
||||
function showSettings() {
|
||||
active.value = true;
|
||||
window.addEventListener("keydown", hideSettings);
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
await window.mainApi.send("logout");
|
||||
}
|
||||
|
||||
return { showSettings, active, logout };
|
||||
},
|
||||
|
||||
template: `
|
||||
<div v-if="active" id="settings" >
|
||||
|
||||
<button class="settings-logout-button button" @click="logout">
|
||||
Logout
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<button v-else class="settings-icon" @click="showSettings">
|
||||
<img src="./assets/settingsIcon.svg">
|
||||
</button> `
|
||||
}
|
||||
|
||||
export default Settings;
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
<template>
|
||||
|
||||
<!-- Settings content -->
|
||||
<div v-if="toggleSettings" id="settings" >
|
||||
|
||||
<p class="account">{{ `Hi, ${account}` }}</p>
|
||||
|
||||
<button
|
||||
class="logout"
|
||||
@click="onLogout"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
|
||||
<p class="version">{{ `Version ${version}` }}</p>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Icon -->
|
||||
<button
|
||||
v-else
|
||||
class="icon"
|
||||
@click="onActive"
|
||||
>
|
||||
<img src="@/render/assets/settingsIcon.svg">
|
||||
</button>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { invokeLogout } from "@/render/ipc";
|
||||
import { version } from "@/render/shared/version";
|
||||
import { ready, account, setAccount, clearAccount } from "@/render/shared/account";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Settings",
|
||||
|
||||
setup() {
|
||||
const toggleSettings = ref(false);
|
||||
|
||||
// Listen for escape key to close settings.
|
||||
const onEscape = (e: any) => {
|
||||
if(e.key === "Escape") {
|
||||
toggleSettings.value = false;
|
||||
}
|
||||
window.removeEventListener("keydown", onEscape);
|
||||
}
|
||||
|
||||
// When settings is showing.
|
||||
const onActive = () => {
|
||||
toggleSettings.value = true;
|
||||
window.addEventListener("keydown", onEscape);
|
||||
}
|
||||
|
||||
// We ask server to log us out.
|
||||
const onLogout = async () => {
|
||||
|
||||
console.log("Submitting logout request.");
|
||||
|
||||
try {
|
||||
await invokeLogout();
|
||||
} catch(e) {
|
||||
console.log('error')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
account,
|
||||
version,
|
||||
onActive,
|
||||
toggleSettings,
|
||||
onLogout
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
#settings {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(235, 235, 235, 0.75);
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
animation-name: appear;
|
||||
animation-duration: 0.5s;
|
||||
}
|
||||
|
||||
@keyframes appear {
|
||||
from {
|
||||
background-color: rgba(235, 235, 235, 0);
|
||||
}
|
||||
to {
|
||||
background-color: rgba(235, 235, 235, 0.75);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 5px;
|
||||
margin-right: 20px;
|
||||
margin-top: 19px;
|
||||
z-index: 2;
|
||||
background-color: Transparent;
|
||||
}
|
||||
|
||||
.icon:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.account {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.logout {
|
||||
font-family: "SF Compact Display";
|
||||
background-color: #B7B7B7;
|
||||
padding: 10px 30px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logout:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.version {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 75%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #575757;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
50
src/render/components/splash.js
Normal file
50
src/render/components/splash.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
const Splash =
|
||||
{
|
||||
template: `
|
||||
<div id="splash">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="57.046"
|
||||
height="52.759"
|
||||
viewBox="0 0 57.046 52.759"
|
||||
>
|
||||
<g transform="translate(-4127 -507)">
|
||||
<g transform="translate(3949 332.206)">
|
||||
<g transform="translate(178 174.794)">
|
||||
<g transform="translate(0 27.405)">
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(0 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(31.692 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
</g>
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(15.822 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
<path
|
||||
d="M36.27,83.67"
|
||||
transform="translate(-23.569 -43.588)"
|
||||
fill="#ff0"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
export default Splash;
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
<template>
|
||||
<div id="splash">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="57.046"
|
||||
height="52.759"
|
||||
viewBox="0 0 57.046 52.759"
|
||||
>
|
||||
<g transform="translate(-4127 -507)">
|
||||
<g transform="translate(3949 332.206)">
|
||||
<g transform="translate(178 174.794)">
|
||||
<g transform="translate(0 27.405)">
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(0 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(31.692 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
</g>
|
||||
<circle
|
||||
cx="12.677"
|
||||
cy="12.677"
|
||||
r="12.677"
|
||||
transform="translate(15.822 0)"
|
||||
fill="#383838"
|
||||
/>
|
||||
<path
|
||||
d="M36.27,83.67"
|
||||
transform="translate(-23.569 -43.588)"
|
||||
fill="#ff0"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Splash",
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
#splash {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
36
src/render/components/ws.js
Normal file
36
src/render/components/ws.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
|
||||
const WS =
|
||||
{
|
||||
setup()
|
||||
{
|
||||
const status = Vue.ref();
|
||||
|
||||
Vue.onMounted(() => {
|
||||
|
||||
window.mainApi.on("ws-status", (val) => {
|
||||
status.value = val;
|
||||
console.log(status.value);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return { status }
|
||||
},
|
||||
|
||||
template: `
|
||||
<div id="ws">
|
||||
|
||||
<img id="ws-logo" src="./assets/connectLogo.svg"
|
||||
:class="{ move: status }"
|
||||
>
|
||||
|
||||
<div id="ws-dots"
|
||||
:class="{ move: status }"
|
||||
>
|
||||
<div class="ws-dot"></div>
|
||||
</div>
|
||||
|
||||
</div> `
|
||||
}
|
||||
|
||||
export default WS;
|
||||
Loading…
Reference in a new issue