made emojipicker work 10x better, also fixed a few bugs
This commit is contained in:
@@ -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 */
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
859
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
12
plugins/mitt.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|||||||
@@ -76,3 +76,13 @@ export interface IReaction {
|
|||||||
Message: IMessage;
|
Message: IMessage;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IEmojiPickerData {
|
||||||
|
opened: boolean;
|
||||||
|
top: number;
|
||||||
|
right: number;
|
||||||
|
openedBy: {
|
||||||
|
type: "message" | "messageInput";
|
||||||
|
messageId?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user