Repair: copy legacy joinDate into new user's created_at when creating users from legacy wallz
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function ConversationList({ conversations, loading, activeId, currentUserId, typingByConversation = {}, onSelect }) {
|
||||
export default function ConversationList({ conversations, loading, activeId, currentUserId, onlineUserIds = [], typingByConversation = {}, onSelect }) {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<div className="flex items-center justify-between border-b border-white/[0.06] px-4 py-3">
|
||||
@@ -28,6 +28,7 @@ export default function ConversationList({ conversations, loading, activeId, cur
|
||||
conv={conversation}
|
||||
isActive={conversation.id === activeId}
|
||||
currentUserId={currentUserId}
|
||||
onlineUserIds={onlineUserIds}
|
||||
typingUsers={typingByConversation[conversation.id] ?? []}
|
||||
onClick={() => onSelect(conversation.id)}
|
||||
/>
|
||||
@@ -37,7 +38,7 @@ export default function ConversationList({ conversations, loading, activeId, cur
|
||||
)
|
||||
}
|
||||
|
||||
function ConversationRow({ conv, isActive, currentUserId, typingUsers, onClick }) {
|
||||
function ConversationRow({ conv, isActive, currentUserId, onlineUserIds, typingUsers, onClick }) {
|
||||
const label = convLabel(conv, currentUserId)
|
||||
const lastMsg = Array.isArray(conv.latest_message) ? conv.latest_message[0] : conv.latest_message
|
||||
const preview = typingUsers.length > 0
|
||||
@@ -45,10 +46,17 @@ function ConversationRow({ conv, isActive, currentUserId, typingUsers, onClick }
|
||||
: lastMsg?.body ? truncate(lastMsg.body, 88) : 'No messages yet'
|
||||
const unread = conv.unread_count ?? 0
|
||||
const myParticipant = conv.all_participants?.find((participant) => participant.user_id === currentUserId)
|
||||
const otherParticipant = conv.all_participants?.find((participant) => participant.user_id !== currentUserId)
|
||||
const isArchived = myParticipant?.is_archived ?? false
|
||||
const isPinned = myParticipant?.is_pinned ?? false
|
||||
const activeMembers = conv.all_participants?.filter((participant) => !participant.left_at).length ?? 0
|
||||
const typeLabel = conv.type === 'group' ? `${activeMembers} members` : 'Direct message'
|
||||
const onlineMembers = conv.type === 'group'
|
||||
? conv.all_participants?.filter((participant) => participant.user_id !== currentUserId && onlineUserIds.includes(Number(participant.user_id)) && !participant.left_at).length ?? 0
|
||||
: 0
|
||||
const isDirectOnline = conv.type === 'direct' && otherParticipant ? onlineUserIds.includes(Number(otherParticipant.user_id)) : false
|
||||
const typeLabel = conv.type === 'group'
|
||||
? (onlineMembers > 0 ? `${onlineMembers} online` : `${activeMembers} members`)
|
||||
: (isDirectOnline ? 'Online now' : 'Direct message')
|
||||
const senderLabel = lastMsg?.sender?.username ? `@${lastMsg.sender.username}` : null
|
||||
const initials = label
|
||||
.split(/\s+/)
|
||||
@@ -64,8 +72,11 @@ function ConversationRow({ conv, isActive, currentUserId, typingUsers, onClick }
|
||||
className={`w-full rounded-[24px] border px-4 py-4 text-left transition ${isActive ? 'border-sky-400/28 bg-sky-500/[0.12] shadow-[0_0_0_1px_rgba(56,189,248,0.08)]' : 'border-white/[0.06] bg-white/[0.03] hover:border-white/[0.1] hover:bg-white/[0.05]'} ${isArchived ? 'opacity-65' : ''}`}
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<div className={`flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl text-sm font-semibold text-white shadow-[0_10px_25px_rgba(0,0,0,0.18)] ${conv.type === 'group' ? 'bg-gradient-to-br from-fuchsia-500 to-violet-600' : 'bg-gradient-to-br from-sky-500 to-cyan-500'}`}>
|
||||
{initials}
|
||||
<div className="relative shrink-0">
|
||||
<div className={`flex h-12 w-12 items-center justify-center rounded-2xl text-sm font-semibold text-white shadow-[0_10px_25px_rgba(0,0,0,0.18)] ${conv.type === 'group' ? 'bg-gradient-to-br from-fuchsia-500 to-violet-600' : 'bg-gradient-to-br from-sky-500 to-cyan-500'}`}>
|
||||
{initials}
|
||||
</div>
|
||||
{isDirectOnline ? <span className="absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded-full border-2 border-[#0a101a] bg-emerald-300 shadow-[0_0_0_6px_rgba(16,185,129,0.08)]" /> : null}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
|
||||
@@ -9,6 +9,7 @@ export default function ConversationThread({
|
||||
realtimeStatus,
|
||||
currentUserId,
|
||||
currentUsername,
|
||||
onlineUserIds,
|
||||
apiFetch,
|
||||
onBack,
|
||||
onMarkRead,
|
||||
@@ -65,6 +66,13 @@ export default function ConversationThread({
|
||||
.map((participant) => participant.user?.username)
|
||||
.filter(Boolean)
|
||||
), [currentUserId, participants])
|
||||
const directParticipant = useMemo(() => (
|
||||
participants.find((participant) => participant.user_id !== currentUserId) ?? null
|
||||
), [currentUserId, participants])
|
||||
const remoteIsOnline = directParticipant ? onlineUserIds.includes(Number(directParticipant.user_id)) : false
|
||||
const remoteIsViewingConversation = directParticipant
|
||||
? presenceUsers.some((user) => Number(user?.id) === Number(directParticipant.user_id))
|
||||
: false
|
||||
|
||||
const filteredMessages = useMemo(() => {
|
||||
const query = threadSearch.trim().toLowerCase()
|
||||
@@ -185,7 +193,7 @@ export default function ConversationThread({
|
||||
}
|
||||
: participant
|
||||
)))
|
||||
onMarkRead?.(conversationId)
|
||||
onMarkRead?.(conversationId, response?.unread_total ?? null)
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
@@ -309,7 +317,7 @@ export default function ConversationThread({
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await apiFetch(`/api/messages/${conversationId}?after_id=${encodeURIComponent(lastServerMessage.id)}`)
|
||||
const data = await apiFetch(`/api/messages/${conversationId}/delta?after_message_id=${encodeURIComponent(lastServerMessage.id)}`)
|
||||
const incoming = normalizeMessages(data.data ?? [], currentUserId)
|
||||
if (incoming.length > 0) {
|
||||
setMessages((prev) => mergeMessageLists(prev, incoming))
|
||||
@@ -622,9 +630,11 @@ export default function ConversationThread({
|
||||
}, [apiFetch, conversation?.title, conversationId, draftTitle, patchConversation])
|
||||
|
||||
const visibleMessages = filteredMessages
|
||||
const messagesWithDecorators = useMemo(() => decorateMessages(visibleMessages, currentUserId, myParticipant?.last_read_at ?? null), [visibleMessages, currentUserId, myParticipant?.last_read_at])
|
||||
const messagesWithDecorators = useMemo(() => decorateMessages(visibleMessages, currentUserId, myParticipant), [visibleMessages, currentUserId, myParticipant])
|
||||
const typingLabel = buildTypingLabel(typingUsers)
|
||||
const presenceLabel = presenceUsers.length > 0 ? `${presenceUsers.length} active now` : null
|
||||
const presenceLabel = conversation?.type === 'group'
|
||||
? (presenceUsers.length > 0 ? `${presenceUsers.length} active now` : null)
|
||||
: (remoteIsViewingConversation ? 'Viewing this conversation' : (remoteIsOnline ? 'Online now' : null))
|
||||
const typingSummary = typingUsers.length > 0
|
||||
? `${typingLabel} ${conversation?.type === 'group' ? '' : 'Reply will appear here instantly.'}`.trim()
|
||||
: null
|
||||
@@ -796,7 +806,7 @@ export default function ConversationThread({
|
||||
const showAvatar = !previous || previous.sender_id !== message.sender_id
|
||||
const endsSequence = !next || next.sender_id !== message.sender_id
|
||||
const seenText = isLastMineMessage(visibleMessages, index, currentUserId)
|
||||
? buildSeenText(participants, currentUserId)
|
||||
? buildSeenText(participants, currentUserId, message)
|
||||
: null
|
||||
|
||||
return (
|
||||
@@ -1037,29 +1047,38 @@ function isLastMineMessage(messages, index, currentUserId) {
|
||||
return true
|
||||
}
|
||||
|
||||
function buildSeenText(participants, currentUserId) {
|
||||
const seenBy = participants
|
||||
.filter((participant) => participant.user_id !== currentUserId && participant.last_read_at)
|
||||
.map((participant) => participant.user?.username)
|
||||
.filter(Boolean)
|
||||
function buildSeenText(participants, currentUserId, message) {
|
||||
const seenBy = participants.filter((participant) => participant.user_id !== currentUserId && participantHasReadMessage(participant, message))
|
||||
|
||||
if (seenBy.length === 0) return 'Sent'
|
||||
if (seenBy.length === 1) return `Seen by @${seenBy[0]}`
|
||||
|
||||
if (seenBy.length === 1) {
|
||||
const readAt = seenBy[0]?.last_read_at
|
||||
return readAt ? `Seen ${formatSeenTime(readAt)}` : 'Seen'
|
||||
}
|
||||
|
||||
return `Seen by ${seenBy.length} people`
|
||||
}
|
||||
|
||||
function decorateMessages(messages, currentUserId, lastReadAt) {
|
||||
function decorateMessages(messages, currentUserId, participant) {
|
||||
let unreadMarked = false
|
||||
const lastReadMessageId = Number(participant?.last_read_message_id ?? 0)
|
||||
const lastReadAt = participant?.last_read_at ?? null
|
||||
|
||||
return messages.map((message, index) => {
|
||||
const previous = messages[index - 1]
|
||||
const currentDay = dayKey(message.created_at)
|
||||
const previousDay = previous ? dayKey(previous.created_at) : null
|
||||
const shouldMarkUnread = !unreadMarked
|
||||
&& !!lastReadAt
|
||||
&& message.sender_id !== currentUserId
|
||||
&& !message.deleted_at
|
||||
&& new Date(message.created_at).getTime() > new Date(lastReadAt).getTime()
|
||||
&& (
|
||||
lastReadMessageId > 0
|
||||
? Number(message.id) > lastReadMessageId
|
||||
: lastReadAt
|
||||
? new Date(message.created_at).getTime() > new Date(lastReadAt).getTime()
|
||||
: true
|
||||
)
|
||||
|
||||
if (shouldMarkUnread) unreadMarked = true
|
||||
|
||||
@@ -1071,6 +1090,26 @@ function decorateMessages(messages, currentUserId, lastReadAt) {
|
||||
})
|
||||
}
|
||||
|
||||
function participantHasReadMessage(participant, message) {
|
||||
const lastReadMessageId = Number(participant?.last_read_message_id ?? 0)
|
||||
if (lastReadMessageId > 0) {
|
||||
return Number(message?.id ?? 0) > 0 && lastReadMessageId >= Number(message.id)
|
||||
}
|
||||
|
||||
if (participant?.last_read_at && message?.created_at) {
|
||||
return new Date(participant.last_read_at).getTime() >= new Date(message.created_at).getTime()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function formatSeenTime(iso) {
|
||||
return new Date(iso).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
function dayKey(iso) {
|
||||
const date = new Date(iso)
|
||||
return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`
|
||||
|
||||
Reference in New Issue
Block a user