dockerize, composte, and various improvements
This commit is contained in:
@@ -1,3 +1,12 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
danger: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li>
|
||||
<button
|
||||
@@ -7,12 +16,4 @@
|
||||
<slot />
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
danger: Boolean,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</template>
|
||||
@@ -1,3 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
opened: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
inverted: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="pop-in">
|
||||
<div
|
||||
@@ -11,15 +24,6 @@
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: {
|
||||
opened: Boolean,
|
||||
inverted: Boolean,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dropdown {
|
||||
transform-origin: top center;
|
||||
@@ -43,11 +47,13 @@ export default {
|
||||
|
||||
@keyframes pop-in {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
transform: scale(0.7);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,55 +1,51 @@
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import emojiJson from '~/assets/json/emoji.json';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export default {
|
||||
emits: ['picked-emoji'],
|
||||
data() {
|
||||
return {
|
||||
emojis: emojiJson,
|
||||
categories: [
|
||||
{ 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;
|
||||
defineEmits(['picked-emoji']);
|
||||
|
||||
const emojis = emojiJson;
|
||||
const emoji = emojis.find((e) => e.short_names[0] === emojiShortName);
|
||||
if (!emoji) return;
|
||||
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%'
|
||||
};
|
||||
},
|
||||
scrollTo(categoryName: string) {
|
||||
const emojiPane = (this.$refs.emojiPane as HTMLDivElement);
|
||||
const category = document.getElementById(categoryName);
|
||||
if (!emojiPane || !category) return;
|
||||
emojiPane.scrollTop = category.offsetTop - 550;
|
||||
}
|
||||
}
|
||||
};
|
||||
const emojiPane = ref(null as null | HTMLDivElement);
|
||||
const categories = [
|
||||
{ name: 'people', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('smileys')).concat(emojiJson.filter((e) => e.category.toLowerCase().includes('people'))) },
|
||||
{ name: 'nature', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('nature')) },
|
||||
{ name: 'food', emojis: emojiJson.filter((e) => e.category.toLowerCase().includes('food')) },
|
||||
{ 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')) }
|
||||
];
|
||||
|
||||
function emojiStyles(emojiShortName: string | undefined, width: number) {
|
||||
if (!emojiShortName) return;
|
||||
|
||||
const emojis = emojiJson;
|
||||
const emoji = emojis.find((e) => e.short_names[0] === emojiShortName);
|
||||
if (!emoji) return;
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div class="p-3">
|
||||
<div class="py-1.5 flex flex-col">
|
||||
<div class="flex-row gap-x-2 overflow-x-scroll">
|
||||
<div class="flex-row gap-x-2 overflow-x-scroll text-[#fefefe]">
|
||||
<button
|
||||
class="p-1.5 bg-inherit hover:backdrop-brightness-125 rounded-md transition-all"
|
||||
@click="scrollTo('people')"
|
||||
|
||||
84
components/FriendChip.vue
Normal file
84
components/FriendChip.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IUser } from '~/types';
|
||||
|
||||
const props = defineProps({
|
||||
user: {
|
||||
type: Object as PropType<IUser>,
|
||||
required: true
|
||||
},
|
||||
request: {
|
||||
type: Object as PropType<{ isRequest: boolean; incoming?: boolean; outgoing?: boolean; id?: string; }>,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
async function cancelFriendRequest() {
|
||||
if (!props.request.id) return;
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
await $fetch(`/api/user/friends/${props.request.id}/cancel`, { method: 'POST', headers });
|
||||
useUserStore().removeFriendRequest(props.request.id);
|
||||
}
|
||||
|
||||
async function acceptFriendRequest() {
|
||||
if (!props.request.id) return;
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
await $fetch(`/api/user/friends/${props.request.id}/accept`, { method: 'POST', headers });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full px-3.5 py-2 flex flex-row border-t border-[var(--tertiary-bg)]">
|
||||
<div class="flex flex-row">
|
||||
<div class="bg-[var(--tertiary-bg)] w-10 h-10 rounded-full mr-2.5" />
|
||||
<div>
|
||||
{{ user.username }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto flex gap-x-2 items-center">
|
||||
<button
|
||||
v-if="request.isRequest && request.incoming"
|
||||
class="w-8 h-8 bg-[var(--tertiary-bg)] rounded-full hover:bg-[var(--tertiary-lightened-bg)] transition-colors flex items-center justify-center"
|
||||
@click="acceptFriendRequest"
|
||||
>
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="none"
|
||||
stroke="var(--primary-placeholder)"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m5 12l5 5L20 7"
|
||||
/></svg>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="request.isRequest"
|
||||
class="w-8 h-8 bg-[var(--tertiary-bg)] rounded-full hover:bg-[var(--tertiary-lightened-bg)] transition-colors flex items-center justify-center"
|
||||
@click="cancelFriendRequest"
|
||||
>
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="none"
|
||||
stroke="var(--primary-placeholder)"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18 6L6 18M6 6l12 12"
|
||||
/></svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,3 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { PropType, computed } from 'vue';
|
||||
import { useServerStore } from '~/stores/serverStore';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IInviteCode, IServer, SafeUser } from '~/types';
|
||||
|
||||
const props = defineProps({
|
||||
invite: {
|
||||
type: Object as PropType<IInviteCode>,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const user = storeToRefs(useUserStore()).user;
|
||||
|
||||
const userInServer = computed(() => {
|
||||
return !!props.invite.server.participants.find((e: SafeUser) => e.id === user.value?.id);
|
||||
});
|
||||
|
||||
async function joinServer(invite: IInviteCode) {
|
||||
if (userInServer.value || !user.value) return;
|
||||
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const { server } = await $fetch('/api/guilds/joinGuild', { method: 'POST', body: { inviteId: invite.id }, headers }) as { server: IServer };
|
||||
if (!server) return;
|
||||
|
||||
useServerStore().addServer(server);
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
props.invite.server.participants.push(user.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-6/12 bg-[var(--secondary-bg)] mb-1 mt-0.5 p-4 rounded-md shadow-md mr-2">
|
||||
<p class="text-sm font-semibold text-zinc-100">
|
||||
@@ -34,43 +66,4 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType } from 'vue';
|
||||
import { useServerStore } from '~/stores/serverStore';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IInviteCode, IServer, IUser, SafeUser } from '~/types';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
invite: {
|
||||
type: Object as PropType<IInviteCode>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
user: storeToRefs(useUserStore()).user,
|
||||
servers: storeToRefs(useServerStore()).servers,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
userInServer(): boolean {
|
||||
return !!this.invite.server.participants.find((e: SafeUser) => e.id === this.user?.id);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async joinServer(invite: IInviteCode) {
|
||||
if (this.userInServer) return;
|
||||
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const { server } = await $fetch('/api/guilds/joinGuild', { method: 'POST', body: { inviteId: invite.id }, headers }) as { server: IServer };
|
||||
if (!server) return;
|
||||
|
||||
useServerStore().addServer(server);
|
||||
this.invite.server.participants.push(this.user);
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</template>
|
||||
@@ -1,191 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative message-wrapper"
|
||||
@mouseleave="overflowShown = false"
|
||||
>
|
||||
<div
|
||||
class="absolute right-0 mr-10 -top-[20px] h-fit opacity-0 pointer-events-none action-buttons z-[5]"
|
||||
:class="(emojiPickerOpen) ? 'opacity-100 pointer-events-auto' : ''"
|
||||
>
|
||||
<div
|
||||
:id="`actions-${message.id}`"
|
||||
class="relative bg-[var(--tertiary-bg)] rounded-md border border-[rgb(32,34,37)] text-[var(--primary-text)] flex overflow-hidden"
|
||||
>
|
||||
<button
|
||||
class="p-1 hover:backdrop-brightness-125 transition-all flex w-fit h-fit"
|
||||
@click="openEmojiPicker()"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m13 19l-1 1l-7.5-7.428A5 5 0 1 1 12 6.006a5 5 0 0 1 8.003 5.996M14 16h6m-3-3v6"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="!shiftPressed && !overflowShown"
|
||||
class="p-1 hover:backdrop-brightness-125 transition-all flex w-fit h-fit"
|
||||
@click="overflowShown = true"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle
|
||||
cx="5"
|
||||
cy="12"
|
||||
r="1"
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="1"
|
||||
/>
|
||||
<circle
|
||||
cx="19"
|
||||
cy="12"
|
||||
r="1"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
v-if="shiftPressed || overflowShown"
|
||||
class="flex"
|
||||
>
|
||||
<button
|
||||
class="p-1 hover:backdrop-brightness-125 transition-all flex w-[28px] h-[28px] items-center justify-center"
|
||||
@click="copy(message.id)"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="bg-[var(--primary-text)] rounded"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="var(--tertiary-bg)"
|
||||
d="M10 7v2H9v6h1v2H6v-2h1V9H6V7h4m6 0a2 2 0 0 1 2 2v6c0 1.11-.89 2-2 2h-4V7m4 2h-2v6h2V9Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="message.creator.id === user?.id"
|
||||
class="p-1 hover:backdrop-brightness-125 transition-all flex w-[28px] h-[28px] items-center justify-center"
|
||||
@click="deleteMessage()"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="var(--primary-danger)"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 7h16m-10 4v6m4-6v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="transition-[backdrop-filter] hover:backdrop-brightness-125 ease-[cubic-bezier(.37,.64,.59,.33)] duration-150 my-4 px-7 py-2 message-wrapper items-center z-[1]"
|
||||
:class="classes"
|
||||
>
|
||||
<div class="message-content">
|
||||
<div class="message-sender-text">
|
||||
<p
|
||||
v-if="showUsername"
|
||||
class="flex flex-row"
|
||||
>
|
||||
<span
|
||||
ref="username"
|
||||
class="mb-1 font-semibold w-fit cursor-pointer hover:underline"
|
||||
@click="openUserProfile()"
|
||||
>
|
||||
{{ message.creator.username }}
|
||||
</span>
|
||||
<span
|
||||
v-if="userIsOwner"
|
||||
class="ml-0.5"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
class="text-yellow-300"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m12 6l4 6l5-4l-2 10H5L3 8l5 4z"
|
||||
/></svg>
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
class="break-words max-w-full whitespace-pre-wrap"
|
||||
v-html="parseMessageBody(message.body, participants)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="invite in message.invites"
|
||||
:key="invite.id"
|
||||
>
|
||||
<InviteCard :invite="invite" />
|
||||
</div>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button
|
||||
v-for="reaction in reactions"
|
||||
:key="reaction.emoji"
|
||||
class="py-0.5 px-1.5 bg-[var(--secondary-bg)] border items-center flex rounded-lg border-[var(--tertiary-bg)] hover:border-[var(--reaction-hover-border)] hover:bg-[var(--reaction-hover)] transition-colors shadow-sm max-h-[30px]"
|
||||
:class="(reaction.users.find((e) => e.id === user?.id)) ? '!border-[var(--reaction-active-border)] hover:!border-[var(--reaction-active-border)]' : ''"
|
||||
@click="toggleReaction(reaction.emoji)"
|
||||
>
|
||||
<div class="flex items-center mr-0.5 w-6 drop-shadow">
|
||||
<span :style="emojiStyles(reaction.emoji, 17)" />
|
||||
</div>
|
||||
<div class="relative overflow-hidden ml-1.5">
|
||||
<div
|
||||
:key="reaction.users.length"
|
||||
class="min-w-[9px] h-6"
|
||||
>
|
||||
<span class="dropshadow-sm">{{ reaction.users.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType } from 'vue';
|
||||
import { IPopupData, IMessage, SafeUser, IReaction } from '~/types';
|
||||
@@ -335,6 +147,194 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative message-wrapper"
|
||||
@mouseleave="overflowShown = false"
|
||||
>
|
||||
<div
|
||||
class="absolute right-0 mr-10 -top-[20px] h-fit opacity-0 pointer-events-none action-buttons z-[5]"
|
||||
:class="(emojiPickerOpen) ? 'opacity-100 pointer-events-auto' : ''"
|
||||
>
|
||||
<div
|
||||
:id="`actions-${message.id}`"
|
||||
class="relative bg-[var(--tertiary-bg)] rounded-md border border-[rgb(32,34,37)] text-[var(--primary-text)] flex overflow-hidden"
|
||||
>
|
||||
<button
|
||||
class="p-1 hover:backdrop-brightness-125 transition-all flex w-fit h-fit"
|
||||
@click="openEmojiPicker()"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m13 19l-1 1l-7.5-7.428A5 5 0 1 1 12 6.006a5 5 0 0 1 8.003 5.996M14 16h6m-3-3v6"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="!shiftPressed && !overflowShown"
|
||||
class="p-1 hover:backdrop-brightness-125 transition-all flex w-fit h-fit"
|
||||
@click="overflowShown = true"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle
|
||||
cx="5"
|
||||
cy="12"
|
||||
r="1"
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="1"
|
||||
/>
|
||||
<circle
|
||||
cx="19"
|
||||
cy="12"
|
||||
r="1"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
v-if="shiftPressed || overflowShown"
|
||||
class="flex"
|
||||
>
|
||||
<button
|
||||
class="p-1 hover:backdrop-brightness-125 transition-all flex w-[28px] h-[28px] items-center justify-center"
|
||||
@click="copy(message.id)"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="bg-[var(--primary-text)] rounded"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="var(--tertiary-bg)"
|
||||
d="M10 7v2H9v6h1v2H6v-2h1V9H6V7h4m6 0a2 2 0 0 1 2 2v6c0 1.11-.89 2-2 2h-4V7m4 2h-2v6h2V9Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
v-if="message.creator.id === user?.id"
|
||||
class="p-1 hover:backdrop-brightness-125 transition-all flex w-[28px] h-[28px] items-center justify-center"
|
||||
@click="deleteMessage()"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="var(--primary-danger)"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 7h16m-10 4v6m4-6v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v3"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="transition-[backdrop-filter] hover:backdrop-brightness-125 ease-[cubic-bezier(.37,.64,.59,.33)] duration-150 my-4 px-7 py-2 message-wrapper items-center z-[1]"
|
||||
:class="classes"
|
||||
>
|
||||
<div class="message-content">
|
||||
<div class="message-sender-text">
|
||||
<p
|
||||
v-if="showUsername"
|
||||
class="flex flex-row"
|
||||
>
|
||||
<span
|
||||
ref="username"
|
||||
class="mb-1 font-semibold w-fit cursor-pointer hover:underline text-[#ffffff]"
|
||||
@click="openUserProfile()"
|
||||
>
|
||||
{{ message.creator.username }}
|
||||
</span>
|
||||
<span
|
||||
v-if="userIsOwner"
|
||||
class="ml-0.5"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
class="text-yellow-300"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m12 6l4 6l5-4l-2 10H5L3 8l5 4z"
|
||||
/></svg>
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
class="break-words max-w-full whitespace-pre-wrap text-[#fafafa]"
|
||||
v-html="parseMessageBody(message.body, participants)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="invite in message.invites"
|
||||
:key="invite.id"
|
||||
>
|
||||
<InviteCard :invite="invite" />
|
||||
</div>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button
|
||||
v-for="reaction in reactions"
|
||||
:key="reaction.emoji"
|
||||
class="py-0.5 px-1.5 mt-1.5 bg-[var(--secondary-bg)] border items-center flex rounded-lg border-[var(--tertiary-bg)] hover:border-[var(--reaction-hover-border)] hover:bg-[var(--reaction-hover)] transition-colors shadow-sm max-h-[30px]"
|
||||
:class="(reaction.users.find((e) => e.id === user?.id)) ? '!border-[var(--reaction-active-border)] hover:!border-[var(--reaction-active-border)]' : ''"
|
||||
@click="toggleReaction(reaction.emoji)"
|
||||
>
|
||||
<div class="flex items-center mr-0.5 w-6 drop-shadow">
|
||||
<span :style="emojiStyles(reaction.emoji, 17)" />
|
||||
</div>
|
||||
<div class="relative overflow-hidden ml-1.5">
|
||||
<div
|
||||
:key="reaction.users.length"
|
||||
class="min-w-[9px] h-6"
|
||||
>
|
||||
<span class="dropshadow-sm text-[#efefef]">{{ reaction.users.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.message-wrapper:hover>div.action-buttons {
|
||||
opacity: 100;
|
||||
|
||||
@@ -1,178 +1,3 @@
|
||||
<template>
|
||||
<div
|
||||
id="messagePane"
|
||||
class="h-full relative bg-[var(--primary-bg)] flex flex-col"
|
||||
@mouseenter="mouseEnter"
|
||||
@mouseleave="mouseLeave"
|
||||
>
|
||||
<div class="px-4 py-3">
|
||||
<div
|
||||
v-if="!channel.DM"
|
||||
class="flex items-center"
|
||||
>
|
||||
<span class="mr-1">
|
||||
<svg
|
||||
class="text-zinc-300/80 my-auto"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m5.41 21l.71-4h-4l.35-2h4l1.06-6h-4l.35-2h4l.71-4h2l-.71 4h6l.71-4h2l-.71 4h4l-.35 2h-4l-1.06 6h4l-.35 2h-4l-.71 4h-2l.71-4h-6l-.71 4h-2M9.53 9l-1.06 6h6l1.06-6h-6Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="font-semibold">{{ channel.name }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center"
|
||||
>
|
||||
<span class="mr-1">
|
||||
<svg
|
||||
class="text-zinc-300/80 my-auto"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="4"
|
||||
/>
|
||||
<path d="M16 12v1.5a2.5 2.5 0 0 0 5 0V12a9 9 0 1 0-5.5 8.28" />
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="font-semibold">{{
|
||||
participants.find((e: SafeUser) => e.id !== user?.id)?.username
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section
|
||||
class="h-full overflow-hidden rounded-lg relative flex flex-col"
|
||||
>
|
||||
<div
|
||||
ref="conversationPane"
|
||||
class="h-[calc(100%-70px)] overflow-y-scroll"
|
||||
>
|
||||
<div class="w-full pb-1 bg-inherit">
|
||||
<div>
|
||||
<div v-if="channel.messages.length === 0">
|
||||
<p>No messages yet</p>
|
||||
</div>
|
||||
<Message
|
||||
v-for="(message, i) in channel.messages"
|
||||
v-else
|
||||
:key="message.id"
|
||||
:message="message"
|
||||
:shift-pressed="shiftPressed"
|
||||
:show-username="calculateMessageDesign(message, i).showUsername"
|
||||
:classes="calculateMessageDesign(message, i).classes"
|
||||
:channel-id="channel.id"
|
||||
:participants="participants"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="search.show"
|
||||
class="absolute bottom-[calc(75px+0.5rem)] mx-4 w-[calc(100vw-88px-240px-32px)] py-3 px-4 bg-[var(--secondary-bg)] rounded-lg shadow-md z-5"
|
||||
>
|
||||
<div class="relative flex flex-col">
|
||||
<div
|
||||
v-for="resultingUser in search.results"
|
||||
:key="resultingUser.id"
|
||||
class="mx-2 my-1 w-[calc(100vw-88px-240px-64px-16px)] px-4 py-3 hover:backdrop-brightness-125 select-none rounded-md transition-all"
|
||||
@click="completeMention(resultingUser)"
|
||||
>
|
||||
{{ resultingUser.username }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex absolute flex-row bottom-0 w-full h-fit bg-inherit">
|
||||
<form
|
||||
class="relative px-4 w-full pt-1.5 h-fit pb-1"
|
||||
@keyup="checkForMentions"
|
||||
@keypress="typing($event)"
|
||||
@submit.prevent="sendMessage"
|
||||
@keydown.enter.exact.prevent="sendMessage"
|
||||
>
|
||||
<div
|
||||
id="textbox"
|
||||
class="px-4 rounded-md w-full min-h-[44px] h-fit bg-[var(--secondary-bg)] placeholder:text-[var(--primary-placeholder)] flex flex-row"
|
||||
>
|
||||
<textarea
|
||||
ref="messageBox"
|
||||
v-model="messageContent"
|
||||
maxlength="5000"
|
||||
type="text"
|
||||
class="bg-transparent focus:outline-none py-2 w-full resize-none leading-relaxed h-[44px]"
|
||||
cols="1"
|
||||
placeholder="Send a Message..."
|
||||
/>
|
||||
<input
|
||||
id="submit"
|
||||
type="submit"
|
||||
class="absolute -top-full -left-full invisible"
|
||||
>
|
||||
<label
|
||||
for="submit"
|
||||
class="py-1 px-1.5 h-fit my-auto cursor-pointer"
|
||||
role="button"
|
||||
><svg
|
||||
width="32"
|
||||
height="26"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14L21 3m0 0l-6.5 18a.55.55 0 0 1-1 0L10 14l-7-3.5a.55.55 0 0 1 0-1L21 3"
|
||||
/>
|
||||
</svg></label>
|
||||
</div>
|
||||
<div class="w-full h-4">
|
||||
<p
|
||||
v-if="usersTyping.length > 0"
|
||||
class="text-sm"
|
||||
>
|
||||
<span v-if="usersTyping.length < 4">
|
||||
<span
|
||||
v-for="(username, i) in usersTyping"
|
||||
:key="username"
|
||||
class="font-semibold"
|
||||
>
|
||||
<span v-if="i === usersTyping.length - 1 && usersTyping.length > 1">and </span>
|
||||
{{ username }}
|
||||
<span v-if="i !== usersTyping.length - 1 && usersTyping.length > 1">, </span>
|
||||
</span>
|
||||
is typing
|
||||
</span>
|
||||
<span v-else>Several users are typing</span>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType } from 'vue';
|
||||
import { useActiveStore } from '~/stores/activeStore';
|
||||
@@ -275,7 +100,10 @@ export default {
|
||||
}
|
||||
},
|
||||
calculateMessageDesign(message: IMessage, i: number) {
|
||||
if (i === 0 || (this.channel.messages[i - 1]?.creator.id !== message.creator.id || new Date(this.channel.messages[i-1]?.createdAt).getTime()+((30*60)*1000)<new Date(this.channel.messages[i]?.createdAt).getTime())) {
|
||||
if (i === 0 || (
|
||||
this.channel.messages[i - 1]?.creator.id !== message.creator.id ||
|
||||
new Date(this.channel.messages[i-1]?.createdAt).getTime()+((30*60)*1000)<new Date(this.channel.messages[i]?.createdAt).getTime()
|
||||
)) {
|
||||
if (i !== this.channel.messages.length - 1) {
|
||||
return { classes: 'mb-0 pb-0.5', showUsername: true };
|
||||
}
|
||||
@@ -392,5 +220,178 @@ export default {
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
id="messagePane"
|
||||
class="h-full relative bg-[var(--primary-bg)] flex flex-col"
|
||||
@mouseenter="mouseEnter"
|
||||
@mouseleave="mouseLeave"
|
||||
>
|
||||
<div class="px-4 py-3">
|
||||
<div
|
||||
v-if="!channel.DM"
|
||||
class="flex items-center"
|
||||
>
|
||||
<span class="mr-1">
|
||||
<svg
|
||||
class="text-zinc-300/80 my-auto"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m5.41 21l.71-4h-4l.35-2h4l1.06-6h-4l.35-2h4l.71-4h2l-.71 4h6l.71-4h2l-.71 4h4l-.35 2h-4l-1.06 6h4l-.35 2h-4l-.71 4h-2l.71-4h-6l-.71 4h-2M9.53 9l-1.06 6h6l1.06-6h-6Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="font-semibold text-[#fefefe]">{{ channel.name }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center"
|
||||
>
|
||||
<span class="mr-1">
|
||||
<svg
|
||||
class="text-zinc-300/80 my-auto"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="4"
|
||||
/>
|
||||
<path d="M16 12v1.5a2.5 2.5 0 0 0 5 0V12a9 9 0 1 0-5.5 8.28" />
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="font-semibold">{{
|
||||
participants.find((e: SafeUser) => e.id !== user?.id)?.username
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section
|
||||
class="h-full overflow-hidden rounded-lg relative flex flex-col"
|
||||
>
|
||||
<div
|
||||
ref="conversationPane"
|
||||
class="h-[calc(100%-70px)] overflow-y-scroll"
|
||||
>
|
||||
<div class="w-full pb-1 bg-inherit">
|
||||
<div>
|
||||
<div v-if="channel.messages.length === 0">
|
||||
<p>No messages yet</p>
|
||||
</div>
|
||||
<Message
|
||||
v-for="(message, i) in channel.messages"
|
||||
v-else
|
||||
:key="message.id"
|
||||
:message="message"
|
||||
:shift-pressed="shiftPressed"
|
||||
:show-username="calculateMessageDesign(message, i).showUsername"
|
||||
:classes="calculateMessageDesign(message, i).classes"
|
||||
:channel-id="channel.id"
|
||||
:participants="participants"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="search.show"
|
||||
class="absolute bottom-[calc(75px+0.5rem)] mx-4 w-[calc(100vw-88px-240px-32px)] py-3 px-4 bg-[var(--secondary-bg)] rounded-lg shadow-md z-5"
|
||||
>
|
||||
<div class="relative flex flex-col">
|
||||
<div
|
||||
v-for="resultingUser in search.results"
|
||||
:key="resultingUser.id"
|
||||
class="mx-2 my-1 w-[calc(100vw-88px-240px-64px-16px)] px-4 py-3 hover:backdrop-brightness-125 select-none rounded-md transition-all"
|
||||
@click="completeMention(resultingUser)"
|
||||
>
|
||||
{{ resultingUser.username }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex absolute flex-row bottom-0 w-full h-fit bg-inherit">
|
||||
<form
|
||||
class="relative px-4 w-full pt-1.5 h-fit pb-1"
|
||||
@keyup="checkForMentions"
|
||||
@keypress="typing($event)"
|
||||
@submit.prevent="sendMessage"
|
||||
@keydown.enter.exact.prevent="sendMessage"
|
||||
>
|
||||
<div
|
||||
id="textbox"
|
||||
class="px-4 rounded-md w-full min-h-[44px] h-fit bg-[var(--secondary-bg)] placeholder:text-[var(--primary-placeholder)] flex flex-row text-[#fefefe]"
|
||||
>
|
||||
<textarea
|
||||
ref="messageBox"
|
||||
v-model="messageContent"
|
||||
maxlength="5000"
|
||||
type="text"
|
||||
class="bg-transparent focus:outline-none py-2 w-full resize-none leading-relaxed h-[44px]"
|
||||
cols="1"
|
||||
placeholder="Send a Message..."
|
||||
/>
|
||||
<button
|
||||
class="p-1 h-fit my-auto transition-colors duration-150 ease-in rounded-md"
|
||||
:class="(messageContent.trim().length > 0) ? 'bg-blue-700 cursor-pointer' : 'text-[#78797A] cursor-default'"
|
||||
@click="sendMessage"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none">
|
||||
<path
|
||||
d="M24 0v24H0V0h24ZM12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036c-.01-.003-.019 0-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.016-.018Zm.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022Zm-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01l-.184-.092Z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M20.235 5.686c.432-1.195-.726-2.353-1.921-1.92L3.709 9.048c-1.199.434-1.344 2.07-.241 2.709l4.662 2.699l4.163-4.163a1 1 0 0 1 1.414 1.414L9.544 15.87l2.7 4.662c.638 1.103 2.274.957 2.708-.241l5.283-14.605Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-full h-4">
|
||||
<p
|
||||
v-if="usersTyping.length > 0"
|
||||
class="text-sm"
|
||||
>
|
||||
<span v-if="usersTyping.length < 4">
|
||||
<span
|
||||
v-for="(username, i) in usersTyping"
|
||||
:key="username"
|
||||
class="font-semibold"
|
||||
>
|
||||
<span v-if="i === usersTyping.length - 1 && usersTyping.length > 1">and </span>
|
||||
{{ username }}
|
||||
<span v-if="i !== usersTyping.length - 1 && usersTyping.length > 1">, </span>
|
||||
</span>
|
||||
is typing
|
||||
</span>
|
||||
<span v-else>Several users are typing</span>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,18 +1,3 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="opened"
|
||||
class="absolute z-10 top-0 bottom-0 left-0 right-0"
|
||||
>
|
||||
<slot />
|
||||
<div
|
||||
class="bg-black/70 w-screen h-screen"
|
||||
@click="$emit('close')"
|
||||
/>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineEmits(['close']);
|
||||
|
||||
@@ -22,4 +7,19 @@ defineProps({
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</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>
|
||||
<nav class="bg-[var(--primary-bg)] h-screen p-4 grid grid-cols-1 grid-rows-[56px_1fr_56px] shadow shadow-black/80">
|
||||
<div>
|
||||
@@ -133,36 +159,4 @@
|
||||
</div>
|
||||
</Modal>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useActiveStore } from '~/stores/activeStore';
|
||||
import { useServerStore } from '~/stores/serverStore';
|
||||
import { IServer } from '~/types';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
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>
|
||||
</template>
|
||||
@@ -1,3 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue';
|
||||
|
||||
defineProps({
|
||||
openedBy: {
|
||||
type: String as PropType<'emojiPicker' | 'userProfile'>,
|
||||
required: true
|
||||
},
|
||||
opened: Boolean
|
||||
});
|
||||
|
||||
defineEmits(['picked-emoji']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="opened"
|
||||
@@ -13,15 +27,4 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
openedBy: {
|
||||
required: true
|
||||
},
|
||||
opened: Boolean
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</template>
|
||||
@@ -1,3 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useActiveStore, useDmStore, useServerStore, useUserStore } from '~/stores';
|
||||
import { IChannel } from '~/types';
|
||||
|
||||
const activeServer = ref({
|
||||
type: storeToRefs(useActiveStore()).type,
|
||||
data: storeToRefs(useActiveStore()).server,
|
||||
});
|
||||
|
||||
const user = storeToRefs(useUserStore()).user;
|
||||
const dms = storeToRefs(useDmStore()).dms;
|
||||
const channelName = ref('');
|
||||
const createChannelModelOpen = ref(false);
|
||||
const serverDropdownOpen = ref(false);
|
||||
const userDropdownOpen = ref(false);
|
||||
|
||||
const userIsOwner = computed(() => {
|
||||
return (
|
||||
activeServer.value.type === 'server' &&
|
||||
activeServer.value.data.server.participants.find((e) => e.id === user.value?.id)?.roles?.some((e) => e.owner === true)
|
||||
);
|
||||
});
|
||||
|
||||
const userIsAdmin = computed(() => {
|
||||
return (
|
||||
activeServer.value.type === 'server' &&
|
||||
activeServer.value.data.server.participants.find((e) => e.id === user.value?.id)?.roles?.some((e) => e.administer === true)
|
||||
);
|
||||
});
|
||||
|
||||
const openCreateChannelModel = () => {
|
||||
createChannelModelOpen.value = true;
|
||||
};
|
||||
|
||||
const createChannel = async () => {
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const channel = await $fetch(
|
||||
`/api/guilds/${activeServer.value.data.server.id}/addChannel`,
|
||||
{ method: 'POST', body: { channelName: channelName.value }, headers }
|
||||
) as IChannel;
|
||||
|
||||
if (!channel) return;
|
||||
|
||||
useServerStore().addChannel(
|
||||
activeServer.value.data.server.id,
|
||||
channel
|
||||
);
|
||||
createChannelModelOpen.value = false;
|
||||
|
||||
navigateTo(`/channel/${channel.id}`);
|
||||
};
|
||||
|
||||
const createInvite = async () => {
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const inviteCode = await $fetch(
|
||||
`/api/guilds/${activeServer.value.data.server.id}/createInvite`,
|
||||
{ method: 'POST', headers }
|
||||
);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
useUserStore().logout();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="bg-[var(--secondary-bg)] min-w-60 w-60 h-screen shadow-sm text-white select-none relative z-[2]">
|
||||
<div
|
||||
@@ -15,18 +81,41 @@
|
||||
<div
|
||||
class="h-[calc(100%-12px)] grid grid-rows-[1fr_56px] bg-[var(--foreground-color)]"
|
||||
>
|
||||
<div class="h-fit">
|
||||
<div class="flex gap-y-1.5 px-1.5 mt-2 flex-col overflow-y-auto overflow-x-hidden">
|
||||
<nuxt-link to="/channel/@me">
|
||||
<button
|
||||
class="flex text-center bg-inherit px-2 py-1.5 w-full transition-all rounded-md drop-shadow-sm gap-1/5 cursor-pointer items-center hover:backdrop-brightness-[1.35]"
|
||||
>
|
||||
<span class="mr-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0-8 0M6 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"
|
||||
/></svg>
|
||||
</span>
|
||||
Friends
|
||||
</button>
|
||||
</nuxt-link>
|
||||
<hr class="w-11/12 mx-auto border border-[var(--tertiary-lightened-bg)]" />
|
||||
<nuxt-link
|
||||
v-for="dm in dms"
|
||||
:key="dm.id"
|
||||
class="hover:no-underline"
|
||||
:to="'/channel/@me/' + dm.id"
|
||||
>
|
||||
<div
|
||||
class="mx-2 my-4 bg-inherit hover:backdrop-brightness-[1.35] px-2 py-2 max-h-10 h-10 overflow-ellipsis transition-all"
|
||||
<button
|
||||
class="flex text-center bg-inherit px-2 py-1.5 w-full transition-all rounded-md drop-shadow-sm gap-1/5 cursor-pointer items-center hover:backdrop-brightness-[1.35]"
|
||||
>
|
||||
{{ dm.dmParticipants?.find((e) => e.id !== user?.id)?.username }}
|
||||
</div>
|
||||
</button>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,7 +227,7 @@
|
||||
>
|
||||
<button
|
||||
:class="(activeServer.data.channel.id === channel.id) ? 'backdrop-brightness-[1.35]' : 'hover:backdrop-brightness-[1.35]'"
|
||||
class="flex text-center bg-inherit px-2 py-1.5 w-full transition-all rounded drop-shadow-sm gap-1/5 cursor-pointer items-center"
|
||||
class="flex text-center bg-inherit px-2 py-1.5 w-full transition-all rounded-md drop-shadow-sm gap-1/5 cursor-pointer items-center"
|
||||
>
|
||||
<span class="h-fit">
|
||||
<svg
|
||||
@@ -159,7 +248,7 @@
|
||||
</nuxt-link>
|
||||
<button
|
||||
v-if="userIsOwner || userIsAdmin"
|
||||
class="flex text-center bg-inherit hover:backdrop-brightness-[1.45] px-2 py-1.5 w-full transition-all rounded drop-shadow-sm cursor-pointer items-center"
|
||||
class="flex text-center bg-inherit hover:backdrop-brightness-[1.45] px-2 py-1.5 w-full transition-all rounded-md drop-shadow-sm cursor-pointer items-center"
|
||||
@click="openCreateChannelModel"
|
||||
>
|
||||
<span>
|
||||
@@ -224,7 +313,7 @@
|
||||
</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
danger="true"
|
||||
:danger="true"
|
||||
@click="logout"
|
||||
>
|
||||
<span>
|
||||
@@ -328,60 +417,4 @@
|
||||
</div>
|
||||
</Modal>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useActiveStore } from '~/stores/activeStore';
|
||||
import { useDmStore } from '~/stores/dmStore';
|
||||
import { useServerStore } from '~/stores/serverStore';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IChannel, IRole } from '~/types';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
activeServer: {
|
||||
type: storeToRefs(useActiveStore()).type,
|
||||
data: storeToRefs(useActiveStore()).server,
|
||||
},
|
||||
user: storeToRefs(useUserStore()).user,
|
||||
dms: storeToRefs(useDmStore()).dms,
|
||||
channelName: '',
|
||||
createChannelModelOpen: false,
|
||||
serverDropdownOpen: false,
|
||||
userDropdownOpen: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
userIsOwner() {
|
||||
return this.activeServer.type === 'server' && this.activeServer.data.server.participants.find((e) => e.id === this.user?.id)?.roles?.some((e) => e.owner === true);
|
||||
},
|
||||
userIsAdmin() {
|
||||
return this.activeServer.type === 'server' && this.activeServer.data.server.participants.find((e) => e.id === this.user?.id)?.roles?.some((e) => e.administer === true);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openCreateChannelModel() {
|
||||
this.createChannelModelOpen = true;
|
||||
},
|
||||
async createChannel() {
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const channel = await $fetch(`/api/guilds/${this.activeServer.data.server.id}/addChannel`, { method: 'POST', body: { channelName: this.channelName }, headers }) as IChannel;
|
||||
|
||||
if (!channel) return;
|
||||
|
||||
useServerStore().addChannel(this.activeServer.data.server.id, channel);
|
||||
this.createChannelModelOpen = false;
|
||||
|
||||
navigateTo(`/channel/${channel.id}`);
|
||||
},
|
||||
async createInvite() {
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const inviteCode = await $fetch(`/api/guilds/${this.activeServer.data.server.id}/createInvite`, { method: 'POST', headers });
|
||||
},
|
||||
logout() {
|
||||
useUserStore().logout();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</template>
|
||||
@@ -1,5 +1,64 @@
|
||||
<script lang="ts" setup>
|
||||
import { useActiveStore } from '~/stores/activeStore';
|
||||
import { useDmStore } from '~/stores/dmStore';
|
||||
import { useEmojiPickerStore } from '~/stores/emojiPickerStore';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IUser, IRole } from '~/types';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
const userData = useUserStore().user;
|
||||
const message = ref('');
|
||||
|
||||
async function fetchUser() {
|
||||
const emojiPickerData = useEmojiPickerStore().emojiPickerData;
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const activeServer = useActiveStore().server;
|
||||
|
||||
const isDm = useRoute().path.includes('@me');
|
||||
|
||||
let user: IUser | null;
|
||||
|
||||
if (isDm) {
|
||||
user = await $fetch(`/api/user/${emojiPickerData.userId}/profile`, { headers }) as IUser | null;
|
||||
} else {
|
||||
user = await $fetch(`/api/user/${emojiPickerData.userId}/${activeServer.server.id}/profile`, { headers }) as IUser | null;
|
||||
}
|
||||
|
||||
return { user, isDm };
|
||||
}
|
||||
|
||||
const { user, isDm } = await fetchUser();
|
||||
const roles = computed(() => {
|
||||
return user?.roles?.filter((e: IRole) => e.owner === false) || [];
|
||||
});
|
||||
const userIsOwner = computed(() => {
|
||||
return user?.roles?.some((e: IRole) => e.owner === true) || false;
|
||||
});
|
||||
|
||||
async function sendDM() {
|
||||
if (!message.value.trim()) return;
|
||||
if (!user) return;
|
||||
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const preExistingDM = useDmStore().getByPartnerId(user.id);
|
||||
|
||||
if (!preExistingDM) return;
|
||||
|
||||
if (preExistingDM && useRoute().path !== `/channel/@me/${preExistingDM.id}`) {
|
||||
await navigateTo(`/channel/@me/${preExistingDM.id}`);
|
||||
}
|
||||
|
||||
await $fetch(`/api/channels/${preExistingDM.id}/sendMessage`, { method: 'post', body: { body: message.value }, headers });
|
||||
message.value = '';
|
||||
|
||||
useEmojiPickerStore().closeEmojiPicker();
|
||||
}
|
||||
|
||||
if (!user || !userData) throw new Error('unknown error');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-[374px] max-h-[475px] overflow-y-scroll">
|
||||
<div class="w-[374px] max-h-[475px] overflow-y-scroll text-[#fefefe]">
|
||||
<div class="relative h-[calc(160px+56px)]">
|
||||
<div class="w-full h-40 absolute">
|
||||
<img
|
||||
@@ -68,71 +127,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { useActiveStore } from '~/stores/activeStore';
|
||||
import { useDmStore } from '~/stores/dmStore';
|
||||
import { useEmojiPickerStore } from '~/stores/emojiPickerStore';
|
||||
import { useUserStore } from '~/stores/userStore';
|
||||
import { IUser, IRole } from '~/types';
|
||||
|
||||
export default {
|
||||
async setup() {
|
||||
const userData = useUserStore().user;
|
||||
async function fetchUser() {
|
||||
const emojiPickerData = useEmojiPickerStore().emojiPickerData;
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const activeServer = useActiveStore().server;
|
||||
|
||||
const isDm = useRoute().path.includes('@me');
|
||||
|
||||
let user: IUser | null;
|
||||
|
||||
if (isDm) {
|
||||
user = await $fetch(`/api/user/${emojiPickerData.userId}/profile`, { headers }) as IUser | null;
|
||||
} else {
|
||||
user = await $fetch(`/api/user/${emojiPickerData.userId}/${activeServer.server.id}/profile`, { headers }) as IUser | null;
|
||||
}
|
||||
|
||||
return { user, isDm };
|
||||
}
|
||||
|
||||
const { user, isDm } = await fetchUser();
|
||||
|
||||
if (!user) return;
|
||||
|
||||
return { user, isDm, fetchUser, userData };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
message: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
roles(): IRole[] {
|
||||
return this.user.roles?.filter((e: IRole) => e.owner === false) || [];
|
||||
},
|
||||
userIsOwner(): boolean {
|
||||
return this.user.roles?.some((e: IRole) => e.owner === true) || false;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async sendDM() {
|
||||
if (!this.message.trim()) return;
|
||||
|
||||
const headers = useRequestHeaders(['cookie']) as Record<string, string>;
|
||||
const preExistingDM = useDmStore().getByPartnerId(this.user.id);
|
||||
|
||||
if (preExistingDM && useRoute().path !== `/channel/@me/${preExistingDM.id}`) {
|
||||
await navigateTo(`/channel/@me/${preExistingDM.id}`);
|
||||
}
|
||||
|
||||
await $fetch(`/api/channels/${preExistingDM.id}/sendMessage`, { method: 'post', body: { body: this.message }, headers });
|
||||
this.message = '';
|
||||
|
||||
useEmojiPickerStore().closeEmojiPicker();
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</template>
|
||||
Reference in New Issue
Block a user