Compare commits
7
Commits
7eea8ff6d1
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67d5764f6e | ||
|
|
786eaffb33 | ||
|
|
4940f1212f | ||
|
|
c166f1d186 | ||
|
|
71b3d2b491 | ||
|
|
a5d40f28c8 | ||
|
|
9bfc5555bc |
@@ -4,6 +4,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Knot.Contracts.Conversations.Application.Abstractions;
|
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||||
using Knot.Contracts.Conversations.Domain;
|
using Knot.Contracts.Conversations.Domain;
|
||||||
|
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||||
using Knot.Modules.Conversations.Infrastructure.SignalR;
|
using Knot.Modules.Conversations.Infrastructure.SignalR;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
using Knot.Shared.Kernel.Storage;
|
using Knot.Shared.Kernel.Storage;
|
||||||
|
|||||||
@@ -575,7 +575,7 @@ const translations = {
|
|||||||
clearChatConfirm: 'Clear chat history for yourself? The other person will keep their history.',
|
clearChatConfirm: 'Clear chat history for yourself? The other person will keep their history.',
|
||||||
clearHistory: 'Clear history',
|
clearHistory: 'Clear history',
|
||||||
clearHistoryConfirm: 'Clear history?',
|
clearHistoryConfirm: 'Clear history?',
|
||||||
deleteChatConfirm: 'Delete this chat? This action cannot be undone.',
|
deleteChatConfirm: 'Delete this chat? This action cannot be undone. The chat will be removed for all participants.',
|
||||||
deleteGroupChatConfirm: 'Delete this chat? This action cannot be undone. The chat will be removed for all participants.',
|
deleteGroupChatConfirm: 'Delete this chat? This action cannot be undone. The chat will be removed for all participants.',
|
||||||
pinChat: 'Pin chat',
|
pinChat: 'Pin chat',
|
||||||
unpinChat: 'Unpin chat',
|
unpinChat: 'Unpin chat',
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ export default function ChatPage() {
|
|||||||
const [activeTab, setActiveTab] = useState('chats');
|
const [activeTab, setActiveTab] = useState('chats');
|
||||||
const { t } = useLang();
|
const { t } = useLang();
|
||||||
|
|
||||||
|
const activeChat = useChatStore((state) => state.activeChat);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
groupCallOpenRef.current = groupCallOpen;
|
groupCallOpenRef.current = groupCallOpen;
|
||||||
groupCallChatIdRef.current = groupCallChatId;
|
groupCallChatIdRef.current = groupCallChatId;
|
||||||
@@ -335,6 +337,16 @@ export default function ChatPage() {
|
|||||||
};
|
};
|
||||||
}, [user?.id]);
|
}, [user?.id]);
|
||||||
|
|
||||||
|
// Join chat group when activeChat changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeChat) {
|
||||||
|
const socket = getSocket();
|
||||||
|
if (socket) {
|
||||||
|
socket.emit('join_chat', activeChat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeChat]);
|
||||||
|
|
||||||
const handleStartCall = (targetUser: UserBasic, type: 'voice' | 'video') => {
|
const handleStartCall = (targetUser: UserBasic, type: 'voice' | 'video') => {
|
||||||
setCallTarget(targetUser);
|
setCallTarget(targetUser);
|
||||||
setCallType(type);
|
setCallType(type);
|
||||||
@@ -371,8 +383,6 @@ export default function ChatPage() {
|
|||||||
setGroupCallOpen(false);
|
setGroupCallOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeChat = useChatStore((state) => state.activeChat);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
|
|||||||
@@ -283,11 +283,7 @@ function ChatListItem({ chat, isActive }: ChatListItemProps) {
|
|||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
open={showDeleteConfirm}
|
open={showDeleteConfirm}
|
||||||
message={
|
message={isFavorites ? t('clearHistoryConfirm') : t('deleteChatConfirm')}
|
||||||
isFavorites ? t('clearHistoryConfirm') :
|
|
||||||
chat.type === 'group' ? t('deleteGroupChatConfirm') :
|
|
||||||
t('deleteChatConfirm')
|
|
||||||
}
|
|
||||||
onConfirm={confirmDelete}
|
onConfirm={confirmDelete}
|
||||||
onCancel={() => setShowDeleteConfirm(false)}
|
onCancel={() => setShowDeleteConfirm(false)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -187,12 +187,31 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
// Refs and logic for tracking session's first unread message to show the divider exactly once per load
|
// Refs and logic for tracking session's first unread message to show the divider exactly once per load
|
||||||
const sessionUnreadRef = useRef<{ chatId: string, msgId: string | null }>({ chatId: '', msgId: null });
|
const sessionUnreadRef = useRef<{ chatId: string, msgId: string | null }>({ chatId: '', msgId: null });
|
||||||
|
|
||||||
if (activeChat && activeChat !== sessionUnreadRef.current.chatId && !isLoadingMessages) {
|
// Update sessionUnreadRef when chat changes OR when messages are marked as read
|
||||||
const firstUnreadMsg = chatMessages.find(
|
useEffect(() => {
|
||||||
(m) => m.senderId !== user?.id && !m.readBy?.some((r) => r.userId === user?.id)
|
if (!activeChat || isLoadingMessages) return;
|
||||||
);
|
|
||||||
sessionUnreadRef.current = { chatId: activeChat, msgId: firstUnreadMsg ? firstUnreadMsg.id : null };
|
// Reset on chat change
|
||||||
}
|
if (activeChat !== sessionUnreadRef.current.chatId) {
|
||||||
|
const firstUnreadMsg = chatMessages.find(
|
||||||
|
(m) => m.senderId !== user?.id && !m.readBy?.some((r) => r.userId === user?.id)
|
||||||
|
);
|
||||||
|
sessionUnreadRef.current = { chatId: activeChat, msgId: firstUnreadMsg ? firstUnreadMsg.id : null };
|
||||||
|
} else {
|
||||||
|
// Update if the first unread message was read (msgId no longer exists in unread list)
|
||||||
|
const firstUnreadMsg = chatMessages.find(
|
||||||
|
(m) => m.senderId !== user?.id && !m.readBy?.some((r) => r.userId === user?.id)
|
||||||
|
);
|
||||||
|
if (sessionUnreadRef.current.msgId && !firstUnreadMsg) {
|
||||||
|
// All messages are now read
|
||||||
|
sessionUnreadRef.current.msgId = null;
|
||||||
|
} else if (firstUnreadMsg && sessionUnreadRef.current.msgId !== firstUnreadMsg.id) {
|
||||||
|
// First unread changed (some messages were read)
|
||||||
|
sessionUnreadRef.current.msgId = firstUnreadMsg.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeChat, chatMessages, user?.id, isLoadingMessages]);
|
||||||
|
|
||||||
const firstUnreadId = activeChat === sessionUnreadRef.current.chatId ? sessionUnreadRef.current.msgId : null;
|
const firstUnreadId = activeChat === sessionUnreadRef.current.chatId ? sessionUnreadRef.current.msgId : null;
|
||||||
const initialScrollChatId = useRef<string | null>(null);
|
const initialScrollChatId = useRef<string | null>(null);
|
||||||
|
|
||||||
@@ -573,6 +592,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
{
|
{
|
||||||
root: scrollContainerRef.current,
|
root: scrollContainerRef.current,
|
||||||
threshold: 0.1,
|
threshold: 0.1,
|
||||||
|
rootMargin: '0px',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -584,7 +604,26 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
unreadElements.forEach((el: Element) => {
|
unreadElements.forEach((el: Element) => {
|
||||||
const id = el.getAttribute('data-message-id');
|
const id = el.getAttribute('data-message-id');
|
||||||
if (id && !sentReadIdsRef.current.has(id)) {
|
if (id && !sentReadIdsRef.current.has(id)) {
|
||||||
observer.observe(el);
|
// Check if element is already visible
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const containerRect = scrollContainerRef.current!.getBoundingClientRect();
|
||||||
|
const isVisible = rect.top >= containerRect.top && rect.bottom <= containerRect.bottom;
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
// Mark as read immediately without waiting for intersection
|
||||||
|
const seqId = parseInt(el.getAttribute('data-sequence-id') || '0', 10);
|
||||||
|
if (seqId > 0) {
|
||||||
|
socket.emit('read_messages', {
|
||||||
|
chatId: activeChat,
|
||||||
|
lastReadMessageId: id,
|
||||||
|
lastReadSequenceId: seqId,
|
||||||
|
});
|
||||||
|
useChatStore.getState().markRead(activeChat, user.id, seqId);
|
||||||
|
sentReadIdsRef.current.add(id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
observer.observe(el);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -1060,7 +1099,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
setShowTopMenu(false);
|
setShowTopMenu(false);
|
||||||
if (activeChat) {
|
if (activeChat) {
|
||||||
setConfirmAction({
|
setConfirmAction({
|
||||||
message: chat.type === 'group' ? t('deleteGroupChatConfirm') : t('deleteChatConfirm'),
|
message: t('deleteChatConfirm'),
|
||||||
action: async () => {
|
action: async () => {
|
||||||
try {
|
try {
|
||||||
await ChatApi.deleteChat(activeChat);
|
await ChatApi.deleteChat(activeChat);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user