@open-mercato/core 0.10.1-develop.7770.1.970b396ebb → 0.10.1-develop.7772.1.9d3d6ce261
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js +6 -7
- package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +2 -2
- package/dist/modules/communication_channels/data/validators.js +2 -1
- package/dist/modules/communication_channels/data/validators.js.map +2 -2
- package/dist/modules/communication_channels/lib/email-mime.js +13 -3
- package/dist/modules/communication_channels/lib/email-mime.js.map +2 -2
- package/dist/modules/communication_channels/lib/thread-matcher.js.map +2 -2
- package/dist/modules/customers/api/activities/route.js +16 -6
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/lib/interactionOrderBy.js +12 -0
- package/dist/modules/customers/lib/interactionOrderBy.js.map +7 -0
- package/dist/modules/customers/lib/link-channel-message-handler.js +12 -11
- package/dist/modules/customers/lib/link-channel-message-handler.js.map +2 -2
- package/dist/modules/customers/lib/personEmailThreads.js +43 -12
- package/dist/modules/customers/lib/personEmailThreads.js.map +3 -3
- package/dist/modules/messages/api/route.js +2 -1
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/messages/commands/messages.js +2 -1
- package/dist/modules/messages/commands/messages.js.map +2 -2
- package/dist/modules/messages/components/MessagesInboxPageClient.js +1 -0
- package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
- package/dist/modules/messages/components/defaults/DefaultMessageListItem.js +10 -8
- package/dist/modules/messages/components/defaults/DefaultMessageListItem.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/MessageHeader.js +1 -1
- package/dist/modules/messages/components/message-detail/panels/MessageHeader.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/MessageListComponent.js +3 -3
- package/dist/modules/messages/components/message-detail/panels/MessageListComponent.js.map +2 -2
- package/dist/modules/messages/components/message-detail/panels/thread-panel.js +1 -1
- package/dist/modules/messages/components/message-detail/panels/thread-panel.js.map +2 -2
- package/dist/modules/messages/components/message-detail/utils.js +3 -3
- package/dist/modules/messages/components/message-detail/utils.js.map +2 -2
- package/dist/modules/messages/data/validators.js +3 -2
- package/dist/modules/messages/data/validators.js.map +2 -2
- package/dist/modules/messages/frontend/messages/view/[token]/page.js +6 -5
- package/dist/modules/messages/frontend/messages/view/[token]/page.js.map +2 -2
- package/dist/modules/messages/lib/forwarding.js +3 -1
- package/dist/modules/messages/lib/forwarding.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/communication_channels/commands/ingest-inbound-message.ts +5 -7
- package/src/modules/communication_channels/data/validators.ts +2 -1
- package/src/modules/communication_channels/lib/adapter.ts +4 -1
- package/src/modules/communication_channels/lib/email-mime.ts +24 -7
- package/src/modules/communication_channels/lib/thread-matcher.ts +2 -1
- package/src/modules/customers/api/activities/route.ts +17 -7
- package/src/modules/customers/lib/interactionOrderBy.ts +9 -0
- package/src/modules/customers/lib/link-channel-message-handler.ts +27 -38
- package/src/modules/customers/lib/personEmailThreads.ts +63 -21
- package/src/modules/messages/api/route.ts +3 -1
- package/src/modules/messages/commands/messages.ts +2 -1
- package/src/modules/messages/components/MessagesInboxPageClient.tsx +2 -0
- package/src/modules/messages/components/defaults/DefaultMessageListItem.tsx +10 -8
- package/src/modules/messages/components/message-detail/panels/MessageHeader.tsx +3 -1
- package/src/modules/messages/components/message-detail/panels/MessageListComponent.tsx +5 -3
- package/src/modules/messages/components/message-detail/panels/thread-panel.tsx +1 -1
- package/src/modules/messages/components/message-detail/utils.ts +3 -3
- package/src/modules/messages/data/validators.ts +4 -3
- package/src/modules/messages/frontend/messages/view/[token]/page.tsx +5 -4
- package/src/modules/messages/i18n/de.json +1 -0
- package/src/modules/messages/i18n/en.json +1 -0
- package/src/modules/messages/i18n/es.json +1 -0
- package/src/modules/messages/i18n/ko.json +1 -0
- package/src/modules/messages/i18n/pl.json +2 -1
- package/src/modules/messages/lib/forwarding.ts +3 -1
|
@@ -3,6 +3,7 @@ import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
|
3
3
|
import { CustomerInteraction } from '../data/entities'
|
|
4
4
|
import { resolveUserNames } from './userNames'
|
|
5
5
|
import { buildEmailVisibilityMikroFilter } from './visibilityFilter'
|
|
6
|
+
import { buildInteractionOccurredAtOrderBy } from './interactionOrderBy'
|
|
6
7
|
import type { ConversationShareGrant } from './conversationShares'
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -45,7 +46,7 @@ export type PersonEmailMessage = {
|
|
|
45
46
|
cc: string[]
|
|
46
47
|
subject: string | null
|
|
47
48
|
bodyText: string | null
|
|
48
|
-
sentAt: string
|
|
49
|
+
sentAt: string | null
|
|
49
50
|
providerKey: string | null
|
|
50
51
|
}
|
|
51
52
|
|
|
@@ -54,7 +55,7 @@ export type PersonEmailThread = {
|
|
|
54
55
|
subject: string | null
|
|
55
56
|
preview: string | null
|
|
56
57
|
participants: string[]
|
|
57
|
-
lastMessageAt: string
|
|
58
|
+
lastMessageAt: string | null
|
|
58
59
|
messageCount: number
|
|
59
60
|
providerKey: string | null
|
|
60
61
|
lastDirection: EmailThreadDirection
|
|
@@ -195,17 +196,20 @@ export async function buildPersonEmailThreads(
|
|
|
195
196
|
em,
|
|
196
197
|
CustomerInteraction,
|
|
197
198
|
interactionWhere as never,
|
|
198
|
-
{
|
|
199
|
+
{
|
|
200
|
+
orderBy: buildInteractionOccurredAtOrderBy('desc'),
|
|
201
|
+
limit: maxThreads * 20,
|
|
202
|
+
},
|
|
199
203
|
dscope,
|
|
200
204
|
)) as CustomerInteraction[]
|
|
201
205
|
|
|
202
|
-
const linkIdByInteraction = new Map<string, { occurredAt: Date; authorUserId: string | null }>()
|
|
206
|
+
const linkIdByInteraction = new Map<string, { occurredAt: Date | null; authorUserId: string | null }>()
|
|
203
207
|
const linkIds: string[] = []
|
|
204
208
|
for (const interaction of interactions) {
|
|
205
209
|
const linkId = interaction.externalMessageId
|
|
206
210
|
if (!linkId || linkIdByInteraction.has(linkId)) continue
|
|
207
211
|
linkIdByInteraction.set(linkId, {
|
|
208
|
-
occurredAt: interaction.occurredAt ??
|
|
212
|
+
occurredAt: interaction.occurredAt ?? null,
|
|
209
213
|
authorUserId: interaction.authorUserId ?? null,
|
|
210
214
|
})
|
|
211
215
|
linkIds.push(linkId)
|
|
@@ -301,14 +305,12 @@ export async function buildPersonEmailThreads(
|
|
|
301
305
|
typeof message?.body === 'string' ? message.body : null,
|
|
302
306
|
)
|
|
303
307
|
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
new Date()
|
|
311
|
-
const sentAt = (sentAtRaw instanceof Date ? sentAtRaw : new Date(sentAtRaw)).toISOString()
|
|
308
|
+
const messageSentAt = message?.sentAt instanceof Date || typeof message?.sentAt === 'string'
|
|
309
|
+
? new Date(message.sentAt)
|
|
310
|
+
: null
|
|
311
|
+
const sentAt = messageSentAt && !Number.isNaN(messageSentAt.getTime())
|
|
312
|
+
? messageSentAt.toISOString()
|
|
313
|
+
: null
|
|
312
314
|
|
|
313
315
|
const threadKey =
|
|
314
316
|
(typeof message?.threadId === 'string' && message.threadId ? message.threadId : null) ??
|
|
@@ -353,11 +355,27 @@ export async function buildPersonEmailThreads(
|
|
|
353
355
|
// ── (5) Finalize thread summaries ───────────────────────────────────────
|
|
354
356
|
const threads: PersonEmailThread[] = []
|
|
355
357
|
for (const thread of threadsByKey.values()) {
|
|
356
|
-
thread.messages.sort((
|
|
358
|
+
thread.messages.sort((left, right) => compareNullableIsochronological(
|
|
359
|
+
left.sentAt,
|
|
360
|
+
right.sentAt,
|
|
361
|
+
left.id,
|
|
362
|
+
right.id,
|
|
363
|
+
))
|
|
364
|
+
const datedMessages = thread.messages.filter((message) => message.sentAt !== null)
|
|
365
|
+
const undatedMessages = thread.messages.filter((message) => message.sentAt === null)
|
|
357
366
|
if (thread.messages.length > maxMessagesPerThread) {
|
|
358
|
-
|
|
367
|
+
const retainedDatedMessages = maxMessagesPerThread > 0
|
|
368
|
+
? datedMessages.slice(-maxMessagesPerThread)
|
|
369
|
+
: []
|
|
370
|
+
const remainingSlots = Math.max(0, maxMessagesPerThread - retainedDatedMessages.length)
|
|
371
|
+
const retainedUndatedMessages = remainingSlots > 0
|
|
372
|
+
? undatedMessages.slice(-remainingSlots)
|
|
373
|
+
: []
|
|
374
|
+
thread.messages = [...retainedDatedMessages, ...retainedUndatedMessages]
|
|
359
375
|
}
|
|
360
|
-
const
|
|
376
|
+
const retainedDatedMessages = thread.messages.filter((message) => message.sentAt !== null)
|
|
377
|
+
const latestDatedMessage = retainedDatedMessages[retainedDatedMessages.length - 1]
|
|
378
|
+
const summaryMessage = latestDatedMessage ?? thread.messages[thread.messages.length - 1]
|
|
361
379
|
const firstWithSubject = thread.messages.find((m) => m.subject)
|
|
362
380
|
const participantSet = new Set<string>()
|
|
363
381
|
for (const message of thread.messages) {
|
|
@@ -366,19 +384,43 @@ export async function buildPersonEmailThreads(
|
|
|
366
384
|
if (message.direction === 'outbound') message.to.forEach((email) => participantSet.add(email))
|
|
367
385
|
}
|
|
368
386
|
thread.subject = firstWithSubject?.subject ?? thread.subject
|
|
369
|
-
thread.preview = truncate(
|
|
387
|
+
thread.preview = truncate(summaryMessage?.bodyText ?? null)
|
|
370
388
|
thread.participants = Array.from(participantSet)
|
|
371
|
-
thread.lastMessageAt =
|
|
372
|
-
thread.lastDirection =
|
|
373
|
-
thread.providerKey =
|
|
389
|
+
thread.lastMessageAt = latestDatedMessage?.sentAt ?? null
|
|
390
|
+
thread.lastDirection = summaryMessage?.direction ?? thread.lastDirection
|
|
391
|
+
thread.providerKey = summaryMessage?.providerKey ?? thread.providerKey
|
|
374
392
|
thread.messageCount = thread.messages.length
|
|
375
393
|
threads.push(thread)
|
|
376
394
|
}
|
|
377
395
|
|
|
378
|
-
threads.sort((
|
|
396
|
+
threads.sort((left, right) => compareNullableIsochronological(
|
|
397
|
+
left.lastMessageAt,
|
|
398
|
+
right.lastMessageAt,
|
|
399
|
+
left.threadKey,
|
|
400
|
+
right.threadKey,
|
|
401
|
+
'desc',
|
|
402
|
+
))
|
|
379
403
|
return threads.slice(0, maxThreads)
|
|
380
404
|
}
|
|
381
405
|
|
|
406
|
+
function compareNullableIsochronological(
|
|
407
|
+
leftValue: string | null,
|
|
408
|
+
rightValue: string | null,
|
|
409
|
+
leftTieBreaker: string,
|
|
410
|
+
rightTieBreaker: string,
|
|
411
|
+
direction: 'asc' | 'desc' = 'asc',
|
|
412
|
+
): number {
|
|
413
|
+
if (leftValue === rightValue) {
|
|
414
|
+
const tie = leftTieBreaker.localeCompare(rightTieBreaker)
|
|
415
|
+
return direction === 'asc' ? tie : -tie
|
|
416
|
+
}
|
|
417
|
+
if (leftValue === null) return 1
|
|
418
|
+
if (rightValue === null) return -1
|
|
419
|
+
return direction === 'asc'
|
|
420
|
+
? leftValue.localeCompare(rightValue)
|
|
421
|
+
: rightValue.localeCompare(leftValue)
|
|
422
|
+
}
|
|
423
|
+
|
|
382
424
|
export type PersonEmailMessageAccessOptions = Pick<
|
|
383
425
|
BuildPersonEmailThreadsOptions,
|
|
384
426
|
'personId' | 'tenantId' | 'organizationId' | 'viewerUserId' | 'userFeatures' | 'sharedConversations' | 'sharedChannelIds'
|
|
@@ -321,7 +321,8 @@ export async function GET(req: Request) {
|
|
|
321
321
|
'r.status as recipient_status',
|
|
322
322
|
'r.read_at',
|
|
323
323
|
])
|
|
324
|
-
.orderBy('m.sent_at'
|
|
324
|
+
.orderBy(sql`${sql.ref('m.sent_at')} desc nulls last`)
|
|
325
|
+
.orderBy('m.id', 'desc')
|
|
325
326
|
.offset(offset)
|
|
326
327
|
.limit(input.pageSize)
|
|
327
328
|
.execute()
|
|
@@ -428,6 +429,7 @@ export async function GET(req: Request) {
|
|
|
428
429
|
senderUserId: message.senderUserId,
|
|
429
430
|
priority: message.priority,
|
|
430
431
|
status: row.recipient_status ?? (row.is_draft ? 'draft' : 'sent'),
|
|
432
|
+
isDraft: row.is_draft,
|
|
431
433
|
hasObjects: (objectsByMessage[message.id] || []).length > 0,
|
|
432
434
|
objectCount: (objectsByMessage[message.id] || []).length,
|
|
433
435
|
hasAttachments: (attachmentCountByMessage[message.id] || 0) > 0,
|
|
@@ -329,6 +329,7 @@ const composeMessageCommand: CommandHandler<unknown, { id: string; threadId: str
|
|
|
329
329
|
// here echoed every inbound email back to its sender through the tenant
|
|
330
330
|
// system email channel and made send-as-user deliver twice.
|
|
331
331
|
const sendViaEmail = input.sendViaEmail
|
|
332
|
+
const sentAt = input.sentAt === undefined ? new Date() : input.sentAt
|
|
332
333
|
const message = trx.create(Message, {
|
|
333
334
|
type: input.type,
|
|
334
335
|
visibility: input.visibility ?? null,
|
|
@@ -347,7 +348,7 @@ const composeMessageCommand: CommandHandler<unknown, { id: string; threadId: str
|
|
|
347
348
|
isDraft: input.isDraft ?? false,
|
|
348
349
|
// #6095: channel ingest passes the provider's timestamp so an imported
|
|
349
350
|
// mailbox sorts by when the mail was received, not when it was imported.
|
|
350
|
-
sentAt: input.isDraft ? null :
|
|
351
|
+
sentAt: input.isDraft ? null : sentAt,
|
|
351
352
|
actionData: input.actionData as MessageActionData | undefined,
|
|
352
353
|
sendViaEmail,
|
|
353
354
|
idempotencyKey: input.idempotencyKey ?? null,
|
|
@@ -45,6 +45,7 @@ type MessageListItem = {
|
|
|
45
45
|
sourceEntityType?: string | null
|
|
46
46
|
priority: string
|
|
47
47
|
status: string
|
|
48
|
+
isDraft: boolean
|
|
48
49
|
hasObjects: boolean
|
|
49
50
|
objectCount: number
|
|
50
51
|
hasAttachments: boolean
|
|
@@ -304,6 +305,7 @@ export function MessagesInboxPageClient() {
|
|
|
304
305
|
bodyFormat: 'text' as const,
|
|
305
306
|
priority: (item.priority as 'low' | 'normal' | 'high' | 'urgent') ?? 'normal',
|
|
306
307
|
sentAt: item.sentAt ? new Date(item.sentAt) : null,
|
|
308
|
+
isDraft: item.isDraft,
|
|
307
309
|
senderName: getMessageListParticipantLabel(item, folder, t),
|
|
308
310
|
hasObjects: item.hasObjects,
|
|
309
311
|
objectCount: item.objectCount,
|
|
@@ -5,15 +5,15 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
|
5
5
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
6
6
|
import { CheckCircle2, FileText, Paperclip, Zap } from 'lucide-react'
|
|
7
7
|
|
|
8
|
-
function formatDateTime(value: Date | null): string {
|
|
9
|
-
if (!value) return
|
|
10
|
-
if (Number.isNaN(value.getTime())) return
|
|
8
|
+
function formatDateTime(value: Date | null, unknownDate: string): string {
|
|
9
|
+
if (!value) return unknownDate
|
|
10
|
+
if (Number.isNaN(value.getTime())) return unknownDate
|
|
11
11
|
return value.toLocaleString()
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function formatSentTime(value: Date | null): string {
|
|
15
|
-
if (!value) return
|
|
16
|
-
if (Number.isNaN(value.getTime())) return
|
|
14
|
+
function formatSentTime(value: Date | null, unknownDate: string): string {
|
|
15
|
+
if (!value) return unknownDate
|
|
16
|
+
if (Number.isNaN(value.getTime())) return unknownDate
|
|
17
17
|
|
|
18
18
|
const now = new Date()
|
|
19
19
|
const isSameDay = now.toDateString() === value.toDateString()
|
|
@@ -41,8 +41,10 @@ export function DefaultMessageListItem({ message, onClick }: MessageListItemProp
|
|
|
41
41
|
const t = useT()
|
|
42
42
|
const senderLabel = message.senderName?.trim() || t('messages.list.noRecipient', '(No recipient)')
|
|
43
43
|
const subject = message.subject.trim() || t('messages.list.noSubject', '(No subject)')
|
|
44
|
-
const
|
|
45
|
-
const
|
|
44
|
+
const unknownDate = t('messages.unknownDate', 'Unknown date')
|
|
45
|
+
const notSentYet = '—'
|
|
46
|
+
const absoluteSentAt = message.isDraft ? notSentYet : formatDateTime(message.sentAt, unknownDate)
|
|
47
|
+
const sentAtLabel = message.isDraft ? notSentYet : formatSentTime(message.sentAt, unknownDate)
|
|
46
48
|
const bodyPreview = truncateWords(message.body || '', 16)
|
|
47
49
|
|
|
48
50
|
return (
|
|
@@ -124,7 +124,9 @@ export function MessageHeader(props: MessageHeaderProps) {
|
|
|
124
124
|
mode="detail"
|
|
125
125
|
title={showSubject ? props.detail.subject : undefined}
|
|
126
126
|
subtitle={`${t('messages.detail.from', 'From')}: ${senderLabel}`}
|
|
127
|
-
statusBadge={<p className="text-xs text-muted-foreground">{
|
|
127
|
+
statusBadge={<p className="text-xs text-muted-foreground">{props.detail.isDraft
|
|
128
|
+
? '—'
|
|
129
|
+
: formatDateTime(props.detail.sentAt, t('messages.unknownDate', 'Unknown date'))}</p>}
|
|
128
130
|
utilityActions={canReply ? (
|
|
129
131
|
<IconButton
|
|
130
132
|
type="button"
|
|
@@ -4,8 +4,8 @@ import type { MessageListItemProps } from '@open-mercato/shared/modules/messages
|
|
|
4
4
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
5
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
6
6
|
|
|
7
|
-
function formatSentTime(value: Date | null): string {
|
|
8
|
-
if (!value || Number.isNaN(value.getTime())) return
|
|
7
|
+
function formatSentTime(value: Date | null, unknownDate: string): string {
|
|
8
|
+
if (!value || Number.isNaN(value.getTime())) return unknownDate
|
|
9
9
|
|
|
10
10
|
const now = new Date()
|
|
11
11
|
if (now.toDateString() === value.toDateString()) {
|
|
@@ -24,7 +24,9 @@ export function MessageListComponent({ message, onClick }: MessageListItemProps)
|
|
|
24
24
|
const t = useT()
|
|
25
25
|
const senderLabel = message.senderName || '—'
|
|
26
26
|
const body = normalizeBody(message.body)
|
|
27
|
-
const sentAtLabel =
|
|
27
|
+
const sentAtLabel = message.isDraft
|
|
28
|
+
? '—'
|
|
29
|
+
: formatSentTime(message.sentAt, t('messages.unknownDate', 'Unknown date'))
|
|
28
30
|
|
|
29
31
|
return (
|
|
30
32
|
<div
|
|
@@ -21,7 +21,7 @@ export function MessageDetailThreadSection({ detail }: { detail: MessageDetail }
|
|
|
21
21
|
{(detail.thread ?? []).map((threadItem) => (
|
|
22
22
|
<article key={threadItem.id} className="rounded border p-3">
|
|
23
23
|
<p className="text-xs text-muted-foreground">
|
|
24
|
-
{getMessageParticipantLabel(threadItem)} • {formatDateTime(threadItem.sentAt)}
|
|
24
|
+
{getMessageParticipantLabel(threadItem)} • {formatDateTime(threadItem.sentAt, t('messages.unknownDate', 'Unknown date'))}
|
|
25
25
|
</p>
|
|
26
26
|
<div className="mt-2 max-h-[60vh] overflow-y-auto pr-1">
|
|
27
27
|
<MarkdownContent
|
|
@@ -24,10 +24,10 @@ export function toErrorMessage(payload: unknown): string | null {
|
|
|
24
24
|
return null
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function formatDateTime(value: string | null | undefined): string {
|
|
28
|
-
if (!value) return
|
|
27
|
+
export function formatDateTime(value: string | null | undefined, unknownDate = '—'): string {
|
|
28
|
+
if (!value) return unknownDate
|
|
29
29
|
const date = new Date(value)
|
|
30
|
-
if (Number.isNaN(date.getTime())) return
|
|
30
|
+
if (Number.isNaN(date.getTime())) return unknownDate
|
|
31
31
|
return date.toLocaleString()
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -151,15 +151,16 @@ const composeMessageBaseSchema = z.object({
|
|
|
151
151
|
* (#6095). Channel ingest sets it to the provider's timestamp so an imported
|
|
152
152
|
* mailbox keeps its real chronology instead of collapsing onto the import
|
|
153
153
|
* minute. Server-only: the HTTP route strips any client-sent value, so a
|
|
154
|
-
* browser caller cannot backdate a message.
|
|
154
|
+
* browser caller cannot backdate a message. Explicit null preserves unknown
|
|
155
|
+
* external occurrence time; omission uses the send time. Ignored for drafts.
|
|
155
156
|
*/
|
|
156
|
-
sentAt: z.coerce.date().optional(),
|
|
157
|
+
sentAt: z.coerce.date().nullable().optional(),
|
|
157
158
|
})
|
|
158
159
|
|
|
159
160
|
type ComposeMessageRefinementValue = Omit<
|
|
160
161
|
z.infer<typeof composeMessageBaseSchema>,
|
|
161
162
|
'sourceChannelType' | 'sentAt'
|
|
162
|
-
> & { sourceChannelType?: string; sentAt?: Date }
|
|
163
|
+
> & { sourceChannelType?: string; sentAt?: Date | null }
|
|
163
164
|
|
|
164
165
|
function refineComposeMessage(value: ComposeMessageRefinementValue, ctx: z.RefinementCtx): void {
|
|
165
166
|
const isDraft = value.isDraft ?? false
|
|
@@ -94,10 +94,10 @@ function toErrorMessage(payload: unknown): string | null {
|
|
|
94
94
|
return null
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function formatDateTime(value: string | null | undefined): string {
|
|
98
|
-
if (!value) return
|
|
97
|
+
function formatDateTime(value: string | null | undefined, unknownDate: string): string {
|
|
98
|
+
if (!value) return unknownDate
|
|
99
99
|
const date = new Date(value)
|
|
100
|
-
if (Number.isNaN(date.getTime())) return
|
|
100
|
+
if (Number.isNaN(date.getTime())) return unknownDate
|
|
101
101
|
return date.toLocaleString()
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -308,7 +308,8 @@ export default function MessageTokenPage({ params }: { params: { token: string }
|
|
|
308
308
|
<header className="space-y-1">
|
|
309
309
|
<h1 className="text-2xl font-semibold">{data.subject}</h1>
|
|
310
310
|
<p className="text-sm text-muted-foreground">
|
|
311
|
-
{t('messages.token.sentAt', 'Sent')}:
|
|
311
|
+
{t('messages.token.sentAt', 'Sent')}:{' '}
|
|
312
|
+
{formatDateTime(data.sentAt, t('messages.unknownDate', 'Unknown date'))}
|
|
312
313
|
</p>
|
|
313
314
|
</header>
|
|
314
315
|
|
|
@@ -266,6 +266,7 @@
|
|
|
266
266
|
"messages.types.default": "Nachricht",
|
|
267
267
|
"messages.types.defaultWithObjects": "Objektnachricht",
|
|
268
268
|
"messages.types.system": "Systembenachrichtigung",
|
|
269
|
+
"messages.unknownDate": "Datum unbekannt",
|
|
269
270
|
"messages.visibility": "Sichtbarkeit",
|
|
270
271
|
"messages.visibilityInternal": "Intern",
|
|
271
272
|
"messages.visibilityInternalHint": "Interne Nachrichten werden an ausgewählte Systemnutzer gesendet.",
|
|
@@ -266,6 +266,7 @@
|
|
|
266
266
|
"messages.types.default": "Message",
|
|
267
267
|
"messages.types.defaultWithObjects": "Object message",
|
|
268
268
|
"messages.types.system": "System notification",
|
|
269
|
+
"messages.unknownDate": "Unknown date",
|
|
269
270
|
"messages.visibility": "Visibility",
|
|
270
271
|
"messages.visibilityInternal": "Internal",
|
|
271
272
|
"messages.visibilityInternalHint": "Internal messages are sent to selected system users.",
|
|
@@ -266,6 +266,7 @@
|
|
|
266
266
|
"messages.types.default": "Mensaje",
|
|
267
267
|
"messages.types.defaultWithObjects": "Mensaje con objeto",
|
|
268
268
|
"messages.types.system": "Notificación del sistema",
|
|
269
|
+
"messages.unknownDate": "Fecha desconocida",
|
|
269
270
|
"messages.visibility": "Visibilidad",
|
|
270
271
|
"messages.visibilityInternal": "Interna",
|
|
271
272
|
"messages.visibilityInternalHint": "Los mensajes internos se envían a los usuarios del sistema seleccionados.",
|
|
@@ -266,6 +266,7 @@
|
|
|
266
266
|
"messages.types.default": "메시지",
|
|
267
267
|
"messages.types.defaultWithObjects": "객체 메시지",
|
|
268
268
|
"messages.types.system": "시스템 알림",
|
|
269
|
+
"messages.unknownDate": "날짜를 알 수 없음",
|
|
269
270
|
"messages.visibility": "공개 범위",
|
|
270
271
|
"messages.visibilityInternal": "내부",
|
|
271
272
|
"messages.visibilityInternalHint": "내부 메시지는 선택한 시스템 사용자에게 전송됩니다.",
|
|
@@ -266,10 +266,11 @@
|
|
|
266
266
|
"messages.types.default": "Wiadomość",
|
|
267
267
|
"messages.types.defaultWithObjects": "Wiadomość z obiektem",
|
|
268
268
|
"messages.types.system": "Powiadomienie systemowe",
|
|
269
|
+
"messages.unknownDate": "Data nieznana",
|
|
269
270
|
"messages.visibility": "Widoczność",
|
|
270
271
|
"messages.visibilityInternal": "Wewnętrzna",
|
|
271
272
|
"messages.visibilityInternalHint": "Wiadomości wewnętrzne są wysyłane do wybranych użytkowników systemu.",
|
|
272
273
|
"messages.visibilityPublic": "Publiczna",
|
|
273
274
|
"messages.visibilityPublicHint": "Wiadomości publiczne są wysyłane wyłącznie na zewnętrzny e-mail.",
|
|
274
|
-
"notifications.categories.messages": "
|
|
275
|
+
"notifications.categories.messages": "Wiadomości"
|
|
275
276
|
}
|
|
@@ -133,6 +133,8 @@ export async function buildForwardThreadSlice(
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
const orderedThreadMessages = Array.from(messagesById.values()).sort((a, b) => {
|
|
136
|
+
if (a.sentAt && !b.sentAt) return -1
|
|
137
|
+
if (!a.sentAt && b.sentAt) return 1
|
|
136
138
|
const aTime = a.sentAt?.getTime() ?? 0
|
|
137
139
|
const bTime = b.sentAt?.getTime() ?? 0
|
|
138
140
|
if (aTime !== bTime) return aTime - bTime
|
|
@@ -230,7 +232,7 @@ export async function buildForwardPreviewFromThreadSlice(
|
|
|
230
232
|
return [
|
|
231
233
|
FORWARD_MARKER,
|
|
232
234
|
`From: ${formatUserLabel(sender, item.senderUserId)}`,
|
|
233
|
-
`Date: ${formatDateLabel(item.sentAt
|
|
235
|
+
`Date: ${formatDateLabel(item.sentAt)}`,
|
|
234
236
|
`Subject: ${normalizeSubject(item.subject)}`,
|
|
235
237
|
`To: ${toLabel}`,
|
|
236
238
|
'',
|