dockerize, composte, and various improvements
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/recommended",
|
||||
"plugin:vue/vue3-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"globals": {
|
||||
@@ -21,7 +21,8 @@
|
||||
"useNuxtApp": true,
|
||||
"NodeJS": true,
|
||||
"useHeadSafe": true,
|
||||
"defineEmits": true
|
||||
"defineEmits": true,
|
||||
"module": true
|
||||
},
|
||||
"parser": "vue-eslint-parser",
|
||||
"parserOptions": {
|
||||
@@ -49,6 +50,12 @@
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"vue/component-tags-order": [
|
||||
"error",
|
||||
{
|
||||
"order": [ "script", "template", "style" ]
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
|
||||
13
Dockerfile
Executable file
13
Dockerfile
Executable file
@@ -0,0 +1,13 @@
|
||||
FROM node:16-alpine
|
||||
|
||||
WORKDIR /frontend
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD [ "npm", "run", "dev" ]
|
||||
@@ -15,43 +15,9 @@
|
||||
--reaction-hover: hsl(230, 31.2%, 12.5%);
|
||||
--reaction-active-border: rgb(88,101,242);
|
||||
--primary-danger: hsl(359, 66.7%, 54.1%);
|
||||
|
||||
/*
|
||||
--background-color: hsl(230, 28%, 7.3%);
|
||||
--foreground-color: hsl(230, 26%, 13%);
|
||||
--primary-accent: hsl(180, 55%, 45%);
|
||||
--message-input-color: hsl(228, 27.3%, 25%);
|
||||
--primary-placeholder: hsl(218, 11%, 65%);
|
||||
|
||||
|
||||
--primary-dark: hsl(225, 7.7%, 10.2%);
|
||||
/* dropdown and emoji picker bg
|
||||
--primary-700: hsl(230, 31.2%, 6.3%);
|
||||
/* code block border
|
||||
--primary-600: hsl(220, 6.8%, 17.3%);
|
||||
/* modal bg
|
||||
--primary-500: hsl(230, 28.7%, 9.8%);
|
||||
/* reaction button bg, code block, and inline code bg
|
||||
--primary-400: hsl(230, 12%, 19.2%);
|
||||
/* action buttons
|
||||
--primary-300: hsl(230, 26%, 15%);
|
||||
/* nav button bg
|
||||
--primary-200: hsl(230, 26%, 21.3%);
|
||||
/* nav button hover bg
|
||||
--primary-text: hsl(216, 3.7%, 73.5%);
|
||||
/* main text color (duh)
|
||||
--reaction-border: hsl(230, 33.4%, 18.7%);
|
||||
/* reaction border on hover
|
||||
--reaction-hover: hsl(230, 31.2%, 12.5%);
|
||||
/* reaction bg on hover
|
||||
--invite-members: var(--primary-accent);
|
||||
/* color of dot next to server members count on invites
|
||||
--primary-danger: hsl(359, 66.7%, 54.1%);
|
||||
*/
|
||||
}
|
||||
|
||||
* {
|
||||
color: var(--primary-text);
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
danger: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li>
|
||||
<button
|
||||
@@ -8,11 +17,3 @@
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
danger: Boolean,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,3 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
opened: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
inverted: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="pop-in">
|
||||
<div
|
||||
@@ -11,15 +24,6 @@
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: {
|
||||
opened: Boolean,
|
||||
inverted: Boolean,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dropdown {
|
||||
transform-origin: top center;
|
||||
@@ -43,11 +47,13 @@ export default {
|
||||
|
||||
@keyframes pop-in {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
transform: scale(0.7);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import emojiJson from '~/assets/json/emoji.json';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export default {
|
||||
emits: ['picked-emoji'],
|
||||
data() {
|
||||
return {
|
||||
emojis: emojiJson,
|
||||
categories: [
|
||||
defineEmits(['picked-emoji']);
|
||||
|
||||
const emojiPane = ref(null as null | HTMLDivElement);
|
||||
const categories = [
|
||||
{ name: 'people', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('smileys')).concat(emojiJson.filter((e) => e.category.toLowerCase().includes('people'))) },
|
||||
{ name: 'nature', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('nature')) },
|
||||
{ name: 'food', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('food')) },
|
||||
@@ -15,11 +14,9 @@ export default {
|
||||
{ name: 'objects', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('objects')) },
|
||||
{ name: 'symbols', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('symbols')) },
|
||||
{ name: 'flags', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('flags')) }
|
||||
],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
emojiStyles(emojiShortName: string | undefined, width: number) {
|
||||
];
|
||||
|
||||
function emojiStyles(emojiShortName: string | undefined, width: number) {
|
||||
if (!emojiShortName) return;
|
||||
|
||||
const emojis = emojiJson;
|
||||
@@ -35,21 +32,20 @@ export default {
|
||||
'background-position': `-${sheet_y + 1}px -${sheet_x + 1}px`,
|
||||
'background-size': '6480% 6480%'
|
||||
};
|
||||
},
|
||||
scrollTo(categoryName: string) {
|
||||
const emojiPane = (this.$refs.emojiPane as HTMLDivElement);
|
||||
}
|
||||
|
||||
function scrollTo(categoryName: string) {
|
||||
const category = document.getElementById(categoryName);
|
||||
if (!emojiPane || !category) return;
|
||||
emojiPane.scrollTop = category.offsetTop - 550;
|
||||
if (!emojiPane.value || !category) return;
|
||||
// eww a magic number, it's just the emoji category picker height + category name height
|
||||
emojiPane.value.scrollTop = category.offsetTop - 93;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-3">
|
||||
<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 text-[#fefefe]">
|
||||
<button
|
||||
class="p-1.5 bg-inherit hover:backdrop-brightness-125 rounded-md transition-all"
|
||||
@click="scrollTo('people')"
|
||||
|
||||
84
components/FriendChip.vue
Normal file
84
components/FriendChip.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IUser } from '~/types';
|
||||
|
||||
const props = defineProps({
|
||||
user: {
|
||||
type: Object as PropType<IUser>,
|
||||
required: true
|
||||
},
|
||||
request: {
|
||||
type: Object as PropType<{ isRequest: boolean; incoming?: boolean; outgoing?: boolean; id?: string; }>,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
async function cancelFriendRequest() {
|
||||
if (!props.request.id) return;
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
await $fetch(`/api/user/friends/${props.request.id}/cancel`, { method: 'POST', headers });
|
||||
useUserStore().removeFriendRequest(props.request.id);
|
||||
}
|
||||
|
||||
async function acceptFriendRequest() {
|
||||
if (!props.request.id) return;
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
await $fetch(`/api/user/friends/${props.request.id}/accept`, { method: 'POST', headers });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full px-3.5 py-2 flex flex-row border-t border-[var(--tertiary-bg)]">
|
||||
<div class="flex flex-row">
|
||||
<div class="bg-[var(--tertiary-bg)] w-10 h-10 rounded-full mr-2.5" />
|
||||
<div>
|
||||
{{ user.username }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto flex gap-x-2 items-center">
|
||||
<button
|
||||
v-if="request.isRequest && request.incoming"
|
||||
class="w-8 h-8 bg-[var(--tertiary-bg)] rounded-full hover:bg-[var(--tertiary-lightened-bg)] transition-colors flex items-center justify-center"
|
||||
@click="acceptFriendRequest"
|
||||
>
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="none"
|
||||
stroke="var(--primary-placeholder)"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m5 12l5 5L20 7"
|
||||
/></svg>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="request.isRequest"
|
||||
class="w-8 h-8 bg-[var(--tertiary-bg)] rounded-full hover:bg-[var(--tertiary-lightened-bg)] transition-colors flex items-center justify-center"
|
||||
@click="cancelFriendRequest"
|
||||
>
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="none"
|
||||
stroke="var(--primary-placeholder)"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18 6L6 18M6 6l12 12"
|
||||
/></svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,3 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { PropType, computed } from 'vue';
|
||||
import { useServerStore } from '~/stores/serverStore';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IInviteCode, IServer, SafeUser } from '~/types';
|
||||
|
||||
const props = defineProps({
|
||||
invite: {
|
||||
type: Object as PropType<IInviteCode>,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const user = storeToRefs(useUserStore()).user;
|
||||
|
||||
const userInServer = computed(() => {
|
||||
return !!props.invite.server.participants.find((e: SafeUser) => e.id === user.value?.id);
|
||||
});
|
||||
|
||||
async function joinServer(invite: IInviteCode) {
|
||||
if (userInServer.value || !user.value) return;
|
||||
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const { server } = await $fetch('/api/guilds/joinGuild', { method: 'POST', body: { inviteId: invite.id }, headers }) as { server: IServer };
|
||||
if (!server) return;
|
||||
|
||||
useServerStore().addServer(server);
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
props.invite.server.participants.push(user.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-6/12 bg-[var(--secondary-bg)] mb-1 mt-0.5 p-4 rounded-md shadow-md mr-2">
|
||||
<p class="text-sm font-semibold text-zinc-100">
|
||||
@@ -35,42 +67,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType } from 'vue';
|
||||
import { useServerStore } from '~/stores/serverStore';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IInviteCode, IServer, IUser, SafeUser } from '~/types';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
invite: {
|
||||
type: Object as PropType<IInviteCode>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
user: storeToRefs(useUserStore()).user,
|
||||
servers: storeToRefs(useServerStore()).servers,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
userInServer(): boolean {
|
||||
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 }) as { server: IServer };
|
||||
if (!server) return;
|
||||
|
||||
useServerStore().addServer(server);
|
||||
this.invite.server.participants.push(this.user);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,191 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative message-wrapper"
|
||||
@mouseleave="overflowShown = false"
|
||||
>
|
||||
<div
|
||||
class="absolute right-0 mr-10 -top-[20px] h-fit opacity-0 pointer-events-none action-buttons z-[5]"
|
||||
:class="(emojiPickerOpen) ? 'opacity-100 pointer-events-auto' : ''"
|
||||
>
|
||||
<div
|
||||
:id="`actions-${message.id}`"
|
||||
class="relative bg-[var(--tertiary-bg)] rounded-md border border-[rgb(32,34,37)] text-[var(--primary-text)] flex overflow-hidden"
|
||||
>
|
||||
<button
|
||||
class="p-1 hover:backdrop-brightness-125 transition-all flex w-fit h-fit"
|
||||
@click="openEmojiPicker()"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m13 19l-1 1l-7.5-7.428A5 5 0 1 1 12 6.006a5 5 0 0 1 8.003 5.996M14 16h6m-3-3v6"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="!shiftPressed && !overflowShown"
|
||||
class="p-1 hover:backdrop-brightness-125 transition-all flex w-fit h-fit"
|
||||
@click="overflowShown = true"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle
|
||||
cx="5"
|
||||
cy="12"
|
||||
r="1"
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="1"
|
||||
/>
|
||||
<circle
|
||||
cx="19"
|
||||
cy="12"
|
||||
r="1"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
v-if="shiftPressed || overflowShown"
|
||||
class="flex"
|
||||
>
|
||||
<button
|
||||
class="p-1 hover:backdrop-brightness-125 transition-all flex w-[28px] h-[28px] items-center justify-center"
|
||||
@click="copy(message.id)"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="bg-[var(--primary-text)] rounded"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="var(--tertiary-bg)"
|
||||
d="M10 7v2H9v6h1v2H6v-2h1V9H6V7h4m6 0a2 2 0 0 1 2 2v6c0 1.11-.89 2-2 2h-4V7m4 2h-2v6h2V9Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="message.creator.id === user?.id"
|
||||
class="p-1 hover:backdrop-brightness-125 transition-all flex w-[28px] h-[28px] items-center justify-center"
|
||||
@click="deleteMessage()"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="var(--primary-danger)"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 7h16m-10 4v6m4-6v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="transition-[backdrop-filter] hover:backdrop-brightness-125 ease-[cubic-bezier(.37,.64,.59,.33)] duration-150 my-4 px-7 py-2 message-wrapper items-center z-[1]"
|
||||
:class="classes"
|
||||
>
|
||||
<div class="message-content">
|
||||
<div class="message-sender-text">
|
||||
<p
|
||||
v-if="showUsername"
|
||||
class="flex flex-row"
|
||||
>
|
||||
<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>
|
||||
<div
|
||||
class="break-words max-w-full whitespace-pre-wrap"
|
||||
v-html="parseMessageBody(message.body, participants)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="invite in message.invites"
|
||||
:key="invite.id"
|
||||
>
|
||||
<InviteCard :invite="invite" />
|
||||
</div>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button
|
||||
v-for="reaction in reactions"
|
||||
:key="reaction.emoji"
|
||||
class="py-0.5 px-1.5 bg-[var(--secondary-bg)] border items-center flex rounded-lg border-[var(--tertiary-bg)] hover:border-[var(--reaction-hover-border)] hover:bg-[var(--reaction-hover)] transition-colors shadow-sm max-h-[30px]"
|
||||
:class="(reaction.users.find((e) => e.id === user?.id)) ? '!border-[var(--reaction-active-border)] hover:!border-[var(--reaction-active-border)]' : ''"
|
||||
@click="toggleReaction(reaction.emoji)"
|
||||
>
|
||||
<div class="flex items-center mr-0.5 w-6 drop-shadow">
|
||||
<span :style="emojiStyles(reaction.emoji, 17)" />
|
||||
</div>
|
||||
<div class="relative overflow-hidden ml-1.5">
|
||||
<div
|
||||
:key="reaction.users.length"
|
||||
class="min-w-[9px] h-6"
|
||||
>
|
||||
<span class="dropshadow-sm">{{ reaction.users.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType } from 'vue';
|
||||
import { IPopupData, IMessage, SafeUser, IReaction } from '~/types';
|
||||
@@ -335,6 +147,194 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative message-wrapper"
|
||||
@mouseleave="overflowShown = false"
|
||||
>
|
||||
<div
|
||||
class="absolute right-0 mr-10 -top-[20px] h-fit opacity-0 pointer-events-none action-buttons z-[5]"
|
||||
:class="(emojiPickerOpen) ? 'opacity-100 pointer-events-auto' : ''"
|
||||
>
|
||||
<div
|
||||
:id="`actions-${message.id}`"
|
||||
class="relative bg-[var(--tertiary-bg)] rounded-md border border-[rgb(32,34,37)] text-[var(--primary-text)] flex overflow-hidden"
|
||||
>
|
||||
<button
|
||||
class="p-1 hover:backdrop-brightness-125 transition-all flex w-fit h-fit"
|
||||
@click="openEmojiPicker()"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m13 19l-1 1l-7.5-7.428A5 5 0 1 1 12 6.006a5 5 0 0 1 8.003 5.996M14 16h6m-3-3v6"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="!shiftPressed && !overflowShown"
|
||||
class="p-1 hover:backdrop-brightness-125 transition-all flex w-fit h-fit"
|
||||
@click="overflowShown = true"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle
|
||||
cx="5"
|
||||
cy="12"
|
||||
r="1"
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="1"
|
||||
/>
|
||||
<circle
|
||||
cx="19"
|
||||
cy="12"
|
||||
r="1"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
v-if="shiftPressed || overflowShown"
|
||||
class="flex"
|
||||
>
|
||||
<button
|
||||
class="p-1 hover:backdrop-brightness-125 transition-all flex w-[28px] h-[28px] items-center justify-center"
|
||||
@click="copy(message.id)"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="bg-[var(--primary-text)] rounded"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="var(--tertiary-bg)"
|
||||
d="M10 7v2H9v6h1v2H6v-2h1V9H6V7h4m6 0a2 2 0 0 1 2 2v6c0 1.11-.89 2-2 2h-4V7m4 2h-2v6h2V9Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="message.creator.id === user?.id"
|
||||
class="p-1 hover:backdrop-brightness-125 transition-all flex w-[28px] h-[28px] items-center justify-center"
|
||||
@click="deleteMessage()"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="var(--primary-danger)"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 7h16m-10 4v6m4-6v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="transition-[backdrop-filter] hover:backdrop-brightness-125 ease-[cubic-bezier(.37,.64,.59,.33)] duration-150 my-4 px-7 py-2 message-wrapper items-center z-[1]"
|
||||
:class="classes"
|
||||
>
|
||||
<div class="message-content">
|
||||
<div class="message-sender-text">
|
||||
<p
|
||||
v-if="showUsername"
|
||||
class="flex flex-row"
|
||||
>
|
||||
<span
|
||||
ref="username"
|
||||
class="mb-1 font-semibold w-fit cursor-pointer hover:underline text-[#ffffff]"
|
||||
@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>
|
||||
<div
|
||||
class="break-words max-w-full whitespace-pre-wrap text-[#fafafa]"
|
||||
v-html="parseMessageBody(message.body, participants)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="invite in message.invites"
|
||||
:key="invite.id"
|
||||
>
|
||||
<InviteCard :invite="invite" />
|
||||
</div>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button
|
||||
v-for="reaction in reactions"
|
||||
:key="reaction.emoji"
|
||||
class="py-0.5 px-1.5 mt-1.5 bg-[var(--secondary-bg)] border items-center flex rounded-lg border-[var(--tertiary-bg)] hover:border-[var(--reaction-hover-border)] hover:bg-[var(--reaction-hover)] transition-colors shadow-sm max-h-[30px]"
|
||||
:class="(reaction.users.find((e) => e.id === user?.id)) ? '!border-[var(--reaction-active-border)] hover:!border-[var(--reaction-active-border)]' : ''"
|
||||
@click="toggleReaction(reaction.emoji)"
|
||||
>
|
||||
<div class="flex items-center mr-0.5 w-6 drop-shadow">
|
||||
<span :style="emojiStyles(reaction.emoji, 17)" />
|
||||
</div>
|
||||
<div class="relative overflow-hidden ml-1.5">
|
||||
<div
|
||||
:key="reaction.users.length"
|
||||
class="min-w-[9px] h-6"
|
||||
>
|
||||
<span class="dropshadow-sm text-[#efefef]">{{ reaction.users.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.message-wrapper:hover>div.action-buttons {
|
||||
opacity: 100;
|
||||
|
||||
@@ -1,178 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
id="messagePane"
|
||||
class="h-full relative bg-[var(--primary-bg)] flex flex-col"
|
||||
@mouseenter="mouseEnter"
|
||||
@mouseleave="mouseLeave"
|
||||
>
|
||||
<div class="px-4 py-3">
|
||||
<div
|
||||
v-if="!channel.DM"
|
||||
class="flex items-center"
|
||||
>
|
||||
<span class="mr-1">
|
||||
<svg
|
||||
class="text-zinc-300/80 my-auto"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m5.41 21l.71-4h-4l.35-2h4l1.06-6h-4l.35-2h4l.71-4h2l-.71 4h6l.71-4h2l-.71 4h4l-.35 2h-4l-1.06 6h4l-.35 2h-4l-.71 4h-2l.71-4h-6l-.71 4h-2M9.53 9l-1.06 6h6l1.06-6h-6Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="font-semibold">{{ channel.name }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center"
|
||||
>
|
||||
<span class="mr-1">
|
||||
<svg
|
||||
class="text-zinc-300/80 my-auto"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="4"
|
||||
/>
|
||||
<path d="M16 12v1.5a2.5 2.5 0 0 0 5 0V12a9 9 0 1 0-5.5 8.28" />
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="font-semibold">{{
|
||||
participants.find((e: SafeUser) => e.id !== user?.id)?.username
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section
|
||||
class="h-full overflow-hidden rounded-lg relative flex flex-col"
|
||||
>
|
||||
<div
|
||||
ref="conversationPane"
|
||||
class="h-[calc(100%-70px)] overflow-y-scroll"
|
||||
>
|
||||
<div class="w-full pb-1 bg-inherit">
|
||||
<div>
|
||||
<div v-if="channel.messages.length === 0">
|
||||
<p>No messages yet</p>
|
||||
</div>
|
||||
<Message
|
||||
v-for="(message, i) in channel.messages"
|
||||
v-else
|
||||
:key="message.id"
|
||||
:message="message"
|
||||
:shift-pressed="shiftPressed"
|
||||
:show-username="calculateMessageDesign(message, i).showUsername"
|
||||
:classes="calculateMessageDesign(message, i).classes"
|
||||
:channel-id="channel.id"
|
||||
:participants="participants"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="search.show"
|
||||
class="absolute bottom-[calc(75px+0.5rem)] mx-4 w-[calc(100vw-88px-240px-32px)] py-3 px-4 bg-[var(--secondary-bg)] rounded-lg shadow-md z-5"
|
||||
>
|
||||
<div class="relative flex flex-col">
|
||||
<div
|
||||
v-for="resultingUser in search.results"
|
||||
:key="resultingUser.id"
|
||||
class="mx-2 my-1 w-[calc(100vw-88px-240px-64px-16px)] px-4 py-3 hover:backdrop-brightness-125 select-none rounded-md transition-all"
|
||||
@click="completeMention(resultingUser)"
|
||||
>
|
||||
{{ resultingUser.username }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex absolute flex-row bottom-0 w-full h-fit bg-inherit">
|
||||
<form
|
||||
class="relative px-4 w-full pt-1.5 h-fit pb-1"
|
||||
@keyup="checkForMentions"
|
||||
@keypress="typing($event)"
|
||||
@submit.prevent="sendMessage"
|
||||
@keydown.enter.exact.prevent="sendMessage"
|
||||
>
|
||||
<div
|
||||
id="textbox"
|
||||
class="px-4 rounded-md w-full min-h-[44px] h-fit bg-[var(--secondary-bg)] placeholder:text-[var(--primary-placeholder)] flex flex-row"
|
||||
>
|
||||
<textarea
|
||||
ref="messageBox"
|
||||
v-model="messageContent"
|
||||
maxlength="5000"
|
||||
type="text"
|
||||
class="bg-transparent focus:outline-none py-2 w-full resize-none leading-relaxed h-[44px]"
|
||||
cols="1"
|
||||
placeholder="Send a Message..."
|
||||
/>
|
||||
<input
|
||||
id="submit"
|
||||
type="submit"
|
||||
class="absolute -top-full -left-full invisible"
|
||||
>
|
||||
<label
|
||||
for="submit"
|
||||
class="py-1 px-1.5 h-fit my-auto cursor-pointer"
|
||||
role="button"
|
||||
><svg
|
||||
width="32"
|
||||
height="26"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14L21 3m0 0l-6.5 18a.55.55 0 0 1-1 0L10 14l-7-3.5a.55.55 0 0 1 0-1L21 3"
|
||||
/>
|
||||
</svg></label>
|
||||
</div>
|
||||
<div class="w-full h-4">
|
||||
<p
|
||||
v-if="usersTyping.length > 0"
|
||||
class="text-sm"
|
||||
>
|
||||
<span v-if="usersTyping.length < 4">
|
||||
<span
|
||||
v-for="(username, i) in usersTyping"
|
||||
:key="username"
|
||||
class="font-semibold"
|
||||
>
|
||||
<span v-if="i === usersTyping.length - 1 && usersTyping.length > 1">and </span>
|
||||
{{ username }}
|
||||
<span v-if="i !== usersTyping.length - 1 && usersTyping.length > 1">, </span>
|
||||
</span>
|
||||
is typing
|
||||
</span>
|
||||
<span v-else>Several users are typing</span>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType } from 'vue';
|
||||
import { useActiveStore } from '~/stores/activeStore';
|
||||
@@ -275,7 +100,10 @@ export default {
|
||||
}
|
||||
},
|
||||
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 === 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 };
|
||||
}
|
||||
@@ -392,5 +220,178 @@ export default {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
id="messagePane"
|
||||
class="h-full relative bg-[var(--primary-bg)] flex flex-col"
|
||||
@mouseenter="mouseEnter"
|
||||
@mouseleave="mouseLeave"
|
||||
>
|
||||
<div class="px-4 py-3">
|
||||
<div
|
||||
v-if="!channel.DM"
|
||||
class="flex items-center"
|
||||
>
|
||||
<span class="mr-1">
|
||||
<svg
|
||||
class="text-zinc-300/80 my-auto"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m5.41 21l.71-4h-4l.35-2h4l1.06-6h-4l.35-2h4l.71-4h2l-.71 4h6l.71-4h2l-.71 4h4l-.35 2h-4l-1.06 6h4l-.35 2h-4l-.71 4h-2l.71-4h-6l-.71 4h-2M9.53 9l-1.06 6h6l1.06-6h-6Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="font-semibold text-[#fefefe]">{{ channel.name }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center"
|
||||
>
|
||||
<span class="mr-1">
|
||||
<svg
|
||||
class="text-zinc-300/80 my-auto"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="4"
|
||||
/>
|
||||
<path d="M16 12v1.5a2.5 2.5 0 0 0 5 0V12a9 9 0 1 0-5.5 8.28" />
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="font-semibold">{{
|
||||
participants.find((e: SafeUser) => e.id !== user?.id)?.username
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section
|
||||
class="h-full overflow-hidden rounded-lg relative flex flex-col"
|
||||
>
|
||||
<div
|
||||
ref="conversationPane"
|
||||
class="h-[calc(100%-70px)] overflow-y-scroll"
|
||||
>
|
||||
<div class="w-full pb-1 bg-inherit">
|
||||
<div>
|
||||
<div v-if="channel.messages.length === 0">
|
||||
<p>No messages yet</p>
|
||||
</div>
|
||||
<Message
|
||||
v-for="(message, i) in channel.messages"
|
||||
v-else
|
||||
:key="message.id"
|
||||
:message="message"
|
||||
:shift-pressed="shiftPressed"
|
||||
:show-username="calculateMessageDesign(message, i).showUsername"
|
||||
:classes="calculateMessageDesign(message, i).classes"
|
||||
:channel-id="channel.id"
|
||||
:participants="participants"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="search.show"
|
||||
class="absolute bottom-[calc(75px+0.5rem)] mx-4 w-[calc(100vw-88px-240px-32px)] py-3 px-4 bg-[var(--secondary-bg)] rounded-lg shadow-md z-5"
|
||||
>
|
||||
<div class="relative flex flex-col">
|
||||
<div
|
||||
v-for="resultingUser in search.results"
|
||||
:key="resultingUser.id"
|
||||
class="mx-2 my-1 w-[calc(100vw-88px-240px-64px-16px)] px-4 py-3 hover:backdrop-brightness-125 select-none rounded-md transition-all"
|
||||
@click="completeMention(resultingUser)"
|
||||
>
|
||||
{{ resultingUser.username }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex absolute flex-row bottom-0 w-full h-fit bg-inherit">
|
||||
<form
|
||||
class="relative px-4 w-full pt-1.5 h-fit pb-1"
|
||||
@keyup="checkForMentions"
|
||||
@keypress="typing($event)"
|
||||
@submit.prevent="sendMessage"
|
||||
@keydown.enter.exact.prevent="sendMessage"
|
||||
>
|
||||
<div
|
||||
id="textbox"
|
||||
class="px-4 rounded-md w-full min-h-[44px] h-fit bg-[var(--secondary-bg)] placeholder:text-[var(--primary-placeholder)] flex flex-row text-[#fefefe]"
|
||||
>
|
||||
<textarea
|
||||
ref="messageBox"
|
||||
v-model="messageContent"
|
||||
maxlength="5000"
|
||||
type="text"
|
||||
class="bg-transparent focus:outline-none py-2 w-full resize-none leading-relaxed h-[44px]"
|
||||
cols="1"
|
||||
placeholder="Send a Message..."
|
||||
/>
|
||||
<button
|
||||
class="p-1 h-fit my-auto transition-colors duration-150 ease-in rounded-md"
|
||||
:class="(messageContent.trim().length > 0) ? 'bg-blue-700 cursor-pointer' : 'text-[#78797A] cursor-default'"
|
||||
@click="sendMessage"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none">
|
||||
<path
|
||||
d="M24 0v24H0V0h24ZM12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036c-.01-.003-.019 0-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.016-.018Zm.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01l-.184-.092Z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M20.235 5.686c.432-1.195-.726-2.353-1.921-1.92L3.709 9.048c-1.199.434-1.344 2.07-.241 2.709l4.662 2.699l4.163-4.163a1 1 0 0 1 1.414 1.414L9.544 15.87l2.7 4.662c.638 1.103 2.274.957 2.708-.241l5.283-14.605Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-full h-4">
|
||||
<p
|
||||
v-if="usersTyping.length > 0"
|
||||
class="text-sm"
|
||||
>
|
||||
<span v-if="usersTyping.length < 4">
|
||||
<span
|
||||
v-for="(username, i) in usersTyping"
|
||||
:key="username"
|
||||
class="font-semibold"
|
||||
>
|
||||
<span v-if="i === usersTyping.length - 1 && usersTyping.length > 1">and </span>
|
||||
{{ username }}
|
||||
<span v-if="i !== usersTyping.length - 1 && usersTyping.length > 1">, </span>
|
||||
</span>
|
||||
is typing
|
||||
</span>
|
||||
<span v-else>Several users are typing</span>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,18 +1,3 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="opened"
|
||||
class="absolute z-10 top-0 bottom-0 left-0 right-0"
|
||||
>
|
||||
<slot />
|
||||
<div
|
||||
class="bg-black/70 w-screen h-screen"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineEmits(['close']);
|
||||
|
||||
@@ -23,3 +8,18 @@ defineProps({
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="opened"
|
||||
class="absolute z-10 top-0 bottom-0 left-0 right-0 text-[#fefefe]"
|
||||
>
|
||||
<slot />
|
||||
<div
|
||||
class="bg-black/70 w-screen h-screen"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
@@ -1,3 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import { useActiveStore } from '~/stores/activeStore';
|
||||
import { useServerStore } from '~/stores/serverStore';
|
||||
import { IServer } from '~/types';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const createServerModalOpen = ref(false);
|
||||
const serverName = ref('');
|
||||
const servers = storeToRefs(useServerStore()).servers;
|
||||
const activeConversation = ref({
|
||||
type: storeToRefs(useActiveStore()).type,
|
||||
server: storeToRefs(useActiveStore()).server
|
||||
});
|
||||
|
||||
async function createServer() {
|
||||
const serverStore = useServerStore();
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const server: IServer = await $fetch('/api/channels/create', { method: 'post', body: { serverName: serverName.value }, headers });
|
||||
createServerModalOpen.value = false;
|
||||
serverName.value = '';
|
||||
serverStore.addServer(server);
|
||||
|
||||
navigateTo(`/channel/${server.channels[0]?.id}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<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>
|
||||
@@ -134,35 +160,3 @@
|
||||
</Modal>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useActiveStore } from '~/stores/activeStore';
|
||||
import { useServerStore } from '~/stores/serverStore';
|
||||
import { IServer } from '~/types';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
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>
|
||||
@@ -1,3 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue';
|
||||
|
||||
defineProps({
|
||||
openedBy: {
|
||||
type: String as PropType<'emojiPicker' | 'userProfile'>,
|
||||
required: true
|
||||
},
|
||||
opened: Boolean
|
||||
});
|
||||
|
||||
defineEmits(['picked-emoji']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="opened"
|
||||
@@ -14,14 +28,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
openedBy: {
|
||||
required: true
|
||||
},
|
||||
opened: Boolean
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,3 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useActiveStore, useDmStore, useServerStore, useUserStore } from '~/stores';
|
||||
import { IChannel } from '~/types';
|
||||
|
||||
const activeServer = ref({
|
||||
type: storeToRefs(useActiveStore()).type,
|
||||
data: storeToRefs(useActiveStore()).server,
|
||||
});
|
||||
|
||||
const user = storeToRefs(useUserStore()).user;
|
||||
const dms = storeToRefs(useDmStore()).dms;
|
||||
const channelName = ref('');
|
||||
const createChannelModelOpen = ref(false);
|
||||
const serverDropdownOpen = ref(false);
|
||||
const userDropdownOpen = ref(false);
|
||||
|
||||
const userIsOwner = computed(() => {
|
||||
return (
|
||||
activeServer.value.type === 'server' &&
|
||||
activeServer.value.data.server.participants.find((e) => e.id === user.value?.id)?.roles?.some((e) => e.owner === true)
|
||||
);
|
||||
});
|
||||
|
||||
const userIsAdmin = computed(() => {
|
||||
return (
|
||||
activeServer.value.type === 'server' &&
|
||||
activeServer.value.data.server.participants.find((e) => e.id === user.value?.id)?.roles?.some((e) => e.administer === true)
|
||||
);
|
||||
});
|
||||
|
||||
const openCreateChannelModel = () => {
|
||||
createChannelModelOpen.value = true;
|
||||
};
|
||||
|
||||
const createChannel = async () => {
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const channel = await $fetch(
|
||||
`/api/guilds/${activeServer.value.data.server.id}/addChannel`,
|
||||
{ method: 'POST', body: { channelName: channelName.value }, headers }
|
||||
) as IChannel;
|
||||
|
||||
if (!channel) return;
|
||||
|
||||
useServerStore().addChannel(
|
||||
activeServer.value.data.server.id,
|
||||
channel
|
||||
);
|
||||
createChannelModelOpen.value = false;
|
||||
|
||||
navigateTo(`/channel/${channel.id}`);
|
||||
};
|
||||
|
||||
const createInvite = async () => {
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const inviteCode = await $fetch(
|
||||
`/api/guilds/${activeServer.value.data.server.id}/createInvite`,
|
||||
{ method: 'POST', headers }
|
||||
);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
useUserStore().logout();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="bg-[var(--secondary-bg)] min-w-60 w-60 h-screen shadow-sm text-white select-none relative z-[2]">
|
||||
<div
|
||||
@@ -15,18 +81,41 @@
|
||||
<div
|
||||
class="h-[calc(100%-12px)] grid grid-rows-[1fr_56px] bg-[var(--foreground-color)]"
|
||||
>
|
||||
<div class="h-fit">
|
||||
<div class="flex gap-y-1.5 px-1.5 mt-2 flex-col overflow-y-auto overflow-x-hidden">
|
||||
<nuxt-link to="/channel/@me">
|
||||
<button
|
||||
class="flex text-center bg-inherit px-2 py-1.5 w-full transition-all rounded-md drop-shadow-sm gap-1/5 cursor-pointer items-center hover:backdrop-brightness-[1.35]"
|
||||
>
|
||||
<span class="mr-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0-8 0M6 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"
|
||||
/></svg>
|
||||
</span>
|
||||
Friends
|
||||
</button>
|
||||
</nuxt-link>
|
||||
<hr class="w-11/12 mx-auto border border-[var(--tertiary-lightened-bg)]" />
|
||||
<nuxt-link
|
||||
v-for="dm in dms"
|
||||
:key="dm.id"
|
||||
class="hover:no-underline"
|
||||
:to="'/channel/@me/' + dm.id"
|
||||
>
|
||||
<div
|
||||
class="mx-2 my-4 bg-inherit hover:backdrop-brightness-[1.35] px-2 py-2 max-h-10 h-10 overflow-ellipsis transition-all"
|
||||
<button
|
||||
class="flex text-center bg-inherit px-2 py-1.5 w-full transition-all rounded-md drop-shadow-sm gap-1/5 cursor-pointer items-center hover:backdrop-brightness-[1.35]"
|
||||
>
|
||||
{{ dm.dmParticipants?.find((e) => e.id !== user?.id)?.username }}
|
||||
</div>
|
||||
</button>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,7 +227,7 @@
|
||||
>
|
||||
<button
|
||||
:class="(activeServer.data.channel.id === channel.id) ? 'backdrop-brightness-[1.35]' : 'hover:backdrop-brightness-[1.35]'"
|
||||
class="flex text-center bg-inherit px-2 py-1.5 w-full transition-all rounded drop-shadow-sm gap-1/5 cursor-pointer items-center"
|
||||
class="flex text-center bg-inherit px-2 py-1.5 w-full transition-all rounded-md drop-shadow-sm gap-1/5 cursor-pointer items-center"
|
||||
>
|
||||
<span class="h-fit">
|
||||
<svg
|
||||
@@ -159,7 +248,7 @@
|
||||
</nuxt-link>
|
||||
<button
|
||||
v-if="userIsOwner || userIsAdmin"
|
||||
class="flex text-center bg-inherit hover:backdrop-brightness-[1.45] px-2 py-1.5 w-full transition-all rounded drop-shadow-sm cursor-pointer items-center"
|
||||
class="flex text-center bg-inherit hover:backdrop-brightness-[1.45] px-2 py-1.5 w-full transition-all rounded-md drop-shadow-sm cursor-pointer items-center"
|
||||
@click="openCreateChannelModel"
|
||||
>
|
||||
<span>
|
||||
@@ -224,7 +313,7 @@
|
||||
</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
danger="true"
|
||||
:danger="true"
|
||||
@click="logout"
|
||||
>
|
||||
<span>
|
||||
@@ -329,59 +418,3 @@
|
||||
</Modal>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useActiveStore } from '~/stores/activeStore';
|
||||
import { useDmStore } from '~/stores/dmStore';
|
||||
import { useServerStore } from '~/stores/serverStore';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IChannel, IRole } from '~/types';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
activeServer: {
|
||||
type: storeToRefs(useActiveStore()).type,
|
||||
data: storeToRefs(useActiveStore()).server,
|
||||
},
|
||||
user: storeToRefs(useUserStore()).user,
|
||||
dms: storeToRefs(useDmStore()).dms,
|
||||
channelName: '',
|
||||
createChannelModelOpen: false,
|
||||
serverDropdownOpen: false,
|
||||
userDropdownOpen: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
userIsOwner() {
|
||||
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.type === 'server' && this.activeServer.data.server.participants.find((e) => e.id === this.user?.id)?.roles?.some((e) => e.administer === true);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openCreateChannelModel() {
|
||||
this.createChannelModelOpen = true;
|
||||
},
|
||||
async createChannel() {
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const channel = await $fetch(`/api/guilds/${this.activeServer.data.server.id}/addChannel`, { method: 'POST', body: { channelName: this.channelName }, headers }) as IChannel;
|
||||
|
||||
if (!channel) return;
|
||||
|
||||
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 });
|
||||
},
|
||||
logout() {
|
||||
useUserStore().logout();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,5 +1,64 @@
|
||||
<script lang="ts" setup>
|
||||
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';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
const userData = useUserStore().user;
|
||||
const message = ref('');
|
||||
|
||||
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();
|
||||
const roles = computed(() => {
|
||||
return user?.roles?.filter((e: IRole) => e.owner === false) || [];
|
||||
});
|
||||
const userIsOwner = computed(() => {
|
||||
return user?.roles?.some((e: IRole) => e.owner === true) || false;
|
||||
});
|
||||
|
||||
async function sendDM() {
|
||||
if (!message.value.trim()) return;
|
||||
if (!user) return;
|
||||
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const preExistingDM = useDmStore().getByPartnerId(user.id);
|
||||
|
||||
if (!preExistingDM) return;
|
||||
|
||||
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: message.value }, headers });
|
||||
message.value = '';
|
||||
|
||||
useEmojiPickerStore().closeEmojiPicker();
|
||||
}
|
||||
|
||||
if (!user || !userData) throw new Error('unknown error');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-[374px] max-h-[475px] overflow-y-scroll">
|
||||
<div class="w-[374px] max-h-[475px] overflow-y-scroll text-[#fefefe]">
|
||||
<div class="relative h-[calc(160px+56px)]">
|
||||
<div class="w-full h-40 absolute">
|
||||
<img
|
||||
@@ -69,70 +128,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
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>
|
||||
26
docker-compose.yml
Executable file
26
docker-compose.yml
Executable file
@@ -0,0 +1,26 @@
|
||||
version: '3'
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- '6379:6379'
|
||||
postgres:
|
||||
image: postgres
|
||||
environment:
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DB=discorddb
|
||||
ports:
|
||||
- '5432:5432'
|
||||
nuxt:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
volumes:
|
||||
- .:/frontend
|
||||
ports:
|
||||
- '3000:3000'
|
||||
- '24678:24678' # HMR port
|
||||
depends_on:
|
||||
- redis
|
||||
- postgres
|
||||
@@ -15,10 +15,6 @@ export default defineNuxtConfig({
|
||||
'@/assets/css/main.css'
|
||||
],
|
||||
|
||||
devtools: {
|
||||
enabled: false
|
||||
},
|
||||
|
||||
modules: [
|
||||
[
|
||||
'@pinia/nuxt',
|
||||
@@ -31,7 +27,6 @@ export default defineNuxtConfig({
|
||||
],
|
||||
},
|
||||
],
|
||||
'@nuxt/devtools'
|
||||
],
|
||||
|
||||
postcss: {
|
||||
|
||||
11427
package-lock.json
generated
11427
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "nuxi dev",
|
||||
"build": "nuxi build",
|
||||
@@ -9,7 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@pinia/nuxt": "^0.4.6",
|
||||
"@prisma/client": "^4.8.0",
|
||||
"@prisma/client": "^4.13.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"emoji-regex": "^10.2.1",
|
||||
@@ -23,7 +24,6 @@
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/devtools": "^0.4.1",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/emoji-js": "^3.5.0",
|
||||
"@types/node": "^18.15.11",
|
||||
@@ -34,7 +34,7 @@
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-plugin-vue": "^9.11.0",
|
||||
"postcss": "^8.4.23",
|
||||
"prisma": "^4.8.0",
|
||||
"prisma": "^4.13.0",
|
||||
"tailwindcss": "^3.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,43 @@
|
||||
<!-- eslint-disable vue/no-multiple-template-root -->
|
||||
<script lang="ts" setup>
|
||||
import { useActiveStore } from '~/stores/activeStore';
|
||||
import { useDmStore } from '~/stores/dmStore';
|
||||
import { useEmojiPickerStore } from '~/stores/emojiPickerStore';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IChannel, SafeUser } from '~/types';
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth'
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const emojiPickerData = storeToRefs(useEmojiPickerStore()).emojiPickerData
|
||||
|
||||
if (useDmStore().dms.find((e) => { return e.id === route.params.dmId; } ) == undefined) navigateTo('/');
|
||||
if (useActiveStore().server.channel.id !== route.params.dmId) {
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const dm: IChannel = await $fetch(`/api/channels/${route.params.dmId}`, { headers });
|
||||
if (!dm) throw new Error('couldnt find dm.');
|
||||
useDmStore().addDM(dm);
|
||||
if (typeof route.params.dmId !== 'string') throw new Error('route.params.dmId must be a string, but got an array presumably?');
|
||||
useActiveStore().setActiveDM(dm);
|
||||
useEmojiPickerStore().closeEmojiPicker();
|
||||
}
|
||||
const participants: SafeUser[] | undefined = useActiveStore().dm.dmParticipants;
|
||||
if (!participants) throw new Error('no one is in this dm?');
|
||||
const channel = useActiveStore().dm;
|
||||
const friend = participants.find((e) => e.id !== useUserStore().user?.id)?.username;
|
||||
useHeadSafe({
|
||||
title: `@${friend} - Blop`
|
||||
});
|
||||
|
||||
function pickedEmoji(emoji: string) {
|
||||
const { $emit } = useNuxtApp();
|
||||
$emit('pickedEmoji', emoji);
|
||||
useEmojiPickerStore().closeEmojiPicker();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MessagePane
|
||||
:channel="channel"
|
||||
@@ -17,63 +56,3 @@
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useActiveStore } from '~/stores/activeStore';
|
||||
import { useDmStore } from '~/stores/dmStore';
|
||||
import { useEmojiPickerStore } from '~/stores/emojiPickerStore';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IChannel, IMessage, SafeUser } from '~/types';
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth'
|
||||
});
|
||||
|
||||
export default {
|
||||
async setup() {
|
||||
const route = useRoute();
|
||||
|
||||
if (useDmStore().dms.find((e) => { return e.id === route.params.dmId; } ) == undefined) navigateTo('/');
|
||||
if (useActiveStore().server.channel.id !== route.params.dmId) {
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const dm: IChannel = await $fetch(`/api/channels/${route.params.dmId}`, { headers });
|
||||
|
||||
if (!dm) throw new Error('couldnt find dm.');
|
||||
useDmStore().addDM(dm);
|
||||
|
||||
if (typeof route.params.dmId !== 'string') throw new Error('route.params.dmId must be a string, but got an array presumably?');
|
||||
useActiveStore().setActiveDM(dm);
|
||||
useEmojiPickerStore().closeEmojiPicker();
|
||||
}
|
||||
|
||||
const participants: SafeUser[] | undefined = useActiveStore().dm.dmParticipants;
|
||||
|
||||
if (!participants) throw new Error('no one is in this dm?');
|
||||
|
||||
const channel = useActiveStore().dm;
|
||||
|
||||
const friend = participants.find((e) => e.id !== useUserStore().user?.id)?.username;
|
||||
|
||||
useHeadSafe({
|
||||
title: `@${friend} - Blop`
|
||||
});
|
||||
|
||||
return {
|
||||
channel,
|
||||
participants
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
emojiPickerData: storeToRefs(useEmojiPickerStore()).emojiPickerData,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
pickedEmoji(emoji: string) {
|
||||
const { $emit } = useNuxtApp();
|
||||
$emit('pickedEmoji', emoji);
|
||||
useEmojiPickerStore().closeEmojiPicker();
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,36 +1,120 @@
|
||||
<template>
|
||||
<form @submit.prevent="startDM">
|
||||
<input v-model="userId">
|
||||
<input type="submit">
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { useActiveStore } from '~/stores/activeStore';
|
||||
import { useDmStore } from '~/stores/dmStore';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IChannel } from '~/types';
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth'
|
||||
});
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
userId: ''
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
useActiveStore().setActiveHome();
|
||||
},
|
||||
methods: {
|
||||
async startDM() {
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const server: IChannel = await $fetch('/api/channels/createDM', { method: 'post', body: { partnerId: this.userId }, headers });
|
||||
const userId = ref('');
|
||||
const user = useUserStore().user;
|
||||
const selectedTab = ref('all' as 'all' | 'pending');
|
||||
|
||||
async function startDM() {
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const server: IChannel = await $fetch('/api/channels/createDM', { method: 'post', body: { partnerId: userId.value }, headers });
|
||||
useDmStore().addDM(server);
|
||||
useRouter().push({ path: '/channel/@me/' + server.id });
|
||||
}
|
||||
|
||||
function changeTab(type: 'all' | 'pending') {
|
||||
selectedTab.value = type;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
useActiveStore().setActiveHome();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="h-full relative bg-[var(--primary-bg)] flex flex-col text-[#fefefe]"
|
||||
>
|
||||
<header class="py-3 px-2">
|
||||
<div class="flex flex-row items-center">
|
||||
<span class="flex items-center mr-4">
|
||||
<span class="mr-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="26"
|
||||
height="26"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0-8 0M6 21v-2a4 4 0 0 1 4-4h.5m7.5 7l3.35-3.284a2.143 2.143 0 0 0 .005-3.071a2.242 2.242 0 0 0-3.129-.006l-.224.22l-.223-.22a2.242 2.242 0 0 0-3.128-.006a2.143 2.143 0 0 0-.006 3.071L18 22z"
|
||||
/></svg>
|
||||
</span>
|
||||
<span class="text-xl">
|
||||
Friends
|
||||
</span>
|
||||
</span>
|
||||
<div class="flex flex-row gap-x-2">
|
||||
<button
|
||||
class="px-2.5 py-0.5 bg-inherit backdrop-filter rounded-md transition-all drop-shadow-sm"
|
||||
:class="(selectedTab === 'all') ? 'backdrop-brightness-[1.35]' : 'hover:backdrop-brightness-[1.35]'"
|
||||
@click="changeTab('all')"
|
||||
>
|
||||
All
|
||||
</button>
|
||||
<button
|
||||
class="px-2.5 py-0.5 bg-inherit backdrop-filter rounded-md transition-all drop-shadow-sm"
|
||||
:class="(selectedTab === 'pending') ? 'backdrop-brightness-[1.35]' : 'hover:backdrop-brightness-[1.35]'"
|
||||
@click="changeTab('pending')"
|
||||
>
|
||||
Pending
|
||||
<span
|
||||
v-if="user.incomingFriendRequests?.length > 0"
|
||||
class="text-sm px-1 py-px rounded bg-blue-700"
|
||||
>{{ user?.incomingFriendRequests?.length }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="px-3 py-1.5 w-full">
|
||||
<div v-if="selectedTab === 'all'">
|
||||
<div v-if="user.friends?.length > 0">
|
||||
<div
|
||||
v-for="friend in user?.friends"
|
||||
:key="friend.id"
|
||||
>
|
||||
<FriendChip
|
||||
:user="friend"
|
||||
:request="{ isRequest: false }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
No friends yet...
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="selectedTab === 'pending'">
|
||||
<div
|
||||
v-for="outgoingRequest in user?.outgoingFriendRequests"
|
||||
:key="outgoingRequest.id"
|
||||
>
|
||||
<FriendChip
|
||||
:user="outgoingRequest.recipient"
|
||||
:request="{ isRequest: true, outgoing: true, id: outgoingRequest.id }"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="incomingRequest in user?.incomingFriendRequests"
|
||||
:key="incomingRequest.id"
|
||||
>
|
||||
<FriendChip
|
||||
:user="incomingRequest.sender"
|
||||
:request="{ isRequest: true, incoming: true, id: incomingRequest.id }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,36 +1,17 @@
|
||||
<!-- eslint-disable vue/no-multiple-template-root -->
|
||||
<template>
|
||||
<MessagePane
|
||||
:channel="server.channel"
|
||||
:participants="server.server.participants"
|
||||
/>
|
||||
<div
|
||||
class="fixed mx-3"
|
||||
:style="`top: ${emojiPickerData.top}px; ${(emojiPickerData.right !== undefined) ? `right: ${emojiPickerData.right}px;` : `left: ${emojiPickerData.left}px`}`"
|
||||
>
|
||||
<Transition>
|
||||
<Popup
|
||||
:opened="emojiPickerData.opened"
|
||||
:openedBy="emojiPickerData.type"
|
||||
@pickedEmoji="pickedEmoji($event)"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { useActiveStore } from '~/stores/activeStore';
|
||||
import { useEmojiPickerStore } from '~/stores/emojiPickerStore';
|
||||
import { useServerStore } from '~/stores/serverStore';
|
||||
import { IChannel, IMessage, IServer } from '~/types';
|
||||
import { IChannel, IServer } from '~/types';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth'
|
||||
});
|
||||
|
||||
export default {
|
||||
async setup() {
|
||||
const route = useRoute();
|
||||
const emojiPickerData = storeToRefs(useEmojiPickerStore()).emojiPickerData;
|
||||
|
||||
if (useServerStore().servers.find((e) => { return e.channels.some((e) => e.id === route.params.channelId); } ) == undefined) navigateTo('/');
|
||||
if (useActiveStore().server.channel.id !== route.params.channelId) {
|
||||
@@ -55,29 +36,37 @@ export default {
|
||||
title: `#${server.channel.name} | ${server.server.name} - Blop`
|
||||
});
|
||||
|
||||
return {
|
||||
server
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
emojiPickerData: storeToRefs(useEmojiPickerStore()).emojiPickerData,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
const { $io } = useNuxtApp();
|
||||
|
||||
(await $io).on(`addChannel-${this.server.server.id}`, (ev) => {
|
||||
const newChannel = ev as IChannel;
|
||||
useServerStore().addChannel(this.server.server.id, newChannel);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
pickedEmoji(emoji: string) {
|
||||
function pickedEmoji(emoji: string) {
|
||||
const { $emit } = useNuxtApp();
|
||||
$emit('pickedEmoji', emoji);
|
||||
useEmojiPickerStore().closeEmojiPicker();
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const { $io } = useNuxtApp();
|
||||
|
||||
(await $io).on(`addChannel-${server.server.id}`, (ev) => {
|
||||
const newChannel = ev as IChannel;
|
||||
useServerStore().addChannel(server.server.id, newChannel);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MessagePane
|
||||
:channel="server.channel"
|
||||
:participants="server.server.participants"
|
||||
/>
|
||||
<div
|
||||
class="fixed mx-3"
|
||||
:style="`top: ${emojiPickerData.top}px; ${(emojiPickerData.right !== undefined) ? `right: ${emojiPickerData.right}px;` : `left: ${emojiPickerData.left}px`}`"
|
||||
>
|
||||
<Transition>
|
||||
<Popup
|
||||
:opened="emojiPickerData.opened"
|
||||
:openedBy="emojiPickerData.type"
|
||||
@pickedEmoji="pickedEmoji($event)"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,31 +1,18 @@
|
||||
<template>
|
||||
<div v-if="user.isLoggedIn">
|
||||
Hello, {{ user.user?.username }}
|
||||
<button @click="user.logout">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<nuxt-link to="/login">
|
||||
Login
|
||||
</nuxt-link>
|
||||
or
|
||||
<nuxt-link to="/signup">
|
||||
Sign Up
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
|
||||
definePageMeta({
|
||||
middleware: 'auth'
|
||||
});
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
return { user: useUserStore() };
|
||||
}
|
||||
};
|
||||
const user = useUserStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
Hello, {{ user.user?.username }}
|
||||
<button @click="user.logout">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { useDmStore } from '~/stores/dmStore';
|
||||
import { useServerStore } from '~/stores/serverStore';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IChannel, IServer, SafeUser } from '~/types';
|
||||
import { ref } from 'vue';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'clean'
|
||||
});
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
|
||||
async function login() {
|
||||
if (!username.value || !password.value) return;
|
||||
const loginData = await $fetch('/api/login', {
|
||||
method: 'post', body: {
|
||||
username: username.value,
|
||||
password: password.value
|
||||
},
|
||||
}) as { token: string; user: SafeUser; };
|
||||
|
||||
const token = useCookie('sessionToken');
|
||||
token.value = loginData.token;
|
||||
|
||||
useUserStore().setUser(loginData.user);
|
||||
|
||||
useServerStore().setServers(loginData.user.servers || [] as IServer[]);
|
||||
useDmStore().setDms(loginData.user.channels || [] as IChannel[]);
|
||||
|
||||
return navigateTo('/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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)] text-[#fefefe]">
|
||||
<div class="bg-[var(--secondary-bg)] rounded-xl shadow-2xl flex flex-row overflow-hidden">
|
||||
<img
|
||||
src="/nahil-naseer-xljtGZ2-P3Y-unsplash.jpg"
|
||||
@@ -42,46 +77,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useDmStore } from '~/stores/dmStore';
|
||||
import { useServerStore } from '~/stores/serverStore';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IChannel, IServer, SafeUser } from '~/types';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'clean'
|
||||
});
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
password: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
if (!this.username || !this.password) return;
|
||||
const loginData = await $fetch('/api/login', {
|
||||
method: 'post', body: {
|
||||
username: this.username,
|
||||
password: this.password
|
||||
},
|
||||
}) as { token: string; user: SafeUser; };
|
||||
|
||||
const userId = useCookie('userId');
|
||||
userId.value = loginData.user.id;
|
||||
const token = useCookie('sessionToken');
|
||||
token.value = loginData.token;
|
||||
|
||||
useUserStore().setUser(loginData.user);
|
||||
|
||||
useServerStore().setServers(loginData.user.servers || [] as IServer[]);
|
||||
useDmStore().setDms(loginData.user.channels || [] as IChannel[]);
|
||||
|
||||
return navigateTo('/');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,5 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import { useDmStore } from '~/stores/dmStore';
|
||||
import { useServerStore } from '~/stores/serverStore';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IChannel, IServer, SafeUser } from '~/types';
|
||||
import { ref } from 'vue';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'clean'
|
||||
});
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const email = ref('');
|
||||
|
||||
async function signup() {
|
||||
if (!username.value || !password.value || !email.value) return;
|
||||
const signupData = await $fetch('/api/signup', {
|
||||
method: 'post', body: {
|
||||
username: username.value,
|
||||
email: email.value,
|
||||
password: password.value
|
||||
},
|
||||
}) as { token: string; user: SafeUser; };
|
||||
|
||||
const token = useCookie('sessionToken');
|
||||
token.value = signupData.token;
|
||||
|
||||
useUserStore().setUser(signupData.user);
|
||||
|
||||
useServerStore().setServers(signupData.user.servers || [] as IServer[]);
|
||||
useDmStore().setDms(signupData.user.channels || [] as IChannel[]);
|
||||
|
||||
return navigateTo('/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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)] text-[#fefefe]">
|
||||
<div class="bg-[var(--secondary-bg)] rounded-xl shadow-2xl flex flex-row overflow-hidden">
|
||||
<img
|
||||
src="/annie-spratt-8mqOw4DBBSg-unsplash.jpg"
|
||||
@@ -49,48 +86,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useDmStore } from '~/stores/dmStore';
|
||||
import { useServerStore } from '~/stores/serverStore';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IChannel, IServer, SafeUser } from '~/types';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'clean'
|
||||
});
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
email: '',
|
||||
password: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async signup() {
|
||||
if (!this.username || !this.password || !this.email) return;
|
||||
const signupData = await $fetch('/api/signup', {
|
||||
method: 'post', body: {
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
password: this.password
|
||||
},
|
||||
}) as { token: string; user: SafeUser; };
|
||||
|
||||
const userId = useCookie('userId');
|
||||
userId.value = signupData.user.id;
|
||||
const token = useCookie('sessionToken');
|
||||
token.value = signupData.token;
|
||||
|
||||
useUserStore().setUser(signupData.user);
|
||||
|
||||
useServerStore().setServers(signupData.user.servers || [] as IServer[]);
|
||||
useDmStore().setDms(signupData.user.channels || [] as IChannel[]);
|
||||
|
||||
return navigateTo('/');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,16 +1,3 @@
|
||||
<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';
|
||||
|
||||
@@ -29,3 +16,16 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-[var(--primary-bg)] h-full">
|
||||
<Popup
|
||||
:opened="true"
|
||||
:openedBy="'userInfo'"
|
||||
/>
|
||||
<Popup
|
||||
:opened="true"
|
||||
:openedBy="'emojiPicker'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +1,6 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IServer } from '~/types';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (!event.context.user.authenticated) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IChannel, IServer, SafeUser } from '~/types';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (!event.context.user.authenticated) {
|
||||
@@ -44,6 +43,9 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
},
|
||||
messages: {
|
||||
orderBy: {
|
||||
createdAt: 'asc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
body: true,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (!event.context.user.authenticated) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
import emojiRegex from 'emoji-regex';
|
||||
import { Prisma, PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (!event.context.user.authenticated) {
|
||||
@@ -37,6 +37,15 @@ export default defineEventHandler(async (event) => {
|
||||
}
|
||||
},
|
||||
reactions: {
|
||||
where: {
|
||||
users: {
|
||||
some: {
|
||||
NOT: {
|
||||
id: undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
emoji: true,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { IChannel, IServer, SafeUser, IMessage } from '~/types';
|
||||
import { Server } from 'socket.io';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
declare global {
|
||||
let io: Server;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IServer } from '~/types';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (!event.context.user.authenticated) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IChannel, SafeUser } from '~/types';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (!event.context.user.authenticated) {
|
||||
@@ -10,15 +9,17 @@ export default defineEventHandler(async (event) => {
|
||||
});
|
||||
}
|
||||
|
||||
const { partnerId } = await readBody(event);
|
||||
const body = await readBody(event);
|
||||
|
||||
if (!partnerId) {
|
||||
if (!body || !body.partnerId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'A friend is required to create a DM.',
|
||||
});
|
||||
}
|
||||
|
||||
const { partnerId } = body;
|
||||
|
||||
const partner = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: partnerId
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { SafeUser } from '~/types';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (!event.context.user.authenticated) {
|
||||
@@ -17,6 +16,34 @@ export default defineEventHandler(async (event) => {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
outgoingFriendRequests: {
|
||||
where: {
|
||||
status: 'sent'
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
recipient: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
incomingFriendRequests: {
|
||||
where: {
|
||||
status: 'sent'
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
sender: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
friends: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IChannel, IServer, SafeUser } from '~/types';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (!event.context.user.authenticated) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IInviteCode, IServer, SafeUser } from '~/types';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (!event.context.user.authenticated) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IServer } from '~/types';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (!event.context.user.authenticated) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IInviteCode, IServer } from '~/types';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (!event.context.user.authenticated) {
|
||||
|
||||
@@ -2,9 +2,8 @@ import bcryptjs from 'bcryptjs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as dotenv from 'dotenv';
|
||||
import crypto from 'node:crypto';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { IUser } from '~/types';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
dotenv.config();
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -19,13 +18,47 @@ export default defineEventHandler(async (event) => {
|
||||
});
|
||||
}
|
||||
|
||||
let user = await prisma.user.findFirst({
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
username: body.username
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
outgoingFriendRequests: {
|
||||
where: {
|
||||
status: 'sent'
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
recipient: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
incomingFriendRequests: {
|
||||
where: {
|
||||
status: 'sent'
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
sender: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
friends: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
}
|
||||
},
|
||||
passwordhash: true,
|
||||
servers: {
|
||||
select: {
|
||||
|
||||
@@ -2,9 +2,8 @@ import bcryptjs from 'bcryptjs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import * as dotenv from 'dotenv';
|
||||
import crypto from 'node:crypto';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { IUser, SafeUser } from '~/types';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
dotenv.config();
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -50,6 +49,40 @@ export default defineEventHandler(async (event) => {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
outgoingFriendRequests: {
|
||||
where: {
|
||||
status: 'sent'
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
recipient: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
incomingFriendRequests: {
|
||||
where: {
|
||||
status: 'sent'
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
sender: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
friends: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
}
|
||||
},
|
||||
servers: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { IServer } from '~/types';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (!event.context.user.authenticated) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (!event.context.user.authenticated) {
|
||||
|
||||
69
server/api/user/friends/[id]/accept.post.ts
Normal file
69
server/api/user/friends/[id]/accept.post.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (!event.context.user.authenticated) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Unauthenticated',
|
||||
});
|
||||
}
|
||||
|
||||
const requestId = event.context.params?.id;
|
||||
|
||||
if (!requestId) return;
|
||||
|
||||
const request = await prisma.friendRequest.findFirst({
|
||||
where: {
|
||||
id: requestId
|
||||
}
|
||||
});
|
||||
|
||||
if (!request?.id) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'friend request not found.'
|
||||
});
|
||||
}
|
||||
|
||||
if (request?.recipientId !== event.context.user.id) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'You do not have permission to accept this friend request.'
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: event.context.user.id
|
||||
},
|
||||
data: {
|
||||
friends: {
|
||||
connect: [{ id: request.senderId }]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: request.senderId
|
||||
},
|
||||
data: {
|
||||
friends: {
|
||||
connect: [{ id: event.context.user.id }]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await prisma.friendRequest.update({
|
||||
where: {
|
||||
id: requestId
|
||||
},
|
||||
data: {
|
||||
status: 'accepted'
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
message: 'friend request accepted successfully.'
|
||||
};
|
||||
});
|
||||
37
server/api/user/friends/[id]/cancel.post.ts
Normal file
37
server/api/user/friends/[id]/cancel.post.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (!event.context.user.authenticated) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Unauthenticated',
|
||||
});
|
||||
}
|
||||
|
||||
const requestId = event.context.params?.id;
|
||||
|
||||
if (!requestId) return;
|
||||
|
||||
const request = await prisma.friendRequest.findFirst({
|
||||
where: {
|
||||
id: requestId
|
||||
}
|
||||
});
|
||||
|
||||
if (request?.senderId !== event.context.user.id && request?.recipientId !== event.context.user.id) {
|
||||
throw createError({
|
||||
statusCode: 403,
|
||||
statusMessage: 'You do not have permission to cancel this friend request.'
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.friendRequest.delete({
|
||||
where: {
|
||||
id: requestId
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
message: 'successfully cancelled this friend request.'
|
||||
};
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { IChannel, IServer } from '~/types';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
if (!event.context.user.authenticated) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { sessionToken } = parseCookies(event);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const cookies = parseCookies(event);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Redis from 'ioredis';
|
||||
const redis = new Redis();
|
||||
import redis from '~/server/utils/redis';
|
||||
|
||||
const INCREMENT_LIMIT = 5;
|
||||
const LIMIT_TIME = 700; // milliseconds
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Server } from 'socket.io';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { IChannel, IServer, IUser, SafeUser } from '~~/types';
|
||||
import { Socket } from 'socket.io';
|
||||
const prisma = new PrismaClient();
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import { IChannel, IServer, IUser, SafeUser } from '~/types';
|
||||
import prisma from '~/server/utils/prisma';
|
||||
|
||||
export default defineEventHandler(({ node }) => {
|
||||
if (global.io) return;
|
||||
|
||||
4
server/utils/prisma.ts
Normal file
4
server/utils/prisma.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default prisma;
|
||||
6
server/utils/redis.ts
Normal file
6
server/utils/redis.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import Redis from 'ioredis';
|
||||
const redis = new Redis({
|
||||
host: 'redis'
|
||||
});
|
||||
|
||||
export default redis;
|
||||
7
stores/index.ts
Normal file
7
stores/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { useActiveStore } from "./activeStore";
|
||||
import { useDmStore } from "./dmStore";
|
||||
import { useUserStore } from "./userStore";
|
||||
import { useServerStore } from "./serverStore";
|
||||
import { useEmojiPickerStore } from "./emojiPickerStore";
|
||||
|
||||
export { useActiveStore, useDmStore, useEmojiPickerStore, useServerStore, useUserStore };
|
||||
@@ -12,14 +12,14 @@ export const useUserStore = defineStore('userStore', {
|
||||
const that = this;
|
||||
|
||||
return new Promise<boolean>(resolve => {
|
||||
function checkFlag() {
|
||||
function checkAuthStatus() {
|
||||
if(that.isLoggedIn === true) {
|
||||
resolve(true);
|
||||
}
|
||||
|
||||
setTimeout(checkFlag, 100);
|
||||
setTimeout(checkAuthStatus, 100);
|
||||
}
|
||||
checkFlag();
|
||||
checkAuthStatus();
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -28,13 +28,17 @@ export const useUserStore = defineStore('userStore', {
|
||||
this.user = user;
|
||||
this.isLoggedIn = true;
|
||||
},
|
||||
removeFriendRequest(friendRequestId: string) {
|
||||
const type = (this.user?.incomingFriendRequests?.find((e) => e.id === friendRequestId)) ? 'incomingFriendRequests' : 'outgoingFriendRequests';
|
||||
|
||||
this.user[type] = this.user[type].filter((e) => e.id !== friendRequestId);
|
||||
},
|
||||
async logout() {
|
||||
const { $io, $emit } = useNuxtApp();
|
||||
|
||||
(await $io).disconnect();
|
||||
await $fetch('/api/user/logout');
|
||||
useCookie('sessionToken').value = null;
|
||||
useCookie('userId').value = null;
|
||||
|
||||
this.user = null;
|
||||
this.isLoggedIn = false;
|
||||
|
||||
@@ -2,6 +2,9 @@ export interface IUser {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
friends?: Array<IUser>;
|
||||
outgoingFriendRequests?: Array<IFriendRequest>;
|
||||
incomingFriendRequests?: Array<IFriendRequest>;
|
||||
online?: boolean;
|
||||
passwordhash: string;
|
||||
servers?: Array<IServer>;
|
||||
@@ -86,3 +89,9 @@ export interface IPopupData {
|
||||
};
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export interface IFriendRequest {
|
||||
id: string;
|
||||
recipient?: IUser;
|
||||
sender?: IUser;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export default function parseBody(body: string, participants: SafeUser[]) {
|
||||
[/~~\s?([^\n]+)~~/g, '<s>$1</s>'],
|
||||
|
||||
// code lines and blocks
|
||||
[/```(.+?)```/g, '<pre class=\'codeblock\'><code>$1</code></pre>'],
|
||||
[/```( )?(.+?)```/g, '<pre class=\'codeblock\'><code>$2</code></pre>'],
|
||||
[/(?<!`)`(.+?)`(?!`)/g, '<code class=\'inline-code\'>$1</code>'],
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user