a bunch of bug fixes and improvements

This commit is contained in:
Zoe
2023-04-28 00:28:37 -05:00
parent a7c91b382a
commit b88b3207b3
27 changed files with 494 additions and 184 deletions

View File

@@ -20,7 +20,8 @@
"storeToRefs": true,
"useNuxtApp": true,
"NodeJS": true,
"useHeadSafe": true
"useHeadSafe": true,
"defineEmits": true
},
"parser": "vue-eslint-parser",
"parserOptions": {

View File

@@ -2,9 +2,6 @@
import emojiJson from '~/assets/json/emoji.json';
export default {
props: {
opened: Boolean,
},
emits: ['picked-emoji'],
data() {
return {
@@ -22,7 +19,9 @@ export default {
};
},
methods: {
emojiStyles(emojiShortName: string, width: number) {
emojiStyles(emojiShortName: string | undefined, width: number) {
if (!emojiShortName) return;
const emojis = emojiJson;
const emoji = emojis.find((e) => e.short_names[0] === emojiShortName);
if (!emoji) return;
@@ -38,17 +37,17 @@ export default {
};
},
scrollTo(categoryName: string) {
const emojiPane = document.getElementById('emojiPane');
const emojiPane = (this.$refs.emojiPane as HTMLDivElement);
const category = document.getElementById(categoryName);
if (!emojiPane || !category) return;
emojiPane.scrollTop = category.offsetTop - 96;
emojiPane.scrollTop = category.offsetTop - 550;
}
}
};
</script>
<template>
<div>
<div class="p-3">
<div class="py-1.5 flex flex-col">
<div class="flex-row gap-x-2 overflow-x-scroll">
<button
@@ -232,13 +231,13 @@ export default {
</div>
<div
id="emoji-pane"
class="overflow-hidden overflow-y-scroll scroll-smooth EmojiPicker max-h-[450px]"
ref="emojiPane"
class="overflow-hidden overflow-y-scroll scroll-smooth EmojiPicker max-h-[calc(475px-24px-48px)]"
>
<div
v-for="category in categories"
: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">
{{

View File

@@ -16,7 +16,7 @@
<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"
/>
<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 class="flex w-full justify-end">
@@ -40,7 +40,7 @@
import { PropType } from 'vue';
import { useServerStore } from '~/stores/serverStore';
import { useUserStore } from '~/stores/userStore';
import { IInviteCode, IUser } from '~/types';
import { IInviteCode, IServer, IUser, SafeUser } from '~/types';
export default {
props: {
@@ -57,16 +57,18 @@ export default {
},
computed: {
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: {
async joinServer(invite: IInviteCode) {
if (this.userInServer) return;
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;
this.servers?.push(server);
useServerStore().addServer(server);
this.invite.server.participants.push(this.user);
},
}

View File

@@ -118,15 +118,40 @@
>
<div class="message-content">
<div class="message-sender-text">
<p
v-if="showUsername"
class="mb-1 font-semibold w-fit"
<p
v-if="showUsername"
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
class="break-words max-w-full"
v-html="message.body"
<div
class="break-words max-w-full whitespace-pre-wrap"
v-html="parseMessageBody(message.body, participants)"
/>
</div>
<div
@@ -206,6 +231,10 @@ export default {
computed: {
reactions(): IReaction[] {
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() {
@@ -214,7 +243,8 @@ export default {
if (useEmojiPickerStore().emojiPickerData.openedBy?.messageId !== this.message.id) return;
const replacementEmoji = emojiJson.find((e) => e.short_names[0] === emoji);
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);
});
},
@@ -222,11 +252,15 @@ export default {
async toggleReaction(emoji: string) {
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);
},
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}`);
if (!actionButtons) return;
@@ -235,6 +269,7 @@ export default {
if (top + 522 > window.innerHeight) top = window.innerHeight - 522;
const payload = {
type: 'emojiPicker',
top,
right: actionButtons.clientWidth + 40,
openedBy: {
@@ -243,7 +278,35 @@ export default {
}
} 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) {
const emojis = emojiJson;
@@ -298,6 +361,7 @@ pre.codeblock code {
}
code.inline-code {
color: var(--primary-accent);
background-color: var(--secondary-bg);
padding: 0.2rem;
font-size: 85%;

View File

@@ -1,5 +1,6 @@
<template>
<div
id="messagePane"
class="h-full relative bg-[var(--primary-bg)] flex flex-col"
@mouseenter="mouseEnter"
@mouseleave="mouseLeave"
@@ -77,8 +78,8 @@
:key="message.id"
:message="message"
:shift-pressed="shiftPressed"
:show-username="i === 0 || channel.messages[i - 1]?.creator.id !== message.creator.id"
:classes="calculateMessageClasses(message, i)"
:show-username="calculateMessageDesign(message, i).showUsername"
:classes="calculateMessageDesign(message, i).classes"
:channel-id="channel.id"
:participants="participants"
/>
@@ -214,21 +215,21 @@ export default {
methods: {
async sendMessage() {
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 });
if (!message) return;
if (this.channel.messages.includes(message)) return;
message.body = parseMessageBody(message.body, this.participants);
this.channel.messages.push(message);
useActiveStore().addMessage(message);
this.messageContent = '';
const conversationDiv = this.$refs.conversationPane as HTMLDivElement;
if (!conversationDiv) throw new Error('wtf');
this.scrollToBottom();
setTimeout(() => {
this.scrollToBottom();
});
},
scrollToBottom() {
const conversationDiv = this.$refs.conversationPane as HTMLDivElement;
@@ -273,20 +274,20 @@ export default {
this.shiftPressed = false;
}
},
calculateMessageClasses(message: IMessage, i: number) {
if (i === 0 || 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 'mb-0 pb-0.5';
calculateMessageDesign(message: IMessage, i: number) {
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) {
return { classes: 'mb-0 pb-0.5', showUsername: true };
}
} else {
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 {
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() {
const input = this.$refs.messageBox as HTMLTextAreaElement;
@@ -342,7 +343,7 @@ export default {
completeMention(user: SafeUser) {
this.messageContent = this.messageContent.replace('@' + this.search.content, `<@${user.id}>`);
this.search.show = false;
this.$refs.messageBox.focus();
(this.$refs.messageBox as HTMLInputElement).focus();
},
async listenToWebsocket(conversationDiv: HTMLElement) {
let { $io } = useNuxtApp();
@@ -357,17 +358,12 @@ export default {
return;
}
message.body = parseMessageBody(message.body, this.participants);
if (this.channel.messages.find((e) => e.id === message.id)) {
// message is already in the server, replace it with the updated message
useActiveStore().updateMessage(message);
return;
}
if (message.creator.id === this.user?.id) return;
if (!document.hasFocus()) {
new Notification(`Message from @${message.creator.username}`, { body: message.body, tag: this.channel.id.toString() });
}

View File

@@ -1,9 +1,14 @@
<template>
<nav class="bg-[var(--primary-bg)] h-screen p-4 grid grid-cols-1 grid-rows-[56px_1fr_56px] shadow shadow-black/80">
<div>
<nuxt-link to="/channel/@me">
<nuxt-link
to="/channel/@me"
draggable="false"
>
<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>
<svg
@@ -51,9 +56,12 @@
v-for="server in servers"
:key="server.id"
:to="'/channel/' + server.channels[0]?.id"
draggable="false"
>
<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
xmlns="http://www.w3.org/2000/svg"
@@ -73,6 +81,7 @@
<button
class="p-3 rounded-full transition-colors ease-in-out hover:bg-[var(--tertiary-lightened-bg)] duration-300 cursor-pointer"
@click="createServerModalOpen = true"
>
<svg
width="32"
@@ -89,17 +98,71 @@
/>
</svg>
</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>
</template>
<script lang="ts">
import { useActiveStore } from '~/stores/activeStore';
import { useServerStore } from '~/stores/serverStore';
import { IServer } from '~/types';
export default {
data() {
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>

View File

@@ -1,13 +1,16 @@
<template>
<div
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
v-if="openedBy === 'emojiPicker'"
@picked-emoji="$emit('picked-emoji', $event)"
/>
<UserProfile
v-else
/>
</div>
</div>
</template>
@@ -16,10 +19,9 @@
export default {
props: {
openedBy: {
type: String,
required: true
},
opened: Boolean
}
},
};
</script>

View File

@@ -292,6 +292,41 @@
</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>
</template>
@@ -319,10 +354,10 @@ export default {
},
computed: {
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() {
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: {
@@ -337,12 +372,14 @@ export default {
useServerStore().addChannel(this.activeServer.data.server.id, channel);
this.createChannelModelOpen = false;
navigateTo(`/channel/${channel.id}`);
},
async createInvite() {
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
const inviteCode = await $fetch(`/api/guilds/${this.activeServer.data.server.id}/createInvite`, { method: 'POST', headers });
},
async logout() {
logout() {
useUserStore().logout();
}
}

View File

@@ -1,16 +1,138 @@
<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>
<!--
<script>
import { IUser } from '~/types';
<script lang="ts">
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 {
props: {
user: {
type: IUser,
required: true
async setup() {
const userData = useUserStore().user;
async function fetchUser() {
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>

View File

@@ -13,7 +13,7 @@ import { useUserStore } from '~/stores/userStore';
import { useServerStore } from '~/stores/serverStore';
import { useDmStore } from '~/stores/dmStore';
import { useActiveStore } from '~/stores/activeStore';
import { IChannel } from '~/types';
import { IChannel, IServer, SafeUser } from '~/types';
export default {
async setup() {
@@ -25,8 +25,8 @@ export default {
if (!userStore.isLoggedIn) {
const [userData, serverData] = await Promise.all([
$fetch('/api/getCurrentUser', { headers }),
$fetch('/api/user/getServers', { headers })
$fetch('/api/getCurrentUser', { headers }) as Promise<SafeUser | null>,
$fetch('/api/user/getServers', { headers }) as Promise<{ dms: IChannel[], servers: IServer[] } | null>
]);
if (!userData || !serverData) throw new Error('No user data or server data');
@@ -40,7 +40,7 @@ export default {
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 });
if (!dmData) throw new Error('Could not find dm.');
@@ -49,12 +49,14 @@ export default {
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([
$fetch(`/api/channels/${route.params.channelId}`, { headers }) as unknown as IChannel,
$fetch(`/api/channels/${route.params.channelId}/guild`, { headers })
$fetch(`/api/channels/${route.params.channelId}`, { headers }) as Promise<IChannel | null>,
$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.');
useServerStore().addServer(server);
useActiveStore().setActiveServer(channel, useServerStore().servers);
@@ -62,14 +64,8 @@ export default {
if (isDm && !route.params.dmId) {
// 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>

19
package-lock.json generated
View File

@@ -4734,19 +4734,6 @@
"version": "1.0.0",
"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": {
"version": "1.1.1",
"license": "MIT"
@@ -13849,12 +13836,6 @@
"fs.realpath": {
"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": {
"version": "1.1.1"
},

View File

@@ -5,8 +5,8 @@
:participants="participants"
/>
<div
class="fixed mr-3"
:style="`top: ${emojiPickerData.top}px; right: ${emojiPickerData.right}px`"
class="fixed mx-3"
:style="`top: ${emojiPickerData.top}px; ${(emojiPickerData.right !== undefined) ? `right: ${emojiPickerData.right}px;` : `left: ${emojiPickerData.left}px`}`"
>
<Transition>
<Popup
@@ -22,9 +22,8 @@
import { useActiveStore } from '~/stores/activeStore';
import { useDmStore } from '~/stores/dmStore';
import { useEmojiPickerStore } from '~/stores/emojiPickerStore';
import { useServerStore } from '~/stores/serverStore';
import { useUserStore } from '~/stores/userStore';
import { IChannel, IMessage, IServer, SafeUser } from '~/types';
import { IChannel, IMessage, SafeUser } from '~/types';
definePageMeta({
middleware: 'auth'
@@ -53,10 +52,6 @@ export default {
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;
useHeadSafe({
@@ -70,12 +65,7 @@ export default {
},
data() {
return {
// socket: storeToRefs(useGlobalStore()).socket as unknown as Server,
emojiPickerData: storeToRefs(useEmojiPickerStore()).emojiPickerData,
emojiPickerStyles: {
top: storeToRefs(useEmojiPickerStore()).emojiPickerData.value.top + 'px',
right: storeToRefs(useEmojiPickerStore()).emojiPickerData.value.right + 'px',
}
};
},
methods: {

View File

@@ -21,7 +21,7 @@ export default {
};
},
mounted() {
useActiveStore().type = 'dm';
useActiveStore().setActiveHome();
},
methods: {
async startDM() {

View File

@@ -1,12 +1,12 @@
<!-- eslint-disable vue/no-multiple-template-root -->
<template>
<MessagePane
:channel="channel"
:participants="server.participants"
:channel="server.channel"
:participants="server.server.participants"
/>
<div
class="fixed mr-3"
:style="`top: ${emojiPickerData.top}px; right: ${emojiPickerData.right}px`"
class="fixed mx-3"
:style="`top: ${emojiPickerData.top}px; ${(emojiPickerData.right !== undefined) ? `right: ${emojiPickerData.right}px;` : `left: ${emojiPickerData.left}px`}`"
>
<Transition>
<Popup
@@ -49,38 +49,27 @@ export default {
}
useEmojiPickerStore().closeEmojiPicker();
const channel = useActiveStore().server.channel;
const server = useActiveStore().server.server;
channel.messages?.forEach((e: IMessage) => {
e.body = parseMessageBody(e.body, server.participants);
});
const server = useActiveStore().server;
useHeadSafe({
title: `#${channel.name} | ${server.name} - Blop`
title: `#${server.channel.name} | ${server.server.name} - Blop`
});
return {
channel,
server
};
},
data() {
return {
// socket: storeToRefs(useGlobalStore()).socket as unknown as Server,
emojiPickerData: storeToRefs(useEmojiPickerStore()).emojiPickerData,
emojiPickerStyles: {
top: storeToRefs(useEmojiPickerStore()).emojiPickerData.value.top + 'px',
right: storeToRefs(useEmojiPickerStore()).emojiPickerData.value.right + 'px',
}
};
},
async mounted() {
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;
useServerStore().addChannel(this.server.id, newChannel);
useServerStore().addChannel(this.server.server.id, newChannel);
});
},
methods: {

View File

@@ -2,7 +2,7 @@
<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">
<img
src="/plants.jpg"
src="/nahil-naseer-xljtGZ2-P3Y-unsplash.jpg"
class="h-96 w-64 object-cover"
/>
<div class="p-4 flex flex-col text-center">
@@ -47,7 +47,7 @@
import { useDmStore } from '~/stores/dmStore';
import { useServerStore } from '~/stores/serverStore';
import { useUserStore } from '~/stores/userStore';
import { SafeUser } from '~/types';
import { IChannel, IServer, SafeUser } from '~/types';
definePageMeta({
layout: 'clean'
@@ -77,8 +77,8 @@ export default {
useUserStore().setUser(loginData.user);
useServerStore().setServers(loginData.user.servers);
useDmStore().setDms(loginData.user.channels);
useServerStore().setServers(loginData.user.servers || [] as IServer[]);
useDmStore().setDms(loginData.user.channels || [] as IChannel[]);
return navigateTo('/');
}

View File

@@ -2,7 +2,7 @@
<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">
<img
src="/flowery-plants.jpg"
src="/annie-spratt-8mqOw4DBBSg-unsplash.jpg"
class="h-96 w-64 object-cover filter brightness-95"
/>
<div class="p-4 flex flex-col text-center">
@@ -54,7 +54,7 @@
import { useDmStore } from '~/stores/dmStore';
import { useServerStore } from '~/stores/serverStore';
import { useUserStore } from '~/stores/userStore';
import { SafeUser } from '~/types';
import { IChannel, IServer, SafeUser } from '~/types';
definePageMeta({
layout: 'clean'
@@ -86,8 +86,8 @@ export default {
useUserStore().setUser(signupData.user);
useServerStore().setServers(signupData.user.servers);
useDmStore().setDms(signupData.user.channels);
useServerStore().setServers(signupData.user.servers || [] as IServer[]);
useDmStore().setDms(signupData.user.channels || [] as IChannel[]);
return navigateTo('/');
}

View File

@@ -1,3 +1,31 @@
<template>
<p>hey</p>
</template>
<div class="bg-[var(--primary-bg)] h-full">
<Popup
:opened="true"
:openedBy="'userInfo'"
/>
<Popup
:opened="true"
:openedBy="'emojiPicker'"
/>
</div>
</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>

View File

@@ -105,4 +105,6 @@ model Reaction {
messageId String?
Message Message? @relation(fields: [messageId], references: [id])
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

View File

@@ -1,5 +1,5 @@
import emojiRegex from 'emoji-regex';
import { PrismaClient } from '@prisma/client';
import { Prisma, PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default defineEventHandler(async (event) => {
@@ -123,21 +123,30 @@ export default defineEventHandler(async (event) => {
}
});
} else {
reaction = await prisma.reaction.create({
data: {
emoji,
users: {
connect: [{
id: event.context.user.id,
}]
},
Message: {
connect: {
id: message.id,
try {
reaction = await prisma.reaction.create({
data: {
emoji,
users: {
connect: [{
id: event.context.user.id,
}]
},
Message: {
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;

View File

@@ -21,7 +21,7 @@ export default defineEventHandler(async (event) => {
const channelId = event.context.params.id;
if (!req || !channelId) {
if (!req.body || !channelId || !req.body.trim()) {
throw createError({
statusCode: 400,
statusMessage: 'A body is required to send a message.',
@@ -166,6 +166,7 @@ export default defineEventHandler(async (event) => {
select: {
id: true,
body: true,
createdAt: true,
creator: {
select: {
id: true,

View File

@@ -21,19 +21,6 @@ export default defineEventHandler(async (event) => {
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({
data: {
name: serverName,

View File

@@ -1,5 +1,4 @@
import { IChannel, IMessage, IServer } from '~/types';
import { useServerStore } from './serverStore';
import { IChannel, IMessage, IRole, IServer, SafeUser } from '~/types';
export const useActiveStore = defineStore('activeStore', {
state: () => ({
@@ -11,11 +10,25 @@ export const useActiveStore = defineStore('activeStore', {
}
}),
actions: {
setActiveHome() {
this.server = {
server: {} as IServer,
channel: {} as IChannel
};
this.type = 'dm';
},
setActiveDM(dm: IChannel) {
this.server = {
server: {} as IServer,
channel: {} as IChannel
};
this.type = 'dm';
this.dm = dm;
},
setActiveServer(channel: IChannel, servers: IServer[]) {
this.dm = {} as IChannel;
this.type = 'server';
const activeServer = servers.find((e: IServer) => {
@@ -32,13 +45,39 @@ export const useActiveStore = defineStore('activeStore', {
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;
this.server.server = activeServer;
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) {
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);
},
updateMessage(message: IMessage) {
@@ -52,11 +91,10 @@ export const useActiveStore = defineStore('activeStore', {
},
removeMessage(messageId: string) {
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);
}
}
});

View File

@@ -1,4 +1,4 @@
import { IChannel } from '~/types';
import { IChannel, IUser, SafeUser } from '~/types';
export const useDmStore = defineStore('dmStore', {
state: () => ({
@@ -18,6 +18,9 @@ export const useDmStore = defineStore('dmStore', {
},
getById(id: string) {
return this.dms.find((e) => e.id === id);
},
getByPartnerId(id: string) {
return this.dms.find((e) => e.dmParticipants?.some((e: SafeUser) => e.id === id));
}
}
});

View File

@@ -2,15 +2,13 @@ import { IPopupData } from '~/types';
export const useEmojiPickerStore = defineStore('emojiPickerStore', {
state: () => ({
emojiPickerData: {type: 'emojiPicker'} as IPopupData,
emojiPickerData: {} as IPopupData,
}),
actions: {
openEmojiPicker(payload: IPopupData) {
this.emojiPickerData.type = 'emojiPicker';
this.emojiPickerData.top = payload.top;
this.emojiPickerData.right = payload.right;
this.emojiPickerData.openedBy = payload.openedBy;
this.emojiPickerData.opened = true;
console.log(this.emojiPickerData, payload);
this.emojiPickerData = { ...payload, opened: true };
},
toggleEmojiPicker(payload: IPopupData) {
let messageId;
@@ -31,7 +29,7 @@ export const useEmojiPickerStore = defineStore('emojiPickerStore', {
},
closeEmojiPicker() {
if (this.emojiPickerData.openedBy) this.emojiPickerData.openedBy.messageId = '';
this.emojiPickerData.opened = false;
this.emojiPickerData = { opened: false } as IPopupData;
},
}
});

View File

@@ -77,10 +77,12 @@ export interface IReaction {
export interface IPopupData {
opened: boolean;
top: number;
right: number;
right?: number;
left?: number;
type: 'emojiPicker' | 'userInfo';
openedBy?: {
type: 'message' | 'messageInput';
messageId?: string;
};
userId?: string;
}