@open-mercato/core 0.10.1-develop.7771.1.615a6c9ba9 → 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.
Files changed (64) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/communication_channels/commands/ingest-inbound-message.js +6 -7
  3. package/dist/modules/communication_channels/commands/ingest-inbound-message.js.map +2 -2
  4. package/dist/modules/communication_channels/data/validators.js +2 -1
  5. package/dist/modules/communication_channels/data/validators.js.map +2 -2
  6. package/dist/modules/communication_channels/lib/email-mime.js +13 -3
  7. package/dist/modules/communication_channels/lib/email-mime.js.map +2 -2
  8. package/dist/modules/communication_channels/lib/thread-matcher.js.map +2 -2
  9. package/dist/modules/customers/api/activities/route.js +16 -6
  10. package/dist/modules/customers/api/activities/route.js.map +2 -2
  11. package/dist/modules/customers/lib/interactionOrderBy.js +12 -0
  12. package/dist/modules/customers/lib/interactionOrderBy.js.map +7 -0
  13. package/dist/modules/customers/lib/link-channel-message-handler.js +12 -11
  14. package/dist/modules/customers/lib/link-channel-message-handler.js.map +2 -2
  15. package/dist/modules/customers/lib/personEmailThreads.js +43 -12
  16. package/dist/modules/customers/lib/personEmailThreads.js.map +3 -3
  17. package/dist/modules/messages/api/route.js +2 -1
  18. package/dist/modules/messages/api/route.js.map +2 -2
  19. package/dist/modules/messages/commands/messages.js +2 -1
  20. package/dist/modules/messages/commands/messages.js.map +2 -2
  21. package/dist/modules/messages/components/MessagesInboxPageClient.js +1 -0
  22. package/dist/modules/messages/components/MessagesInboxPageClient.js.map +2 -2
  23. package/dist/modules/messages/components/defaults/DefaultMessageListItem.js +10 -8
  24. package/dist/modules/messages/components/defaults/DefaultMessageListItem.js.map +2 -2
  25. package/dist/modules/messages/components/message-detail/panels/MessageHeader.js +1 -1
  26. package/dist/modules/messages/components/message-detail/panels/MessageHeader.js.map +2 -2
  27. package/dist/modules/messages/components/message-detail/panels/MessageListComponent.js +3 -3
  28. package/dist/modules/messages/components/message-detail/panels/MessageListComponent.js.map +2 -2
  29. package/dist/modules/messages/components/message-detail/panels/thread-panel.js +1 -1
  30. package/dist/modules/messages/components/message-detail/panels/thread-panel.js.map +2 -2
  31. package/dist/modules/messages/components/message-detail/utils.js +3 -3
  32. package/dist/modules/messages/components/message-detail/utils.js.map +2 -2
  33. package/dist/modules/messages/data/validators.js +3 -2
  34. package/dist/modules/messages/data/validators.js.map +2 -2
  35. package/dist/modules/messages/frontend/messages/view/[token]/page.js +6 -5
  36. package/dist/modules/messages/frontend/messages/view/[token]/page.js.map +2 -2
  37. package/dist/modules/messages/lib/forwarding.js +3 -1
  38. package/dist/modules/messages/lib/forwarding.js.map +2 -2
  39. package/package.json +7 -7
  40. package/src/modules/communication_channels/commands/ingest-inbound-message.ts +5 -7
  41. package/src/modules/communication_channels/data/validators.ts +2 -1
  42. package/src/modules/communication_channels/lib/adapter.ts +4 -1
  43. package/src/modules/communication_channels/lib/email-mime.ts +24 -7
  44. package/src/modules/communication_channels/lib/thread-matcher.ts +2 -1
  45. package/src/modules/customers/api/activities/route.ts +17 -7
  46. package/src/modules/customers/lib/interactionOrderBy.ts +9 -0
  47. package/src/modules/customers/lib/link-channel-message-handler.ts +27 -38
  48. package/src/modules/customers/lib/personEmailThreads.ts +63 -21
  49. package/src/modules/messages/api/route.ts +3 -1
  50. package/src/modules/messages/commands/messages.ts +2 -1
  51. package/src/modules/messages/components/MessagesInboxPageClient.tsx +2 -0
  52. package/src/modules/messages/components/defaults/DefaultMessageListItem.tsx +10 -8
  53. package/src/modules/messages/components/message-detail/panels/MessageHeader.tsx +3 -1
  54. package/src/modules/messages/components/message-detail/panels/MessageListComponent.tsx +5 -3
  55. package/src/modules/messages/components/message-detail/panels/thread-panel.tsx +1 -1
  56. package/src/modules/messages/components/message-detail/utils.ts +3 -3
  57. package/src/modules/messages/data/validators.ts +4 -3
  58. package/src/modules/messages/frontend/messages/view/[token]/page.tsx +5 -4
  59. package/src/modules/messages/i18n/de.json +1 -0
  60. package/src/modules/messages/i18n/en.json +1 -0
  61. package/src/modules/messages/i18n/es.json +1 -0
  62. package/src/modules/messages/i18n/ko.json +1 -0
  63. package/src/modules/messages/i18n/pl.json +2 -1
  64. 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
- { orderBy: { occurredAt: 'desc', createdAt: 'desc' }, limit: maxThreads * 20 },
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 ?? interaction.createdAt,
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 sentAtRaw =
305
- (typeof message?.sentAt === 'string' || message?.sentAt instanceof Date
306
- ? message.sentAt
307
- : null) ??
308
- (link.createdAt instanceof Date || typeof link.createdAt === 'string' ? link.createdAt : null) ??
309
- linkIdByInteraction.get(linkId)?.occurredAt ??
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((a, b) => a.sentAt.localeCompare(b.sentAt))
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
- thread.messages = thread.messages.slice(thread.messages.length - maxMessagesPerThread)
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 last = thread.messages[thread.messages.length - 1]
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(last?.bodyText ?? null)
387
+ thread.preview = truncate(summaryMessage?.bodyText ?? null)
370
388
  thread.participants = Array.from(participantSet)
371
- thread.lastMessageAt = last?.sentAt ?? thread.lastMessageAt
372
- thread.lastDirection = last?.direction ?? thread.lastDirection
373
- thread.providerKey = last?.providerKey ?? 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((a, b) => b.lastMessageAt.localeCompare(a.lastMessageAt))
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', 'desc')
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 : input.sentAt ?? new Date(),
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 absoluteSentAt = formatDateTime(message.sentAt)
45
- const sentAtLabel = formatSentTime(message.sentAt)
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">{formatDateTime(props.detail.sentAt)}</p>}
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 = formatSentTime(message.sentAt)
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. Ignored for drafts.
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')}: {formatDateTime(data.sentAt)}
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": "Wiadomo\u015bci"
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 ?? item.createdAt)}`,
235
+ `Date: ${formatDateLabel(item.sentAt)}`,
234
236
  `Subject: ${normalizeSubject(item.subject)}`,
235
237
  `To: ${toLabel}`,
236
238
  '',