This commit is contained in:
2026-08-22 20:34:43 +02:00
parent 120b6cf4d5
commit da151c13ed
14 changed files with 366 additions and 21 deletions
+78 -5
View File
@@ -1,9 +1,82 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useRouter } from 'vue-router'
import { useConversationStore } from '@/stores/conversation'
import { useUserStore } from '@/stores/user'
import { useAuthStore } from '@/stores/auth'
const router = useRouter()
const conversationStore = useConversationStore()
const userStore = useUserStore()
const authStore = useAuthStore()
const { conversations } = storeToRefs(conversationStore)
const showCreateDialog = ref(false)
const selectedUserIds = ref<string[]>([])
const submitting = ref(false)
const users = computed(() => userStore.users.filter(user => user.id !== authStore.currentUser?.id))
onMounted(async () => {
await Promise.all([userStore.fetchUsers(), conversationStore.fetchConversations()])
})
const createConversation = async () => {
if (!selectedUserIds.value.length) return
submitting.value = true
try {
const conversation = await conversationStore.createConversation(selectedUserIds.value)
showCreateDialog.value = false
selectedUserIds.value = []
await router.push(`/conversation/${conversation.id}`)
} finally { submitting.value = false }
}
</script>
<template>
<div>
Hello
</div>
</template>
<v-navigation-drawer permanent width="280" color="grey-lighten-5">
<div class="d-flex align-center px-4 py-4">
<span class="text-h6 font-weight-medium">Messages</span>
<v-spacer />
<v-btn icon="mdi-square-edit-outline" size="small" variant="text" aria-label="Nouvelle discussion" @click="showCreateDialog = true" />
</div>
<v-divider />
<v-list density="comfortable" nav>
<v-list-item
v-for="conversation in conversations"
:key="conversation.id"
:to="`/conversation/${conversation.id}`"
:title="conversation.title"
:subtitle="conversation.last_message ?? 'Aucun message'"
:class="{ 'font-weight-bold': conversation.unread_count > 0 }"
rounded="lg"
>
<template #prepend><v-avatar color="primary" size="34"><v-icon icon="mdi-account-multiple-outline" /></v-avatar></template>
<template #append><v-chip v-if="conversation.unread_count > 0" color="primary" size="small">{{ conversation.unread_count }}</v-chip></template>
</v-list-item>
<v-list-item v-if="!conversationStore.loading && !conversations.length" class="text-medium-emphasis" title="Aucune discussion" subtitle="Commencez une conversation" />
</v-list>
</v-navigation-drawer>
<v-main class="conversation-main">
<router-view />
<div v-if="!$route.params.channelId" class="empty-state">
<v-icon icon="mdi-message-text-outline" size="64" color="grey" />
<div class="text-h6 mt-4">Vos discussions</div>
<div class="text-body-2 text-medium-emphasis">Sélectionnez une discussion ou commencez-en une nouvelle.</div>
<v-btn class="mt-4" color="primary" prepend-icon="mdi-plus" @click="showCreateDialog = true">Nouvelle discussion</v-btn>
</div>
</v-main>
<v-dialog v-model="showCreateDialog" width="420">
<v-card>
<v-card-title>Nouvelle discussion</v-card-title>
<v-card-text><v-select v-model="selectedUserIds" :items="users" item-title="username" item-value="id" label="Participants" multiple chips closable-chips /></v-card-text>
<v-card-actions><v-spacer /><v-btn @click="showCreateDialog = false">Annuler</v-btn><v-btn color="primary" :loading="submitting" :disabled="!selectedUserIds.length" @click="createConversation">Créer</v-btn></v-card-actions>
</v-card>
</v-dialog>
</template>
<style scoped>
.conversation-main { height: 100%; position: relative; }
.empty-state { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
</style>
+46 -3
View File
@@ -10,9 +10,11 @@ import {onReloadAll} from '@/plugins/events.ts'
import EmojiPicker from '@/components/EmojiPicker.vue'
import {useEmojiStore, type Emoji} from '@/stores/emoji.ts'
import {useAuthStore} from '@/stores/auth.ts'
import {useConversationStore} from '@/stores/conversation'
import {useRoute, useRouter} from 'vue-router'
const props = defineProps<{
serverId: string
serverId?: string
channelId: string
}>();
@@ -22,6 +24,9 @@ const serverStore = useServerStore();
const userStore = useUserStore();
const emojiStore = useEmojiStore();
const authStore = useAuthStore();
const conversationStore = useConversationStore()
const route = useRoute()
const router = useRouter()
const {renderMarkdown} = useMarkdown()
const messageContainer = ref<HTMLElement | null>(null);
@@ -35,6 +40,16 @@ const paginationLockScrollTop = ref(0);
const lastScrollTop = ref(0);
const markedMessageByChannel = new Map<string, string>();
const reactionPending = ref(new Set<string>());
const isConversation = computed(() => route.name === 'home-conversation')
const addParticipantDialog = ref(false)
const selectedParticipantIds = ref<string[]>([])
const addingParticipants = ref(false)
const currentConversation = computed(() => conversationStore.conversations.find(item => item.id === channelId.value))
const availableParticipants = computed(() => userStore.users.filter(user =>
user.id !== authStore.currentUser?.id &&
!currentConversation.value?.participants.some(participant => participant.id === user.id) &&
!selectedParticipantIds.value.includes(user.id),
))
const markCurrentChannelRead = async (targetChannelId: string) => {
if (messageStore.activeChannelId !== targetChannelId || !newestId.value) return;
@@ -47,12 +62,24 @@ const markCurrentChannelRead = async (targetChannelId: string) => {
if (messageStore.activeChannelId !== targetChannelId) return;
markedMessageByChannel.set(targetChannelId, messageId);
serverStore.applyChannelReadState(props.serverId, targetChannelId, readState.unread_count);
if (isConversation.value) conversationStore.applyReadState(targetChannelId, readState.unread_count)
else if (props.serverId) serverStore.applyChannelReadState(props.serverId, targetChannelId, readState.unread_count);
} catch (error) {
console.error('Erreur lors de la mise à jour de la lecture:', error);
}
};
const forkConversation = async () => {
if (!selectedParticipantIds.value.length) return
addingParticipants.value = true
try {
const conversation = await conversationStore.forkConversation(channelId.value, selectedParticipantIds.value)
addParticipantDialog.value = false
selectedParticipantIds.value = []
await router.push(`/conversation/${conversation.id}`)
} finally { addingParticipants.value = false }
}
const showRecentMessagesButton = computed(() =>
!loading.value && (hasMoreAfter.value || !isAtBottom.value),
);
@@ -247,7 +274,7 @@ watch(isAtBottom, async (atBottom) => {
watch(channelId, async (newChannelId) => {
if (newChannelId) {
await emojiStore.fetchEmojis(props.serverId);
if (props.serverId) await emojiStore.fetchEmojis(props.serverId);
await messageStore.fetchMessages(newChannelId);
await scrollToBottom();
await markCurrentChannelRead(newChannelId);
@@ -261,6 +288,12 @@ watch(() => props.serverId, (newServerId) => {
<template>
<v-container class="pa-0 fill-height d-flex flex-column channel-layout" fluid>
<v-sheet v-if="isConversation" class="px-4 py-2 d-flex align-center" border>
<v-icon class="mr-3" icon="mdi-account-multiple-outline" />
<span class="font-weight-medium">{{ currentConversation?.title ?? 'Discussion' }}</span>
<v-spacer />
<v-btn prepend-icon="mdi-account-plus" variant="text" @click="addParticipantDialog = true">Ajouter</v-btn>
</v-sheet>
<div
ref="messageContainer"
class="flex-grow-1 overflow-y-auto w-100 message-container"
@@ -372,6 +405,16 @@ watch(() => props.serverId, (newServerId) => {
</v-textarea>
</v-sheet>
</v-container>
<v-dialog v-model="addParticipantDialog" width="420">
<v-card>
<v-card-title>Nouvelle discussion de groupe</v-card-title>
<v-card-text>
<v-select v-model="selectedParticipantIds" :items="availableParticipants" item-title="username" item-value="id" label="Ajouter des participants" multiple chips closable-chips />
</v-card-text>
<v-card-actions><v-spacer /><v-btn @click="addParticipantDialog = false">Annuler</v-btn><v-btn color="primary" :loading="addingParticipants" :disabled="!selectedParticipantIds.length" @click="forkConversation">Créer</v-btn></v-card-actions>
</v-card>
</v-dialog>
</template>
<style scoped>
+2 -1
View File
@@ -167,7 +167,7 @@ function onChannelContextMenu(event: MouseEvent, channel: any) {
<v-btn block variant="text" prepend-icon="mdi-cog" @click="showServerSettings = true">Gérer le serveur</v-btn>
</v-sheet>
<v-list
<v-list
v-model:opened="openedCategories"
open-strategy="multiple"
density="compact"
@@ -230,6 +230,7 @@ function onChannelContextMenu(event: MouseEvent, channel: any) {
</v-list-item>
</template>
</v-list>
</v-navigation-drawer>
<CreateChannelDialog
+8
View File
@@ -45,6 +45,14 @@ const router = createRouter({
path: '',
name: 'home',
component: () => import('@/pages/index.vue'),
children: [
{
path: 'conversation/:channelId(default|[0-9a-fA-F-]{36})',
name: 'home-conversation',
component: () => import('@/pages/server/channel/index.vue'),
props: true,
},
],
},
{
path: 'server/:serverId(default|[0-9a-fA-F-]{36})',
+48
View File
@@ -0,0 +1,48 @@
import { defineStore } from 'pinia'
import { useApi } from '@/composables/useApi'
export interface ConversationParticipant { id: string; username: string }
export interface Conversation {
id: string
title: string
participants: ConversationParticipant[]
last_message: string | null
unread_count: number
created_at: string
updated_at: string
}
export const useConversationStore = defineStore('conversation', {
state: () => ({ conversations: [] as Conversation[], loading: false }),
actions: {
async fetchConversations() {
this.loading = true
try {
const response = await useApi().get('/conversations')
if (!response.ok) throw new Error(`Conversation loading failed (${response.status})`)
this.conversations = await response.json()
} finally { this.loading = false }
},
async createConversation(userIds: string[]) {
const response = await useApi().post('/conversations', { user_ids: userIds })
if (!response.ok) throw new Error(`Conversation creation failed (${response.status})`)
const conversation = await response.json() as Conversation
const index = this.conversations.findIndex(item => item.id === conversation.id)
if (index >= 0) this.conversations[index] = conversation
else this.conversations.unshift(conversation)
return conversation
},
async forkConversation(id: string, userIds: string[]) {
const response = await useApi().post(`/conversations/${id}/fork`, { user_ids: userIds })
if (!response.ok) throw new Error(`Conversation fork failed (${response.status})`)
const conversation = await response.json() as Conversation
this.conversations.unshift(conversation)
return conversation
},
applyReadState(id: string, unreadCount: number) {
const conversation = this.conversations.find(item => item.id === id)
if (conversation) conversation.unread_count = unreadCount
},
reset() { this.conversations = []; this.loading = false }
}
})