chat wip
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ark-ui/vue": "^5.36.2",
|
"@ark-ui/vue": "^5.36.2",
|
||||||
"@lucide/vue": "^1.14.0",
|
"@lucide/vue": "^1.14.0",
|
||||||
|
"@tanstack/query-persist-client-core": "^5.100.10",
|
||||||
"@tanstack/vue-query": "^5.100.10",
|
"@tanstack/vue-query": "^5.100.10",
|
||||||
"@tauri-apps/plugin-global-shortcut": "^2.3.1",
|
"@tauri-apps/plugin-global-shortcut": "^2.3.1",
|
||||||
"@tauri-apps/plugin-opener": "~2",
|
"@tauri-apps/plugin-opener": "~2",
|
||||||
|
|||||||
22
new-client/src/shared/api/qUser.ts
Normal file
22
new-client/src/shared/api/qUser.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type { ResponseError, User } from '@shared/api/generated-chad-api.ts'
|
||||||
|
import type { MaybeRefOrGetter } from 'vue'
|
||||||
|
import api from '@shared/api/client.ts'
|
||||||
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
|
import { toValue } from 'vue'
|
||||||
|
|
||||||
|
export function qUser(username: MaybeRefOrGetter<string>) {
|
||||||
|
return useQuery<User, ResponseError>({
|
||||||
|
queryKey: ['user', username],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.chad.userGet({ id: toValue(username) })
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
staleTime: Infinity,
|
||||||
|
// persister: experimental_createQueryPersister({
|
||||||
|
// storage: localStorage,
|
||||||
|
// maxAge: 1000 * 60 * 60 * 24,
|
||||||
|
// prefix: 'CHAD_USER_',
|
||||||
|
// }),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chad-avatar" v-bind="api.getRootProps()">
|
<div class="chad-avatar" :class="{ highlighted }" v-bind="api.getRootProps()">
|
||||||
<span class="chad-avatar__fallback" v-bind="api.getFallbackProps()">{{ fallback }}</span>
|
<span class="chad-avatar__fallback" v-bind="api.getFallbackProps()">{{ fallback }}</span>
|
||||||
<img class="chad-avatar__image" :alt="fallback" :src="src" v-bind="api.getImageProps()">
|
<img class="chad-avatar__image" :alt="fallback" :src="src" v-bind="api.getImageProps()">
|
||||||
</div>
|
</div>
|
||||||
@@ -18,6 +18,7 @@ defineProps<{
|
|||||||
src: string
|
src: string
|
||||||
fallback: string
|
fallback: string
|
||||||
size?: 'sm'
|
size?: 'sm'
|
||||||
|
highlighted?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const service = useMachine(avatar.machine, { id: useId() })
|
const service = useMachine(avatar.machine, { id: useId() })
|
||||||
@@ -27,24 +28,31 @@ const api = computed(() => avatar.connect(service, normalizeProps))
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.chad-avatar {
|
.chad-avatar {
|
||||||
|
$self: &;
|
||||||
|
|
||||||
outline: var(--border-w) solid var(--ink);
|
outline: var(--border-w) solid var(--ink);
|
||||||
outline-offset: calc(var(--border-w) * -1);
|
outline-offset: calc(var(--border-w) * -1);
|
||||||
width: 40px;
|
width: 32px;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
|
|
||||||
&__fallback {
|
&__fallback {
|
||||||
@include font-label;
|
@include font-label;
|
||||||
|
|
||||||
height: 100%;
|
display: block;
|
||||||
display: flex;
|
text-align: center;
|
||||||
align-items: center;
|
line-height: 32px;
|
||||||
justify-content: center;
|
background-color: var(--grey-2);
|
||||||
|
|
||||||
|
#{$self}.highlighted & {
|
||||||
|
background-color: var(--yellow);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__image {
|
&__image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,11 +7,14 @@
|
|||||||
:type="type"
|
:type="type"
|
||||||
:data-full="full || undefined"
|
:data-full="full || undefined"
|
||||||
>
|
>
|
||||||
<slot />
|
<ChadSpinner v-if="loading" class="chad-button__spinner" />
|
||||||
|
<slot v-else />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import ChadSpinner from '@shared/components/ui/Spinner.vue'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'ChadButton',
|
name: 'ChadButton',
|
||||||
})
|
})
|
||||||
@@ -61,7 +64,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[data-disabled],
|
&[data-disabled],
|
||||||
&[data-loading],
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
background-color: var(--grey-1);
|
background-color: var(--grey-1);
|
||||||
@@ -69,6 +71,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[data-loading] {
|
&[data-loading] {
|
||||||
|
background-color: var(--grey-1);
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
new-client/src/shared/components/ui/Spinner.vue
Normal file
43
new-client/src/shared/components/ui/Spinner.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chad-spinner" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineOptions({
|
||||||
|
name: 'ChadSpinner',
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.chad-spinner {
|
||||||
|
--color1: var(--yellow);
|
||||||
|
--color2: var(--ink);
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 36px;
|
||||||
|
height: 12px;
|
||||||
|
|
||||||
|
background-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
var(--color1) 0%,
|
||||||
|
var(--color1) 33.333%,
|
||||||
|
transparent 33.333%,
|
||||||
|
transparent 66.666%,
|
||||||
|
var(--color2) 66.666%,
|
||||||
|
var(--color2) 100%
|
||||||
|
);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
animation: chad-spinner 400ms infinite;
|
||||||
|
}
|
||||||
|
@keyframes chad-spinner {
|
||||||
|
from {
|
||||||
|
--color1: var(--yellow);
|
||||||
|
--color2: var(--ink);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
--color1: var(--ink);
|
||||||
|
--color2: var(--yellow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
12
new-client/src/shared/composables/use-user-details.ts
Normal file
12
new-client/src/shared/composables/use-user-details.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { MaybeRefOrGetter } from 'vue'
|
||||||
|
import { qUser } from '@shared/api/qUser.ts'
|
||||||
|
import { readonly } from 'vue'
|
||||||
|
|
||||||
|
export function useUserDetails(username: MaybeRefOrGetter<string>) {
|
||||||
|
const { data: user, isFetching } = qUser(username)
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: readonly(user),
|
||||||
|
isFetching,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,10 @@
|
|||||||
|
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<ChannelList class="sidebar__channel-list" />
|
<ChannelList class="sidebar__channel-list" />
|
||||||
|
|
||||||
|
<ChadButton style="margin: var(--space-2);" @click="logout">
|
||||||
|
Logout
|
||||||
|
</ChadButton>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@@ -29,7 +33,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import ChadButton from '@shared/components/ui/Button.vue'
|
||||||
import { useApp } from '@shared/composables/use-app.js'
|
import { useApp } from '@shared/composables/use-app.js'
|
||||||
|
import { useAuth } from '@shared/composables/use-auth.ts'
|
||||||
import ChannelList from '@widgets/channel-list/ui/ChannelList.vue'
|
import ChannelList from '@widgets/channel-list/ui/ChannelList.vue'
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -37,6 +43,7 @@ defineOptions({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const { version } = useApp()
|
const { version } = useApp()
|
||||||
|
const { logout } = useAuth()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -15,15 +15,16 @@ export function qChatMessages() {
|
|||||||
queryFn: async ({ pageParam }) => {
|
queryFn: async ({ pageParam }) => {
|
||||||
const response = await api.chad.chatMessages({ cursor: pageParam, limit: 30 })
|
const response = await api.chad.chatMessages({ cursor: pageParam, limit: 30 })
|
||||||
|
|
||||||
return {
|
return response.data
|
||||||
messages: response.data.messages.reverse(),
|
// return {
|
||||||
nextCursor: response.data.nextCursor,
|
// messages: response.data.messages.reverse(),
|
||||||
}
|
// nextCursor: response.data.nextCursor,
|
||||||
|
// }
|
||||||
},
|
},
|
||||||
select: data => ({
|
// select: data => ({
|
||||||
pages: [...data.pages].reverse(),
|
// pages: [...data.pages].reverse(),
|
||||||
pageParams: [...data.pageParams].reverse(),
|
// pageParams: [...data.pageParams].reverse(),
|
||||||
}),
|
// }),
|
||||||
initialPageParam: undefined,
|
initialPageParam: undefined,
|
||||||
getNextPageParam: (lastPage) => {
|
getNextPageParam: (lastPage) => {
|
||||||
return lastPage.nextCursor
|
return lastPage.nextCursor
|
||||||
|
|||||||
16
new-client/src/widgets/chat/api/qMessageAttachment.ts
Normal file
16
new-client/src/widgets/chat/api/qMessageAttachment.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { ResponseError } from '@shared/api/generated-chad-api.ts'
|
||||||
|
import type { MaybeRefOrGetter } from 'vue'
|
||||||
|
import api from '@shared/api/client.ts'
|
||||||
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
|
import { toValue } from 'vue'
|
||||||
|
|
||||||
|
export function qMessageAttachment(attachmentId: MaybeRefOrGetter<string>) {
|
||||||
|
return useQuery<Blob, ResponseError>({
|
||||||
|
queryKey: ['message-attachment', attachmentId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.chad.attachmentGet(toValue(attachmentId), { format: 'blob' })
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,11 +2,12 @@ import { createGlobalState } from '@vueuse/core'
|
|||||||
import { qChatMessages } from '../api/qChatMessages.ts'
|
import { qChatMessages } from '../api/qChatMessages.ts'
|
||||||
|
|
||||||
export const useChat = createGlobalState(() => {
|
export const useChat = createGlobalState(() => {
|
||||||
const { data: messages, hasNextPage: hasMoreMessages, fetchNextPage } = qChatMessages()
|
const { data: messages, hasNextPage: hasMoreMessages, fetchNextPage, isFetching } = qChatMessages()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messages,
|
messages,
|
||||||
hasMoreMessages,
|
hasMoreMessages,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
|
isFetching,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat">
|
<div class="chat">
|
||||||
<button v-if="hasMoreMessages" type="button" @click="fetchNextPage()">
|
<ChatMessages class="chat__messages" />
|
||||||
Load more
|
<ChatInput class="chat__input" />
|
||||||
</button>
|
|
||||||
|
|
||||||
<div v-if="messages" class="chat__messages">
|
|
||||||
<template v-for="(page, pageIdx) in messages.pages" :key="pageIdx">
|
|
||||||
<ChatMessage v-for="message in page.messages" :key="message.id" :message="message" />
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ChatMessage from '@widgets/chat/ui/ChatMessage.vue'
|
import ChatInput from '@widgets/chat/ui/ChatInput.vue'
|
||||||
import { useChat } from '../composables/use-chat'
|
import ChatMessages from '@widgets/chat/ui/ChatMessages.vue'
|
||||||
|
|
||||||
const { messages, hasMoreMessages, fetchNextPage } = useChat()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.chat {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr auto;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__messages {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
16
new-client/src/widgets/chat/ui/ChatInput.vue
Normal file
16
new-client/src/widgets/chat/ui/ChatInput.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chat-input">
|
||||||
|
<ChadInput placeholder="Write a message..." />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ChadInput from '@shared/components/ui/Input.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.chat-input {
|
||||||
|
padding: var(--space-3) var(--space-6);
|
||||||
|
border-top: var(--border-w) solid var(--ink);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,32 +1,72 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat-message">
|
<div class="chat-message">
|
||||||
<ChadAvatar class="chat-message__avatar" src="" :fallback="initials" />
|
<ChadAvatar class="chat-message__avatar" src="" :fallback="initials" :highlighted="isMyMessage" />
|
||||||
<p class="chat-message__sender">
|
<div>
|
||||||
{{ message.senderId }}
|
<div class="chat-message__top">
|
||||||
</p>
|
<p class="chat-message__sender">
|
||||||
<p class="chat-message__body">
|
{{ !sender ? message.senderId : sender.displayName }}
|
||||||
{{ message.text }}
|
</p>
|
||||||
</p>
|
|
||||||
|
<p class="chat-message__datetime">
|
||||||
|
{{ datetime }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="chat-message__body">
|
||||||
|
{{ message.text }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-if="message.attachments.length > 0" class="chat-message__attachments">
|
||||||
|
<ChatMessageAttachment v-for="attachmentId in message.attachments" :key="attachmentId" :attachment-id="attachmentId" />
|
||||||
|
</div>
|
||||||
|
</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 ChadAvatar from '@shared/components/ui/Avatar.vue'
|
||||||
import { computed } from 'vue'
|
import { useAuth } from '@shared/composables/use-auth.ts'
|
||||||
|
import { useUserDetails } from '@shared/composables/use-user-details.ts'
|
||||||
|
import ChatMessageAttachment from '@widgets/chat/ui/ChatMessageAttachment.vue'
|
||||||
|
import { format, isThisYear, isToday } from 'date-fns'
|
||||||
|
import { computed, toRef } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
message: ChatMessage
|
message: ChatMessage
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const { me } = useAuth()
|
||||||
|
|
||||||
|
const isMyMessage = computed(() => {
|
||||||
|
return props.message.senderId === me.value?.id
|
||||||
|
})
|
||||||
|
|
||||||
|
const { user: sender, isFetching: fetchingSender } = useUserDetails(toRef(() => props.message.senderId))
|
||||||
|
|
||||||
const initials = computed(() => props.message.senderId.slice(props.message.senderId.length - 2))
|
const initials = computed(() => props.message.senderId.slice(props.message.senderId.length - 2))
|
||||||
|
|
||||||
|
const datetime = computed(() => {
|
||||||
|
let formatStr = 'd MMMM yyyy, HH:mm'
|
||||||
|
|
||||||
|
if (isToday(props.message.createdAt)) {
|
||||||
|
formatStr = 'HH:mm'
|
||||||
|
}
|
||||||
|
else if (isThisYear(props.message.createdAt)) {
|
||||||
|
formatStr = 'd MMMM, HH:mm'
|
||||||
|
}
|
||||||
|
|
||||||
|
return format(props.message.createdAt, formatStr)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.chat-message {
|
.chat-message {
|
||||||
|
$self: &;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
grid-template-areas: 'avatar sender' 'avatar body';
|
grid-template-areas: 'avatar top' 'avatar body';
|
||||||
column-gap: var(--space-3);
|
column-gap: var(--space-3);
|
||||||
padding: var(--space-3) var(--space-6);
|
padding: var(--space-3) var(--space-6);
|
||||||
|
|
||||||
@@ -34,14 +74,36 @@ const initials = computed(() => props.message.senderId.slice(props.message.sende
|
|||||||
grid-area: avatar;
|
grid-area: avatar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__top {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
align-items: center;
|
||||||
|
grid-area: top;
|
||||||
|
//margin-top: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
&__sender {
|
&__sender {
|
||||||
@include font-body-bold;
|
@include font-body-bold;
|
||||||
|
}
|
||||||
|
|
||||||
grid-area: sender;
|
&__datetime {
|
||||||
|
@include font-micro;
|
||||||
|
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--grey-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__body {
|
&__body {
|
||||||
grid-area: body;
|
grid-area: body;
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__attachments {
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
|
||||||
|
> *:not(:last-child) {
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
46
new-client/src/widgets/chat/ui/ChatMessageAttachment.vue
Normal file
46
new-client/src/widgets/chat/ui/ChatMessageAttachment.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chat-message-attachment">
|
||||||
|
<p v-if="isFetching">
|
||||||
|
Loading...
|
||||||
|
</p>
|
||||||
|
<template v-else-if="attachment">
|
||||||
|
<img v-if="isImage" :src="src" :alt="attachmentId" draggable="false">
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { qMessageAttachment } from '@widgets/chat/api/qMessageAttachment.ts'
|
||||||
|
import { computed, toRef } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
attachmentId: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { isFetching, data: attachment } = qMessageAttachment(toRef(() => props.attachmentId))
|
||||||
|
|
||||||
|
const isImage = computed(() => {
|
||||||
|
return attachment.value?.type.startsWith('image/')
|
||||||
|
})
|
||||||
|
|
||||||
|
const src = computed(() => {
|
||||||
|
if (!attachment.value || !isImage.value)
|
||||||
|
return undefined
|
||||||
|
|
||||||
|
return URL.createObjectURL(attachment.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.chat-message-attachment {
|
||||||
|
outline: var(--border-w) solid var(--ink);
|
||||||
|
outline-offset: calc(var(--border-w) * -1);
|
||||||
|
background-color: var(--grey-1);
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
max-width: 260px;
|
||||||
|
max-height: 160px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
59
new-client/src/widgets/chat/ui/ChatMessages.vue
Normal file
59
new-client/src/widgets/chat/ui/ChatMessages.vue
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chat-messages">
|
||||||
|
<div v-if="messages" ref="scroll" class="chat-messages__list">
|
||||||
|
<template v-for="(page, pageIdx) in messages.pages" :key="pageIdx">
|
||||||
|
<ChatMessage v-for="message in page.messages" :key="message.id" :message="message" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChadSpinner v-if="isFetching" class="chat-messages__spinner" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ChadSpinner from '@shared/components/ui/Spinner.vue'
|
||||||
|
import { useInfiniteScroll } from '@vueuse/core'
|
||||||
|
import ChatMessage from '@widgets/chat/ui/ChatMessage.vue'
|
||||||
|
import { useTemplateRef } from 'vue'
|
||||||
|
import { useChat } from '../composables/use-chat'
|
||||||
|
|
||||||
|
const scrollRef = useTemplateRef('scroll')
|
||||||
|
|
||||||
|
const { messages, hasMoreMessages, fetchNextPage, isFetching } = useChat()
|
||||||
|
|
||||||
|
useInfiniteScroll(
|
||||||
|
scrollRef,
|
||||||
|
() => {
|
||||||
|
fetchNextPage()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
distance: 500,
|
||||||
|
direction: 'top',
|
||||||
|
interval: 300,
|
||||||
|
canLoadMore: () => hasMoreMessages.value && !isFetching.value,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.chat-messages {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&__spinner {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--space-4);
|
||||||
|
left: 50%;
|
||||||
|
translate: -50% 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__list {
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -818,6 +818,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.100.10.tgz#aeb34d301fd4ff9762e67dfa018adc33b7a18be4"
|
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.100.10.tgz#aeb34d301fd4ff9762e67dfa018adc33b7a18be4"
|
||||||
integrity sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==
|
integrity sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==
|
||||||
|
|
||||||
|
"@tanstack/query-persist-client-core@^5.100.10":
|
||||||
|
version "5.100.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/@tanstack/query-persist-client-core/-/query-persist-client-core-5.100.10.tgz#efe9ea2b7cea90330129afdf7393f09fd06ad358"
|
||||||
|
integrity sha512-O9Pey40DhTTDBABS0bHr+KNL5/VMf6PrqjexS8WoDDtnkaoWM+y0MSe0V9E5W+BwvkjM33mB3aYcCxa175gZTQ==
|
||||||
|
dependencies:
|
||||||
|
"@tanstack/query-core" "5.100.10"
|
||||||
|
|
||||||
"@tanstack/vue-query@^5.100.10":
|
"@tanstack/vue-query@^5.100.10":
|
||||||
version "5.100.10"
|
version "5.100.10"
|
||||||
resolved "https://registry.yarnpkg.com/@tanstack/vue-query/-/vue-query-5.100.10.tgz#eafed0d51887c12dcbba5edb1b7108522ebc3891"
|
resolved "https://registry.yarnpkg.com/@tanstack/vue-query/-/vue-query-5.100.10.tgz#eafed0d51887c12dcbba5edb1b7108522ebc3891"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Type } from 'typebox'
|
|||||||
|
|
||||||
export const GetUserQuerySchema = Type.Partial(Type.Object({
|
export const GetUserQuerySchema = Type.Partial(Type.Object({
|
||||||
username: Type.String(),
|
username: Type.String(),
|
||||||
|
id: Type.String(),
|
||||||
}), { $id: 'GetUserQuery' })
|
}), { $id: 'GetUserQuery' })
|
||||||
|
|
||||||
export const UserPreferencesSchema = Type.Object({
|
export const UserPreferencesSchema = Type.Object({
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
|
|||||||
},
|
},
|
||||||
async (req, reply) => {
|
async (req, reply) => {
|
||||||
const user = await fastify.prisma.user.findFirst({
|
const user = await fastify.prisma.user.findFirst({
|
||||||
where: { username: req.query.username },
|
where: { username: req.query.username, id: req.query.id },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user