feat: implement message grouping, improve styling

This commit is contained in:
SauceyRed 2025-05-29 22:19:31 +02:00
parent 6a3c8e8982
commit 53687a0ec3
Signed by: sauceyred
GPG key ID: 270B096EF6E9A462
4 changed files with 155 additions and 59 deletions

View file

@ -1,6 +1,6 @@
<template>
<div class="message">
<div>
<div v-if="props.type == 'normal'" class="message normal-message" :class="{ 'message-margin-bottom': props.marginBottom }">
<div class="left-column">
<img v-if="props.img" class="message-author-avatar" :src="props.img" :alt="username">
<Icon v-else name="lucide:user" class="message-author-avatar" />
</div>
@ -11,10 +11,6 @@
</span>
<span class="message-date" :title="date.toString()">
{{ messageDate }}
<!--
<div class="message-date-hover" v-if="showHover">
</div>
-->
</span>
</div>
<div class="message-text">
@ -22,17 +18,45 @@
</div>
</div>
</div>
<div v-else ref="messageElement" class="message compact-message">
<div class="left-column">
<div>
<span :class="{ 'invisible': dateHidden }" class="message-date" :title="date.toString()">
{{ messageDate }}
</span>
</div>
</div>
<div class="message-data">
<div class="message-text">
{{ text }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
const props = defineProps<{ class?: string, img?: string | null, username: string, text: string, timestamp: number, format: "12" | "24" }>();
const props = defineProps<{
class?: string,
img?: string | null,
username: string,
text: string,
timestamp: number,
format: "12" | "24",
type: "normal" | "compact",
marginBottom: boolean
}>();
const messageDate = ref<string>();
const showHover = ref(false);
const messageElement = ref<HTMLDivElement>();
const dateHidden = ref<boolean>(true);
const date = new Date(props.timestamp);
console.log("message:", props.text);
console.log("Message.vue: message:", props.text);
console.log("Message.vue: message type:", props.type);
let dateHour = date.getHours();
let dateMinute = date.getMinutes();
if (props.format == "12") {
@ -47,7 +71,19 @@ if (props.format == "12") {
}
} else {
messageDate.value = `${dateHour}:${dateMinute < 10 ? "0" + dateMinute : dateMinute}`
}
}
onMounted(() => {
messageElement.value?.addEventListener("mouseenter", (e: Event) => {
console.log("mouse enter");
dateHidden.value = false;
});
messageElement.value?.addEventListener("mouseleave", (e: Event) => {
console.log("mouse leave");
dateHidden.value = true;
});
});
//function toggleTooltip(e: Event) {
// showHover.value = !showHover.value;
@ -59,12 +95,15 @@ if (props.format == "12") {
.message {
text-align: left;
/* border: 1px solid lightcoral; */
margin-bottom: 1dvh;
display: grid;
grid-template-columns: auto 1fr;
grid-template-columns: 1fr 19fr;
align-items: center;
}
.message-margin-bottom {
margin-bottom: 1dvh;
}
.message-metadata {
display: flex;
gap: .5dvw;
@ -86,22 +125,25 @@ if (props.format == "12") {
}
.message-author-avatar {
margin-right: 1dvw;
width: 3em;
height: 2.3em;
width: 2.3em;
border-radius: 50%;
}
.left-column {
margin-right: .5dvw;
text-align: center;
align-content: center;
}
.author-username {
margin-right: .5dvw;
color: white;
}
.message-date {
font-size: small;
font-size: .7em;
color: rgb(150, 150, 150);
}
.message-date:hover {
cursor: default;
}

View file

@ -1,18 +1,23 @@
<template>
<div id="message-area">
<div id="messages" ref="messagesElement">
<Message v-for="message of messages" :username="message.user.display_name ?? message.user.username" :text="message.message"
:timestamp="uuidToTimestamp(message.uuid)" :img="message.user.avatar" format="12" />
<div id="message-area">
<div id="messages" ref="messagesElement">
<div v-for="(message, i) of messages">
<Message :username="message.user.display_name ?? message.user.username"
:text="message.message" :timestamp="messageTimestamps[message.uuid]" :img="message.user.avatar"
format="12" :type="messagesType[message.uuid]"
:margin-bottom="messages[i + 1] && messagesType[messages[i + 1].uuid] == 'normal'" />
</div>
</div>
<div id="message-box" class="rounded-corners">
<form id="message-form" @submit="sendMessage">
<input v-model="messageInput" id="message-box-input" class="rounded-corners" type="text"
name="message-input" autocomplete="off">
<button id="submit-button" type="submit">
<Icon name="lucide:send" />
</button>
</form>
</div>
</div>
<div id="message-box" class="rounded-corners">
<form id="message-form" @submit="sendMessage">
<input v-model="messageInput" id="message-box-input" class="rounded-corners" type="text" name="message-input" autocomplete="off">
<button id="submit-button" type="submit">
<Icon name="lucide:send" />
</button>
</form>
</div>
</div>
</template>
<script lang="ts" setup>
@ -21,12 +26,59 @@ import scrollToBottom from '~/utils/scrollToBottom';
const props = defineProps<{ channelUrl: string, amount?: number, offset?: number }>();
const messageTimestamps = ref<Record<string, number>>({});
const messagesType = ref<Record<string, "normal" | "compact">>({});
const messagesRes: MessageResponse[] | undefined = await fetchWithApi(
`${props.channelUrl}/messages`,
{ query: { "amount": props.amount ?? 100, "offset": props.offset ?? 0 } }
);
if (messagesRes) {
messagesRes.reverse();
messagesRes.reverse();
console.log("messages res:", messagesRes.map(msg => msg.message));
const firstMessageByUsers = ref<Record<string, MessageResponse | undefined>>({});
for (const message of messagesRes) {
messageTimestamps.value[message.uuid] = uuidToTimestamp(message.uuid);
console.log("message:", message.message);
const firstByUser = firstMessageByUsers.value[message.user.uuid];
if (firstByUser) {
console.log("first by user exists");
if (message.user.uuid != firstByUser.user.uuid) {
console.log("message is by new user, setting their first message")
firstMessageByUsers.value[message.user.uuid] = message;
console.log("RETURNING FALSE");
messagesType.value[message.uuid] = "normal";
continue;
}
} else {
console.log("first by user doesn't exist");
console.log(`setting first post by user ${message.user.username} to "${message.message}" with timestamp ${messageTimestamps.value[message.uuid]}`);
firstMessageByUsers.value[message.user.uuid] = message;
console.log("RETURNING FALSE");
messagesType.value[message.uuid] = "normal";
continue;
}
const messageGroupingMaxDifference = useRuntimeConfig().public.messageGroupingMaxDifference;
const prevTimestamp = messageTimestamps.value[firstByUser.uuid];
const timestamp = messageTimestamps.value[message.uuid];
console.log("first message timestamp:", prevTimestamp);
console.log("timestamp:", timestamp);
const diff = (timestamp - prevTimestamp);
console.log("min diff:", messageGroupingMaxDifference);
console.log("diff:", diff);
const lessThanMax = diff <= messageGroupingMaxDifference;
console.log("group?", lessThanMax);
if (!lessThanMax) {
console.log("diff exceeds max");
console.log(`setting first post by user ${message.user.username} to "${message.message}" with timestamp ${messageTimestamps.value[message.uuid]}`)
firstMessageByUsers.value[message.user.uuid] = message;
messagesType.value[message.uuid] = "normal";
continue;
}
console.log("RETURNING " + lessThanMax.toString().toUpperCase());
messagesType.value[message.uuid] = "compact";
}
}
const messages = ref<MessageResponse[]>([]);
@ -49,29 +101,28 @@ if (accessToken && apiBase) {
do {
console.log("Trying to connect to channel WebSocket...");
ws = new WebSocket(`${apiBase.replace("http", "ws").replace("3000", "8080")}/${props.channelUrl}/socket`,
["Authorization", accessToken]
);
if (ws) break;
await sleep(5000);
} while (!ws);
ws.addEventListener("open", (event) => {
console.log("WebSocket connected!");
});
ws.addEventListener("message", async (event) => {
console.log("event data:", event.data);
messages.value?.push(
JSON.parse(event.data)
);
await nextTick();
if (messagesElement.value) {
console.log("scrolling to bottom");
scrollToBottom(messagesElement);
}
});
["Authorization", accessToken]
);
if (ws) break;
await sleep(5000);
} while (!ws);
ws.addEventListener("open", (event) => {
console.log("WebSocket connected!");
});
ws.addEventListener("message", async (event) => {
console.log("event data:", event.data);
console.log("message uuid:", event.data.uuid);
const parsedData = JSON.parse(event.data);
messageTimestamps.value[parsedData.uuid] = uuidToTimestamp(parsedData.uuid);
messages.value.push(parsedData);
await nextTick();
if (messagesElement.value) {
console.log("scrolling to bottom");
scrollToBottom(messagesElement);
}
});
} else {
await refresh();
@ -82,7 +133,7 @@ function sendMessage(e: Event) {
const text = messageInput.value;
console.log("text:", text);
if (text) {
ws.send(text);
ws.send(text);
messageInput.value = "";
console.log("MESSAGE SENT!!!");
}
@ -97,13 +148,11 @@ onMounted(async () => {
</script>
<style scoped>
#message-area {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 8fr 1fr;
justify-content: space-between;
padding-top: 3dvh;
padding-left: 1dvw;
padding-right: 1dvw;
overflow: hidden;
@ -118,7 +167,7 @@ onMounted(async () => {
padding-bottom: 1dvh;
padding-top: 1dvh;
margin-bottom: 1dvh;
margin-top: 1dvh;
margin-top: 2dvh;
}
#message-form {
@ -152,5 +201,4 @@ onMounted(async () => {
#submit-button:hover {
color: rgb(255, 255, 255);
}
</style>