Repair: copy legacy joinDate into new user's created_at when creating users from legacy wallz

This commit is contained in:
2026-03-22 09:13:39 +01:00
parent e8b5edf5d2
commit 2608be7420
80 changed files with 3991 additions and 723 deletions

View File

@@ -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()}`