Compare commits
30 commits
3fc8933b1e
...
457405186a
Author | SHA1 | Date | |
---|---|---|---|
457405186a | |||
cd1f294600 | |||
51d8909a51 | |||
c06c732afb | |||
8812bf7f40 | |||
8c46f78dd3 | |||
fdabe96a68 | |||
5b0c98bb20 | |||
6e74481891 | |||
daa13dbbed | |||
a8ee8122ee | |||
2ff892b0da | |||
53c2f93791 | |||
21f441bccf | |||
84b7c01251 | |||
c2b06f40b4 | |||
5958697b6c | |||
292bd64ed4 | |||
069d3392d2 | |||
64b10b48aa | |||
4f4063fa88 | |||
c746f816d8 | |||
b28920898c | |||
b51efc01e9 | |||
0ea9c8f168 | |||
11802040bd | |||
704de034b7 | |||
fef618795d | |||
950d27b2cf | |||
7ddc2acb02 |
12 changed files with 347 additions and 30 deletions
34
app.vue
34
app.vue
|
@ -6,9 +6,43 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import ContextMenu from '~/components/ContextMenu.vue';
|
||||||
|
import { render } from 'vue';
|
||||||
|
|
||||||
const banner = useState("banner", () => false);
|
const banner = useState("banner", () => false);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.removeEventListener("contextmenu", contextMenuHandler);
|
||||||
|
document.addEventListener("contextmenu", (e) => {
|
||||||
|
contextMenuHandler(e);
|
||||||
|
});
|
||||||
|
document.addEventListener("mousedown", (e) => {
|
||||||
|
if (e.target instanceof HTMLDivElement && e.target.closest("#context-menu")) return;
|
||||||
|
console.log("click");
|
||||||
|
console.log("target:", e.target);
|
||||||
|
console.log(e.target instanceof HTMLDivElement);
|
||||||
|
removeContextMenu();
|
||||||
|
if (e.target instanceof HTMLElement && e.target.classList.contains("message-text") && e.target.contentEditable) {
|
||||||
|
e.target.contentEditable = "false";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener("keyup", (e) => {
|
||||||
|
const messageReply = document.getElementById("message-reply") as HTMLDivElement;
|
||||||
|
if (e.key == "Escape" && messageReply) {
|
||||||
|
e.preventDefault();
|
||||||
|
messageReply.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function contextMenuHandler(e: MouseEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
//console.log("Opened context menu");
|
||||||
|
//createContextMenu(e, [
|
||||||
|
// { name: "Wah", callback: () => { return } }
|
||||||
|
//]);
|
||||||
|
}
|
||||||
|
|
||||||
let currentTheme = "dark" // default theme
|
let currentTheme = "dark" // default theme
|
||||||
const savedTheme = localStorage.getItem("selectedTheme");
|
const savedTheme = localStorage.getItem("selectedTheme");
|
||||||
if (savedTheme) {
|
if (savedTheme) {
|
||||||
|
|
44
components/ContextMenu.vue
Normal file
44
components/ContextMenu.vue
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<div v-for="item of props.menuItems" class="context-menu-item" @click="runCallback(item)">
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { ContextMenuItem } from '~/types/interfaces';
|
||||||
|
|
||||||
|
const props = defineProps<{ menuItems: ContextMenuItem[], cursorX: number, cursorY: number }>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const contextMenu = document.getElementById("context-menu");
|
||||||
|
if (contextMenu) {
|
||||||
|
contextMenu.style.left = props.cursorX.toString() + "px";
|
||||||
|
contextMenu.style.top = props.cursorY.toString() + "px";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function runCallback(item: ContextMenuItem) {
|
||||||
|
removeContextMenu();
|
||||||
|
item.callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
#context-menu {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 10dvw;
|
||||||
|
border: .15rem solid cyan;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-item:hover {
|
||||||
|
background-color: rgb(50, 50, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -1,13 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="props.type == 'normal'" :id="props.last ? 'last-message' : undefined" class="message normal-message">
|
<div v-if="props.type == 'normal' || props.replyMessage" ref="messageElement" @contextmenu="createContextMenu($event, menuItems)" :id="props.last ? 'last-message' : undefined"
|
||||||
|
class="message normal-message" :class="{ 'mentioned': (props.replyMessage || props.isMentioned) && props.message.user.uuid != props.me.uuid && props.replyMessage?.user.uuid == props.me.uuid }" :data-message-id="props.messageId"
|
||||||
|
:editing.sync="props.editing" :replying-to.sync="props.replyingTo">
|
||||||
|
<MessageReply v-if="props.replyMessage" :author="props.replyMessage.user.display_name || props.replyMessage.user.username" :text="props.replyMessage?.message"
|
||||||
|
:id="props.message.uuid" :reply-id="props.replyMessage.uuid" max-width="reply" />
|
||||||
<div class="left-column">
|
<div class="left-column">
|
||||||
<img v-if="props.img" class="message-author-avatar" :src="props.img" :alt="username" />
|
<img v-if="props.img" class="message-author-avatar" :src="props.img" :alt="author?.display_name || author?.username" />
|
||||||
<Icon v-else name="lucide:user" class="message-author-avatar" />
|
<Icon v-else name="lucide:user" class="message-author-avatar" />
|
||||||
</div>
|
</div>
|
||||||
<div class="message-data">
|
<div class="message-data">
|
||||||
<div class="message-metadata">
|
<div class="message-metadata">
|
||||||
<span class="message-author-username" tabindex="0">
|
<span class="message-author-username" tabindex="0">
|
||||||
{{ username }}
|
{{ author?.display_name || author?.username }}
|
||||||
</span>
|
</span>
|
||||||
<span class="message-date" :title="date.toString()">
|
<span class="message-date" :title="date.toString()">
|
||||||
<span v-if="getDayDifference(date, currentDate) === 1">Yesterday at</span>
|
<span v-if="getDayDifference(date, currentDate) === 1">Yesterday at</span>
|
||||||
|
@ -18,7 +22,9 @@
|
||||||
<div class="message-text" v-html="sanitized" tabindex="0"></div>
|
<div class="message-text" v-html="sanitized" tabindex="0"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else ref="messageElement" :id="props.last ? 'last-message' : undefined" class="message grouped-message" :class="{ 'message-margin-bottom': props.marginBottom }">
|
<div v-else ref="messageElement" @contextmenu="createContextMenu($event, menuItems)" :id="props.last ? 'last-message' : undefined"
|
||||||
|
class="message grouped-message" :class="{ 'message-margin-bottom': props.marginBottom, 'mentioned': props.replyMessage || props.isMentioned }"
|
||||||
|
:data-message-id="props.messageId" :editing.sync="props.editing" :replying-to.sync="props.replyingTo">
|
||||||
<div class="left-column">
|
<div class="left-column">
|
||||||
<span :class="{ 'invisible': dateHidden }" class="message-date side-message-date" :title="date.toString()">
|
<span :class="{ 'invisible': dateHidden }" class="message-date side-message-date" :title="date.toString()">
|
||||||
{{ date.toLocaleTimeString(undefined, { timeStyle: "short" }) }}
|
{{ date.toLocaleTimeString(undefined, { timeStyle: "short" }) }}
|
||||||
|
@ -33,18 +39,9 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { parse } from 'marked';
|
import { parse } from 'marked';
|
||||||
|
import type { MessageProps } from '~/types/props';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<MessageProps>();
|
||||||
class?: string,
|
|
||||||
img?: string | null,
|
|
||||||
username: string,
|
|
||||||
text: string,
|
|
||||||
timestamp: number,
|
|
||||||
format: "12" | "24",
|
|
||||||
type: "normal" | "grouped",
|
|
||||||
marginBottom: boolean,
|
|
||||||
last: boolean
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const messageElement = ref<HTMLDivElement>();
|
const messageElement = ref<HTMLDivElement>();
|
||||||
|
|
||||||
|
@ -53,8 +50,9 @@ const dateHidden = ref<boolean>(true);
|
||||||
const date = new Date(props.timestamp);
|
const date = new Date(props.timestamp);
|
||||||
const currentDate: Date = new Date()
|
const currentDate: Date = new Date()
|
||||||
|
|
||||||
console.log("message:", props.text);
|
console.log("[MSG] message to render:", props.message);
|
||||||
console.log("author:", props.username);
|
console.log("author:", props.author);
|
||||||
|
console.log("[MSG] reply message:", props.replyMessage);
|
||||||
|
|
||||||
const sanitized = ref<string>();
|
const sanitized = ref<string>();
|
||||||
|
|
||||||
|
@ -72,20 +70,31 @@ onMounted(async () => {
|
||||||
});
|
});
|
||||||
console.log("adding listeners")
|
console.log("adding listeners")
|
||||||
await nextTick();
|
await nextTick();
|
||||||
messageElement.value?.addEventListener("mouseenter", (e: Event) => {
|
if (messageElement.value?.classList.contains("grouped-message")) {
|
||||||
dateHidden.value = false;
|
messageElement.value?.addEventListener("mouseenter", (e: Event) => {
|
||||||
});
|
dateHidden.value = false;
|
||||||
|
});
|
||||||
messageElement.value?.addEventListener("mouseleave", (e: Event) => {
|
|
||||||
dateHidden.value = true;
|
messageElement.value?.addEventListener("mouseleave", (e: Event) => {
|
||||||
});
|
dateHidden.value = true;
|
||||||
console.log("added listeners");
|
});
|
||||||
|
console.log("added listeners");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//function toggleTooltip(e: Event) {
|
//function toggleTooltip(e: Event) {
|
||||||
// showHover.value = !showHover.value;
|
// showHover.value = !showHover.value;
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ name: "Reply", callback: () => { if (messageElement.value) replyToMessage(messageElement.value, props) } }
|
||||||
|
]
|
||||||
|
|
||||||
|
console.log("me:", props.me);
|
||||||
|
if (props.author?.uuid == props.me.uuid) {
|
||||||
|
menuItems.push({ name: "Edit", callback: () => { if (messageElement.value) editMessage(messageElement.value, props) } });
|
||||||
|
}
|
||||||
|
|
||||||
function getDayDifference(date1: Date, date2: Date) {
|
function getDayDifference(date1: Date, date2: Date) {
|
||||||
const midnight1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate());
|
const midnight1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate());
|
||||||
const midnight2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate());
|
const midnight2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate());
|
||||||
|
@ -111,6 +120,11 @@ function getDayDifference(date1: Date, date2: Date) {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-reply-preview {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.message:hover {
|
.message:hover {
|
||||||
background-color: var(--chat-highlighted-background-color);
|
background-color: var(--chat-highlighted-background-color);
|
||||||
}
|
}
|
||||||
|
@ -140,6 +154,8 @@ function getDayDifference(date1: Date, date2: Date) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
grid-row: 2;
|
||||||
|
grid-column: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-author {
|
.message-author {
|
||||||
|
@ -158,6 +174,8 @@ function getDayDifference(date1: Date, date2: Date) {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
grid-row: 2;
|
||||||
|
grid-column: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.author-username {
|
.author-username {
|
||||||
|
@ -184,6 +202,15 @@ function getDayDifference(date1: Date, date2: Date) {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.mentioned {
|
||||||
|
background-color: rgba(0, 255, 166, 0.123);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mentioned:hover {
|
||||||
|
background-color: rgba(90, 255, 200, 0.233);
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style module>
|
<style module>
|
||||||
|
|
|
@ -5,7 +5,9 @@
|
||||||
:text="message.message" :timestamp="messageTimestamps[message.uuid]" :img="message.user.avatar"
|
:text="message.message" :timestamp="messageTimestamps[message.uuid]" :img="message.user.avatar"
|
||||||
format="12" :type="messagesType[message.uuid]"
|
format="12" :type="messagesType[message.uuid]"
|
||||||
:margin-bottom="(messages[i + 1] && messagesType[messages[i + 1].uuid] == 'normal') ?? false"
|
:margin-bottom="(messages[i + 1] && messagesType[messages[i + 1].uuid] == 'normal') ?? false"
|
||||||
:last="i == messages.length - 1" />
|
:last="i == messages.length - 1" :message-id="message.uuid" :author="message.user" :me="me"
|
||||||
|
:message="message" :is-reply="message.reply_to"
|
||||||
|
:reply-message="message.reply_to ? getReplyMessage(message.reply_to) : undefined" />
|
||||||
</div>
|
</div>
|
||||||
<div id="message-box" class="rounded-corners">
|
<div id="message-box" class="rounded-corners">
|
||||||
<form id="message-form" @submit="sendMessage">
|
<form id="message-form" @submit="sendMessage">
|
||||||
|
@ -37,11 +39,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { MessageResponse, ScrollPosition } from '~/types/interfaces';
|
import type { MessageResponse, ScrollPosition, UserResponse } from '~/types/interfaces';
|
||||||
import scrollToBottom from '~/utils/scrollToBottom';
|
import scrollToBottom from '~/utils/scrollToBottom';
|
||||||
|
|
||||||
const props = defineProps<{ channelUrl: string, amount?: number, offset?: number }>();
|
const props = defineProps<{ channelUrl: string, amount?: number, offset?: number }>();
|
||||||
|
|
||||||
|
const me = await fetchWithApi("/me") as UserResponse;
|
||||||
|
|
||||||
const messageTimestamps = ref<Record<string, number>>({});
|
const messageTimestamps = ref<Record<string, number>>({});
|
||||||
const messagesType = ref<Record<string, "normal" | "grouped">>({});
|
const messagesType = ref<Record<string, "normal" | "grouped">>({});
|
||||||
const messageGroupingMaxDifference = useRuntimeConfig().public.messageGroupingMaxDifference
|
const messageGroupingMaxDifference = useRuntimeConfig().public.messageGroupingMaxDifference
|
||||||
|
@ -114,6 +118,7 @@ if (messagesRes) {
|
||||||
messagesRes.reverse();
|
messagesRes.reverse();
|
||||||
console.log("messages res:", messagesRes.map(msg => msg.message));
|
console.log("messages res:", messagesRes.map(msg => msg.message));
|
||||||
for (const message of messagesRes) {
|
for (const message of messagesRes) {
|
||||||
|
console.log("[MSG] processing message:", message);
|
||||||
groupMessage(message);
|
groupMessage(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,6 +190,7 @@ if (accessToken && apiBase) {
|
||||||
console.log("event data:", event.data);
|
console.log("event data:", event.data);
|
||||||
console.log("message uuid:", event.data.uuid);
|
console.log("message uuid:", event.data.uuid);
|
||||||
const parsedData = JSON.parse(event.data);
|
const parsedData = JSON.parse(event.data);
|
||||||
|
console.log("[MSG] parsed message:", parsedData);
|
||||||
|
|
||||||
console.log("parsed message type:", messagesType.value[parsedData.uuid]);
|
console.log("parsed message type:", messagesType.value[parsedData.uuid]);
|
||||||
console.log("parsed message timestamp:", messageTimestamps.value[parsedData.uuid]);
|
console.log("parsed message timestamp:", messageTimestamps.value[parsedData.uuid]);
|
||||||
|
@ -203,11 +209,18 @@ if (accessToken && apiBase) {
|
||||||
function sendMessage(e: Event) {
|
function sendMessage(e: Event) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (messageInput.value && messageInput.value.trim() !== "") {
|
if (messageInput.value && messageInput.value.trim() !== "") {
|
||||||
const message = {
|
const message: Record<string, string> = {
|
||||||
message: messageInput.value.trim().replace(/\n/g, "<br>") // trim, and replace \n with <br>
|
message: messageInput.value.trim().replace(/\n/g, "<br>") // trim, and replace \n with <br>
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("message:", message);
|
const messageReply = document.getElementById("message-reply") as HTMLDivElement;
|
||||||
|
console.log("[MSG] message reply:", messageReply);
|
||||||
|
if (messageReply && messageReply.dataset.messageId) {
|
||||||
|
console.log("[MSG] message is a reply");
|
||||||
|
message.reply_to = messageReply.dataset.messageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[MSG] sent message:", message);
|
||||||
ws.send(JSON.stringify(message));
|
ws.send(JSON.stringify(message));
|
||||||
|
|
||||||
// reset input field
|
// reset input field
|
||||||
|
@ -220,10 +233,22 @@ function sendMessage(e: Event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getReplyMessage(id: string) {
|
||||||
|
console.log("[REPLYMSG] id:", id);
|
||||||
|
const messagesValues = Object.values(messages.value);
|
||||||
|
console.log("[REPLYMSG] messages values:", messagesValues);
|
||||||
|
for (const message of messagesValues) {
|
||||||
|
console.log("[REPLYMSG] message:", message);
|
||||||
|
console.log("[REPLYMSG] IDs match?", message.uuid == id);
|
||||||
|
if (message.uuid == id) return message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (import.meta.server) return;
|
if (import.meta.server) return;
|
||||||
|
console.log("[MSG] messages keys:", Object.values(messages.value));
|
||||||
if (messagesElement.value) {
|
if (messagesElement.value) {
|
||||||
scrollToBottom(messagesElement.value);
|
scrollToBottom(messagesElement.value);
|
||||||
let fetched = false;
|
let fetched = false;
|
||||||
|
@ -301,6 +326,7 @@ router.beforeEach((to, from, next) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
#message-box {
|
#message-box {
|
||||||
|
margin-top: auto; /* force it to the bottom of the screen */
|
||||||
margin-bottom: 2dvh;
|
margin-bottom: 2dvh;
|
||||||
margin-left: 1dvw;
|
margin-left: 1dvw;
|
||||||
margin-right: 1dvw;
|
margin-right: 1dvw;
|
||||||
|
|
92
components/MessageReply.vue
Normal file
92
components/MessageReply.vue
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<template>
|
||||||
|
<div :id="props.maxWidth == 'full' ? 'message-reply' : undefined" :class="{ 'message-reply-preview' : props.maxWidth == 'reply' }"
|
||||||
|
:data-message-id="props.id" @click="scrollToReply">
|
||||||
|
<p id="reply-text">Replying to <span id="reply-author-field">{{ props.author }}:</span> <span v-html="sanitized"></span></p>
|
||||||
|
<!-- <span id="message-reply-cancel"><Icon name="lucide:x" /></span> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
|
import { parse } from "marked";
|
||||||
|
|
||||||
|
const props = defineProps<{ author: string, text: string, id: string, replyId: string, maxWidth: "full" | "reply" }>();
|
||||||
|
|
||||||
|
const existingReply = document.getElementById("message-reply");
|
||||||
|
|
||||||
|
if (existingReply) {
|
||||||
|
existingReply.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("text:", props.text);
|
||||||
|
|
||||||
|
const sanitized = ref<string>();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
|
||||||
|
const parsed = await parse(props.text.trim().replaceAll("<br>", " "), { gfm: true });
|
||||||
|
|
||||||
|
sanitized.value = DOMPurify.sanitize(parsed, {
|
||||||
|
ALLOWED_TAGS: [],
|
||||||
|
ALLOW_DATA_ATTR: false,
|
||||||
|
ALLOW_SELF_CLOSE_IN_ATTR: false,
|
||||||
|
ALLOWED_ATTR: [],
|
||||||
|
KEEP_CONTENT: true
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("sanitized:", sanitized.value);
|
||||||
|
|
||||||
|
const messageBoxInput = document.getElementById("message-textbox-input") as HTMLDivElement;
|
||||||
|
if (messageBoxInput) {
|
||||||
|
messageBoxInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function scrollToReply(e: MouseEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log("clicked on reply box");
|
||||||
|
const reply = document.querySelector(`.message[data-message-id="${props.replyId}"]`);
|
||||||
|
if (reply) {
|
||||||
|
console.log("reply:", reply);
|
||||||
|
console.log("scrolling into view");
|
||||||
|
reply.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
#message-reply, .message-reply-preview {
|
||||||
|
display: flex;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--padding-color);
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#message-reply {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-reply-preview {
|
||||||
|
width: 30%;
|
||||||
|
margin-left: .5dvw;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reply-text {
|
||||||
|
color: rgb(150, 150, 150);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#reply-author-field {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
7
types/hooks.ts
Normal file
7
types/hooks.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import type { RuntimeNuxtHooks } from 'nuxt/schema';
|
||||||
|
|
||||||
|
declare module "nuxt/schema" {
|
||||||
|
interface RuntimeNuxtHooks {
|
||||||
|
"app:message:right-clicked": (payload: { messageId: string }) => void
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,7 +44,8 @@ export interface MessageResponse {
|
||||||
channel_uuid: string,
|
channel_uuid: string,
|
||||||
user_uuid: string,
|
user_uuid: string,
|
||||||
message: string,
|
message: string,
|
||||||
user: UserResponse
|
reply_to: string | null,
|
||||||
|
user: UserResponse,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InviteResponse {
|
export interface InviteResponse {
|
||||||
|
@ -84,3 +85,8 @@ export interface ScrollPosition {
|
||||||
offsetTop: number,
|
offsetTop: number,
|
||||||
offsetLeft: number
|
offsetLeft: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContextMenuItem {
|
||||||
|
name: string,
|
||||||
|
callback: (...args: any[]) => any;
|
||||||
|
}
|
||||||
|
|
20
types/props.ts
Normal file
20
types/props.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import type { MessageResponse, UserResponse } from "./interfaces";
|
||||||
|
|
||||||
|
export interface MessageProps {
|
||||||
|
class?: string,
|
||||||
|
img?: string | null,
|
||||||
|
author?: UserResponse
|
||||||
|
text: string,
|
||||||
|
timestamp: number,
|
||||||
|
format: "12" | "24",
|
||||||
|
type: "normal" | "grouped",
|
||||||
|
marginBottom: boolean,
|
||||||
|
last: boolean,
|
||||||
|
messageId: string,
|
||||||
|
replyingTo?: boolean,
|
||||||
|
editing?: boolean,
|
||||||
|
me: UserResponse
|
||||||
|
message: MessageResponse,
|
||||||
|
replyMessage?: MessageResponse
|
||||||
|
isMentioned?: boolean,
|
||||||
|
}
|
17
utils/createContextMenu.ts
Normal file
17
utils/createContextMenu.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { render } from "vue";
|
||||||
|
import ContextMenu from "~/components/ContextMenu.vue";
|
||||||
|
import type { ContextMenuItem } from "~/types/interfaces";
|
||||||
|
|
||||||
|
export default (e: MouseEvent, menuItems: ContextMenuItem[]) => {
|
||||||
|
console.log("Rendering new context menu");
|
||||||
|
const menuContainer = document.createElement("div");
|
||||||
|
menuContainer.id = "context-menu";
|
||||||
|
document.body.appendChild(menuContainer);
|
||||||
|
const contextMenu = h(ContextMenu, {
|
||||||
|
menuItems,
|
||||||
|
cursorX: e.clientX,
|
||||||
|
cursorY: e.clientY
|
||||||
|
});
|
||||||
|
render(contextMenu, menuContainer);
|
||||||
|
console.log("Rendered");
|
||||||
|
}
|
24
utils/editMessage.ts
Normal file
24
utils/editMessage.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import type { MessageProps } from "~/types/props";
|
||||||
|
|
||||||
|
export default async (element: HTMLDivElement, props: MessageProps) => {
|
||||||
|
console.log("message:", element);
|
||||||
|
const me = await fetchWithApi("/me") as any;
|
||||||
|
if (props.author?.uuid == me.uuid) {
|
||||||
|
const text = element.getElementsByClassName("message-text")[0] as HTMLDivElement;
|
||||||
|
text.contentEditable = "true";
|
||||||
|
text.focus();
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(text);
|
||||||
|
range.collapse(false);
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection?.removeAllRanges();
|
||||||
|
selection?.addRange(range);
|
||||||
|
element.addEventListener("keyup", (e) => {
|
||||||
|
console.log("key released:", e.key);
|
||||||
|
if (e.key == "Escape") {
|
||||||
|
text.contentEditable = "false";
|
||||||
|
}
|
||||||
|
text.blur();
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
}
|
6
utils/removeContextMenu.ts
Normal file
6
utils/removeContextMenu.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export default () => {
|
||||||
|
const contextMenu = document.getElementById("context-menu");
|
||||||
|
if (contextMenu) {
|
||||||
|
contextMenu.remove();
|
||||||
|
}
|
||||||
|
}
|
14
utils/replyToMessage.ts
Normal file
14
utils/replyToMessage.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { render } from "vue";
|
||||||
|
import MessageReply from "~/components/MessageReply.vue";
|
||||||
|
import type { MessageProps } from "~/types/props";
|
||||||
|
|
||||||
|
export default (element: HTMLDivElement, props: MessageProps) => {
|
||||||
|
console.log("element:", element);
|
||||||
|
const messageBox = document.getElementById("message-box") as HTMLDivElement;
|
||||||
|
if (messageBox) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
const messageReply = h(MessageReply, { author: props.author?.display_name || props.author!.username, text: props.text || "", id: props.message.uuid, replyId: props.replyMessage?.uuid || element.dataset.messageId!, maxWidth: "full" });
|
||||||
|
messageBox.prepend(div);
|
||||||
|
render(messageReply, div);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue