brutalism design

This commit is contained in:
2026-05-22 06:01:27 +06:00
parent 5d45c9674f
commit d06892e990
9 changed files with 68 additions and 57 deletions

View File

@@ -4,6 +4,7 @@ import { useClients } from '@shared/composables/use-clients'
import { useEventBus } from '@shared/composables/use-event-bus.ts'
import { useSignaling } from '@shared/composables/use-signaling'
import { watch, watchEffect } from 'vue'
import { queryClient } from '../plugins/tanstack-query'
export default function () {
const { authorized } = useAuth()
@@ -33,8 +34,9 @@ export default function () {
removeClient(socketId)
})
socket.on('disconnect', () => {
socket.on('disconnect', async () => {
clearClients()
await queryClient.resetQueries()
})
socket.on('chat:new-message', (message: ChatMessage) => {

View File

@@ -1,8 +1,18 @@
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 {
install(app) {
app.use(VueQueryPlugin, {})
app.use(VueQueryPlugin, {
queryClient,
})
},
} as FunctionPlugin

View File

@@ -35,7 +35,7 @@ export function useChatScroll(target: TemplateRef<HTMLElement>, options?: MaybeR
getArrivedState()
}, {
childList: true,
// subtree: true,
subtree: true,
})
useEventListener(el, 'scroll', () => {

View File

@@ -1,16 +1,21 @@
import type { ChatMessage } from '@shared/api/generated-chad-api.ts'
import { useSignaling } from '@shared/composables/use-signaling'
import { createGlobalState } from '@vueuse/core'
import { shallowRef, triggerRef, watch } from 'vue'
import { computed, shallowRef, triggerRef, watch } from 'vue'
import { qChatMessages } from '../api/qChatMessages.ts'
export interface MessageGroup {
senderUsername: string
messages: ChatMessage[]
}
export const useChat = createGlobalState(() => {
const { data: messages, hasNextPage: hasMoreMessages, fetchNextPage, isFetching } = qChatMessages()
const { socket, connected } = useSignaling()
const feedItems = shallowRef<ChatMessage[]>([])
watch(connected, (isConnected) => {
watch(connected, async (isConnected) => {
if (!isConnected)
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 }
})

View File

@@ -1,30 +1,25 @@
<template>
<div class="chat-message">
<ChadAvatar class="chat-message__avatar" src="" :fallback="initials" :highlighted="isMyMessage" />
<div>
<div class="chat-message__top">
<p class="chat-message__sender">
{{ !sender ? message.senderUsername : sender.displayName }}
</p>
<p class="chat-message__datetime">
{{ datetime }}
</p>
</div>
<p class="chat-message__body">
{{ message.text }}
<div class="chat-message__top">
<p class="chat-message__sender">
{{ !sender ? message.senderUsername : sender.displayName }}
</p>
<ChatMessageAttachments v-if="message.attachments.length > 0" :attachments="message.attachments" class="chat-message__attachments" />
<p class="chat-message__datetime">
{{ datetime }}
</p>
</div>
<p class="chat-message__body">
{{ message.text }}
</p>
<ChatMessageAttachments v-if="message.attachments.length > 0" :attachments="message.attachments" class="chat-message__attachments" />
</div>
</template>
<script setup lang="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 ChatMessageAttachments from '@widgets/chat/ui/ChatMessageAttachments.vue'
import { format, isThisYear, isToday } from 'date-fns'
@@ -34,16 +29,8 @@ const props = defineProps<{
message: ChatMessage
}>()
const { me } = useAuth()
const isMyMessage = computed(() => {
return props.message.senderUsername === me.value?.username
})
const { user: sender } = useUserDetails(toRef(() => props.message.senderUsername))
const initials = computed(() => props.message.senderUsername.slice(props.message.senderUsername.length - 2))
const datetime = computed(() => {
let formatStr = 'd MMMM yyyy, HH:mm'
@@ -62,27 +49,23 @@ const datetime = computed(() => {
.chat-message {
$self: &;
display: grid;
grid-template-columns: auto 1fr;
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;
&:not(:last-child) {
margin-bottom: var(--space-3);
}
&__top {
display: flex;
gap: var(--space-2);
align-items: center;
grid-area: top;
//margin-top: var(--space-1);
}
&__sender {
@include font-body-bold;
#{$self}:not(:first-child) & {
display: none;
}
}
&__datetime {
@@ -93,7 +76,6 @@ const datetime = computed(() => {
}
&__body {
grid-area: body;
margin-top: var(--space-1);
}

View File

@@ -31,21 +31,17 @@ const notImages = computed(() => {
<style lang="scss">
.chat-message-attachments {
> *:not(:last-child) {
margin-bottom: var(--space-3);
}
overflow-x: auto;
&__images {
&__images,
&__list {
display: flex;
gap: var(--space-2);
flex-wrap: nowrap;
overflow-x: auto;
}
&__list {
> *:not(:last-child) {
margin-bottom: var(--space-2);
}
margin-top: var(--space-3);
}
}
</style>

View File

@@ -41,7 +41,6 @@ const isMyMessage = computed(() => {
padding-inline: var(--space-6);
&__avatar {
align-self: flex-start;
position: sticky;
top: var(--space-3);
}

View File

@@ -1,8 +1,11 @@
<template>
<div class="chat-messages">
<div v-if="messages" ref="scroll" class="chat-messages__list">
<ChatMessage v-for="message in messages" :key="message.id" :message="message" />
<ChatMessage v-for="item in feedItems" :key="item.id" :message="item" />
<div v-if="messageGroups.length" ref="scroll" class="chat-messages__list">
<ChatMessageGroup
v-for="group in messageGroups"
:key="`group-${group.messages[0].id}`"
:group="group"
/>
</div>
<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 { useEventBus } from '@shared/composables/use-event-bus.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 { useChat } from '../composables/use-chat'
@@ -26,7 +29,7 @@ const eventBus = useEventBus()
const scrollRef = useTemplateRef('scroll')
const { messages, feedItems, hasMoreMessages, fetchNextPage, isFetching } = useChat()
const { messageGroups, hasMoreMessages, fetchNextPage, isFetching } = useChat()
const hasUnreadMessages = ref(false)

2
server/.gitignore vendored
View File

@@ -18,3 +18,5 @@ node_modules
.env*
*.db
uploads