507 lines
No EOL
9.6 KiB
Vue
507 lines
No EOL
9.6 KiB
Vue
<template>
|
|
<div
|
|
class="message"
|
|
:class="`${message.isChild}ChildMessage`"
|
|
:id="`${message.modifier}Message`"
|
|
:key="message.id"
|
|
>
|
|
|
|
<!-- Three types of messages: self (sf), friend (fr), and crimata (ai) -->
|
|
<!-- !denoted by the modifier attribute -->
|
|
|
|
<!-- Shows when a new session is created with Crimata. -->
|
|
<div
|
|
v-if="message.context === 'launch'"
|
|
class="divider"
|
|
>
|
|
<div>new session</div>
|
|
</div>
|
|
|
|
<!-- Includes bubble and context. -->
|
|
<div
|
|
:id="`${message.modifier}MessageBox`"
|
|
class="messageBox"
|
|
>
|
|
|
|
<!-- Render undo button for messages to friends. -->
|
|
<button
|
|
v-if="undoOption === true"
|
|
class="undoButton"
|
|
>
|
|
undo
|
|
</button>
|
|
|
|
<!-- message bubble -->
|
|
<div
|
|
class="bubble"
|
|
:class="`${message.modifier}-${message.isChild}ChildBubble`"
|
|
:id="`${message.modifier}Bubble`"
|
|
>
|
|
|
|
<!-- Notification dot for new messages. -->
|
|
<div v-if="notify === true"
|
|
class="notify"
|
|
/>
|
|
|
|
<!-- Show loader when audio is being transcribed. -->
|
|
<div
|
|
v-if="message.content.text === ''"
|
|
class="contentLoader"
|
|
>
|
|
<div class="contentLoaderDot"></div>
|
|
<div class="contentLoaderDot"></div>
|
|
<div class="contentLoaderDot"></div>
|
|
</div>
|
|
|
|
<!-- Show question mark if transcription comes back as unknown. -->
|
|
<div
|
|
v-else-if="message.content.text === false"
|
|
class="questionMark"
|
|
>
|
|
?
|
|
</div>
|
|
|
|
<!-- Else, show text. -->
|
|
<div v-else>
|
|
{{ message.content.text }}
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- message context -->
|
|
<span
|
|
v-if="message.isChild === 'last' || message.isChild === 'none'"
|
|
class="context"
|
|
:class="{ 'context-undo-anim': undoOption }"
|
|
>
|
|
|
|
<!-- Render Crimata icon or friend's initials. -->
|
|
<div v-if="message.modifier == 'ai'" class="photo">
|
|
<img src="@/assets/crimata.svg"/>
|
|
</div>
|
|
|
|
<!-- Render Crimata icon or friend's initials. -->
|
|
<div v-if="message.modifier == 'fr'" class="photo"
|
|
:class="{ playing: isPlaying }"
|
|
>
|
|
{{ message.context[0] }}
|
|
</div>
|
|
|
|
{{ message.context }}
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<script lang='ts'>
|
|
|
|
import { defineComponent, ref, onMounted, onBeforeUpdate } from 'vue';
|
|
import { clientRequest } from "@/modules/message";
|
|
import { useIpc } from "@/modules/ipc";
|
|
|
|
export default defineComponent({
|
|
name: "MessageItem",
|
|
|
|
props: ["message"],
|
|
|
|
setup(props) {
|
|
|
|
let addListener = false;
|
|
|
|
const undoOption = ref(false);
|
|
|
|
const notify = ref(false)
|
|
const isPlaying = ref(false);
|
|
|
|
const { post } = useIpc()
|
|
|
|
if (!props.message.seen) {
|
|
console.log("MSG:Checking...")
|
|
|
|
// newMessages always render with blue dot.
|
|
if (props.message.newMessage) {
|
|
console.log("MSG:New message, notifying.")
|
|
props.message.seen = true;
|
|
notify.value = true;
|
|
}
|
|
|
|
else {
|
|
|
|
// Do nothing if window is visible on setup.
|
|
if (document.visibilityState === "visible") {
|
|
props.message.seen = true;
|
|
}
|
|
|
|
// We must listen for window visible event.
|
|
else {
|
|
console.log("MSG:Unseen, adding listener.")
|
|
addListener = true;
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Callback for window visibilityState.
|
|
const onVisibilityChange = (event: any) => {
|
|
if (document.visibilityState === "visible") {
|
|
console.log("MSG:Window visible, notifying.")
|
|
|
|
props.message.seen = true;
|
|
notify.value = true; // triggers blue dot anim.
|
|
|
|
// Once complete we can remove.
|
|
document.removeEventListener("visibilitychange", onVisibilityChange)
|
|
}
|
|
}
|
|
|
|
const onUndo = () => {
|
|
post("client-message", clientRequest("launch", {name: false}, "undo"))
|
|
}
|
|
|
|
const onStopPlayback = (e: any) => {
|
|
window.ipcRenderer.removeAllListeners("stop-playback-anim");
|
|
isPlaying.value = false
|
|
}
|
|
|
|
onBeforeUpdate(() => {
|
|
|
|
if (props.message.context.substring(0, 3) === 'To ') {
|
|
console.log("MSG:Applying undo anim")
|
|
undoOption.value = true;
|
|
}
|
|
|
|
})
|
|
|
|
onMounted(() => {
|
|
|
|
// Listen for window focus event.
|
|
if (addListener) {
|
|
document.addEventListener("visibilitychange", onVisibilityChange)
|
|
}
|
|
|
|
// Turn on audio playback animation if audio.
|
|
if (props.message.content.audio === true) {
|
|
window.ipcRenderer.on("stop-playback-anim", onStopPlayback);
|
|
isPlaying.value = true
|
|
}
|
|
|
|
})
|
|
|
|
return {
|
|
onUndo,
|
|
notify,
|
|
isPlaying,
|
|
undoOption
|
|
}
|
|
}
|
|
|
|
})
|
|
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
.message {
|
|
width: 100vw;
|
|
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
padding-top: 9px;
|
|
padding-bottom: 9px;
|
|
|
|
}
|
|
|
|
.message:first-child {
|
|
margin-top: 55px;
|
|
}
|
|
|
|
.message:last-child {
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.divider {
|
|
width: 100vw;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
|
|
font-family: "SF Compact Display";
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
color: #9B9B9B;
|
|
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.firstChildMessage {
|
|
padding-bottom: 2px;
|
|
}
|
|
|
|
.middleChildMessage {
|
|
padding-top: 2px;
|
|
padding-bottom: 2px;
|
|
}
|
|
|
|
.lastChildMessage {
|
|
padding-top: 2px;
|
|
}
|
|
|
|
#aiMessage {
|
|
align-items: flex-start;
|
|
}
|
|
|
|
#sfMessage {
|
|
align-items: flex-end;
|
|
}
|
|
|
|
#frMessage {
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.messageBox {
|
|
position: relative;
|
|
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
// Animate on render.
|
|
animation-name: appear;
|
|
animation-duration: 0.25s;
|
|
}
|
|
|
|
#aiMessageBox {
|
|
margin-left: 20px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
#sfMessageBox {
|
|
margin-right: 20px;
|
|
align-items: flex-end;
|
|
}
|
|
|
|
#frMessageBox {
|
|
margin-left: 20px;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.bubble {
|
|
position: relative;
|
|
|
|
max-width: 66vw;
|
|
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
font-family: "SF Pro Text";
|
|
font-size: 14px;
|
|
|
|
padding: 10px;
|
|
|
|
border-radius: 18px;
|
|
}
|
|
|
|
@keyframes appear {
|
|
10% {
|
|
transform: scale(0.3);
|
|
}
|
|
100% {
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
#aiBubble {
|
|
background-color: white;
|
|
}
|
|
|
|
#sfBubble {
|
|
background-color: #58c4fd;
|
|
color: white;
|
|
}
|
|
|
|
#frBubble {
|
|
background-color: white;
|
|
}
|
|
|
|
.sf-firstChildBubble {
|
|
border-bottom-right-radius: 9px;
|
|
}
|
|
|
|
.sf-middleChildBubble {
|
|
border-top-right-radius: 9px;
|
|
border-bottom-right-radius: 9px;
|
|
}
|
|
|
|
.sf-lastChildBubble {
|
|
border-top-right-radius: 9px;
|
|
}
|
|
|
|
.ai-firstChildBubble, .fr-firstChildBubble {
|
|
border-bottom-left-radius: 9px;
|
|
}
|
|
|
|
.ai-middleChildBubble, .fr-middleChildBubble {
|
|
border-top-left-radius: 9px;
|
|
border-bottom-left-radius: 9px;
|
|
}
|
|
|
|
.ai-lastChildBubble, .fr-lastChildBubble {
|
|
border-top-left-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);
|
|
}
|
|
}
|
|
|
|
.context {
|
|
position: relative;
|
|
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
font-family: "SF Compact Display";
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.context-undo-anim {
|
|
animation-name: context-anim;
|
|
animation-duration: 3s;
|
|
}
|
|
|
|
@keyframes context-anim {
|
|
0%, 90% {
|
|
transform: translateX(-45px);
|
|
}
|
|
100% {
|
|
transform: translateX(0px);
|
|
}
|
|
}
|
|
|
|
.photo {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
|
|
margin-right: 5px;
|
|
|
|
width: 30px;
|
|
height: 30px;
|
|
|
|
font-size: 14px;
|
|
|
|
background-color: white;
|
|
border-radius: 15px;
|
|
}
|
|
|
|
// Apply for audio message playback.
|
|
.playing {
|
|
animation-name: circle1;
|
|
animation-duration: 2s;
|
|
animation-iteration-count: infinite;
|
|
}
|
|
|
|
@keyframes circle1 {
|
|
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);
|
|
}
|
|
}
|
|
|
|
.contentLoader {
|
|
display: flex;
|
|
}
|
|
|
|
.contentLoaderDot {
|
|
width: 5px;
|
|
height: 5px;
|
|
|
|
margin: 2px;
|
|
border-radius: 2.5px;
|
|
background-color: #357CA2;
|
|
}
|
|
|
|
.questionMark {
|
|
font-weight: 900;
|
|
color: #357CA2;
|
|
}
|
|
|
|
.undoButton {
|
|
position: absolute;
|
|
|
|
bottom: 0px;
|
|
right: 0px;
|
|
|
|
opacity: 0;
|
|
|
|
border: none;
|
|
outline: none;
|
|
|
|
margin: 0px;
|
|
padding: 0px;
|
|
|
|
background-color: Transparent;
|
|
|
|
font-family: "SF Compact Display";
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
color: #9B9B9B;
|
|
|
|
animation-name: undo-anim;
|
|
animation-duration: 3s;
|
|
}
|
|
|
|
.undoButton:hover {
|
|
cursor: pointer;
|
|
}
|
|
|
|
@keyframes undo-anim {
|
|
0%, 90% {
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
</style>
|
|
<!--
|
|
// If this is a message to a friend, we render undo button.
|
|
if (props.message.context.substring(0, 3) === 'To ') {
|
|
console.log("MSG:Applying undo anim")
|
|
undoOption.value = true;
|
|
} --> |