Compare commits
No commits in common. "457405186a1815dbfba6470526dc1ab8cb390fc1" and "3fc8933b1ec5723f29abdd65c215a8c90e151a2b" have entirely different histories.
457405186a
...
3fc8933b1e
12 changed files with 30 additions and 347 deletions
34
app.vue
34
app.vue
|
@ -6,43 +6,9 @@
|
||||||
</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) {
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
<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,17 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="props.type == 'normal' || props.replyMessage" ref="messageElement" @contextmenu="createContextMenu($event, menuItems)" :id="props.last ? 'last-message' : undefined"
|
<div v-if="props.type == 'normal'" :id="props.last ? 'last-message' : undefined" class="message normal-message">
|
||||||
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="author?.display_name || author?.username" />
|
<img v-if="props.img" class="message-author-avatar" :src="props.img" :alt="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">
|
||||||
{{ author?.display_name || author?.username }}
|
{{ 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>
|
||||||
|
@ -22,9 +18,7 @@
|
||||||
<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" @contextmenu="createContextMenu($event, menuItems)" :id="props.last ? 'last-message' : undefined"
|
<div v-else ref="messageElement" :id="props.last ? 'last-message' : undefined" class="message grouped-message" :class="{ 'message-margin-bottom': props.marginBottom }">
|
||||||
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" }) }}
|
||||||
|
@ -39,9 +33,18 @@
|
||||||
<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<MessageProps>();
|
const props = defineProps<{
|
||||||
|
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>();
|
||||||
|
|
||||||
|
@ -50,9 +53,8 @@ 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("[MSG] message to render:", props.message);
|
console.log("message:", props.text);
|
||||||
console.log("author:", props.author);
|
console.log("author:", props.username);
|
||||||
console.log("[MSG] reply message:", props.replyMessage);
|
|
||||||
|
|
||||||
const sanitized = ref<string>();
|
const sanitized = ref<string>();
|
||||||
|
|
||||||
|
@ -70,31 +72,20 @@ onMounted(async () => {
|
||||||
});
|
});
|
||||||
console.log("adding listeners")
|
console.log("adding listeners")
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (messageElement.value?.classList.contains("grouped-message")) {
|
messageElement.value?.addEventListener("mouseenter", (e: Event) => {
|
||||||
messageElement.value?.addEventListener("mouseenter", (e: Event) => {
|
dateHidden.value = false;
|
||||||
dateHidden.value = false;
|
});
|
||||||
});
|
|
||||||
|
|
||||||
messageElement.value?.addEventListener("mouseleave", (e: Event) => {
|
messageElement.value?.addEventListener("mouseleave", (e: Event) => {
|
||||||
dateHidden.value = true;
|
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());
|
||||||
|
@ -120,11 +111,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
@ -154,8 +140,6 @@ 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 {
|
||||||
|
@ -174,8 +158,6 @@ 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 {
|
||||||
|
@ -202,15 +184,6 @@ 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,9 +5,7 @@
|
||||||
: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" :message-id="message.uuid" :author="message.user" :me="me"
|
:last="i == messages.length - 1" />
|
||||||
: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">
|
||||||
|
@ -39,13 +37,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { MessageResponse, ScrollPosition, UserResponse } from '~/types/interfaces';
|
import type { MessageResponse, ScrollPosition } 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
|
||||||
|
@ -118,7 +114,6 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,7 +185,6 @@ 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]);
|
||||||
|
@ -209,18 +203,11 @@ 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: Record<string, string> = {
|
const message = {
|
||||||
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>
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageReply = document.getElementById("message-reply") as HTMLDivElement;
|
console.log("message:", message);
|
||||||
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
|
||||||
|
@ -233,22 +220,10 @@ 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;
|
||||||
|
@ -326,7 +301,6 @@ 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;
|
||||||
|
|
|
@ -1,92 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,7 +0,0 @@
|
||||||
import type { RuntimeNuxtHooks } from 'nuxt/schema';
|
|
||||||
|
|
||||||
declare module "nuxt/schema" {
|
|
||||||
interface RuntimeNuxtHooks {
|
|
||||||
"app:message:right-clicked": (payload: { messageId: string }) => void
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -44,8 +44,7 @@ export interface MessageResponse {
|
||||||
channel_uuid: string,
|
channel_uuid: string,
|
||||||
user_uuid: string,
|
user_uuid: string,
|
||||||
message: string,
|
message: string,
|
||||||
reply_to: string | null,
|
user: UserResponse
|
||||||
user: UserResponse,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InviteResponse {
|
export interface InviteResponse {
|
||||||
|
@ -85,8 +84,3 @@ export interface ScrollPosition {
|
||||||
offsetTop: number,
|
offsetTop: number,
|
||||||
offsetLeft: number
|
offsetLeft: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContextMenuItem {
|
|
||||||
name: string,
|
|
||||||
callback: (...args: any[]) => any;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
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,
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
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");
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
export default () => {
|
|
||||||
const contextMenu = document.getElementById("context-menu");
|
|
||||||
if (contextMenu) {
|
|
||||||
contextMenu.remove();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
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