brutalism design
This commit is contained in:
@@ -4,6 +4,7 @@ import { useClients } from '@shared/composables/use-clients'
|
|||||||
import { useEventBus } from '@shared/composables/use-event-bus.ts'
|
import { useEventBus } from '@shared/composables/use-event-bus.ts'
|
||||||
import { useSignaling } from '@shared/composables/use-signaling'
|
import { useSignaling } from '@shared/composables/use-signaling'
|
||||||
import { watch, watchEffect } from 'vue'
|
import { watch, watchEffect } from 'vue'
|
||||||
|
import { queryClient } from '../plugins/tanstack-query'
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const { authorized } = useAuth()
|
const { authorized } = useAuth()
|
||||||
@@ -33,8 +34,9 @@ export default function () {
|
|||||||
removeClient(socketId)
|
removeClient(socketId)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', async () => {
|
||||||
clearClients()
|
clearClients()
|
||||||
|
await queryClient.resetQueries()
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('chat:new-message', (message: ChatMessage) => {
|
socket.on('chat:new-message', (message: ChatMessage) => {
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
import type { FunctionPlugin } from 'vue'
|
import type { FunctionPlugin } from 'vue'
|
||||||
import { VueQueryPlugin } from '@tanstack/vue-query'
|
import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query'
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
install(app) {
|
install(app) {
|
||||||
app.use(VueQueryPlugin, {})
|
app.use(VueQueryPlugin, {
|
||||||
|
queryClient,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
} as FunctionPlugin
|
} as FunctionPlugin
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function useChatScroll(target: TemplateRef<HTMLElement>, options?: MaybeR
|
|||||||
getArrivedState()
|
getArrivedState()
|
||||||
}, {
|
}, {
|
||||||
childList: true,
|
childList: true,
|
||||||
// subtree: true,
|
subtree: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
useEventListener(el, 'scroll', () => {
|
useEventListener(el, 'scroll', () => {
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
import type { ChatMessage } from '@shared/api/generated-chad-api.ts'
|
import type { ChatMessage } from '@shared/api/generated-chad-api.ts'
|
||||||
import { useSignaling } from '@shared/composables/use-signaling'
|
import { useSignaling } from '@shared/composables/use-signaling'
|
||||||
import { createGlobalState } from '@vueuse/core'
|
import { createGlobalState } from '@vueuse/core'
|
||||||
import { shallowRef, triggerRef, watch } from 'vue'
|
import { computed, shallowRef, triggerRef, watch } from 'vue'
|
||||||
import { qChatMessages } from '../api/qChatMessages.ts'
|
import { qChatMessages } from '../api/qChatMessages.ts'
|
||||||
|
|
||||||
|
export interface MessageGroup {
|
||||||
|
senderUsername: string
|
||||||
|
messages: ChatMessage[]
|
||||||
|
}
|
||||||
|
|
||||||
export const useChat = createGlobalState(() => {
|
export const useChat = createGlobalState(() => {
|
||||||
const { data: messages, hasNextPage: hasMoreMessages, fetchNextPage, isFetching } = qChatMessages()
|
const { data: messages, hasNextPage: hasMoreMessages, fetchNextPage, isFetching } = qChatMessages()
|
||||||
|
|
||||||
const { socket, connected } = useSignaling()
|
const { socket, connected } = useSignaling()
|
||||||
const feedItems = shallowRef<ChatMessage[]>([])
|
const feedItems = shallowRef<ChatMessage[]>([])
|
||||||
|
|
||||||
watch(connected, (isConnected) => {
|
watch(connected, async (isConnected) => {
|
||||||
if (!isConnected)
|
if (!isConnected)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -20,5 +25,17 @@ export const useChat = createGlobalState(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return { messages, feedItems, hasMoreMessages, fetchNextPage, isFetching }
|
const messageGroups = computed<MessageGroup[]>(() => {
|
||||||
|
const all = [...(messages.value ?? []), ...feedItems.value]
|
||||||
|
return all.reduce((groups, msg) => {
|
||||||
|
const last = groups.at(-1)
|
||||||
|
if (last?.senderUsername === msg.senderUsername)
|
||||||
|
last.messages.push(msg)
|
||||||
|
else
|
||||||
|
groups.push({ senderUsername: msg.senderUsername, messages: [msg] })
|
||||||
|
return groups
|
||||||
|
}, [] as MessageGroup[])
|
||||||
|
})
|
||||||
|
|
||||||
|
return { messageGroups, hasMoreMessages, fetchNextPage, isFetching }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat-message">
|
<div class="chat-message">
|
||||||
<ChadAvatar class="chat-message__avatar" src="" :fallback="initials" :highlighted="isMyMessage" />
|
|
||||||
<div>
|
|
||||||
<div class="chat-message__top">
|
<div class="chat-message__top">
|
||||||
<p class="chat-message__sender">
|
<p class="chat-message__sender">
|
||||||
{{ !sender ? message.senderUsername : sender.displayName }}
|
{{ !sender ? message.senderUsername : sender.displayName }}
|
||||||
@@ -18,13 +16,10 @@
|
|||||||
|
|
||||||
<ChatMessageAttachments v-if="message.attachments.length > 0" :attachments="message.attachments" class="chat-message__attachments" />
|
<ChatMessageAttachments v-if="message.attachments.length > 0" :attachments="message.attachments" class="chat-message__attachments" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ChatMessage } from '@shared/api/generated-chad-api.ts'
|
import type { ChatMessage } from '@shared/api/generated-chad-api.ts'
|
||||||
import ChadAvatar from '@shared/components/ui/Avatar.vue'
|
|
||||||
import { useAuth } from '@shared/composables/use-auth.ts'
|
|
||||||
import { useUserDetails } from '@shared/composables/use-user-details.ts'
|
import { useUserDetails } from '@shared/composables/use-user-details.ts'
|
||||||
import ChatMessageAttachments from '@widgets/chat/ui/ChatMessageAttachments.vue'
|
import ChatMessageAttachments from '@widgets/chat/ui/ChatMessageAttachments.vue'
|
||||||
import { format, isThisYear, isToday } from 'date-fns'
|
import { format, isThisYear, isToday } from 'date-fns'
|
||||||
@@ -34,16 +29,8 @@ const props = defineProps<{
|
|||||||
message: ChatMessage
|
message: ChatMessage
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { me } = useAuth()
|
|
||||||
|
|
||||||
const isMyMessage = computed(() => {
|
|
||||||
return props.message.senderUsername === me.value?.username
|
|
||||||
})
|
|
||||||
|
|
||||||
const { user: sender } = useUserDetails(toRef(() => props.message.senderUsername))
|
const { user: sender } = useUserDetails(toRef(() => props.message.senderUsername))
|
||||||
|
|
||||||
const initials = computed(() => props.message.senderUsername.slice(props.message.senderUsername.length - 2))
|
|
||||||
|
|
||||||
const datetime = computed(() => {
|
const datetime = computed(() => {
|
||||||
let formatStr = 'd MMMM yyyy, HH:mm'
|
let formatStr = 'd MMMM yyyy, HH:mm'
|
||||||
|
|
||||||
@@ -62,27 +49,23 @@ const datetime = computed(() => {
|
|||||||
.chat-message {
|
.chat-message {
|
||||||
$self: &;
|
$self: &;
|
||||||
|
|
||||||
display: grid;
|
&:not(:last-child) {
|
||||||
grid-template-columns: auto 1fr;
|
margin-bottom: var(--space-3);
|
||||||
grid-template-areas: 'avatar top' 'avatar body';
|
|
||||||
column-gap: var(--space-3);
|
|
||||||
padding-block: var(--space-3);
|
|
||||||
padding-inline: var(--space-6);
|
|
||||||
|
|
||||||
&__avatar {
|
|
||||||
grid-area: avatar;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__top {
|
&__top {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
grid-area: top;
|
|
||||||
//margin-top: var(--space-1);
|
//margin-top: var(--space-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__sender {
|
&__sender {
|
||||||
@include font-body-bold;
|
@include font-body-bold;
|
||||||
|
|
||||||
|
#{$self}:not(:first-child) & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__datetime {
|
&__datetime {
|
||||||
@@ -93,7 +76,6 @@ const datetime = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__body {
|
&__body {
|
||||||
grid-area: body;
|
|
||||||
margin-top: var(--space-1);
|
margin-top: var(--space-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,21 +31,17 @@ const notImages = computed(() => {
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.chat-message-attachments {
|
.chat-message-attachments {
|
||||||
> *:not(:last-child) {
|
overflow-x: auto;
|
||||||
margin-bottom: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__images {
|
&__images,
|
||||||
|
&__list {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
overflow-x: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__list {
|
&__list {
|
||||||
> *:not(:last-child) {
|
margin-top: var(--space-3);
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ const isMyMessage = computed(() => {
|
|||||||
padding-inline: var(--space-6);
|
padding-inline: var(--space-6);
|
||||||
|
|
||||||
&__avatar {
|
&__avatar {
|
||||||
align-self: flex-start;
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: var(--space-3);
|
top: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat-messages">
|
<div class="chat-messages">
|
||||||
<div v-if="messages" ref="scroll" class="chat-messages__list">
|
<div v-if="messageGroups.length" ref="scroll" class="chat-messages__list">
|
||||||
<ChatMessage v-for="message in messages" :key="message.id" :message="message" />
|
<ChatMessageGroup
|
||||||
<ChatMessage v-for="item in feedItems" :key="item.id" :message="item" />
|
v-for="group in messageGroups"
|
||||||
|
:key="`group-${group.messages[0].id}`"
|
||||||
|
:group="group"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ChadSpinner v-if="isFetching" class="chat-messages__spinner" />
|
<ChadSpinner v-if="isFetching" class="chat-messages__spinner" />
|
||||||
@@ -18,7 +21,7 @@ import { ArrowDown } from '@lucide/vue'
|
|||||||
import ChadSpinner from '@shared/components/ui/Spinner.vue'
|
import ChadSpinner from '@shared/components/ui/Spinner.vue'
|
||||||
import { useEventBus } from '@shared/composables/use-event-bus.ts'
|
import { useEventBus } from '@shared/composables/use-event-bus.ts'
|
||||||
import { useChatScroll } from '@widgets/chat/composables/use-chat-scroll.ts'
|
import { useChatScroll } from '@widgets/chat/composables/use-chat-scroll.ts'
|
||||||
import ChatMessage from '@widgets/chat/ui/ChatMessage.vue'
|
import ChatMessageGroup from '@widgets/chat/ui/ChatMessageGroup.vue'
|
||||||
import { onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
import { onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
||||||
import { useChat } from '../composables/use-chat'
|
import { useChat } from '../composables/use-chat'
|
||||||
|
|
||||||
@@ -26,7 +29,7 @@ const eventBus = useEventBus()
|
|||||||
|
|
||||||
const scrollRef = useTemplateRef('scroll')
|
const scrollRef = useTemplateRef('scroll')
|
||||||
|
|
||||||
const { messages, feedItems, hasMoreMessages, fetchNextPage, isFetching } = useChat()
|
const { messageGroups, hasMoreMessages, fetchNextPage, isFetching } = useChat()
|
||||||
|
|
||||||
const hasUnreadMessages = ref(false)
|
const hasUnreadMessages = ref(false)
|
||||||
|
|
||||||
|
|||||||
2
server/.gitignore
vendored
2
server/.gitignore
vendored
@@ -18,3 +18,5 @@ node_modules
|
|||||||
|
|
||||||
.env*
|
.env*
|
||||||
*.db
|
*.db
|
||||||
|
|
||||||
|
uploads
|
||||||
|
|||||||
Reference in New Issue
Block a user