made emojipicker work 10x better, also fixed a few bugs

This commit is contained in:
Zoe
2023-01-20 00:48:49 -06:00
parent bf5245bec4
commit b6d3b045aa
15 changed files with 625 additions and 586 deletions

View File

@@ -7,7 +7,7 @@
--foreground-color: hsl(230,26%,13%); --foreground-color: hsl(230,26%,13%);
--primary-accent: hsl(180,55%,45%); --primary-accent: hsl(180,55%,45%);
--message-input-color: hsl(228,27.3%,25%); --message-input-color: hsl(228,27.3%,25%);
--primary-placeholder: hsl(180,25%,65%); --primary-placeholder: hsl(218,11%,65%);
--primary-dark: hsl(225, 7.7%, 10.2%); /* dropdown and emoji picker bg */ --primary-dark: hsl(225, 7.7%, 10.2%); /* dropdown and emoji picker bg */

View File

@@ -1,17 +1,11 @@
<template> <template>
<div class="relative message-wrapper" <div class="relative message-wrapper"
@mouseenter="mouseEnter()" @mouseleave="overflowShown = false">
@mouseleave="mouseLeave()">
<div class="absolute right-0 mr-10 -top-[20px] h-fit opacity-0 pointer-events-none action-buttons z-[5]" <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' : ''"> :class="(emojiPickerOpen) ? 'opacity-100 pointer-events-auto' : ''">
<div class="absolute top-0 w-[375px]" <div :id="`actions-${message.id}`"
:style="emojiPickerStyles">
<EmojiPicker v-on:pickedEmoji="pickedEmoji($event)"
:opened="emojiPickerOpen" />
</div>
<div id="actions"
class="relative bg-[var(--primary-400)] rounded-md border border-[rgb(32,34,37)] text-[var(--primary-text)] flex overflow-hidden"> class="relative bg-[var(--primary-400)] rounded-md border border-[rgb(32,34,37)] text-[var(--primary-text)] flex overflow-hidden">
<button @click="emojiPickerOpen = !emojiPickerOpen" <button @click="openEmojiPicker()"
class="p-1 hover:backdrop-brightness-125 transition-all flex w-fit h-fit"> class="p-1 hover:backdrop-brightness-125 transition-all flex w-fit h-fit">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
width="20" width="20"
@@ -25,8 +19,8 @@
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" /> 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> </svg>
</button> </button>
<button v-if="!actionButtonOverflowMenuOpen" <button v-if="!shiftPressed && !overflowShown"
@click="actionButtonOverflowMenuOpen = true" @click="overflowShown = true"
class="p-1 hover:backdrop-brightness-125 transition-all flex w-fit h-fit"> class="p-1 hover:backdrop-brightness-125 transition-all flex w-fit h-fit">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
width="20" width="20"
@@ -49,8 +43,7 @@
</g> </g>
</svg> </svg>
</button> </button>
<div @click="actionButtonOverflowMenuOpen = false" <div v-if="shiftPressed || overflowShown"
v-if="actionButtonOverflowMenuOpen"
class="flex"> class="flex">
<button @click="copy(message.id)" <button @click="copy(message.id)"
class="p-1 hover:backdrop-brightness-125 transition-all flex text-[var(--primary-400)] w-[28px] h-[28px] items-center justify-center"> class="p-1 hover:backdrop-brightness-125 transition-all flex text-[var(--primary-400)] w-[28px] h-[28px] items-center justify-center">
@@ -118,7 +111,7 @@
<script lang="ts"> <script lang="ts">
import { PropType } from 'vue'; import { PropType } from 'vue';
import { IMessage } from '~/types'; import { IEmojiPickerData, IMessage } from '~/types';
import { useGlobalStore } from '~/stores/store'; import { useGlobalStore } from '~/stores/store';
import { useClipboard } from '@vueuse/core' import { useClipboard } from '@vueuse/core'
import emojiJson from '~/assets/json/emoji.json'; import emojiJson from '~/assets/json/emoji.json';
@@ -133,6 +126,10 @@ export default {
type: Boolean, type: Boolean,
required: true required: true
}, },
shiftPressed: {
type: Boolean,
required: true
},
classes: { classes: {
type: String, type: String,
required: true required: true
@@ -142,8 +139,7 @@ export default {
return { return {
user: storeToRefs(useGlobalStore()).user, user: storeToRefs(useGlobalStore()).user,
emojiPickerOpen: false, emojiPickerOpen: false,
emojiPickerStyles: this.calculateEmojiPickerRight(), overflowShown: false
actionButtonOverflowMenuOpen: false,
} }
}, },
setup() { setup() {
@@ -153,6 +149,16 @@ export default {
copy copy
} }
}, },
mounted() {
const { $listen } = useNuxtApp()
$listen('pickedEmoji', (emoji) => {
if (useGlobalStore().emojiPickerData.openedBy.messageId !== this.message.id) return;
const replacementEmoji = emojiJson.find((e) => e.short_name === emoji);
if (!replacementEmoji?.emoji) return;
if (this.message.reactions?.find((e) => e.emoji.name === replacementEmoji.emoji)) return
this.toggleReaction(replacementEmoji.emoji)
});
},
methods: { methods: {
async toggleReaction(emoji: string) { async toggleReaction(emoji: string) {
const route = useRoute() const route = useRoute()
@@ -162,6 +168,25 @@ export default {
useGlobalStore().updateMessage(this.message.id, message) useGlobalStore().updateMessage(this.message.id, message)
}, },
openEmojiPicker() {
const actionButtons = document.getElementById(`actions-${this.message.id}`);
if (!actionButtons) return;
const elementRect = actionButtons.getBoundingClientRect();
let top = elementRect.top + window.pageYOffset;
if (top + 522 > window.innerHeight) top = window.innerHeight - 522;
const payload = {
top,
right: actionButtons.clientWidth + 40,
openedBy: {
type: "message",
messageId: this.message.id
}
} as IEmojiPickerData
useGlobalStore().toggleEmojiPicker(payload)
},
emojiStyles(emoji: string, width: number) { emojiStyles(emoji: string, width: number) {
const emojis = emojiJson.filter((e) => e.has_img_twitter) const emojis = emojiJson.filter((e) => e.has_img_twitter)
const twemoji = emojis.find((e) => e.emoji === emoji) const twemoji = emojis.find((e) => e.emoji === emoji)
@@ -179,44 +204,10 @@ export default {
'background-size': '1037px 1037px' 'background-size': '1037px 1037px'
} }
}, },
pickedEmoji(emoji: string) {
const replacementEmoji = emojiJson.find((e) => e.short_name === emoji);
if (!replacementEmoji?.emoji) return;
if (this.message.reactions?.find((e) => e.emoji.name === replacementEmoji.emoji)) return
this.toggleReaction(replacementEmoji.emoji)
this.emojiPickerOpen = false;
},
async deleteMessage() { async deleteMessage() {
const route = useRoute() const route = useRoute()
await $fetch(`/api/channels/${route.params.id}/messages/${this.message.id}/delete`, { method: "POST" }) await $fetch(`/api/channels/${route.params.id}/messages/${this.message.id}/delete`, { method: "POST" })
}, },
calculateEmojiPickerRight() {
const actions = document.getElementById('actions')
if (!actions) return {}
const right = actions.clientWidth + 8
return {
right: right + 'px'
}
},
keyPressed(ev: KeyboardEvent) {
if (ev.key === 'Shift') {
this.actionButtonOverflowMenuOpen = true
}
},
keyUnpressed(ev: KeyboardEvent) {
if (ev.key === 'Shift') {
this.actionButtonOverflowMenuOpen = false
}
},
mouseEnter() {
document.body.addEventListener('keydown', this.keyPressed, false);
document.body.addEventListener('keyup', this.keyUnpressed, false);
},
mouseLeave() {
this.actionButtonOverflowMenuOpen = false
document.body.removeEventListener('keydown', this.keyPressed, false)
document.body.removeEventListener('keyup', this.keyUnpressed, false)
}
} }
} }
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="h-full relative text-white bg-[var(--background-color)] grid grid-rows-[48px_1fr]"> <div class="h-full relative text-white bg-[var(--background-color)] grid grid-rows-[48px_1fr]" @mouseenter="mouseEnter" @mouseleave="mouseLeave">
<div class="w-full px-4 py-3 z-[1]"> <div class="w-full px-4 py-3 z-[1]">
<div v-if="!server.DM" <div v-if="!server.DM"
class="flex items-center"> class="flex items-center">
@@ -42,7 +42,7 @@
</div> </div>
<section <section
class="bg-[var(--foreground-color)] my-3 mx-1 h-[calc(100%-24px)] overflow-hidden rounded-lg relative grid grid-rows-[1fr_70px]"> class="bg-[var(--foreground-color)] mb-3 mx-1 h-[calc(100%-12px)] overflow-hidden rounded-lg relative grid grid-rows-[1fr_70px]">
<div class="h-full overflow-y-scroll" id="conversation-pane"> <div class="h-full overflow-y-scroll" id="conversation-pane">
<div class="w-full pb-1 bg-inherit"> <div class="w-full pb-1 bg-inherit">
<div> <div>
@@ -52,8 +52,9 @@
<Message v-else <Message v-else
v-for="(message, i) in server.messages" v-for="(message, i) in server.messages"
:message="message" :message="message"
:classes="calculateMessageClasses(message, i)" :shiftPressed="shiftPressed"
:showUsername="i === 0 || server.messages[i - 1]?.creator.id !== message.creator.id" /> :showUsername="i === 0 || server.messages[i - 1]?.creator.id !== message.creator.id"
:classes="calculateMessageClasses(message, i)" />
</div> </div>
</div> </div>
<div v-if="showSearch" <div v-if="showSearch"
@@ -131,6 +132,7 @@ export default {
server: storeToRefs(useGlobalStore()).activeChannel, server: storeToRefs(useGlobalStore()).activeChannel,
messageContent: '', messageContent: '',
canSendNotifications: false, canSendNotifications: false,
shiftPressed: false,
servers: storeToRefs(useGlobalStore()).servers, servers: storeToRefs(useGlobalStore()).servers,
usersTyping: [] as string[], usersTyping: [] as string[],
socket: storeToRefs(useGlobalStore()).socket as unknown as Server, socket: storeToRefs(useGlobalStore()).socket as unknown as Server,
@@ -196,6 +198,25 @@ export default {
this.socket.emit(`typing`, this.server.id); this.socket.emit(`typing`, this.server.id);
}, },
mouseEnter() {
document.body.addEventListener('keydown', this.keyPressed, false);
document.body.addEventListener('keyup', this.keyUnpressed, false);
},
mouseLeave() {
this.shiftPressed = false
document.body.removeEventListener('keydown', this.keyPressed, false)
document.body.removeEventListener('keyup', this.keyUnpressed, false)
},
keyPressed(ev: KeyboardEvent) {
if (ev.key === 'Shift') {
this.shiftPressed = true
}
},
keyUnpressed(ev: KeyboardEvent) {
if (ev.key === 'Shift') {
this.shiftPressed = false
}
},
calculateMessageClasses(message: IMessage, i: number) { calculateMessageClasses(message: IMessage, i: number) {
if (i === 0 || this.server.messages[i - 1]?.creator.id !== message.creator.id) { if (i === 0 || this.server.messages[i - 1]?.creator.id !== message.creator.id) {
if (i !== this.server.messages.length - 1 || this.server.messages[i + 1]?.creator.id === message.creator.id) { if (i !== this.server.messages.length - 1 || this.server.messages[i + 1]?.creator.id === message.creator.id) {
@@ -291,9 +312,7 @@ export default {
if (this.server.messages.find((e) => e.id === message.id)) { if (this.server.messages.find((e) => e.id === message.id)) {
// message is already in the server, replace it with the updated message // message is already in the server, replace it with the updated message
console.log(message.id, message.body)
useGlobalStore().updateMessage(message.id, message) useGlobalStore().updateMessage(message.id, message)
console.log('raw', useGlobalStore().activeChannel.messages.find((e) => e.id === message.id)?.body)
return; return;
} }

View File

@@ -81,7 +81,7 @@
<div v-if="createServerModelOpen" <div v-if="createServerModelOpen"
class="absolute z-10 top-0 bottom-0 left-0 right-0"> class="absolute z-10 top-0 bottom-0 left-0 right-0">
<div <div
class="p-4 z-20 absolute bg-[var(--primary-600)] shadow-md rounded-md -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 text-white"> class="p-4 z-20 absolute bg-[var(--primary-500)] shadow-md rounded-md -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 text-white">
<h2 class="font-semibold text-xl"> <h2 class="font-semibold text-xl">
Create a server: Create a server:
</h2> </h2>
@@ -90,14 +90,14 @@
class="w-3/5"> class="w-3/5">
<input v-model="serverName" <input v-model="serverName"
type="text" type="text"
class="py-2 px-3 rounded-md mb-2 bg-zinc-700 shadow-md border border-zinc-700/80" class="py-2 px-3 rounded-md mb-2 bg-[var(--message-input-color)] shadow-md placeholder:text-[var(--primary-placeholder)]"
placeholder="Server name" /> placeholder="Server name" />
<input type="submit" <input type="submit"
class="py-2 px-3 rounded-md bg-zinc-700 shadow-md border border-zinc-700/80" /> class="py-2 px-3 rounded-md bg-[var(--message-input-color)] shadow-md" />
</form> </form>
</div> </div>
</div> </div>
<div class="bg-zinc-900/80 w-screen h-screen" <div class="bg-black/70 w-screen h-screen"
@click="createServerModelOpen = false"> @click="createServerModelOpen = false">
</div> </div>
</div> </div>

View File

@@ -10,7 +10,7 @@
</section> </section>
<div <div
class="h-[calc(100%-24px)] my-3 mx-1 grid grid-rows-[1fr_56px] bg-[var(--foreground-color)] rounded-lg"> class="h-[calc(100%-12px)] mb-3 mx-1 grid grid-rows-[1fr_56px] bg-[var(--foreground-color)] rounded-lg">
<div class="h-fit"> <div class="h-fit">
<nuxt-link v-for="dm in dms" <nuxt-link v-for="dm in dms"
:to="'/channel/@me/' + dm.id"> :to="'/channel/@me/' + dm.id">
@@ -95,7 +95,7 @@
<div <div
class="h-[calc(100%-24px)] my-3 mx-1 grid grid-rows-[1fr_56px] bg-[var(--foreground-color)] rounded-lg"> class="h-[calc(100%-12px)] mb-3 mx-1 grid grid-rows-[1fr_56px] bg-[var(--foreground-color)] rounded-lg">
<div class="flex gap-y-1.5 px-1.5 mt-2 flex-col overflow-x-scroll"> <div class="flex gap-y-1.5 px-1.5 mt-2 flex-col overflow-x-scroll">
<button <button
class="flex text-center bg-inherit hover:backdrop-brightness-[1.35] 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 hover:backdrop-brightness-[1.35] px-2 py-1.5 w-full transition-all rounded drop-shadow-sm gap-1/5 cursor-pointer items-center"
@@ -221,11 +221,11 @@
<div v-if="createChannelModelOpen" <div v-if="createChannelModelOpen"
class="absolute z-10 top-0 bottom-0 left-0 right-0"> class="absolute z-10 top-0 bottom-0 left-0 right-0">
<div class="bg-[var(--primary-600)] w-screen h-screen" <div class="bg-black/70 w-screen h-screen"
@click="createChannelModelOpen = false"> @click="createChannelModelOpen = false">
</div> </div>
<div <div
class="p-4 z-20 absolute bg-zinc-800 shadow-md rounded-md -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 text-white"> class="p-4 z-20 absolute bg-[var(--primary-500)] shadow-md rounded-md -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 text-white">
<h2 class="font-semibold text-xl"> <h2 class="font-semibold text-xl">
Create a channel: Create a channel:
</h2> </h2>
@@ -234,10 +234,10 @@
class="w-3/5"> class="w-3/5">
<input v-model="channelName" <input v-model="channelName"
type="text" type="text"
class="py-2 px-3 rounded-md mb-2 bg-zinc-700 shadow-md border border-zinc-700/80" class="py-2 px-3 rounded-md mb-2 bg-[var(--message-input-color)] shadow-md"
placeholder="Channel name" /> placeholder="Channel name" />
<input type="submit" <input type="submit"
class="py-2 px-3 rounded-md bg-zinc-700 shadow-md border border-zinc-700/80" /> class="py-2 px-3 rounded-md bg-[var(--message-input-color)] shadow-md" />
</form> </form>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
// https://v3.nuxtjs.org/api/configuration/nuxt.config // https://v3.nuxtjs.org/api/configuration/nuxt.config
export default { export default {
ssr: false, ssr: true,
app: { app: {
head: { head: {
meta: [ meta: [

859
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"emoji-datasource-twitter": "^14.0.0", "emoji-datasource-twitter": "^14.0.0",
"emoji-regex": "^10.2.1", "emoji-regex": "^10.2.1",
"mitt": "^3.0.0",
"nuxt": "^3.0.0", "nuxt": "^3.0.0",
"pinia": "^2.0.28", "pinia": "^2.0.28",
"socket.io": "^4.5.4", "socket.io": "^4.5.4",

View File

@@ -1,5 +1,12 @@
<template> <template>
<MessagePane /> <MessagePane />
<div class="fixed mr-3"
:style="`top: ${emojiPickerData.top}px; right: ${emojiPickerData.right}px`">
<Transition>
<EmojiPicker v-on:pickedEmoji="pickedEmoji($event)"
:opened="emojiPickerData.opened" />
</Transition>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -11,6 +18,15 @@ definePageMeta({
}) })
export default { export default {
data() {
return {
emojiPickerData: storeToRefs(useGlobalStore()).emojiPickerData,
emojiPickerStyles: {
top: storeToRefs(useGlobalStore()).emojiPickerData.top + 'px',
right: storeToRefs(useGlobalStore()).emojiPickerData.right + 'px',
}
}
},
async setup() { async setup() {
const route = useRoute() const route = useRoute()
const headers = useRequestHeaders(['cookie']) as Record<string, string> const headers = useRequestHeaders(['cookie']) as Record<string, string>
@@ -22,6 +38,7 @@ export default {
if (typeof route.params.id !== 'string') throw new Error('route.params.id must be a string, but got an array presumably?') if (typeof route.params.id !== 'string') throw new Error('route.params.id must be a string, but got an array presumably?')
useGlobalStore().setActiveServer('dms', route.params.id); useGlobalStore().setActiveServer('dms', route.params.id);
useGlobalStore().setActiveChannel(server) useGlobalStore().setActiveChannel(server)
useGlobalStore().closeEmojiPicker()
server.messages?.forEach((e) => { server.messages?.forEach((e) => {
e.body = parseMessageBody(e.body, useGlobalStore().activeChannel) e.body = parseMessageBody(e.body, useGlobalStore().activeChannel)
@@ -34,9 +51,29 @@ export default {
async updated() { async updated() {
const route = useRoute() const route = useRoute()
if (typeof route.params.id !== 'string') throw new Error('route.params.id must be a string, but got an array presumably?') if (typeof route.params.id !== 'string') throw new Error('route.params.id must be a string, but got an array presumably?')
if (useGlobalStore().activeServer !== this.server) { if (useGlobalStore().activeServer.id !== this.server.id) {
useGlobalStore().closeEmojiPicker()
useGlobalStore().setActiveServer('dms', route.params.id) useGlobalStore().setActiveServer('dms', route.params.id)
} }
}, },
methods: {
pickedEmoji(emoji: string) {
const { $emit } = useNuxtApp()
$emit('pickedEmoji', emoji)
useGlobalStore().closeEmojiPicker()
},
}
} }
</script> </script>
<style>
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>

View File

@@ -1,5 +1,12 @@
<template> <template>
<MessagePane /> <MessagePane />
<div class="fixed mr-3"
:style="`top: ${emojiPickerData.top}px; right: ${emojiPickerData.right}px`">
<Transition>
<EmojiPicker v-on:pickedEmoji="pickedEmoji($event)"
:opened="emojiPickerData.opened" />
</Transition>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -15,6 +22,11 @@ export default {
data() { data() {
return { return {
socket: storeToRefs(useGlobalStore()).socket as unknown as Server, socket: storeToRefs(useGlobalStore()).socket as unknown as Server,
emojiPickerData: storeToRefs(useGlobalStore()).emojiPickerData,
emojiPickerStyles: {
top: storeToRefs(useGlobalStore()).emojiPickerData.top + 'px',
right: storeToRefs(useGlobalStore()).emojiPickerData.right + 'px',
}
} }
}, },
mounted() { mounted() {
@@ -31,7 +43,8 @@ export default {
this.server = await $fetch(`/api/channels/${route.params.id}`, { headers }); this.server = await $fetch(`/api/channels/${route.params.id}`, { headers });
if (typeof route.params.id !== 'string') throw new Error('route.params.id must be a string, but got an array presumiably?') if (typeof route.params.id !== 'string') throw new Error('route.params.id must be a string, but got an array presumiably?')
if (useGlobalStore().activeServer.id !== this.server.id) { if (useGlobalStore().activeChannel.id !== this.server.id) {
useGlobalStore().closeEmojiPicker()
useGlobalStore().setActiveServer('servers', route.params.id) useGlobalStore().setActiveServer('servers', route.params.id)
// update the server with the refreshed data // update the server with the refreshed data
useGlobalStore().updateServer(route.params.id, this.server.server) useGlobalStore().updateServer(route.params.id, this.server.server)
@@ -49,6 +62,7 @@ export default {
if (typeof route.params.id !== 'string') throw new Error('route.params.id must be a string, but got an array presumiably?') if (typeof route.params.id !== 'string') throw new Error('route.params.id must be a string, but got an array presumiably?')
useGlobalStore().setActiveServer('servers', route.params.id) useGlobalStore().setActiveServer('servers', route.params.id)
useGlobalStore().setActiveChannel(server) useGlobalStore().setActiveChannel(server)
useGlobalStore().closeEmojiPicker()
server.messages?.forEach((e) => { server.messages?.forEach((e) => {
e.body = parseMessageBody(e.body, useGlobalStore().activeChannel) e.body = parseMessageBody(e.body, useGlobalStore().activeChannel)
@@ -58,5 +72,12 @@ export default {
server, server,
} }
}, },
methods: {
pickedEmoji(emoji: string) {
const { $emit } = useNuxtApp()
$emit('pickedEmoji', emoji)
useGlobalStore().closeEmojiPicker()
},
}
} }
</script> </script>

12
plugins/mitt.ts Normal file
View File

@@ -0,0 +1,12 @@
import mitt from 'mitt'
export default defineNuxtPlugin(() => {
const emitter = mitt()
return {
provide: {
emit: emitter.emit, // Will emit an event
listen: emitter.on // Will register a listener for an event
}
}
})

View File

@@ -1,34 +1,50 @@
generator client {
provider = "prisma-client-js"
}
datasource db { datasource db {
provider = "postgresql" provider = "postgresql"
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
generator client { model User {
provider = "prisma-client-js" id String @id @default(cuid())
email String @unique
username String @unique
passwordhash String
createdAt DateTime @default(now())
messages Message[]
session Session[]
channels Channel[] @relation("ChannelToUser")
Reactions Reaction[] @relation("ReactionToUser")
roles Role[] @relation("RoleToUser")
servers Server[] @relation("ServerToUser")
incomingFriendRequests friendRequest[] @relation("FriendRequestToUser")
outgoingFriendRequests friendRequest[] @relation("UserToFriendRequest")
friends User[] @relation("UserToFriends")
// This second "side" of the UserFriends relation exists solely
// to satisfy prisma's requirements; we won't access it directly.
symmetricFriends User[] @relation("UserToFriends")
} }
model User { model friendRequest {
id String @id @default(cuid()) id String @id @default(cuid())
email String @unique sender User @relation(fields: [senderId], references: [id], name: "UserToFriendRequest")
username String @unique recipient User @relation(fields: [recipientId], references: [id], name: "FriendRequestToUser")
passwordhash String senderId String
servers Server[] recipientId String
messages Message[] status String @default("sent")
session Session[]
channels Channel[]
roles Role[]
createdAt DateTime @default(now())
Reactions Reaction[]
} }
model Server { model Server {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
participants User[]
channels Channel[]
roles Role[]
InviteCode InviteCode[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
channels Channel[]
InviteCode InviteCode[]
roles Role[]
participants User[] @relation("ServerToUser")
} }
model Role { model Role {
@@ -36,43 +52,43 @@ model Role {
name String name String
administrator Boolean @default(false) administrator Boolean @default(false)
owner Boolean @default(false) owner Boolean @default(false)
users User[]
server Server? @relation(fields: [serverId], references: [id])
serverId String? serverId String?
server Server? @relation(fields: [serverId], references: [id])
users User[] @relation("RoleToUser")
} }
model Channel { model Channel {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
server Server? @relation(fields: [serverId], references: [id])
serverId String? serverId String?
messages Message[]
DM Boolean @default(false) DM Boolean @default(false)
dmParticipants User[] server Server? @relation(fields: [serverId], references: [id])
messages Message[]
dmParticipants User[] @relation("ChannelToUser")
} }
model Message { model Message {
id String @id @default(cuid()) id String @id @default(cuid())
body String body String
channel Channel @relation(fields: [channelId], references: [id])
creator User @relation(fields: [userId], references: [id])
userId String userId String
channelId String channelId String
invites InviteCode[]
reactions Reaction[]
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
invites InviteCode[]
channel Channel @relation(fields: [channelId], references: [id])
creator User @relation(fields: [userId], references: [id])
reactions Reaction[]
} }
model InviteCode { model InviteCode {
id String @id @default(cuid()) id String @id @default(cuid())
server Server @relation(fields: [serverId], references: [id])
expires Boolean @default(false) expires Boolean @default(false)
expiryDate DateTime? expiryDate DateTime?
maxUses Int @default(0) maxUses Int @default(0)
serverId String serverId String
message Message? @relation(fields: [messageId], references: [id])
messageId String? messageId String?
message Message? @relation(fields: [messageId], references: [id])
server Server @relation(fields: [serverId], references: [id])
} }
model Session { model Session {
@@ -82,16 +98,11 @@ model Session {
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
} }
model ExpiredSession {
id String @id @default(cuid())
token String
}
model Reaction { model Reaction {
id String @id @default(cuid()) id String @id @default(cuid())
emoji Json emoji Json
count Int count Int
users User[]
Message Message? @relation(fields: [messageId], references: [id])
messageId String? messageId String?
Message Message? @relation(fields: [messageId], references: [id])
users User[] @relation("ReactionToUser")
} }

View File

@@ -17,6 +17,12 @@ export default defineEventHandler(async (event) => {
select: { select: {
id: true, id: true,
username: true, username: true,
friends: {
select: {
id: true,
username: true,
}
}
} }
}) as SafeUser | null; }) as SafeUser | null;

View File

@@ -1,7 +1,7 @@
import { channel } from "diagnostics_channel"; import { channel } from "diagnostics_channel";
import { serve } from "esbuild"; import { serve } from "esbuild";
import { Socket } from "socket.io-client"; import { Socket } from "socket.io-client";
import { SafeUser, IServer, IChannel, IMessage } from "../types"; import { SafeUser, IServer, IChannel, IMessage, IEmojiPickerData } from "../types";
export const useGlobalStore = defineStore('global', { export const useGlobalStore = defineStore('global', {
state: () => ({ state: () => ({
@@ -11,6 +11,7 @@ export const useGlobalStore = defineStore('global', {
user: {} as SafeUser, user: {} as SafeUser,
dms: [] as IChannel[], dms: [] as IChannel[],
servers: [] as IServer[], servers: [] as IServer[],
emojiPickerData: {} as IEmojiPickerData,
socket: null as unknown socket: null as unknown
}), }),
actions: { actions: {
@@ -89,6 +90,33 @@ export const useGlobalStore = defineStore('global', {
if (!this.activeChannel.messages.find(m => m.id === messageId)) return; if (!this.activeChannel.messages.find(m => m.id === messageId)) return;
this.activeChannel.messages = this.activeChannel.messages.filter(m => m.id !== messageId) this.activeChannel.messages = this.activeChannel.messages.filter(m => m.id !== messageId)
}, },
openEmojiPicker(payload: IEmojiPickerData) {
this.emojiPickerData.top = payload.top;
this.emojiPickerData.right = payload.right;
this.emojiPickerData.openedBy = payload.openedBy;
this.emojiPickerData.opened = true;
},
toggleEmojiPicker(payload: IEmojiPickerData) {
let messageId;
if (this.emojiPickerData.openedBy === undefined) {
messageId = null
} else {
messageId = this.emojiPickerData.openedBy.messageId || null
}
console.log(!this.emojiPickerData.opened || payload.openedBy.messageId !== messageId, this.emojiPickerData.opened, payload.openedBy.messageId, messageId)
if (!this.emojiPickerData.opened || payload.openedBy.messageId !== messageId) {
this.openEmojiPicker(payload)
} else {
this.closeEmojiPicker()
}
},
closeEmojiPicker() {
console.log('closeEmojiPicker')
if (this.emojiPickerData.openedBy) this.emojiPickerData.openedBy.messageId = '';
this.emojiPickerData.opened = false;
},
logout() { logout() {
this.dms = [] this.dms = []
this.servers = [] this.servers = []

View File

@@ -75,4 +75,14 @@ export interface IReaction {
users: IUser[]; users: IUser[];
Message: IMessage; Message: IMessage;
messageId: string; messageId: string;
}
export interface IEmojiPickerData {
opened: boolean;
top: number;
right: number;
openedBy: {
type: "message" | "messageInput";
messageId?: string;
};
} }