Chat View and Sidebar chat list
This commit is contained in:
1 parent
5b59d3df75
commit
73fe8a95d4
7 files changed
+350
-39
No files matched your search
@@ -0,0 +1,269 @@
|
||||
<script setup>
|
||||
import {
|
||||
ref, computed, reactive,
|
||||
onMounted, onUnmounted,
|
||||
} from "vue";
|
||||
|
||||
import * as backend from '../backend';
|
||||
import * as ui from '../ui';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Chat State
|
||||
const chat = ref({});
|
||||
const messages = ref([]);
|
||||
const currentThreadId = ref(null);
|
||||
|
||||
// UI State
|
||||
const expandedThreads = ref({}); // Object<int: bool>, to simulate a Set<int>
|
||||
const editingMessageId = ref(null);
|
||||
const noteMode = ref(false);
|
||||
|
||||
let unsubscribeChat = null;
|
||||
|
||||
|
||||
// === Chat Functionality === //
|
||||
|
||||
const inputFieldElem = ref(null);
|
||||
const chatContainerElem = ref(null);
|
||||
|
||||
async function getChatMessages() {
|
||||
let response = await backend.sendAction("get-chat-messages", {'chatId': props.id});
|
||||
let messageObjects = response['messages'];
|
||||
return messageObjects;
|
||||
}
|
||||
|
||||
function addMessage (message) {
|
||||
requestRescroll();
|
||||
messages.value.push(message);
|
||||
}
|
||||
|
||||
async function sendMessage () {
|
||||
let elem = inputFieldElem.value;
|
||||
|
||||
let content = elem.innerText;
|
||||
elem.innerText = "";
|
||||
|
||||
if (!content.trim()) return; // Don't send empty messages
|
||||
|
||||
let threadId = currentThreadId.value;
|
||||
let messageId = editingMessageId.value;
|
||||
let isNote = noteMode.value;
|
||||
|
||||
if (messageId == null) {
|
||||
backend.sendAction("send-chat-message", {
|
||||
'chatId': props.id,
|
||||
'content': content,
|
||||
'threadId': threadId,
|
||||
'kind': isNote ? 'note' : null,
|
||||
}).then((response) => {
|
||||
if (response['error']) {
|
||||
print("Error sending chat message: ${response['error']}");
|
||||
}
|
||||
});
|
||||
|
||||
const newMessage = {
|
||||
type: isNote ? 'note' : 'message',
|
||||
sender: 'user',
|
||||
content,
|
||||
threadId,
|
||||
};
|
||||
addMessage(newMessage);
|
||||
} else {
|
||||
const messageRef = messages.firstWhere((element) => element.id == messageId);
|
||||
|
||||
backend.sendAction("update-chat-message", {
|
||||
'messageId': messageId,
|
||||
'content': content,
|
||||
'kind': isNote ? 'note' : null,
|
||||
}).then((response) => {
|
||||
if (response['error']) {
|
||||
print("Error updating message: ${response['error']}");
|
||||
}
|
||||
});
|
||||
|
||||
messageRef.type = isNote ? 'note' : 'message';
|
||||
messageRef.content = content;
|
||||
}
|
||||
|
||||
if (isNote) noteMode.value = false;
|
||||
}
|
||||
|
||||
|
||||
// === UI Helper Functions === //
|
||||
|
||||
function requestRescroll () {
|
||||
const elem = chatContainerElem.value;
|
||||
|
||||
const scroll = elem.scrollTop;
|
||||
const max = elem.scrollTopMax;
|
||||
|
||||
// Rescroll in 2 frames
|
||||
window.requestAnimationFrame(() => {
|
||||
window.requestAnimationFrame(() => {
|
||||
// Chat was fully scrolled down
|
||||
if (scroll == max) {
|
||||
elem.scrollTop = elem.scrollTopMax;
|
||||
} else {
|
||||
// Do nothing, scroll remains in place
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Something different has to be done when previous messages are added
|
||||
}
|
||||
|
||||
function msgClass (msg) {
|
||||
if (msg.sender == 'user') {
|
||||
return ['user']
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function inputKeyDown (event) {
|
||||
if (event.key == "Enter" && event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
// === Initialize === //
|
||||
|
||||
onMounted(async () => {
|
||||
requestRescroll();
|
||||
|
||||
messages.value = (await getChatMessages()).map(it => {
|
||||
return {
|
||||
type: it.kind,
|
||||
sender: it.sender,
|
||||
content: it.content,
|
||||
threadId: it.threadId,
|
||||
};
|
||||
});
|
||||
|
||||
if (unsubscribeChat) unsubscribeChat();
|
||||
unsubscribeChat = backend.onChatMessages(props.id, it => {
|
||||
if (it.sender == 'user') return;
|
||||
|
||||
addMessage({
|
||||
type: it.kind,
|
||||
sender: it.sender,
|
||||
content: it.content,
|
||||
threadId: it.threadId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (unsubscribeChat) {
|
||||
unsubscribeChat();
|
||||
unsubscribeChat = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<h1>{{ chat.name || 'Chat #' + chat.id }}</h1>
|
||||
|
||||
<div class="messages-container" ref="chatContainerElem">
|
||||
<div v-for="msg in messages" class="message" :class="msgClass(msg)">
|
||||
<div v-if="msg.type == 'message'" class="sender">{{msg.sender}}</div>
|
||||
|
||||
<!--message v-if="!msg.collapse" :value="msg.content" :format="msg.format"></message-->
|
||||
<div v-if="!msg.collapse" class="content">{{msg.content}}</div>
|
||||
|
||||
<div v-if="msg.collapse">
|
||||
<a href="#" @click.prevent="msg.collapse = false">Show</a>
|
||||
</div>
|
||||
<div v-if="msg.collapse === false">
|
||||
<a href="#" @click.prevent="msg.collapse = true">Hide</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-container">
|
||||
<div class="input-field" ref="inputFieldElem" @keydown="inputKeyDown($event)" contenteditable></div>
|
||||
|
||||
<div class="input-row">
|
||||
<button id="send-button" @click="sendMessage()">
|
||||
Send
|
||||
<span style="font-size: 66%;">(Ctrl+Enter)</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
|
||||
.message .content {
|
||||
white-space: preserve
|
||||
}
|
||||
|
||||
.message .sender {
|
||||
font-weight: bold;
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
.message.user .content {
|
||||
right: 100%;
|
||||
width: auto;
|
||||
}
|
||||
.input-container {
|
||||
padding: 10px;
|
||||
background-color: var(--color-background-soft);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.input-field {
|
||||
white-space: preserve;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.input-field:empty::before {
|
||||
opacity: 50%;
|
||||
content: "Type your message...";
|
||||
}
|
||||
|
||||
.input-row {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#send-button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: #4caf50;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
|
||||
height: min-content;
|
||||
|
||||
/* align-self: end; /* for flex rows */
|
||||
float: right; /* for flow rows */
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user