init
This commit is contained in:
@@ -145,13 +145,23 @@ function onServerContextMenu(event: MouseEvent, server: Server) {
|
||||
:to="`/server/${server.id}`"
|
||||
@contextmenu="onServerContextMenu($event, server)"
|
||||
>
|
||||
<v-avatar
|
||||
:style="{ backgroundColor: getServerColor(server.name) }"
|
||||
class="d-flex align-center justify-center mx-auto mb-9 font-weight-bold text-caption text-white"
|
||||
size="28"
|
||||
<v-badge
|
||||
class="server-badge d-flex mx-auto mb-9"
|
||||
:content="server.unread_count"
|
||||
:model-value="(server.unread_count ?? 0) > 0"
|
||||
color="primary"
|
||||
location="bottom right"
|
||||
offset-x="2"
|
||||
offset-y="2"
|
||||
>
|
||||
{{ getServerInitials(server.name) }}
|
||||
</v-avatar>
|
||||
<v-avatar
|
||||
:style="{ backgroundColor: getServerColor(server.name) }"
|
||||
class="d-flex align-center justify-center font-weight-bold text-caption text-white"
|
||||
size="36"
|
||||
>
|
||||
{{ getServerInitials(server.name) }}
|
||||
</v-avatar>
|
||||
</v-badge>
|
||||
</router-link>
|
||||
|
||||
<v-btn
|
||||
@@ -233,4 +243,17 @@ function onServerContextMenu(event: MouseEvent, server: Server) {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.server-badge {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.server-badge :deep(.v-badge__badge) {
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
padding: 0 5px;
|
||||
font-size: 0.7rem;
|
||||
line-height: 22px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'highlight.js/styles/github-dark.css'
|
||||
import {computed, nextTick, ref, watch} from 'vue';
|
||||
import {storeToRefs} from 'pinia';
|
||||
import {useMessageStore} from '@/stores/message';
|
||||
import {useServerStore} from '@/stores/server';
|
||||
import {useUserStore} from "@/stores/user.ts";
|
||||
import {useMarkdown} from '@/composables/useMarkdown'
|
||||
|
||||
@@ -13,11 +14,12 @@ const props = defineProps<{
|
||||
|
||||
const channelId = computed(() => props.channelId);
|
||||
const messageStore = useMessageStore();
|
||||
const serverStore = useServerStore();
|
||||
const userStore = useUserStore();
|
||||
const {renderMarkdown} = useMarkdown()
|
||||
|
||||
const messageContainer = ref<HTMLElement | null>(null);
|
||||
const {messages, loading, loadingBefore, loadingAfter, hasMoreAfter, isAtBottom} = storeToRefs(messageStore);
|
||||
const {messages, loading, loadingBefore, loadingAfter, hasMoreAfter, isAtBottom, newestId} = storeToRefs(messageStore);
|
||||
const newMessage = ref('');
|
||||
|
||||
const SCROLL_LOAD_THRESHOLD = 120;
|
||||
@@ -25,6 +27,24 @@ const SCROLL_BOTTOM_TOLERANCE = 4;
|
||||
const paginationLock = ref<'before' | 'after' | null>(null);
|
||||
const paginationLockScrollTop = ref(0);
|
||||
const lastScrollTop = ref(0);
|
||||
const markedMessageByChannel = new Map<string, string>();
|
||||
|
||||
const markCurrentChannelRead = async (targetChannelId: string) => {
|
||||
if (messageStore.activeChannelId !== targetChannelId || !newestId.value) return;
|
||||
|
||||
const messageId = newestId.value;
|
||||
if (markedMessageByChannel.get(targetChannelId) === messageId) return;
|
||||
|
||||
try {
|
||||
const readState = await messageStore.markChannelRead(targetChannelId, messageId);
|
||||
if (messageStore.activeChannelId !== targetChannelId) return;
|
||||
|
||||
markedMessageByChannel.set(targetChannelId, messageId);
|
||||
serverStore.applyChannelReadState(props.serverId, targetChannelId, readState.unread_count);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la mise à jour de la lecture:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const showRecentMessagesButton = computed(() =>
|
||||
!loading.value && (hasMoreAfter.value || !isAtBottom.value),
|
||||
@@ -163,6 +183,7 @@ const returnToRecentMessages = async () => {
|
||||
paginationLock.value = null;
|
||||
await messageStore.fetchMessages(channelId.value);
|
||||
await scrollToBottom();
|
||||
await markCurrentChannelRead(channelId.value);
|
||||
};
|
||||
|
||||
// Only explicit initial loads and realtime messages received while at the
|
||||
@@ -173,9 +194,17 @@ watch(messages, async () => {
|
||||
}
|
||||
}, {deep: true, flush: 'post'});
|
||||
|
||||
watch(isAtBottom, async (atBottom) => {
|
||||
if (atBottom) {
|
||||
await markCurrentChannelRead(channelId.value);
|
||||
}
|
||||
});
|
||||
|
||||
watch(channelId, async (newChannelId) => {
|
||||
if (newChannelId) {
|
||||
await messageStore.fetchMessages(newChannelId);
|
||||
await scrollToBottom();
|
||||
await markCurrentChannelRead(newChannelId);
|
||||
}
|
||||
}, {immediate: true})
|
||||
</script>
|
||||
|
||||
@@ -177,9 +177,22 @@ function onChannelContextMenu(event: MouseEvent, channel: any) {
|
||||
:key="channel.id"
|
||||
:title="channel.name"
|
||||
:to="`/server/${serverId}/channel/${channel.id}`"
|
||||
:class="{ 'font-weight-bold': (channel.unread_count ?? 0) > 0 }"
|
||||
link
|
||||
@contextmenu="onChannelContextMenu($event, channel)"
|
||||
/>
|
||||
>
|
||||
<template #append>
|
||||
<v-chip
|
||||
v-if="(channel.unread_count ?? 0) > 0"
|
||||
color="primary"
|
||||
density="compact"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
{{ channel.unread_count }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list-group>
|
||||
|
||||
<!-- Canal orphelin (racine) -->
|
||||
@@ -188,9 +201,22 @@ function onChannelContextMenu(event: MouseEvent, channel: any) {
|
||||
:key="item.Channel.id"
|
||||
:title="item.Channel.name"
|
||||
:to="`/server/${serverId}/channel/${item.Channel.id}`"
|
||||
:class="{ 'font-weight-bold': (item.Channel.unread_count ?? 0) > 0 }"
|
||||
link
|
||||
@contextmenu="onChannelContextMenu($event, item.Channel)"
|
||||
/>
|
||||
>
|
||||
<template #append>
|
||||
<v-chip
|
||||
v-if="(item.Channel.unread_count ?? 0) > 0"
|
||||
color="primary"
|
||||
density="compact"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
{{ item.Channel.unread_count }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-navigation-drawer>
|
||||
|
||||
@@ -9,6 +9,7 @@ interface Channel {
|
||||
category_id?: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
unread_count?: number
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -65,4 +66,4 @@ export const useChannelStore = defineStore('channel', {
|
||||
this.error = null;
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,6 +17,13 @@ export interface Message {
|
||||
reply_to_id: string | null;
|
||||
}
|
||||
|
||||
export interface ReadStateResponse {
|
||||
channel_id: string;
|
||||
last_read_message_id: string | null;
|
||||
updated_at: string | null;
|
||||
unread_count: number;
|
||||
}
|
||||
|
||||
interface MessagePage {
|
||||
messages: Message[];
|
||||
oldest_id: string | null;
|
||||
@@ -224,6 +231,17 @@ export const useMessageStore = defineStore("message", {
|
||||
}
|
||||
},
|
||||
|
||||
async markChannelRead(channelId: string, messageId: string): Promise<ReadStateResponse> {
|
||||
const response = await useApi().put(`/channels/${channelId}/read-state`, {
|
||||
last_read_message_id: messageId,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Read state update failed (${response.status})`);
|
||||
}
|
||||
|
||||
return await response.json() as ReadStateResponse;
|
||||
},
|
||||
|
||||
addRealtimeMessage(message: Message) {
|
||||
if (message.channel_id !== this.activeChannelId) return;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface Server {
|
||||
is_default: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
unread_count?: number
|
||||
}
|
||||
|
||||
export const useServerStore = defineStore("server", {
|
||||
@@ -33,7 +34,10 @@ export const useServerStore = defineStore("server", {
|
||||
|
||||
const server: Server = await response.json();
|
||||
const index = this.servers.findIndex(item => item.id === server.id);
|
||||
if (index >= 0) this.servers[index] = server;
|
||||
if (index >= 0) {
|
||||
server.unread_count ??= this.servers[index].unread_count ?? 0;
|
||||
this.servers[index] = server;
|
||||
}
|
||||
else this.servers.push(server);
|
||||
return server;
|
||||
},
|
||||
@@ -70,7 +74,10 @@ export const useServerStore = defineStore("server", {
|
||||
}
|
||||
const updated = await response.json();
|
||||
const index = this.servers.findIndex(server => server.id === serverId);
|
||||
if (index >= 0) this.servers[index] = updated;
|
||||
if (index >= 0) {
|
||||
updated.unread_count ??= this.servers[index].unread_count ?? 0;
|
||||
this.servers[index] = updated;
|
||||
}
|
||||
return updated;
|
||||
},
|
||||
async fetchServerTree(serverId: string) {
|
||||
@@ -103,6 +110,27 @@ export const useServerStore = defineStore("server", {
|
||||
|
||||
return tree.items;
|
||||
},
|
||||
applyChannelReadState(serverId: string, channelId: string, unreadCount: number) {
|
||||
let previousUnreadCount = 0;
|
||||
|
||||
for (const item of this.currentTree) {
|
||||
const channels = "Category" in item ? item.Category[1] : "Channel" in item ? [item.Channel] : [];
|
||||
const channel = channels.find((candidate: { id: string }) => candidate.id === channelId);
|
||||
if (channel) {
|
||||
previousUnreadCount = channel.unread_count ?? 0;
|
||||
channel.unread_count = unreadCount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const server = this.servers.find(candidate => candidate.id === serverId);
|
||||
if (server) {
|
||||
server.unread_count = Math.max(
|
||||
0,
|
||||
(server.unread_count ?? 0) - previousUnreadCount + unreadCount,
|
||||
);
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
this.servers = [];
|
||||
this.loading = false;
|
||||
|
||||
Reference in New Issue
Block a user