dockerize, composte, and various improvements
This commit is contained in:
131
.eslintrc.json
131
.eslintrc.json
@@ -1,64 +1,71 @@
|
|||||||
{
|
{
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"es2021": true
|
"es2021": true
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:vue/recommended",
|
"plugin:vue/vue3-recommended",
|
||||||
"plugin:@typescript-eslint/recommended"
|
"plugin:@typescript-eslint/recommended"
|
||||||
],
|
],
|
||||||
"globals": {
|
"globals": {
|
||||||
"useCookie": true,
|
"useCookie": true,
|
||||||
"$fetch": true,
|
"$fetch": true,
|
||||||
"definePageMeta": true,
|
"definePageMeta": true,
|
||||||
"navigateTo": true,
|
"navigateTo": true,
|
||||||
"useRoute": true,
|
"useRoute": true,
|
||||||
"useRouter": true,
|
"useRouter": true,
|
||||||
"useRequestHeaders": true,
|
"useRequestHeaders": true,
|
||||||
"parseMessageBody": true,
|
"parseMessageBody": true,
|
||||||
"storeToRefs": true,
|
"storeToRefs": true,
|
||||||
"useNuxtApp": true,
|
"useNuxtApp": true,
|
||||||
"NodeJS": true,
|
"NodeJS": true,
|
||||||
"useHeadSafe": true,
|
"useHeadSafe": true,
|
||||||
"defineEmits": true
|
"defineEmits": true,
|
||||||
},
|
"module": true
|
||||||
"parser": "vue-eslint-parser",
|
},
|
||||||
"parserOptions": {
|
"parser": "vue-eslint-parser",
|
||||||
"parser": "@typescript-eslint/parser",
|
"parserOptions": {
|
||||||
"ecmaVersion": "latest",
|
"parser": "@typescript-eslint/parser",
|
||||||
"sourceType": "module"
|
"ecmaVersion": "latest",
|
||||||
},
|
"sourceType": "module"
|
||||||
"plugins": [
|
},
|
||||||
"vue",
|
"plugins": [
|
||||||
"@typescript-eslint"
|
"vue",
|
||||||
],
|
"@typescript-eslint"
|
||||||
"rules": {
|
],
|
||||||
"indent": [
|
"rules": {
|
||||||
"error",
|
"indent": [
|
||||||
"tab"
|
"error",
|
||||||
],
|
"tab"
|
||||||
"linebreak-style": [
|
],
|
||||||
"error",
|
"linebreak-style": [
|
||||||
"unix"
|
"error",
|
||||||
],
|
"unix"
|
||||||
"quotes": [
|
],
|
||||||
"error",
|
"quotes": [
|
||||||
"single"
|
"error",
|
||||||
],
|
"single"
|
||||||
"semi": [
|
],
|
||||||
"error",
|
"semi": [
|
||||||
"always"
|
"error",
|
||||||
]
|
"always"
|
||||||
},
|
],
|
||||||
"overrides": [
|
"vue/component-tags-order": [
|
||||||
{
|
"error",
|
||||||
"files": ["*.vue"],
|
{
|
||||||
"rules": {
|
"order": [ "script", "template", "style" ]
|
||||||
"vue/multi-word-component-names": "off",
|
}
|
||||||
"vue/attribute-hyphenation": "off",
|
]
|
||||||
"vue/html-self-closing": "off"
|
},
|
||||||
}
|
"overrides": [
|
||||||
}
|
{
|
||||||
]
|
"files": ["*.vue"],
|
||||||
|
"rules": {
|
||||||
|
"vue/multi-word-component-names": "off",
|
||||||
|
"vue/attribute-hyphenation": "off",
|
||||||
|
"vue/html-self-closing": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
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-hover: hsl(230, 31.2%, 12.5%);
|
||||||
--reaction-active-border: rgb(88,101,242);
|
--reaction-active-border: rgb(88,101,242);
|
||||||
--primary-danger: hsl(359, 66.7%, 54.1%);
|
--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;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
danger: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
@@ -8,11 +17,3 @@
|
|||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</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>
|
<template>
|
||||||
<Transition name="pop-in">
|
<Transition name="pop-in">
|
||||||
<div
|
<div
|
||||||
@@ -11,15 +24,6 @@
|
|||||||
</Transition>
|
</Transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
opened: Boolean,
|
|
||||||
inverted: Boolean,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.dropdown {
|
.dropdown {
|
||||||
transform-origin: top center;
|
transform-origin: top center;
|
||||||
@@ -43,11 +47,13 @@ export default {
|
|||||||
|
|
||||||
@keyframes pop-in {
|
@keyframes pop-in {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(0);
|
transform: scale(0.7);
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,55 +1,51 @@
|
|||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import emojiJson from '~/assets/json/emoji.json';
|
import emojiJson from '~/assets/json/emoji.json';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
export default {
|
defineEmits(['picked-emoji']);
|
||||||
emits: ['picked-emoji'],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
emojis: emojiJson,
|
|
||||||
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')) },
|
|
||||||
{ name: 'activities', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('activities')) },
|
|
||||||
{ name: 'travel', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('travel')) },
|
|
||||||
{ 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) {
|
|
||||||
if (!emojiShortName) return;
|
|
||||||
|
|
||||||
const emojis = emojiJson;
|
const emojiPane = ref(null as null | HTMLDivElement);
|
||||||
const emoji = emojis.find((e) => e.short_names[0] === emojiShortName);
|
const categories = [
|
||||||
if (!emoji) return;
|
{ name: 'people', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('smileys')).concat(emojiJson.filter((e) => e.category.toLowerCase().includes('people'))) },
|
||||||
const sheet_x = (emoji.sheet_y * (width + 2));
|
{ name: 'nature', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('nature')) },
|
||||||
const sheet_y = (emoji.sheet_x * (width + 2));
|
{ name: 'food', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('food')) },
|
||||||
return {
|
{ name: 'activities', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('activities')) },
|
||||||
background: 'url(/32.png)',
|
{ name: 'travel', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('travel')) },
|
||||||
width: `${width}px`,
|
{ name: 'objects', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('objects')) },
|
||||||
height: `${width}px`,
|
{ name: 'symbols', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('symbols')) },
|
||||||
display: 'inline-block',
|
{ name: 'flags', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('flags')) }
|
||||||
'background-position': `-${sheet_y + 1}px -${sheet_x + 1}px`,
|
];
|
||||||
'background-size': '6480% 6480%'
|
|
||||||
};
|
function emojiStyles(emojiShortName: string | undefined, width: number) {
|
||||||
},
|
if (!emojiShortName) return;
|
||||||
scrollTo(categoryName: string) {
|
|
||||||
const emojiPane = (this.$refs.emojiPane as HTMLDivElement);
|
const emojis = emojiJson;
|
||||||
const category = document.getElementById(categoryName);
|
const emoji = emojis.find((e) => e.short_names[0] === emojiShortName);
|
||||||
if (!emojiPane || !category) return;
|
if (!emoji) return;
|
||||||
emojiPane.scrollTop = category.offsetTop - 550;
|
const sheet_x = (emoji.sheet_y * (width + 2));
|
||||||
}
|
const sheet_y = (emoji.sheet_x * (width + 2));
|
||||||
}
|
return {
|
||||||
};
|
background: 'url(/32.png)',
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${width}px`,
|
||||||
|
display: 'inline-block',
|
||||||
|
'background-position': `-${sheet_y + 1}px -${sheet_x + 1}px`,
|
||||||
|
'background-size': '6480% 6480%'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollTo(categoryName: string) {
|
||||||
|
const category = document.getElementById(categoryName);
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<div class="py-1.5 flex flex-col">
|
<div class="py-1.5 flex flex-col">
|
||||||
<div class="flex-row gap-x-2 overflow-x-scroll">
|
<div class="flex-row gap-x-2 overflow-x-scroll text-[#fefefe]">
|
||||||
<button
|
<button
|
||||||
class="p-1.5 bg-inherit hover:backdrop-brightness-125 rounded-md transition-all"
|
class="p-1.5 bg-inherit hover:backdrop-brightness-125 rounded-md transition-all"
|
||||||
@click="scrollTo('people')"
|
@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>
|
<template>
|
||||||
<div class="w-6/12 bg-[var(--secondary-bg)] mb-1 mt-0.5 p-4 rounded-md shadow-md mr-2">
|
<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">
|
<p class="text-sm font-semibold text-zinc-100">
|
||||||
@@ -35,42 +67,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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">
|
<script lang="ts">
|
||||||
import { PropType } from 'vue';
|
import { PropType } from 'vue';
|
||||||
import { IPopupData, IMessage, SafeUser, IReaction } from '~/types';
|
import { IPopupData, IMessage, SafeUser, IReaction } from '~/types';
|
||||||
@@ -335,6 +147,194 @@ export default {
|
|||||||
};
|
};
|
||||||
</script>
|
</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>
|
<style>
|
||||||
.message-wrapper:hover>div.action-buttons {
|
.message-wrapper:hover>div.action-buttons {
|
||||||
opacity: 100;
|
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">
|
<script lang="ts">
|
||||||
import { PropType } from 'vue';
|
import { PropType } from 'vue';
|
||||||
import { useActiveStore } from '~/stores/activeStore';
|
import { useActiveStore } from '~/stores/activeStore';
|
||||||
@@ -275,7 +100,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
calculateMessageDesign(message: IMessage, i: number) {
|
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) {
|
if (i !== this.channel.messages.length - 1) {
|
||||||
return { classes: 'mb-0 pb-0.5', showUsername: true };
|
return { classes: 'mb-0 pb-0.5', showUsername: true };
|
||||||
}
|
}
|
||||||
@@ -392,5 +220,178 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</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">
|
<script setup lang="ts">
|
||||||
defineEmits(['close']);
|
defineEmits(['close']);
|
||||||
|
|
||||||
@@ -23,3 +8,18 @@ defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</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>
|
<template>
|
||||||
<nav class="bg-[var(--primary-bg)] h-screen p-4 grid grid-cols-1 grid-rows-[56px_1fr_56px] shadow shadow-black/80">
|
<nav class="bg-[var(--primary-bg)] h-screen p-4 grid grid-cols-1 grid-rows-[56px_1fr_56px] shadow shadow-black/80">
|
||||||
<div>
|
<div>
|
||||||
@@ -134,35 +160,3 @@
|
|||||||
</Modal>
|
</Modal>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</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>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="opened"
|
v-if="opened"
|
||||||
@@ -14,14 +28,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<template>
|
||||||
<aside class="bg-[var(--secondary-bg)] min-w-60 w-60 h-screen shadow-sm text-white select-none relative z-[2]">
|
<aside class="bg-[var(--secondary-bg)] min-w-60 w-60 h-screen shadow-sm text-white select-none relative z-[2]">
|
||||||
<div
|
<div
|
||||||
@@ -15,18 +81,41 @@
|
|||||||
<div
|
<div
|
||||||
class="h-[calc(100%-12px)] grid grid-rows-[1fr_56px] bg-[var(--foreground-color)]"
|
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
|
<nuxt-link
|
||||||
v-for="dm in dms"
|
v-for="dm in dms"
|
||||||
:key="dm.id"
|
:key="dm.id"
|
||||||
class="hover:no-underline"
|
class="hover:no-underline"
|
||||||
:to="'/channel/@me/' + dm.id"
|
:to="'/channel/@me/' + dm.id"
|
||||||
>
|
>
|
||||||
<div
|
<button
|
||||||
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"
|
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 }}
|
{{ dm.dmParticipants?.find((e) => e.id !== user?.id)?.username }}
|
||||||
</div>
|
</button>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,7 +227,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
:class="(activeServer.data.channel.id === channel.id) ? 'backdrop-brightness-[1.35]' : 'hover:backdrop-brightness-[1.35]'"
|
: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">
|
<span class="h-fit">
|
||||||
<svg
|
<svg
|
||||||
@@ -159,7 +248,7 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<button
|
<button
|
||||||
v-if="userIsOwner || userIsAdmin"
|
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"
|
@click="openCreateChannelModel"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
@@ -224,7 +313,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
danger="true"
|
:danger="true"
|
||||||
@click="logout"
|
@click="logout"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
@@ -329,59 +418,3 @@
|
|||||||
</Modal>
|
</Modal>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</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>
|
<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="relative h-[calc(160px+56px)]">
|
||||||
<div class="w-full h-40 absolute">
|
<div class="w-full h-40 absolute">
|
||||||
<img
|
<img
|
||||||
@@ -69,70 +128,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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'
|
'@/assets/css/main.css'
|
||||||
],
|
],
|
||||||
|
|
||||||
devtools: {
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
|
|
||||||
modules: [
|
modules: [
|
||||||
[
|
[
|
||||||
'@pinia/nuxt',
|
'@pinia/nuxt',
|
||||||
@@ -31,7 +27,6 @@ export default defineNuxtConfig({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
'@nuxt/devtools'
|
|
||||||
],
|
],
|
||||||
|
|
||||||
postcss: {
|
postcss: {
|
||||||
|
|||||||
11431
package-lock.json
generated
11431
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxi dev",
|
"dev": "nuxi dev",
|
||||||
"build": "nuxi build",
|
"build": "nuxi build",
|
||||||
@@ -9,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pinia/nuxt": "^0.4.6",
|
"@pinia/nuxt": "^0.4.6",
|
||||||
"@prisma/client": "^4.8.0",
|
"@prisma/client": "^4.13.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"emoji-regex": "^10.2.1",
|
"emoji-regex": "^10.2.1",
|
||||||
@@ -23,7 +24,6 @@
|
|||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt/devtools": "^0.4.1",
|
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@types/emoji-js": "^3.5.0",
|
"@types/emoji-js": "^3.5.0",
|
||||||
"@types/node": "^18.15.11",
|
"@types/node": "^18.15.11",
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"eslint": "^8.38.0",
|
"eslint": "^8.38.0",
|
||||||
"eslint-plugin-vue": "^9.11.0",
|
"eslint-plugin-vue": "^9.11.0",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.23",
|
||||||
"prisma": "^4.8.0",
|
"prisma": "^4.13.0",
|
||||||
"tailwindcss": "^3.3.1"
|
"tailwindcss": "^3.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,43 @@
|
|||||||
<!-- eslint-disable vue/no-multiple-template-root -->
|
<!-- 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>
|
<template>
|
||||||
<MessagePane
|
<MessagePane
|
||||||
:channel="channel"
|
:channel="channel"
|
||||||
@@ -17,63 +56,3 @@
|
|||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<script lang="ts" setup>
|
||||||
<form @submit.prevent="startDM">
|
|
||||||
<input v-model="userId">
|
|
||||||
<input type="submit">
|
|
||||||
</form>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { useActiveStore } from '~/stores/activeStore';
|
import { useActiveStore } from '~/stores/activeStore';
|
||||||
import { useDmStore } from '~/stores/dmStore';
|
import { useDmStore } from '~/stores/dmStore';
|
||||||
|
import { useUserStore } from '~/stores/userStore';
|
||||||
import { IChannel } from '~/types';
|
import { IChannel } from '~/types';
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'auth'
|
middleware: 'auth'
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
const userId = ref('');
|
||||||
data() {
|
const user = useUserStore().user;
|
||||||
return {
|
const selectedTab = ref('all' as 'all' | 'pending');
|
||||||
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 });
|
|
||||||
|
|
||||||
useDmStore().addDM(server);
|
async function startDM() {
|
||||||
useRouter().push({ path: '/channel/@me/' + server.id });
|
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>
|
</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,4 +1,57 @@
|
|||||||
<!-- eslint-disable vue/no-multiple-template-root -->
|
<!-- eslint-disable vue/no-multiple-template-root -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useActiveStore } from '~/stores/activeStore';
|
||||||
|
import { useEmojiPickerStore } from '~/stores/emojiPickerStore';
|
||||||
|
import { useServerStore } from '~/stores/serverStore';
|
||||||
|
import { IChannel, IServer } from '~/types';
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
middleware: 'auth'
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||||
|
const [channel, server] = await Promise.all([
|
||||||
|
await $fetch(`/api/channels/${route.params.channelId}`, { headers }) as IChannel,
|
||||||
|
await $fetch(`/api/channels/${route.params.channelId}/guild`, { headers }) as IServer,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!server) throw new Error('server not found, this means that the channel is serverless but not a dm????');
|
||||||
|
useServerStore().addServer(server);
|
||||||
|
|
||||||
|
if (typeof route.params.channelId !== 'string') throw new Error('route.params.id must be a string, but got an array presumably?');
|
||||||
|
useActiveStore().setActiveServer(channel, useServerStore().servers);
|
||||||
|
useEmojiPickerStore().closeEmojiPicker();
|
||||||
|
}
|
||||||
|
useEmojiPickerStore().closeEmojiPicker();
|
||||||
|
|
||||||
|
const server = useActiveStore().server;
|
||||||
|
|
||||||
|
useHeadSafe({
|
||||||
|
title: `#${server.channel.name} | ${server.server.name} - Blop`
|
||||||
|
});
|
||||||
|
|
||||||
|
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>
|
<template>
|
||||||
<MessagePane
|
<MessagePane
|
||||||
:channel="server.channel"
|
:channel="server.channel"
|
||||||
@@ -17,67 +70,3 @@
|
|||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { useActiveStore } from '~/stores/activeStore';
|
|
||||||
import { useEmojiPickerStore } from '~/stores/emojiPickerStore';
|
|
||||||
import { useServerStore } from '~/stores/serverStore';
|
|
||||||
import { IChannel, IMessage, IServer } from '~/types';
|
|
||||||
|
|
||||||
definePageMeta({
|
|
||||||
middleware: 'auth'
|
|
||||||
});
|
|
||||||
|
|
||||||
export default {
|
|
||||||
async setup() {
|
|
||||||
const route = useRoute();
|
|
||||||
|
|
||||||
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) {
|
|
||||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
|
||||||
const [channel, server] = await Promise.all([
|
|
||||||
await $fetch(`/api/channels/${route.params.channelId}`, { headers }) as IChannel,
|
|
||||||
await $fetch(`/api/channels/${route.params.channelId}/guild`, { headers }) as IServer,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!server) throw new Error('server not found, this means that the channel is serverless but not a dm????');
|
|
||||||
useServerStore().addServer(server);
|
|
||||||
|
|
||||||
if (typeof route.params.channelId !== 'string') throw new Error('route.params.id must be a string, but got an array presumably?');
|
|
||||||
useActiveStore().setActiveServer(channel, useServerStore().servers);
|
|
||||||
useEmojiPickerStore().closeEmojiPicker();
|
|
||||||
}
|
|
||||||
useEmojiPickerStore().closeEmojiPicker();
|
|
||||||
|
|
||||||
const server = useActiveStore().server;
|
|
||||||
|
|
||||||
useHeadSafe({
|
|
||||||
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) {
|
|
||||||
const { $emit } = useNuxtApp();
|
|
||||||
$emit('pickedEmoji', emoji);
|
|
||||||
useEmojiPickerStore().closeEmojiPicker();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
@@ -1,31 +1,18 @@
|
|||||||
<template>
|
<script lang="ts" setup>
|
||||||
<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">
|
|
||||||
import { useUserStore } from '~/stores/userStore';
|
import { useUserStore } from '~/stores/userStore';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: 'auth'
|
middleware: 'auth'
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
const user = useUserStore();
|
||||||
setup() {
|
|
||||||
return { user: useUserStore() };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</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>
|
<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">
|
<div class="bg-[var(--secondary-bg)] rounded-xl shadow-2xl flex flex-row overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src="/nahil-naseer-xljtGZ2-P3Y-unsplash.jpg"
|
src="/nahil-naseer-xljtGZ2-P3Y-unsplash.jpg"
|
||||||
@@ -42,46 +77,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<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">
|
<div class="bg-[var(--secondary-bg)] rounded-xl shadow-2xl flex flex-row overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src="/annie-spratt-8mqOw4DBBSg-unsplash.jpg"
|
src="/annie-spratt-8mqOw4DBBSg-unsplash.jpg"
|
||||||
@@ -49,48 +86,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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">
|
<script lang="ts">
|
||||||
import { useEmojiPickerStore } from '~/stores/emojiPickerStore';
|
import { useEmojiPickerStore } from '~/stores/emojiPickerStore';
|
||||||
|
|
||||||
@@ -29,3 +16,16 @@ export default {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</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 {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
|
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { IServer } from '~/types';
|
import { IServer } from '~/types';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import prisma from '~/server/utils/prisma';
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
if (!event.context.user.authenticated) {
|
if (!event.context.user.authenticated) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { IChannel, IServer, SafeUser } from '~/types';
|
import { IChannel, IServer, SafeUser } from '~/types';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import prisma from '~/server/utils/prisma';
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
if (!event.context.user.authenticated) {
|
if (!event.context.user.authenticated) {
|
||||||
@@ -44,6 +43,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
messages: {
|
messages: {
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'asc',
|
||||||
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
body: true,
|
body: true,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import prisma from '~/server/utils/prisma';
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
if (!event.context.user.authenticated) {
|
if (!event.context.user.authenticated) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { Prisma } from '@prisma/client';
|
||||||
import emojiRegex from 'emoji-regex';
|
import emojiRegex from 'emoji-regex';
|
||||||
import { Prisma, PrismaClient } from '@prisma/client';
|
import prisma from '~/server/utils/prisma';
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
if (!event.context.user.authenticated) {
|
if (!event.context.user.authenticated) {
|
||||||
@@ -37,6 +37,15 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
reactions: {
|
reactions: {
|
||||||
|
where: {
|
||||||
|
users: {
|
||||||
|
some: {
|
||||||
|
NOT: {
|
||||||
|
id: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
emoji: true,
|
emoji: true,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { IChannel, IServer, SafeUser, IMessage } from '~/types';
|
import { IChannel, IServer, SafeUser, IMessage } from '~/types';
|
||||||
import { Server } from 'socket.io';
|
import { Server } from 'socket.io';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import prisma from '~/server/utils/prisma';
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
let io: Server;
|
let io: Server;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { IServer } from '~/types';
|
import { IServer } from '~/types';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import prisma from '~/server/utils/prisma';
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
if (!event.context.user.authenticated) {
|
if (!event.context.user.authenticated) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { IChannel, SafeUser } from '~/types';
|
import { IChannel, SafeUser } from '~/types';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import prisma from '~/server/utils/prisma';
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
if (!event.context.user.authenticated) {
|
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({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
statusMessage: 'A friend is required to create a DM.',
|
statusMessage: 'A friend is required to create a DM.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { partnerId } = body;
|
||||||
|
|
||||||
const partner = await prisma.user.findFirst({
|
const partner = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: partnerId
|
id: partnerId
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import { SafeUser } from '~/types';
|
import { SafeUser } from '~/types';
|
||||||
const prisma = new PrismaClient();
|
import prisma from '~/server/utils/prisma';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
if (!event.context.user.authenticated) {
|
if (!event.context.user.authenticated) {
|
||||||
@@ -17,6 +16,34 @@ export default defineEventHandler(async (event) => {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
username: 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: {
|
friends: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { IChannel, IServer, SafeUser } from '~/types';
|
import { IChannel, IServer, SafeUser } from '~/types';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import prisma from '~/server/utils/prisma';
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
if (!event.context.user.authenticated) {
|
if (!event.context.user.authenticated) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { IInviteCode, IServer, SafeUser } from '~/types';
|
import { IInviteCode, IServer, SafeUser } from '~/types';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import prisma from '~/server/utils/prisma';
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
if (!event.context.user.authenticated) {
|
if (!event.context.user.authenticated) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { IServer } from '~/types';
|
import { IServer } from '~/types';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import prisma from '~/server/utils/prisma';
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
if (!event.context.user.authenticated) {
|
if (!event.context.user.authenticated) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { IInviteCode, IServer } from '~/types';
|
import { IInviteCode, IServer } from '~/types';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import prisma from '~/server/utils/prisma';
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
if (!event.context.user.authenticated) {
|
if (!event.context.user.authenticated) {
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import bcryptjs from 'bcryptjs';
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from 'dotenv';
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import { IUser } from '~/types';
|
import { IUser } from '~/types';
|
||||||
const prisma = new PrismaClient();
|
import prisma from '~/server/utils/prisma';
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
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: {
|
where: {
|
||||||
username: body.username
|
username: body.username
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
username: 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,
|
passwordhash: true,
|
||||||
servers: {
|
servers: {
|
||||||
select: {
|
select: {
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import bcryptjs from 'bcryptjs';
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import * as dotenv from 'dotenv';
|
import * as dotenv from 'dotenv';
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import { IUser, SafeUser } from '~/types';
|
import { IUser, SafeUser } from '~/types';
|
||||||
const prisma = new PrismaClient();
|
import prisma from '~/server/utils/prisma';
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
@@ -50,6 +49,40 @@ export default defineEventHandler(async (event) => {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
username: 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: {
|
servers: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { IServer } from '~/types';
|
import { IServer } from '~/types';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import prisma from '~/server/utils/prisma';
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
if (!event.context.user.authenticated) {
|
if (!event.context.user.authenticated) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import prisma from '~/server/utils/prisma';
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
if (!event.context.user.authenticated) {
|
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';
|
import { IChannel, IServer } from '~/types';
|
||||||
const prisma = new PrismaClient();
|
import prisma from '~/server/utils/prisma';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
if (!event.context.user.authenticated) {
|
if (!event.context.user.authenticated) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import prisma from '~/server/utils/prisma';
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { sessionToken } = parseCookies(event);
|
const { sessionToken } = parseCookies(event);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
import prisma from '~/server/utils/prisma';
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const cookies = parseCookies(event);
|
const cookies = parseCookies(event);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import Redis from 'ioredis';
|
import redis from '~/server/utils/redis';
|
||||||
const redis = new Redis();
|
|
||||||
|
|
||||||
const INCREMENT_LIMIT = 5;
|
const INCREMENT_LIMIT = 5;
|
||||||
const LIMIT_TIME = 700; // milliseconds
|
const LIMIT_TIME = 700; // milliseconds
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Server } from 'socket.io';
|
import { Server, Socket } from 'socket.io';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { IChannel, IServer, IUser, SafeUser } from '~/types';
|
||||||
import { IChannel, IServer, IUser, SafeUser } from '~~/types';
|
import prisma from '~/server/utils/prisma';
|
||||||
import { Socket } from 'socket.io';
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
export default defineEventHandler(({ node }) => {
|
export default defineEventHandler(({ node }) => {
|
||||||
if (global.io) return;
|
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;
|
const that = this;
|
||||||
|
|
||||||
return new Promise<boolean>(resolve => {
|
return new Promise<boolean>(resolve => {
|
||||||
function checkFlag() {
|
function checkAuthStatus() {
|
||||||
if(that.isLoggedIn === true) {
|
if(that.isLoggedIn === true) {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(checkFlag, 100);
|
setTimeout(checkAuthStatus, 100);
|
||||||
}
|
}
|
||||||
checkFlag();
|
checkAuthStatus();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -28,13 +28,17 @@ export const useUserStore = defineStore('userStore', {
|
|||||||
this.user = user;
|
this.user = user;
|
||||||
this.isLoggedIn = true;
|
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() {
|
async logout() {
|
||||||
const { $io, $emit } = useNuxtApp();
|
const { $io, $emit } = useNuxtApp();
|
||||||
|
|
||||||
(await $io).disconnect();
|
(await $io).disconnect();
|
||||||
await $fetch('/api/user/logout');
|
await $fetch('/api/user/logout');
|
||||||
useCookie('sessionToken').value = null;
|
useCookie('sessionToken').value = null;
|
||||||
useCookie('userId').value = null;
|
|
||||||
|
|
||||||
this.user = null;
|
this.user = null;
|
||||||
this.isLoggedIn = false;
|
this.isLoggedIn = false;
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ export interface IUser {
|
|||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
friends?: Array<IUser>;
|
||||||
|
outgoingFriendRequests?: Array<IFriendRequest>;
|
||||||
|
incomingFriendRequests?: Array<IFriendRequest>;
|
||||||
online?: boolean;
|
online?: boolean;
|
||||||
passwordhash: string;
|
passwordhash: string;
|
||||||
servers?: Array<IServer>;
|
servers?: Array<IServer>;
|
||||||
@@ -86,3 +89,9 @@ export interface IPopupData {
|
|||||||
};
|
};
|
||||||
userId?: string;
|
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>'],
|
[/~~\s?([^\n]+)~~/g, '<s>$1</s>'],
|
||||||
|
|
||||||
// code lines and blocks
|
// 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>'],
|
[/(?<!`)`(.+?)`(?!`)/g, '<code class=\'inline-code\'>$1</code>'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user