a bunch of bug fixes and improvements
This commit is contained in:
@@ -20,7 +20,8 @@
|
|||||||
"storeToRefs": true,
|
"storeToRefs": true,
|
||||||
"useNuxtApp": true,
|
"useNuxtApp": true,
|
||||||
"NodeJS": true,
|
"NodeJS": true,
|
||||||
"useHeadSafe": true
|
"useHeadSafe": true,
|
||||||
|
"defineEmits": true
|
||||||
},
|
},
|
||||||
"parser": "vue-eslint-parser",
|
"parser": "vue-eslint-parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
import emojiJson from '~/assets/json/emoji.json';
|
import emojiJson from '~/assets/json/emoji.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
opened: Boolean,
|
|
||||||
},
|
|
||||||
emits: ['picked-emoji'],
|
emits: ['picked-emoji'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -22,7 +19,9 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
emojiStyles(emojiShortName: string, width: number) {
|
emojiStyles(emojiShortName: string | undefined, width: number) {
|
||||||
|
if (!emojiShortName) return;
|
||||||
|
|
||||||
const emojis = emojiJson;
|
const emojis = emojiJson;
|
||||||
const emoji = emojis.find((e) => e.short_names[0] === emojiShortName);
|
const emoji = emojis.find((e) => e.short_names[0] === emojiShortName);
|
||||||
if (!emoji) return;
|
if (!emoji) return;
|
||||||
@@ -38,17 +37,17 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
scrollTo(categoryName: string) {
|
scrollTo(categoryName: string) {
|
||||||
const emojiPane = document.getElementById('emojiPane');
|
const emojiPane = (this.$refs.emojiPane as HTMLDivElement);
|
||||||
const category = document.getElementById(categoryName);
|
const category = document.getElementById(categoryName);
|
||||||
if (!emojiPane || !category) return;
|
if (!emojiPane || !category) return;
|
||||||
emojiPane.scrollTop = category.offsetTop - 96;
|
emojiPane.scrollTop = category.offsetTop - 550;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="p-3">
|
||||||
<div class="py-1.5 flex flex-col">
|
<div class="py-1.5 flex flex-col">
|
||||||
<div class="flex-row gap-x-2 overflow-x-scroll">
|
<div class="flex-row gap-x-2 overflow-x-scroll">
|
||||||
<button
|
<button
|
||||||
@@ -232,13 +231,13 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="emoji-pane"
|
ref="emojiPane"
|
||||||
class="overflow-hidden overflow-y-scroll scroll-smooth EmojiPicker max-h-[450px]"
|
class="overflow-hidden overflow-y-scroll scroll-smooth EmojiPicker max-h-[calc(475px-24px-48px)]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="category in categories"
|
v-for="category in categories"
|
||||||
:key="category.name"
|
:key="category.name"
|
||||||
class="text-black flex flex-col category bg-[var(--primary-dark)]"
|
class="text-black flex flex-col category bg-[var(--secondary-bg)]"
|
||||||
>
|
>
|
||||||
<h6 class="uppercase text-[var(--primary-text)] sticky top-0 bg-inherit z-10 py-1">
|
<h6 class="uppercase text-[var(--primary-text)] sticky top-0 bg-inherit z-10 py-1">
|
||||||
{{
|
{{
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<span
|
<span
|
||||||
class="before:bg-[var(--primary-accent)] before:h-2 before:w-2 before:inline-block before:my-auto before:rounded-full before:mr-1"
|
class="before:bg-[var(--primary-accent)] before:h-2 before:w-2 before:inline-block before:my-auto before:rounded-full before:mr-1"
|
||||||
/>
|
/>
|
||||||
<span>{{ invite.server.participants.filter((e: IUser) => e.online === true).length }} Online</span>
|
<span>{{ invite.server.participants.filter((e: SafeUser) => !!e.online).length }} Online</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-full justify-end">
|
<div class="flex w-full justify-end">
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
import { PropType } from 'vue';
|
import { PropType } from 'vue';
|
||||||
import { useServerStore } from '~/stores/serverStore';
|
import { useServerStore } from '~/stores/serverStore';
|
||||||
import { useUserStore } from '~/stores/userStore';
|
import { useUserStore } from '~/stores/userStore';
|
||||||
import { IInviteCode, IUser } from '~/types';
|
import { IInviteCode, IServer, IUser, SafeUser } from '~/types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
@@ -57,16 +57,18 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
userInServer(): boolean {
|
userInServer(): boolean {
|
||||||
return !!this.invite.server.participants.find((e: IUser) => e.id === this.user?.id);
|
return !!this.invite.server.participants.find((e: SafeUser) => e.id === this.user?.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async joinServer(invite: IInviteCode) {
|
async joinServer(invite: IInviteCode) {
|
||||||
if (this.userInServer) return;
|
if (this.userInServer) return;
|
||||||
|
|
||||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||||
const { server } = await $fetch('/api/guilds/joinGuild', { method: 'POST', body: { inviteId: invite.id }, headers });
|
const { server } = await $fetch('/api/guilds/joinGuild', { method: 'POST', body: { inviteId: invite.id }, headers }) as { server: IServer };
|
||||||
if (!server) return;
|
if (!server) return;
|
||||||
this.servers?.push(server);
|
|
||||||
|
useServerStore().addServer(server);
|
||||||
this.invite.server.participants.push(this.user);
|
this.invite.server.participants.push(this.user);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,13 +120,38 @@
|
|||||||
<div class="message-sender-text">
|
<div class="message-sender-text">
|
||||||
<p
|
<p
|
||||||
v-if="showUsername"
|
v-if="showUsername"
|
||||||
class="mb-1 font-semibold w-fit"
|
class="flex flex-row"
|
||||||
>
|
>
|
||||||
{{ message.creator.username }}
|
<span
|
||||||
|
ref="username"
|
||||||
|
class="mb-1 font-semibold w-fit cursor-pointer hover:underline"
|
||||||
|
@click="openUserProfile()"
|
||||||
|
>
|
||||||
|
{{ message.creator.username }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="userIsOwner"
|
||||||
|
class="ml-0.5"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
><path
|
||||||
|
class="text-yellow-300"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="m12 6l4 6l5-4l-2 10H5L3 8l5 4z"
|
||||||
|
/></svg>
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p
|
<div
|
||||||
class="break-words max-w-full"
|
class="break-words max-w-full whitespace-pre-wrap"
|
||||||
v-html="message.body"
|
v-html="parseMessageBody(message.body, participants)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -206,6 +231,10 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
reactions(): IReaction[] {
|
reactions(): IReaction[] {
|
||||||
return this.message.reactions?.filter((e) => e.users.length > 0) || [];
|
return this.message.reactions?.filter((e) => e.users.length > 0) || [];
|
||||||
|
},
|
||||||
|
userIsOwner(): boolean {
|
||||||
|
if (useActiveStore().type !== 'server') return false;
|
||||||
|
return !!useActiveStore().server.server.participants.find((e) => e.id === this.message.creator.id)?.roles?.find((e) => e.owner) || false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -214,7 +243,8 @@ export default {
|
|||||||
if (useEmojiPickerStore().emojiPickerData.openedBy?.messageId !== this.message.id) return;
|
if (useEmojiPickerStore().emojiPickerData.openedBy?.messageId !== this.message.id) return;
|
||||||
const replacementEmoji = emojiJson.find((e) => e.short_names[0] === emoji);
|
const replacementEmoji = emojiJson.find((e) => e.short_names[0] === emoji);
|
||||||
if (!replacementEmoji?.emoji) return;
|
if (!replacementEmoji?.emoji) return;
|
||||||
if (this.message.reactions?.find((e) => e.emoji === replacementEmoji.emoji)) return;
|
if (this.message.reactions?.find((e) => e.emoji === replacementEmoji.emoji) &&
|
||||||
|
this.message.reactions?.find((e) => e.emoji === replacementEmoji.emoji)?.users.find((e) => e.id === this.user?.id)) return;
|
||||||
this.toggleReaction(replacementEmoji.emoji);
|
this.toggleReaction(replacementEmoji.emoji);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -222,11 +252,15 @@ export default {
|
|||||||
async toggleReaction(emoji: string) {
|
async toggleReaction(emoji: string) {
|
||||||
let { message } = await $fetch(`/api/channels/${this.channelId}/messages/${this.message.id}/reactions/${emoji}`, { method: 'POST' }) as { message: IMessage };
|
let { message } = await $fetch(`/api/channels/${this.channelId}/messages/${this.message.id}/reactions/${emoji}`, { method: 'POST' }) as { message: IMessage };
|
||||||
|
|
||||||
message.body = parseMessageBody(message.body, this.participants);
|
|
||||||
|
|
||||||
useActiveStore().updateMessage(message);
|
useActiveStore().updateMessage(message);
|
||||||
},
|
},
|
||||||
openEmojiPicker() {
|
openEmojiPicker() {
|
||||||
|
console.log(useEmojiPickerStore().emojiPickerData);
|
||||||
|
if (useEmojiPickerStore().emojiPickerData.opened && useEmojiPickerStore().emojiPickerData.type === 'emojiPicker' && useEmojiPickerStore().emojiPickerData.openedBy?.messageId === this.message.id) {
|
||||||
|
useEmojiPickerStore().closeEmojiPicker();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const actionButtons = document.getElementById(`actions-${this.message.id}`);
|
const actionButtons = document.getElementById(`actions-${this.message.id}`);
|
||||||
if (!actionButtons) return;
|
if (!actionButtons) return;
|
||||||
|
|
||||||
@@ -235,6 +269,7 @@ export default {
|
|||||||
if (top + 522 > window.innerHeight) top = window.innerHeight - 522;
|
if (top + 522 > window.innerHeight) top = window.innerHeight - 522;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
|
type: 'emojiPicker',
|
||||||
top,
|
top,
|
||||||
right: actionButtons.clientWidth + 40,
|
right: actionButtons.clientWidth + 40,
|
||||||
openedBy: {
|
openedBy: {
|
||||||
@@ -243,7 +278,35 @@ export default {
|
|||||||
}
|
}
|
||||||
} as IPopupData;
|
} as IPopupData;
|
||||||
|
|
||||||
useEmojiPickerStore().toggleEmojiPicker(payload);
|
useEmojiPickerStore().openEmojiPicker(payload);
|
||||||
|
},
|
||||||
|
openUserProfile() {
|
||||||
|
const messagePane = document.getElementById('messagePane') as HTMLDivElement;
|
||||||
|
const usernameElement = this.$refs.username as HTMLParagraphElement;
|
||||||
|
if (!usernameElement || !messagePane) return;
|
||||||
|
|
||||||
|
const elementRect = usernameElement.getBoundingClientRect();
|
||||||
|
let top = elementRect.top + window.pageYOffset;
|
||||||
|
const left = window.innerWidth - messagePane.clientWidth + 28 + usernameElement.clientWidth;
|
||||||
|
if (top + 522 > window.innerHeight) top = window.innerHeight - 522;
|
||||||
|
|
||||||
|
if (useEmojiPickerStore().emojiPickerData.opened &&
|
||||||
|
useEmojiPickerStore().emojiPickerData.type === 'userInfo' &&
|
||||||
|
useEmojiPickerStore().emojiPickerData.userId === this.message.creator.id &&
|
||||||
|
useEmojiPickerStore().emojiPickerData.top === top &&
|
||||||
|
useEmojiPickerStore().emojiPickerData.left === left) {
|
||||||
|
useEmojiPickerStore().closeEmojiPicker();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
type: 'userInfo',
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
userId: this.message.creator.id
|
||||||
|
} as IPopupData;
|
||||||
|
|
||||||
|
useEmojiPickerStore().openEmojiPicker(payload);
|
||||||
},
|
},
|
||||||
emojiStyles(emoji: string, width: number) {
|
emojiStyles(emoji: string, width: number) {
|
||||||
const emojis = emojiJson;
|
const emojis = emojiJson;
|
||||||
@@ -298,6 +361,7 @@ pre.codeblock code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
code.inline-code {
|
code.inline-code {
|
||||||
|
color: var(--primary-accent);
|
||||||
background-color: var(--secondary-bg);
|
background-color: var(--secondary-bg);
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
id="messagePane"
|
||||||
class="h-full relative bg-[var(--primary-bg)] flex flex-col"
|
class="h-full relative bg-[var(--primary-bg)] flex flex-col"
|
||||||
@mouseenter="mouseEnter"
|
@mouseenter="mouseEnter"
|
||||||
@mouseleave="mouseLeave"
|
@mouseleave="mouseLeave"
|
||||||
@@ -77,8 +78,8 @@
|
|||||||
:key="message.id"
|
:key="message.id"
|
||||||
:message="message"
|
:message="message"
|
||||||
:shift-pressed="shiftPressed"
|
:shift-pressed="shiftPressed"
|
||||||
:show-username="i === 0 || channel.messages[i - 1]?.creator.id !== message.creator.id"
|
:show-username="calculateMessageDesign(message, i).showUsername"
|
||||||
:classes="calculateMessageClasses(message, i)"
|
:classes="calculateMessageDesign(message, i).classes"
|
||||||
:channel-id="channel.id"
|
:channel-id="channel.id"
|
||||||
:participants="participants"
|
:participants="participants"
|
||||||
/>
|
/>
|
||||||
@@ -214,21 +215,21 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async sendMessage() {
|
async sendMessage() {
|
||||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||||
if (!this.messageContent) return;
|
if (!this.messageContent || !this.messageContent.trim()) return;
|
||||||
|
|
||||||
let message: IMessage = await $fetch(`/api/channels/${this.channel.id}/sendMessage`, { method: 'post', body: { body: this.messageContent }, headers });
|
let message: IMessage = await $fetch(`/api/channels/${this.channel.id}/sendMessage`, { method: 'post', body: { body: this.messageContent }, headers });
|
||||||
|
|
||||||
if (!message) return;
|
if (!message) return;
|
||||||
if (this.channel.messages.includes(message)) return;
|
if (this.channel.messages.includes(message)) return;
|
||||||
|
|
||||||
message.body = parseMessageBody(message.body, this.participants);
|
useActiveStore().addMessage(message);
|
||||||
|
|
||||||
this.channel.messages.push(message);
|
|
||||||
this.messageContent = '';
|
this.messageContent = '';
|
||||||
const conversationDiv = this.$refs.conversationPane as HTMLDivElement;
|
const conversationDiv = this.$refs.conversationPane as HTMLDivElement;
|
||||||
if (!conversationDiv) throw new Error('wtf');
|
if (!conversationDiv) throw new Error('wtf');
|
||||||
|
|
||||||
this.scrollToBottom();
|
setTimeout(() => {
|
||||||
|
this.scrollToBottom();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
const conversationDiv = this.$refs.conversationPane as HTMLDivElement;
|
const conversationDiv = this.$refs.conversationPane as HTMLDivElement;
|
||||||
@@ -273,20 +274,20 @@ export default {
|
|||||||
this.shiftPressed = false;
|
this.shiftPressed = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
calculateMessageClasses(message: IMessage, i: number) {
|
calculateMessageDesign(message: IMessage, i: number) {
|
||||||
if (i === 0 || this.channel.messages[i - 1]?.creator.id !== message.creator.id) {
|
if (i === 0 || (this.channel.messages[i - 1]?.creator.id !== message.creator.id || new Date(this.channel.messages[i-1]?.createdAt).getTime()+((30*60)*1000)<new Date(this.channel.messages[i]?.createdAt).getTime())) {
|
||||||
if (i !== this.channel.messages.length - 1 || this.channel.messages[i + 1]?.creator.id === message.creator.id) {
|
if (i !== this.channel.messages.length - 1) {
|
||||||
return 'mb-0 pb-0.5';
|
return { classes: 'mb-0 pb-0.5', showUsername: true };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (i !== this.channel.messages.length - 1 || this.channel.messages[i + 1]?.creator.id === message.creator.id) {
|
if (i !== this.channel.messages.length - 1 || this.channel.messages[i + 1]?.creator.id === message.creator.id) {
|
||||||
return 'mt-0 mb-0 !py-0.5';
|
return { classes: 'mt-0 mb-0 !py-0.5', showUsername: false };
|
||||||
} else {
|
} else {
|
||||||
return 'mt-0 pt-0.5 pb-1';
|
return { classes: 'mt-0 pt-0.5 pb-1', showUsername: false };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
return {classes: '', showUsername: true };
|
||||||
},
|
},
|
||||||
checkForMentions() {
|
checkForMentions() {
|
||||||
const input = this.$refs.messageBox as HTMLTextAreaElement;
|
const input = this.$refs.messageBox as HTMLTextAreaElement;
|
||||||
@@ -342,7 +343,7 @@ export default {
|
|||||||
completeMention(user: SafeUser) {
|
completeMention(user: SafeUser) {
|
||||||
this.messageContent = this.messageContent.replace('@' + this.search.content, `<@${user.id}>`);
|
this.messageContent = this.messageContent.replace('@' + this.search.content, `<@${user.id}>`);
|
||||||
this.search.show = false;
|
this.search.show = false;
|
||||||
this.$refs.messageBox.focus();
|
(this.$refs.messageBox as HTMLInputElement).focus();
|
||||||
},
|
},
|
||||||
async listenToWebsocket(conversationDiv: HTMLElement) {
|
async listenToWebsocket(conversationDiv: HTMLElement) {
|
||||||
let { $io } = useNuxtApp();
|
let { $io } = useNuxtApp();
|
||||||
@@ -357,17 +358,12 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
message.body = parseMessageBody(message.body, this.participants);
|
|
||||||
|
|
||||||
if (this.channel.messages.find((e) => e.id === message.id)) {
|
if (this.channel.messages.find((e) => e.id === message.id)) {
|
||||||
// message is already in the server, replace it with the updated message
|
// message is already in the server, replace it with the updated message
|
||||||
useActiveStore().updateMessage(message);
|
useActiveStore().updateMessage(message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.creator.id === this.user?.id) return;
|
|
||||||
|
|
||||||
|
|
||||||
if (!document.hasFocus()) {
|
if (!document.hasFocus()) {
|
||||||
new Notification(`Message from @${message.creator.username}`, { body: message.body, tag: this.channel.id.toString() });
|
new Notification(`Message from @${message.creator.username}`, { body: message.body, tag: this.channel.id.toString() });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav class="bg-[var(--primary-bg)] h-screen p-4 grid grid-cols-1 grid-rows-[56px_1fr_56px] shadow shadow-black/80">
|
<nav class="bg-[var(--primary-bg)] h-screen p-4 grid grid-cols-1 grid-rows-[56px_1fr_56px] shadow shadow-black/80">
|
||||||
<div>
|
<div>
|
||||||
<nuxt-link to="/channel/@me">
|
<nuxt-link
|
||||||
|
to="/channel/@me"
|
||||||
|
draggable="false"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="bg-[var(--tertiary-bg)] p-3 rounded-full transition-all hover:rounded-[1.375rem] ease-in-out hover:bg-[var(--tertiary-lightened-bg)] duration-300"
|
class="bg-[var(--tertiary-bg)] p-3 transition-all ease-in-out hover:bg-[var(--tertiary-lightened-bg)] duration-300"
|
||||||
|
:class="(activeConversation.type === 'dm') ? 'rounded-[1.375rem]' : 'rounded-full hover:rounded-[1.375rem]'"
|
||||||
|
aria-label="Home"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<svg
|
<svg
|
||||||
@@ -51,9 +56,12 @@
|
|||||||
v-for="server in servers"
|
v-for="server in servers"
|
||||||
:key="server.id"
|
:key="server.id"
|
||||||
:to="'/channel/' + server.channels[0]?.id"
|
:to="'/channel/' + server.channels[0]?.id"
|
||||||
|
draggable="false"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="bg-[var(--tertiary-bg)] p-3 rounded-full transition-all hover:rounded-[1.375rem] ease-in-out hover:bg-[var(--tertiary-lightened-bg)] duration-300 h-[56px] w-[56px]"
|
class="bg-[var(--tertiary-bg)] p-3 transition-all ease-in-out hover:bg-[var(--tertiary-lightened-bg)] duration-300 h-[56px] w-[56px]"
|
||||||
|
:class="(activeConversation.type === 'server' && activeConversation.server.server.id === server.id) ? 'rounded-[1.375rem]' : 'rounded-full hover:rounded-[1.375rem]'"
|
||||||
|
:aria-label="server.name"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -73,6 +81,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-3 rounded-full transition-colors ease-in-out hover:bg-[var(--tertiary-lightened-bg)] duration-300 cursor-pointer"
|
class="p-3 rounded-full transition-colors ease-in-out hover:bg-[var(--tertiary-lightened-bg)] duration-300 cursor-pointer"
|
||||||
|
@click="createServerModalOpen = true"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="32"
|
width="32"
|
||||||
@@ -89,17 +98,71 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<Modal
|
||||||
|
:opened="createServerModalOpen"
|
||||||
|
@close="createServerModalOpen = false"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-[var(--secondary-bg)] rounded-xl shadow-2xl flex flex-row overflow-hidden z-20 absolute border border-[var(--tertiary-bg)] -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/eberhard-grossgasteiger-eBXIZe1DU7Y-unsplash.jpg"
|
||||||
|
class="h-96 w-64 object-cover"
|
||||||
|
/>
|
||||||
|
<div class="p-4 flex flex-col text-center">
|
||||||
|
<h1 class="font-semibold text-2xl">
|
||||||
|
Create Server
|
||||||
|
</h1>
|
||||||
|
<form
|
||||||
|
class="flex flex-col gap-y-3 my-2"
|
||||||
|
@submit.prevent="createServer"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="serverName"
|
||||||
|
class="px-4 py-2 rounded-md w-full bg-[var(--primary-input)] shadow-2xl placeholder:text-[var(--primary-placeholder)] focus:outline-none"
|
||||||
|
name="name"
|
||||||
|
placeholder="Server Name"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
value="Submit"
|
||||||
|
class="w-full bg-[#5865F2] py-2 px-4 rounded-md cursor-pointer"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { useActiveStore } from '~/stores/activeStore';
|
||||||
import { useServerStore } from '~/stores/serverStore';
|
import { useServerStore } from '~/stores/serverStore';
|
||||||
|
import { IServer } from '~/types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
servers: storeToRefs(useServerStore()).servers
|
createServerModalOpen: false,
|
||||||
|
serverName: '',
|
||||||
|
servers: storeToRefs(useServerStore()).servers,
|
||||||
|
activeConversation: {
|
||||||
|
type: storeToRefs(useActiveStore()).type,
|
||||||
|
server: storeToRefs(useActiveStore()).server
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async createServer() {
|
||||||
|
const serverStore = useServerStore();
|
||||||
|
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||||
|
const server: IServer = await $fetch('/api/channels/create', { method: 'post', body: { serverName: this.serverName }, headers });
|
||||||
|
this.createServerModalOpen = false;
|
||||||
|
this.serverName = '';
|
||||||
|
serverStore.addServer(server);
|
||||||
|
|
||||||
|
navigateTo(`/channel/${server.channels[0].id}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="opened"
|
v-if="opened"
|
||||||
class="z-10 bg-[var(--primary-dark)] w-fit rounded-md shadow-md p-3"
|
class="z-10 bg-[var(--secondary-bg)] w-fit rounded-lg shadow-md border border-[var(--tertiary-bg)] overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class="max-w-[350px] max-h-[450px] overflow-hidden">
|
<div class="max-w-[374px] max-h-[475px] overflow-hidden">
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
v-if="openedBy === 'emojiPicker'"
|
v-if="openedBy === 'emojiPicker'"
|
||||||
@picked-emoji="$emit('picked-emoji', $event)"
|
@picked-emoji="$emit('picked-emoji', $event)"
|
||||||
/>
|
/>
|
||||||
|
<UserProfile
|
||||||
|
v-else
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -16,10 +19,9 @@
|
|||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
openedBy: {
|
openedBy: {
|
||||||
type: String,
|
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
opened: Boolean
|
opened: Boolean
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -292,6 +292,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
:opened="createChannelModelOpen"
|
||||||
|
@close="createChannelModelOpen = false"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="bg-[var(--secondary-bg)] rounded-xl shadow-2xl flex flex-row overflow-hidden z-20 absolute border border-[var(--tertiary-bg)] -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/ryan-klaus-5CkzYaubjkk-unsplash.jpg"
|
||||||
|
class="h-96 w-64 object-cover"
|
||||||
|
/>
|
||||||
|
<div class="p-4 flex flex-col text-center">
|
||||||
|
<h1 class="font-semibold text-2xl">
|
||||||
|
Create Channel
|
||||||
|
</h1>
|
||||||
|
<form
|
||||||
|
class="flex flex-col gap-y-3 my-2"
|
||||||
|
@submit.prevent="createChannel"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="channelName"
|
||||||
|
class="px-4 py-2 rounded-md w-full bg-[var(--primary-input)] shadow-2xl placeholder:text-[var(--primary-placeholder)] focus:outline-none"
|
||||||
|
name="name"
|
||||||
|
placeholder="Channel Name"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
value="Submit"
|
||||||
|
class="w-full bg-[#5865F2] py-2 px-4 rounded-md cursor-pointer"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -319,10 +354,10 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
userIsOwner() {
|
userIsOwner() {
|
||||||
return this.activeServer.data.server && this.activeServer.type === 'server' && this.activeServer.data.server.roles?.find((e: IRole) => e.users.some((el) => el.id === this.user?.id))?.owner;
|
return this.activeServer.type === 'server' && this.activeServer.data.server.participants.find((e) => e.id === this.user?.id)?.roles?.some((e) => e.owner === true);
|
||||||
},
|
},
|
||||||
userIsAdmin() {
|
userIsAdmin() {
|
||||||
return this.activeServer.data.server && this.activeServer.type === 'server' && this.activeServer.data.server.roles?.find((e: IRole) => e.users.some((el) => el.id === this.user?.id))?.administer;
|
return this.activeServer.type === 'server' && this.activeServer.data.server.participants.find((e) => e.id === this.user?.id)?.roles?.some((e) => e.administer === true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -337,12 +372,14 @@ export default {
|
|||||||
|
|
||||||
useServerStore().addChannel(this.activeServer.data.server.id, channel);
|
useServerStore().addChannel(this.activeServer.data.server.id, channel);
|
||||||
this.createChannelModelOpen = false;
|
this.createChannelModelOpen = false;
|
||||||
|
|
||||||
|
navigateTo(`/channel/${channel.id}`);
|
||||||
},
|
},
|
||||||
async createInvite() {
|
async createInvite() {
|
||||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||||
const inviteCode = await $fetch(`/api/guilds/${this.activeServer.data.server.id}/createInvite`, { method: 'POST', headers });
|
const inviteCode = await $fetch(`/api/guilds/${this.activeServer.data.server.id}/createInvite`, { method: 'POST', headers });
|
||||||
},
|
},
|
||||||
async logout() {
|
logout() {
|
||||||
useUserStore().logout();
|
useUserStore().logout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,138 @@
|
|||||||
<template>
|
<template>
|
||||||
<div />
|
<div class="w-[374px] max-h-[475px] overflow-y-scroll">
|
||||||
|
<div class="relative h-[calc(160px+56px)]">
|
||||||
|
<div class="w-full h-40 absolute">
|
||||||
|
<img
|
||||||
|
src="/tansu-topuzoglu-v2mlqhy5dLU-unsplash.jpg"
|
||||||
|
class="h-40 w-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-[28%] aspect-square bg-[var(--primary-bg)] border-2 border-[var(--tertiary-bg)] rounded-xl overflow-hidden left-1/2 -translate-x-1/2 top-28 z-10 absolute">
|
||||||
|
<img
|
||||||
|
src="/daiga-ellaby-snUtnGUp2zU-unsplash.jpg"
|
||||||
|
class="h-40 w-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-3 pb-2">
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="font-semibold">
|
||||||
|
{{ user.username }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<hr class="border-[var(--tertiary-lightened-bg)] my-2" />
|
||||||
|
<div class="m-1 p-2 rounded-lg bg-[var(--tertiary-bg)] flex flex-col gap-y-1">
|
||||||
|
<div v-if="true">
|
||||||
|
<p class="font-semibold text-sm">
|
||||||
|
About Me
|
||||||
|
</p>
|
||||||
|
<div class="text-sm p-1">
|
||||||
|
<p>lorem ipsum</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold text-sm">
|
||||||
|
Member since
|
||||||
|
</p>
|
||||||
|
<div class="text-sm p-1">
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
new Date(user.createdAt).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="!isDm"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
v-if="roles.length < 1"
|
||||||
|
class="font-semibold text-sm"
|
||||||
|
>
|
||||||
|
No Roles
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="user.id !== userData.id">
|
||||||
|
<input
|
||||||
|
v-model="message"
|
||||||
|
class="bg-[var(--secondary-bg)] placeholder:text-[var(--primary-placeholder)] px-2 focus:outline-none py-1 rounded-md w-full border border-[var(--tertiary-lightened-bg)]"
|
||||||
|
:placeholder="`Message @${user.username}`"
|
||||||
|
@keypress.enter="sendDM()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!--
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { IUser } from '~/types';
|
import { useActiveStore } from '~/stores/activeStore';
|
||||||
|
import { useDmStore } from '~/stores/dmStore';
|
||||||
|
import { useEmojiPickerStore } from '~/stores/emojiPickerStore';
|
||||||
|
import { useUserStore } from '~/stores/userStore';
|
||||||
|
import { IUser, IRole } from '~/types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
async setup() {
|
||||||
user: {
|
const userData = useUserStore().user;
|
||||||
type: IUser,
|
async function fetchUser() {
|
||||||
required: true
|
const emojiPickerData = useEmojiPickerStore().emojiPickerData;
|
||||||
|
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||||
|
const activeServer = useActiveStore().server;
|
||||||
|
|
||||||
|
const isDm = useRoute().path.includes('@me');
|
||||||
|
|
||||||
|
let user: IUser | null;
|
||||||
|
|
||||||
|
if (isDm) {
|
||||||
|
user = await $fetch(`/api/user/${emojiPickerData.userId}/profile`, { headers }) as IUser | null;
|
||||||
|
} else {
|
||||||
|
user = await $fetch(`/api/user/${emojiPickerData.userId}/${activeServer.server.id}/profile`, { headers }) as IUser | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user, isDm };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { user, isDm } = await fetchUser();
|
||||||
|
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
return { user, isDm, fetchUser, userData };
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
message: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
roles(): IRole[] {
|
||||||
|
return this.user.roles?.filter((e: IRole) => e.owner === false) || [];
|
||||||
|
},
|
||||||
|
userIsOwner(): boolean {
|
||||||
|
return this.user.roles?.some((e: IRole) => e.owner === true) || false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async sendDM() {
|
||||||
|
if (!this.message.trim()) return;
|
||||||
|
|
||||||
|
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||||
|
const preExistingDM = useDmStore().getByPartnerId(this.user.id);
|
||||||
|
|
||||||
|
if (preExistingDM && useRoute().path !== `/channel/@me/${preExistingDM.id}`) {
|
||||||
|
await navigateTo(`/channel/@me/${preExistingDM.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await $fetch(`/api/channels/${preExistingDM.id}/sendMessage`, { method: 'post', body: { body: this.message }, headers });
|
||||||
|
this.message = '';
|
||||||
|
|
||||||
|
useEmojiPickerStore().closeEmojiPicker();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script> -->
|
</script>
|
||||||
@@ -13,7 +13,7 @@ import { useUserStore } from '~/stores/userStore';
|
|||||||
import { useServerStore } from '~/stores/serverStore';
|
import { useServerStore } from '~/stores/serverStore';
|
||||||
import { useDmStore } from '~/stores/dmStore';
|
import { useDmStore } from '~/stores/dmStore';
|
||||||
import { useActiveStore } from '~/stores/activeStore';
|
import { useActiveStore } from '~/stores/activeStore';
|
||||||
import { IChannel } from '~/types';
|
import { IChannel, IServer, SafeUser } from '~/types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async setup() {
|
async setup() {
|
||||||
@@ -25,8 +25,8 @@ export default {
|
|||||||
|
|
||||||
if (!userStore.isLoggedIn) {
|
if (!userStore.isLoggedIn) {
|
||||||
const [userData, serverData] = await Promise.all([
|
const [userData, serverData] = await Promise.all([
|
||||||
$fetch('/api/getCurrentUser', { headers }),
|
$fetch('/api/getCurrentUser', { headers }) as Promise<SafeUser | null>,
|
||||||
$fetch('/api/user/getServers', { headers })
|
$fetch('/api/user/getServers', { headers }) as Promise<{ dms: IChannel[], servers: IServer[] } | null>
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!userData || !serverData) throw new Error('No user data or server data');
|
if (!userData || !serverData) throw new Error('No user data or server data');
|
||||||
@@ -40,7 +40,7 @@ export default {
|
|||||||
|
|
||||||
const isDm = route.path.includes('@me');
|
const isDm = route.path.includes('@me');
|
||||||
|
|
||||||
if (isDm && route.params.dmId) {
|
if (isDm && route.params.dmId && useActiveStore().dm.id !== route.params.dmId) {
|
||||||
const dmData: IChannel = await $fetch(`/api/channels/${route.params.dmId}`, { headers });
|
const dmData: IChannel = await $fetch(`/api/channels/${route.params.dmId}`, { headers });
|
||||||
|
|
||||||
if (!dmData) throw new Error('Could not find dm.');
|
if (!dmData) throw new Error('Could not find dm.');
|
||||||
@@ -49,12 +49,14 @@ export default {
|
|||||||
useActiveStore().setActiveDM(dmData);
|
useActiveStore().setActiveDM(dmData);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDm && route.params.channelId) {
|
if (!isDm && route.params.channelId && useActiveStore().server.channel.id !== route.params.channelId) {
|
||||||
const [channel, server] = await Promise.all([
|
const [channel, server] = await Promise.all([
|
||||||
$fetch(`/api/channels/${route.params.channelId}`, { headers }) as unknown as IChannel,
|
$fetch(`/api/channels/${route.params.channelId}`, { headers }) as Promise<IChannel | null>,
|
||||||
$fetch(`/api/channels/${route.params.channelId}/guild`, { headers })
|
$fetch(`/api/channels/${route.params.channelId}/guild`, { headers }) as Promise<IServer | null>
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (!server || !channel) throw new Error('No channel or server');
|
||||||
|
|
||||||
if (!server) throw new Error('Could not find server.');
|
if (!server) throw new Error('Could not find server.');
|
||||||
useServerStore().addServer(server);
|
useServerStore().addServer(server);
|
||||||
useActiveStore().setActiveServer(channel, useServerStore().servers);
|
useActiveStore().setActiveServer(channel, useServerStore().servers);
|
||||||
@@ -62,14 +64,8 @@ export default {
|
|||||||
|
|
||||||
if (isDm && !route.params.dmId) {
|
if (isDm && !route.params.dmId) {
|
||||||
// on '/@me'
|
// on '/@me'
|
||||||
useActiveStore().type = 'dm';
|
useActiveStore().setActiveHome();
|
||||||
}
|
}
|
||||||
|
|
||||||
// const socket = ref(null);
|
|
||||||
|
|
||||||
// socket.value = io('127.0.0.1:3000', {
|
|
||||||
// auth: (cb) => cb({ token: useCookie('sessionToken').value })
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -4734,19 +4734,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/fsevents": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -13849,12 +13836,6 @@
|
|||||||
"fs.realpath": {
|
"fs.realpath": {
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
},
|
},
|
||||||
"fsevents": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"function-bind": {
|
"function-bind": {
|
||||||
"version": "1.1.1"
|
"version": "1.1.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
:participants="participants"
|
:participants="participants"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="fixed mr-3"
|
class="fixed mx-3"
|
||||||
:style="`top: ${emojiPickerData.top}px; right: ${emojiPickerData.right}px`"
|
:style="`top: ${emojiPickerData.top}px; ${(emojiPickerData.right !== undefined) ? `right: ${emojiPickerData.right}px;` : `left: ${emojiPickerData.left}px`}`"
|
||||||
>
|
>
|
||||||
<Transition>
|
<Transition>
|
||||||
<Popup
|
<Popup
|
||||||
@@ -22,9 +22,8 @@
|
|||||||
import { useActiveStore } from '~/stores/activeStore';
|
import { useActiveStore } from '~/stores/activeStore';
|
||||||
import { useDmStore } from '~/stores/dmStore';
|
import { useDmStore } from '~/stores/dmStore';
|
||||||
import { useEmojiPickerStore } from '~/stores/emojiPickerStore';
|
import { useEmojiPickerStore } from '~/stores/emojiPickerStore';
|
||||||
import { useServerStore } from '~/stores/serverStore';
|
|
||||||
import { useUserStore } from '~/stores/userStore';
|
import { useUserStore } from '~/stores/userStore';
|
||||||
import { IChannel, IMessage, IServer, SafeUser } from '~/types';
|
import { IChannel, IMessage, SafeUser } from '~/types';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'auth'
|
middleware: 'auth'
|
||||||
@@ -53,10 +52,6 @@ export default {
|
|||||||
|
|
||||||
const channel = useActiveStore().dm;
|
const channel = useActiveStore().dm;
|
||||||
|
|
||||||
channel.messages?.forEach((e: IMessage) => {
|
|
||||||
e.body = parseMessageBody(e.body, participants);
|
|
||||||
});
|
|
||||||
|
|
||||||
const friend = participants.find((e) => e.id !== useUserStore().user?.id)?.username;
|
const friend = participants.find((e) => e.id !== useUserStore().user?.id)?.username;
|
||||||
|
|
||||||
useHeadSafe({
|
useHeadSafe({
|
||||||
@@ -70,12 +65,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// socket: storeToRefs(useGlobalStore()).socket as unknown as Server,
|
|
||||||
emojiPickerData: storeToRefs(useEmojiPickerStore()).emojiPickerData,
|
emojiPickerData: storeToRefs(useEmojiPickerStore()).emojiPickerData,
|
||||||
emojiPickerStyles: {
|
|
||||||
top: storeToRefs(useEmojiPickerStore()).emojiPickerData.value.top + 'px',
|
|
||||||
right: storeToRefs(useEmojiPickerStore()).emojiPickerData.value.right + 'px',
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
useActiveStore().type = 'dm';
|
useActiveStore().setActiveHome();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async startDM() {
|
async startDM() {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<!-- eslint-disable vue/no-multiple-template-root -->
|
<!-- eslint-disable vue/no-multiple-template-root -->
|
||||||
<template>
|
<template>
|
||||||
<MessagePane
|
<MessagePane
|
||||||
:channel="channel"
|
:channel="server.channel"
|
||||||
:participants="server.participants"
|
:participants="server.server.participants"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="fixed mr-3"
|
class="fixed mx-3"
|
||||||
:style="`top: ${emojiPickerData.top}px; right: ${emojiPickerData.right}px`"
|
:style="`top: ${emojiPickerData.top}px; ${(emojiPickerData.right !== undefined) ? `right: ${emojiPickerData.right}px;` : `left: ${emojiPickerData.left}px`}`"
|
||||||
>
|
>
|
||||||
<Transition>
|
<Transition>
|
||||||
<Popup
|
<Popup
|
||||||
@@ -49,38 +49,27 @@ export default {
|
|||||||
}
|
}
|
||||||
useEmojiPickerStore().closeEmojiPicker();
|
useEmojiPickerStore().closeEmojiPicker();
|
||||||
|
|
||||||
const channel = useActiveStore().server.channel;
|
const server = useActiveStore().server;
|
||||||
const server = useActiveStore().server.server;
|
|
||||||
|
|
||||||
channel.messages?.forEach((e: IMessage) => {
|
|
||||||
e.body = parseMessageBody(e.body, server.participants);
|
|
||||||
});
|
|
||||||
|
|
||||||
useHeadSafe({
|
useHeadSafe({
|
||||||
title: `#${channel.name} | ${server.name} - Blop`
|
title: `#${server.channel.name} | ${server.server.name} - Blop`
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
channel,
|
|
||||||
server
|
server
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// socket: storeToRefs(useGlobalStore()).socket as unknown as Server,
|
|
||||||
emojiPickerData: storeToRefs(useEmojiPickerStore()).emojiPickerData,
|
emojiPickerData: storeToRefs(useEmojiPickerStore()).emojiPickerData,
|
||||||
emojiPickerStyles: {
|
|
||||||
top: storeToRefs(useEmojiPickerStore()).emojiPickerData.value.top + 'px',
|
|
||||||
right: storeToRefs(useEmojiPickerStore()).emojiPickerData.value.right + 'px',
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
const { $io } = useNuxtApp();
|
const { $io } = useNuxtApp();
|
||||||
|
|
||||||
(await $io).on(`addChannel-${this.server.id}`, (ev) => {
|
(await $io).on(`addChannel-${this.server.server.id}`, (ev) => {
|
||||||
const newChannel = ev as IChannel;
|
const newChannel = ev as IChannel;
|
||||||
useServerStore().addChannel(this.server.id, newChannel);
|
useServerStore().addChannel(this.server.server.id, newChannel);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="w-screen h-screen flex justify-center items-center bg-[var(--primary-bg)]">
|
<div class="w-screen h-screen flex justify-center items-center bg-[var(--primary-bg)]">
|
||||||
<div class="bg-[var(--secondary-bg)] rounded-xl shadow-2xl flex flex-row overflow-hidden">
|
<div class="bg-[var(--secondary-bg)] rounded-xl shadow-2xl flex flex-row overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src="/plants.jpg"
|
src="/nahil-naseer-xljtGZ2-P3Y-unsplash.jpg"
|
||||||
class="h-96 w-64 object-cover"
|
class="h-96 w-64 object-cover"
|
||||||
/>
|
/>
|
||||||
<div class="p-4 flex flex-col text-center">
|
<div class="p-4 flex flex-col text-center">
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
import { useDmStore } from '~/stores/dmStore';
|
import { useDmStore } from '~/stores/dmStore';
|
||||||
import { useServerStore } from '~/stores/serverStore';
|
import { useServerStore } from '~/stores/serverStore';
|
||||||
import { useUserStore } from '~/stores/userStore';
|
import { useUserStore } from '~/stores/userStore';
|
||||||
import { SafeUser } from '~/types';
|
import { IChannel, IServer, SafeUser } from '~/types';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'clean'
|
layout: 'clean'
|
||||||
@@ -77,8 +77,8 @@ export default {
|
|||||||
|
|
||||||
useUserStore().setUser(loginData.user);
|
useUserStore().setUser(loginData.user);
|
||||||
|
|
||||||
useServerStore().setServers(loginData.user.servers);
|
useServerStore().setServers(loginData.user.servers || [] as IServer[]);
|
||||||
useDmStore().setDms(loginData.user.channels);
|
useDmStore().setDms(loginData.user.channels || [] as IChannel[]);
|
||||||
|
|
||||||
return navigateTo('/');
|
return navigateTo('/');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="w-screen h-screen flex justify-center items-center bg-[var(--primary-bg)]">
|
<div class="w-screen h-screen flex justify-center items-center bg-[var(--primary-bg)]">
|
||||||
<div class="bg-[var(--secondary-bg)] rounded-xl shadow-2xl flex flex-row overflow-hidden">
|
<div class="bg-[var(--secondary-bg)] rounded-xl shadow-2xl flex flex-row overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src="/flowery-plants.jpg"
|
src="/annie-spratt-8mqOw4DBBSg-unsplash.jpg"
|
||||||
class="h-96 w-64 object-cover filter brightness-95"
|
class="h-96 w-64 object-cover filter brightness-95"
|
||||||
/>
|
/>
|
||||||
<div class="p-4 flex flex-col text-center">
|
<div class="p-4 flex flex-col text-center">
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
import { useDmStore } from '~/stores/dmStore';
|
import { useDmStore } from '~/stores/dmStore';
|
||||||
import { useServerStore } from '~/stores/serverStore';
|
import { useServerStore } from '~/stores/serverStore';
|
||||||
import { useUserStore } from '~/stores/userStore';
|
import { useUserStore } from '~/stores/userStore';
|
||||||
import { SafeUser } from '~/types';
|
import { IChannel, IServer, SafeUser } from '~/types';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: 'clean'
|
layout: 'clean'
|
||||||
@@ -86,8 +86,8 @@ export default {
|
|||||||
|
|
||||||
useUserStore().setUser(signupData.user);
|
useUserStore().setUser(signupData.user);
|
||||||
|
|
||||||
useServerStore().setServers(signupData.user.servers);
|
useServerStore().setServers(signupData.user.servers || [] as IServer[]);
|
||||||
useDmStore().setDms(signupData.user.channels);
|
useDmStore().setDms(signupData.user.channels || [] as IChannel[]);
|
||||||
|
|
||||||
return navigateTo('/');
|
return navigateTo('/');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<p>hey</p>
|
<div class="bg-[var(--primary-bg)] h-full">
|
||||||
|
<Popup
|
||||||
|
:opened="true"
|
||||||
|
:openedBy="'userInfo'"
|
||||||
|
/>
|
||||||
|
<Popup
|
||||||
|
:opened="true"
|
||||||
|
:openedBy="'emojiPicker'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { useEmojiPickerStore } from '~/stores/emojiPickerStore';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
emojiPickerData: storeToRefs(useEmojiPickerStore()).emojiPickerData,
|
||||||
|
emojiPickerStyles: {
|
||||||
|
top: storeToRefs(useEmojiPickerStore()).emojiPickerData.value.top + 'px',
|
||||||
|
right: storeToRefs(useEmojiPickerStore()).emojiPickerData.value.right + 'px',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
useEmojiPickerStore().openEmojiPicker({ opened: true, type: 'userInfo', right: 0, top: 0 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -105,4 +105,6 @@ model Reaction {
|
|||||||
messageId String?
|
messageId String?
|
||||||
Message Message? @relation(fields: [messageId], references: [id])
|
Message Message? @relation(fields: [messageId], references: [id])
|
||||||
users User[] @relation("ReactionToUser")
|
users User[] @relation("ReactionToUser")
|
||||||
|
|
||||||
|
@@unique([emoji, messageId])
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 216 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 156 KiB |
@@ -1,5 +1,5 @@
|
|||||||
import emojiRegex from 'emoji-regex';
|
import emojiRegex from 'emoji-regex';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { Prisma, PrismaClient } from '@prisma/client';
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
@@ -123,21 +123,30 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reaction = await prisma.reaction.create({
|
try {
|
||||||
data: {
|
reaction = await prisma.reaction.create({
|
||||||
emoji,
|
data: {
|
||||||
users: {
|
emoji,
|
||||||
connect: [{
|
users: {
|
||||||
id: event.context.user.id,
|
connect: [{
|
||||||
}]
|
id: event.context.user.id,
|
||||||
},
|
}]
|
||||||
Message: {
|
},
|
||||||
connect: {
|
Message: {
|
||||||
id: message.id,
|
connect: {
|
||||||
|
id: message.id,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === 'P2002') {
|
||||||
|
// gracefully fail as it's likely a race condition
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reaction.messageId) return;
|
if (!reaction.messageId) return;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const channelId = event.context.params.id;
|
const channelId = event.context.params.id;
|
||||||
|
|
||||||
if (!req || !channelId) {
|
if (!req.body || !channelId || !req.body.trim()) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
statusMessage: 'A body is required to send a message.',
|
statusMessage: 'A body is required to send a message.',
|
||||||
@@ -166,6 +166,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
body: true,
|
body: true,
|
||||||
|
createdAt: true,
|
||||||
creator: {
|
creator: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|||||||
@@ -21,19 +21,6 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const { serverName } = body;
|
const { serverName } = body;
|
||||||
|
|
||||||
const preExistingServer = await prisma.server.findFirst({
|
|
||||||
where: {
|
|
||||||
name: serverName
|
|
||||||
}
|
|
||||||
}) as IServer | null;
|
|
||||||
|
|
||||||
if (preExistingServer) {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 409,
|
|
||||||
statusMessage: `Server with name ${serverName} already exists.`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = await prisma.server.create({
|
const server = await prisma.server.create({
|
||||||
data: {
|
data: {
|
||||||
name: serverName,
|
name: serverName,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { IChannel, IMessage, IServer } from '~/types';
|
import { IChannel, IMessage, IRole, IServer, SafeUser } from '~/types';
|
||||||
import { useServerStore } from './serverStore';
|
|
||||||
|
|
||||||
export const useActiveStore = defineStore('activeStore', {
|
export const useActiveStore = defineStore('activeStore', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@@ -11,11 +10,25 @@ export const useActiveStore = defineStore('activeStore', {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
|
setActiveHome() {
|
||||||
|
this.server = {
|
||||||
|
server: {} as IServer,
|
||||||
|
channel: {} as IChannel
|
||||||
|
};
|
||||||
|
|
||||||
|
this.type = 'dm';
|
||||||
|
},
|
||||||
setActiveDM(dm: IChannel) {
|
setActiveDM(dm: IChannel) {
|
||||||
|
this.server = {
|
||||||
|
server: {} as IServer,
|
||||||
|
channel: {} as IChannel
|
||||||
|
};
|
||||||
|
|
||||||
this.type = 'dm';
|
this.type = 'dm';
|
||||||
this.dm = dm;
|
this.dm = dm;
|
||||||
},
|
},
|
||||||
setActiveServer(channel: IChannel, servers: IServer[]) {
|
setActiveServer(channel: IChannel, servers: IServer[]) {
|
||||||
|
this.dm = {} as IChannel;
|
||||||
this.type = 'server';
|
this.type = 'server';
|
||||||
|
|
||||||
const activeServer = servers.find((e: IServer) => {
|
const activeServer = servers.find((e: IServer) => {
|
||||||
@@ -32,13 +45,39 @@ export const useActiveStore = defineStore('activeStore', {
|
|||||||
|
|
||||||
const activeChannel = activeServer.channels[activeChannelIndex];
|
const activeChannel = activeServer.channels[activeChannelIndex];
|
||||||
|
|
||||||
|
activeServer.roles.map((role: IRole) => {
|
||||||
|
role.users.map((e: SafeUser) => {
|
||||||
|
const userIndex = activeServer.participants.findIndex((user: SafeUser) => user.id === e.id);
|
||||||
|
|
||||||
|
if (activeServer.participants[userIndex] == undefined) return;
|
||||||
|
|
||||||
|
activeServer.participants[userIndex].roles = activeServer.participants[userIndex].roles || [];
|
||||||
|
|
||||||
|
const userRole = role;
|
||||||
|
|
||||||
|
delete(userRole.users);
|
||||||
|
|
||||||
|
activeServer.participants[userIndex].roles.push(userRole);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
delete(activeServer.roles);
|
||||||
|
|
||||||
if (!activeChannel) return;
|
if (!activeChannel) return;
|
||||||
|
|
||||||
this.server.server = activeServer;
|
this.server.server = activeServer;
|
||||||
this.server.channel = activeChannel;
|
this.server.channel = activeChannel;
|
||||||
},
|
},
|
||||||
|
getMessageById(id: string) {
|
||||||
|
const channel = (this.type === 'server') ? this.server.channel : this.dm;
|
||||||
|
|
||||||
|
return channel.messages.find((e: IMessage) => e.id === id);
|
||||||
|
},
|
||||||
addMessage(message: IMessage) {
|
addMessage(message: IMessage) {
|
||||||
const channel = (this.type === 'server') ? this.server.channel : this.dm;
|
const channel = (this.type === 'server') ? this.server.channel : this.dm;
|
||||||
|
|
||||||
|
if (channel.messages.findIndex((e: IMessage) => e.id === message.id) !== -1) return;
|
||||||
|
|
||||||
channel.messages.push(message);
|
channel.messages.push(message);
|
||||||
},
|
},
|
||||||
updateMessage(message: IMessage) {
|
updateMessage(message: IMessage) {
|
||||||
@@ -52,11 +91,10 @@ export const useActiveStore = defineStore('activeStore', {
|
|||||||
},
|
},
|
||||||
removeMessage(messageId: string) {
|
removeMessage(messageId: string) {
|
||||||
const channel = (this.type === 'server') ? this.server.channel : this.dm;
|
const channel = (this.type === 'server') ? this.server.channel : this.dm;
|
||||||
const messageIndex = channel.messages.findIndex((e: IMessage) => e.id === messageId);
|
|
||||||
|
|
||||||
if (messageIndex == -1) return;
|
if (!channel.messages.find(m => m.id === messageId)) return;
|
||||||
|
|
||||||
delete(channel.messages[messageIndex]);
|
channel.messages = channel.messages.filter((e: IMessage) => e.id !== messageId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IChannel } from '~/types';
|
import { IChannel, IUser, SafeUser } from '~/types';
|
||||||
|
|
||||||
export const useDmStore = defineStore('dmStore', {
|
export const useDmStore = defineStore('dmStore', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@@ -18,6 +18,9 @@ export const useDmStore = defineStore('dmStore', {
|
|||||||
},
|
},
|
||||||
getById(id: string) {
|
getById(id: string) {
|
||||||
return this.dms.find((e) => e.id === id);
|
return this.dms.find((e) => e.id === id);
|
||||||
|
},
|
||||||
|
getByPartnerId(id: string) {
|
||||||
|
return this.dms.find((e) => e.dmParticipants?.some((e: SafeUser) => e.id === id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ import { IPopupData } from '~/types';
|
|||||||
|
|
||||||
export const useEmojiPickerStore = defineStore('emojiPickerStore', {
|
export const useEmojiPickerStore = defineStore('emojiPickerStore', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
emojiPickerData: {type: 'emojiPicker'} as IPopupData,
|
emojiPickerData: {} as IPopupData,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
openEmojiPicker(payload: IPopupData) {
|
openEmojiPicker(payload: IPopupData) {
|
||||||
this.emojiPickerData.type = 'emojiPicker';
|
console.log(this.emojiPickerData, payload);
|
||||||
this.emojiPickerData.top = payload.top;
|
|
||||||
this.emojiPickerData.right = payload.right;
|
this.emojiPickerData = { ...payload, opened: true };
|
||||||
this.emojiPickerData.openedBy = payload.openedBy;
|
|
||||||
this.emojiPickerData.opened = true;
|
|
||||||
},
|
},
|
||||||
toggleEmojiPicker(payload: IPopupData) {
|
toggleEmojiPicker(payload: IPopupData) {
|
||||||
let messageId;
|
let messageId;
|
||||||
@@ -31,7 +29,7 @@ export const useEmojiPickerStore = defineStore('emojiPickerStore', {
|
|||||||
},
|
},
|
||||||
closeEmojiPicker() {
|
closeEmojiPicker() {
|
||||||
if (this.emojiPickerData.openedBy) this.emojiPickerData.openedBy.messageId = '';
|
if (this.emojiPickerData.openedBy) this.emojiPickerData.openedBy.messageId = '';
|
||||||
this.emojiPickerData.opened = false;
|
this.emojiPickerData = { opened: false } as IPopupData;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -77,10 +77,12 @@ export interface IReaction {
|
|||||||
export interface IPopupData {
|
export interface IPopupData {
|
||||||
opened: boolean;
|
opened: boolean;
|
||||||
top: number;
|
top: number;
|
||||||
right: number;
|
right?: number;
|
||||||
|
left?: number;
|
||||||
type: 'emojiPicker' | 'userInfo';
|
type: 'emojiPicker' | 'userInfo';
|
||||||
openedBy?: {
|
openedBy?: {
|
||||||
type: 'message' | 'messageInput';
|
type: 'message' | 'messageInput';
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
};
|
};
|
||||||
|
userId?: string;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user