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

@@ -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>