начало чата
All checks were successful
Deploy / deploy (push) Successful in 3m47s

This commit is contained in:
2026-04-16 02:21:54 +06:00
parent 9f39ee6430
commit 0915d3c64d
26 changed files with 1592 additions and 1112 deletions

View File

@@ -15,6 +15,8 @@ declare module 'vue' {
PrimeCard: typeof import('primevue/card')['default']
PrimeDivider: typeof import('primevue/divider')['default']
PrimeFloatLabel: typeof import('primevue/floatlabel')['default']
PrimeInputGroup: typeof import('primevue/inputgroup')['default']
PrimeInputGroupAddon: typeof import('primevue/inputgroupaddon')['default']
PrimeInputText: typeof import('primevue/inputtext')['default']
PrimePassword: typeof import('primevue/password')['default']
PrimeProgressBar: typeof import('primevue/progressbar')['default']
@@ -23,6 +25,7 @@ declare module 'vue' {
PrimeSelectButton: typeof import('primevue/selectbutton')['default']
PrimeSlider: typeof import('primevue/slider')['default']
PrimeTag: typeof import('primevue/tag')['default']
PrimeTextarea: typeof import('primevue/textarea')['default']
PrimeToast: typeof import('primevue/toast')['default']
PrimeToggleSwitch: typeof import('primevue/toggleswitch')['default']
RouterLink: typeof import('vue-router')['RouterLink']

View File

@@ -0,0 +1,55 @@
import { createGlobalState } from '@vueuse/core'
export interface ChatClientMessage {
text: string
replyTo?: {
messageId: string
}
}
export interface ChatMessage {
id: string
sender: string
text: string
createdAt: string
replyTo?: {
messageId: string
sender: string
text: string
}
}
export const useChat = createGlobalState(() => {
const signaling = useSignaling()
const { emit } = useEventBus()
const messages = shallowRef<ChatMessage[]>([])
watch(signaling.socket, (socket) => {
if (!socket)
return
socket.on('chat:new-message', (message: ChatMessage) => {
messages.value.push(message)
triggerRef(messages)
emit('chat:new-message')
})
socket.on('disconnect', () => {
messages.value = []
})
}, { immediate: true })
function sendMessage(message: ChatClientMessage) {
if (!signaling.connected.value)
return
signaling.socket.value!.emit('chat:message', message)
}
return {
messages,
sendMessage,
}
})

View File

@@ -29,6 +29,8 @@ export interface AppEvents extends Record<EventType, unknown> {
'video:disabled': void
'share:enabled': void
'share:disabled': void
'chat:new-message': void
}
const emitter = mitt<AppEvents>()

View File

@@ -15,7 +15,7 @@ function hashStringToNumber(str: string, cap: number): number {
const oneShots: Howl[] = []
type SfxEvent = 'mic-on' | 'mic-off' | 'stream-on' | 'stream-off' | 'connection'
type SfxEvent = 'mic-on' | 'mic-off' | 'stream-on' | 'stream-off' | 'connection' | 'message'
const EVENT_VOLUME: Record<SfxEvent, number> = {
'mic-on': 0.2,

View File

@@ -66,7 +66,7 @@ export const useSignaling = createSharedComposable(() => {
const uri = host ? `${protocol}//${host}` : ``
socket.value = io(`${uri}/webrtc`, {
socket.value = io(uri, {
path: `${pathname}/ws`,
transports: ['websocket'],
withCredentials: true,

View File

@@ -56,11 +56,9 @@
</div>
</PrimeScrollPanel>
<PrimeScrollPanel class="bg-surface-900 rounded-xl overflow-hidden" style="min-height: 0">
<div class="p-3">
<slot />
</div>
</PrimeScrollPanel>
<div class="bg-surface-900 rounded-xl overflow-hidden p-3 flex flex-col min-h-full">
<slot />
</div>
</div>
<FullscreenGallery />

View File

@@ -1,5 +1,5 @@
<template>
<div class="grid grid-cols-[1fr_1fr] gap-2">
<PrimeScrollPanel class="grid grid-cols-[1fr_1fr] gap-2 min-h-0">
<GalleryCard
v-for="producer in producers"
:key="`producer-${producer.id}`"
@@ -13,7 +13,7 @@
:client="consumer.client"
:consumer="consumer.consumer"
/>
</div>
</PrimeScrollPanel>
</template>
<script setup lang="ts">

View File

@@ -1,17 +1,85 @@
<template>
<div>
<div class="flex items-center justify-center">
<PrimeCard>
<template #content>
The chat is under development.
</template>
</PrimeCard>
<PrimeScrollPanel class="flex-1 min-h-0">
<div v-auto-animate class="flex flex-col gap-3">
<div
v-for="message in messages"
:key="message.id"
class="min-w-64 w-fit max-w-[60%]"
:class="{
'ml-auto': message.sender === me?.username,
}"
>
<p
v-if="message.sender !== me?.username"
class="text-sm text-muted-color mb-1"
>
{{ message.sender }}
</p>
<div
class="px-2 py-1 rounded-lg"
:class="{
'bg-surface-800': message.sender !== me?.username,
'bg-surface-700': message.sender === me?.username,
}"
>
<p v-html="parseMessageText(message.text)" />
<p class="text-right text-sm text-muted-color">
{{ formatDate(message.createdAt) }}
</p>
</div>
</div>
</div>
</PrimeScrollPanel>
<div class="pt-3 mt-auto">
<PrimeInputGroup>
<!-- <PrimeInputGroupAddon> -->
<!-- <PrimeButton severity="secondary" class="shrink-0" disabled> -->
<!-- <template #icon> -->
<!-- <Paperclip /> -->
<!-- </template> -->
<!-- </PrimeButton> -->
<!-- </PrimeInputGroupAddon> -->
<PrimeInputText v-model="text" placeholder="Write a message..." fluid @keydown.enter="sendMessage" />
<PrimeButton class="shrink-0" label="Send" severity="contrast" @click="sendMessage" />
</PrimeInputGroup>
</div>
</template>
<script setup lang="ts">
import { format } from 'date-fns'
import linkifyStr from 'linkify-string'
import { useChat } from '~/composables/use-chat'
definePageMeta({
name: 'Index',
})
const { me } = useClients()
const chat = useChat()
const { messages } = chat
const text = ref('')
function parseMessageText(text: string) {
return linkifyStr(text, { className: 'underline', rel: 'noopener noreferrer', target: '_blank' })
}
function formatDate(date: string) {
return format(date, 'HH:mm')
}
function sendMessage() {
if (!text.value)
return
chat.sendMessage({
text: text.value,
})
text.value = ''
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div>
<PrimeScrollPanel class="min-h-0">
<PrimeDivider align="left">
Audio
</PrimeDivider>
@@ -132,7 +132,7 @@
@click="checkForUpdates"
/>
</template>
</div>
</PrimeScrollPanel>
</template>
<script setup lang="ts">

View File

@@ -54,4 +54,8 @@ export default defineNuxtPlugin(() => {
sfx.playEvent('stream-off')
}
})
on('chat:new-message', () => {
sfx.playEvent('message')
})
})