@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/messages/frontend/messages/view/%5Btoken%5D/page.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport type {\n MessageContentProps,\n MessageObjectAction,\n} from '@open-mercato/shared/modules/messages/types'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { MarkdownContent } from '@open-mercato/ui/backend/markdown/MarkdownContent'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport {\n getMessageUiComponentRegistry,\n} from '../../../../components/utils/typeUiRegistry'\nimport { getMessageObjectType } from '../../../../lib/message-objects-registry'\nimport { getMessageTypeOrDefault } from '../../../../lib/message-types-registry'\n\ntype TokenMessageObject = {\n id: string\n entityModule: string\n entityType: string\n entityId: string\n actionRequired: boolean\n actionType?: string | null\n actionLabel?: string | null\n snapshot?: Record<string, unknown> | null\n}\n\ntype MessageTokenDetailResponse = {\n id: string\n type: string\n subject: string\n body: string\n bodyFormat: 'text' | 'markdown'\n priority: 'low' | 'normal' | 'high' | 'urgent'\n senderUserId: string\n sentAt?: string | null\n actionData?: {\n actions: Array<{\n id: string\n label: string\n labelKey?: string\n variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost'\n icon?: string\n commandId?: string\n href?: string\n isTerminal?: boolean\n confirmRequired?: boolean\n confirmMessage?: string\n }>\n primaryActionId?: string\n expiresAt?: string\n } | null\n actionTaken?: string | null\n actionTakenAt?: string | null\n actionTakenByUserId?: string | null\n objects: TokenMessageObject[]\n requiresAuth: boolean\n recipientUserId: string\n}\n\ntype MessageTokenPreflightResponse = {\n requiresAuth: true\n}\n\ntype MessageTokenResponse = MessageTokenDetailResponse | MessageTokenPreflightResponse\n\nfunction isProtectedPreflight(data: MessageTokenResponse): data is MessageTokenPreflightResponse {\n return data.requiresAuth && !('id' in data)\n}\n\nfunction toErrorMessage(payload: unknown): string | null {\n if (!payload) return null\n if (typeof payload === 'string') return payload\n if (Array.isArray(payload)) {\n for (const entry of payload) {\n const nested = toErrorMessage(entry)\n if (nested) return nested\n }\n return null\n }\n if (typeof payload === 'object') {\n const record = payload as Record<string, unknown>\n return (\n toErrorMessage(record.error)\n ?? toErrorMessage(record.message)\n ?? toErrorMessage(record.detail)\n ?? toErrorMessage(record.details)\n ?? null\n )\n }\n return null\n}\n\nfunction formatDateTime(value: string | null | undefined): string {\n if (!value) return '\u2014'\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return '\u2014'\n return date.toLocaleString()\n}\n\nfunction toObjectActions(\n objectActions: Array<{\n id: string\n labelKey: string\n variant?: 'default' | 'secondary' | 'destructive' | 'outline'\n icon?: string\n commandId?: string\n href?: string\n isTerminal?: boolean\n confirmRequired?: boolean\n confirmMessage?: string\n }>,\n): MessageObjectAction[] {\n return objectActions.map((action) => ({\n id: action.id,\n labelKey: action.labelKey,\n variant: action.variant,\n icon: action.icon,\n commandId: action.commandId,\n href: action.href,\n isTerminal: action.isTerminal,\n confirmRequired: action.confirmRequired,\n confirmMessage: action.confirmMessage,\n }))\n}\n\nfunction mergeTokenActions(\n messageActions: MessageTokenDetailResponse['actionData'],\n defaultActions: Array<{\n id: string\n label: string\n labelKey?: string\n variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost'\n icon?: string\n commandId?: string\n href?: string\n isTerminal?: boolean\n confirmRequired?: boolean\n confirmMessage?: string\n }> | undefined,\n): MessageTokenDetailResponse['actionData'] {\n const deduped = new Map<string, NonNullable<MessageTokenDetailResponse['actionData']>['actions'][number]>()\n\n for (const action of messageActions?.actions ?? []) {\n const normalizedId = action.id.trim()\n if (!normalizedId) continue\n deduped.set(normalizedId, { ...action, id: normalizedId })\n }\n\n for (const action of defaultActions ?? []) {\n const normalizedId = action.id.trim()\n if (!normalizedId || deduped.has(normalizedId)) continue\n deduped.set(normalizedId, { ...action, id: normalizedId })\n }\n\n const actions = Array.from(deduped.values())\n if (actions.length === 0 && !messageActions?.expiresAt) return null\n\n return {\n actions,\n primaryActionId: messageActions?.primaryActionId,\n expiresAt: messageActions?.expiresAt,\n }\n}\n\nexport default function MessageTokenPage({ params }: { params: { token: string } }) {\n const t = useT()\n const messageUiRegistry = React.useMemo(() => getMessageUiComponentRegistry(), [])\n const token = params?.token\n\n const [loading, setLoading] = React.useState(true)\n const [data, setData] = React.useState<MessageTokenResponse | null>(null)\n const [errorMessage, setErrorMessage] = React.useState<string | null>(null)\n const [errorStatus, setErrorStatus] = React.useState<number | null>(null)\n\n React.useEffect(() => {\n let mounted = true\n\n async function run() {\n if (!token) return\n\n setLoading(true)\n setData(null)\n setErrorMessage(null)\n setErrorStatus(null)\n\n try {\n const call = await apiCall<MessageTokenResponse>(`/api/messages/token/${encodeURIComponent(token)}`)\n\n if (!mounted) return\n\n if (!call.ok || !call.result) {\n const status = call.status\n setErrorStatus(status)\n\n const fallback = status === 404\n ? t('messages.token.errors.notFound', 'This message link is invalid or has already been used.')\n : status === 409\n ? t('messages.token.errors.limitExceeded', 'This message link reached its usage limit.')\n : status === 410\n ? t('messages.token.errors.expired', 'This message link has expired.')\n : t('messages.token.errors.generic', 'Unable to load this message.')\n\n setErrorMessage(toErrorMessage(call.result) ?? fallback)\n return\n }\n\n setData(call.result)\n } catch (error) {\n if (!mounted) return\n setErrorMessage(\n error instanceof Error\n ? error.message\n : t('messages.token.errors.generic', 'Unable to load this message.'),\n )\n } finally {\n if (mounted) {\n setLoading(false)\n }\n }\n }\n\n void run()\n\n return () => {\n mounted = false\n }\n }, [t, token])\n\n if (loading) {\n return (\n <main className=\"mx-auto max-w-3xl p-6\">\n <p className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4 animate-spin\" />\n {t('messages.token.loading', 'Loading message...')}\n </p>\n </main>\n )\n }\n\n if (errorMessage || !data) {\n return (\n <main className=\"mx-auto max-w-3xl space-y-3 p-6\">\n <h1 className=\"text-2xl font-semibold\">{t('messages.token.pageTitle', 'Message')}</h1>\n <p className=\"text-sm text-destructive\">{errorMessage ?? t('messages.token.errors.generic', 'Unable to load this message.')}</p>\n {errorStatus ? <p className=\"text-xs text-muted-foreground\">HTTP {errorStatus}</p> : null}\n </main>\n )\n }\n\n if (isProtectedPreflight(data)) {\n return (\n <main className=\"mx-auto max-w-3xl space-y-4 p-6\">\n <h1 className=\"text-2xl font-semibold\">{t('messages.token.authRequired.title', 'Sign in required')}</h1>\n <p className=\"text-sm text-muted-foreground\">\n {t('messages.token.authRequired.description', 'This message includes protected objects. Sign in to continue.')}\n </p>\n <Button asChild>\n <Link href=\"/login\">{t('auth.signIn', 'Sign in')}</Link>\n </Button>\n </main>\n )\n }\n\n const messageType = getMessageTypeOrDefault(data.type)\n const contentComponentKey = messageType.ui?.contentComponent\n const actionsComponentKey = messageType.ui?.actionsComponent\n const ContentComponent = contentComponentKey\n ? messageUiRegistry.contentComponents[contentComponentKey] ?? null\n : null\n const ActionsComponent = actionsComponentKey\n ? messageUiRegistry.actionsComponents[actionsComponentKey] ?? null\n : null\n const resolvedActionData = mergeTokenActions(data.actionData ?? null, messageType.defaultActions)\n\n const contentProps: MessageContentProps = {\n message: {\n id: data.id,\n type: data.type,\n subject: data.subject,\n body: data.body,\n bodyFormat: data.bodyFormat,\n priority: data.priority,\n sentAt: data.sentAt ? new Date(data.sentAt) : null,\n senderUserId: data.senderUserId,\n actionData: resolvedActionData,\n actionTaken: data.actionTaken ?? null,\n actionTakenAt: data.actionTakenAt ? new Date(data.actionTakenAt) : null,\n },\n objects: data.objects.map((objectItem) => ({\n id: objectItem.id,\n entityModule: objectItem.entityModule,\n entityType: objectItem.entityType,\n entityId: objectItem.entityId,\n actionRequired: objectItem.actionRequired,\n snapshot: objectItem.snapshot ?? undefined,\n })),\n attachments: [],\n }\n\n const tokenActions = resolvedActionData?.actions ?? []\n\n return (\n <main className=\"mx-auto max-w-3xl space-y-6 p-6\">\n <header className=\"space-y-1\">\n <h1 className=\"text-2xl font-semibold\">{data.subject}</h1>\n <p className=\"text-sm text-muted-foreground\">\n {t('messages.token.sentAt', 'Sent')}: {formatDateTime(data.sentAt)}\n </p>\n </header>\n\n <section className=\"rounded-xl border bg-card p-4\">\n {ContentComponent ? (\n <ContentComponent {...contentProps} />\n ) : (\n <MarkdownContent\n body={data.body}\n format={data.bodyFormat}\n className=\"text-sm whitespace-pre-wrap [&>*]:mb-2 [&>*:last-child]:mb-0 [&_ul]:ml-4 [&_ul]:list-disc [&_ol]:ml-4 [&_ol]:list-decimal [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:text-xs\"\n />\n )}\n </section>\n\n {tokenActions.length > 0 ? (\n <section className=\"space-y-3 rounded-xl border bg-card p-4\">\n <h2 className=\"text-base font-semibold\">{t('messages.actions.title', 'Actions')}</h2>\n {ActionsComponent ? (\n <ActionsComponent\n message={{\n id: data.id,\n type: data.type,\n actionData: resolvedActionData,\n actionTaken: data.actionTaken ?? null,\n }}\n onExecuteAction={async (_actionId) => {\n return\n }}\n isExecuting={false}\n executingActionId={null}\n />\n ) : (\n <div className=\"flex flex-wrap gap-2\">\n {tokenActions.map((action) => (\n <Button\n key={action.id}\n type=\"button\"\n variant={action.variant ?? 'default'}\n disabled\n >\n {t(action.labelKey ?? action.label, action.label)}\n </Button>\n ))}\n </div>\n )}\n </section>\n ) : null}\n\n <section className=\"space-y-2 rounded-xl border bg-card p-4\">\n <h2 className=\"text-base font-semibold\">{t('messages.attachedObjects', 'Attached objects')}</h2>\n {data.objects.length === 0 ? (\n null\n ) : (\n <div className=\"space-y-2\">\n {data.objects.map((objectItem) => {\n const objectType = getMessageObjectType(objectItem.entityModule, objectItem.entityType)\n const objectActions = toObjectActions(objectType?.actions ?? [])\n const detailComponentKey = `${objectItem.entityModule}:${objectItem.entityType}`\n const DetailComponent = messageUiRegistry.objectDetailComponents[detailComponentKey] ?? null\n\n return DetailComponent ? (\n <DetailComponent\n key={objectItem.id}\n entityId={objectItem.entityId}\n entityModule={objectItem.entityModule}\n entityType={objectItem.entityType}\n snapshot={objectItem.snapshot ?? undefined}\n previewData={undefined}\n actionRequired={objectItem.actionRequired}\n actionType={objectItem.actionType ?? undefined}\n actionLabel={objectItem.actionLabel ?? undefined}\n actions={objectActions}\n actionTaken={data.actionTaken ?? null}\n actionTakenAt={data.actionTakenAt ? new Date(data.actionTakenAt) : null}\n actionTakenByUserId={data.actionTakenByUserId ?? null}\n icon={objectType?.icon}\n onAction={async () => {\n return\n }}\n />\n ) : null\n })}\n </div>\n )}\n </section>\n </main>\n )\n}\n"],
|
|
5
|
-
"mappings": ";AA2OQ,SACE,KADF;AAzOR,YAAY,WAAW;AACvB,OAAO,UAAU;AAKjB,SAAS,YAAY;AACrB,SAAS,uBAAuB;AAChC,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,eAAe;AACxB;AAAA,EACE;AAAA,OACK;AACP,SAAS,4BAA4B;AACrC,SAAS,+BAA+B;AAoDxC,SAAS,qBAAqB,MAAmE;AAC/F,SAAO,KAAK,gBAAgB,EAAE,QAAQ;AACxC;AAEA,SAAS,eAAe,SAAiC;AACvD,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,MAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,eAAW,SAAS,SAAS;AAC3B,YAAM,SAAS,eAAe,KAAK;AACnC,UAAI,OAAQ,QAAO;AAAA,IACrB;AACA,WAAO;AAAA,EACT;AACA,MAAI,OAAO,YAAY,UAAU;AAC/B,UAAM,SAAS;AACf,WACE,eAAe,OAAO,KAAK,KACxB,eAAe,OAAO,OAAO,KAC7B,eAAe,OAAO,MAAM,KAC5B,eAAe,OAAO,OAAO,KAC7B;AAAA,EAEP;AACA,SAAO;AACT;AAEA,SAAS,eAAe,
|
|
4
|
+
"sourcesContent": ["\"use client\"\n\nimport * as React from 'react'\nimport Link from 'next/link'\nimport type {\n MessageContentProps,\n MessageObjectAction,\n} from '@open-mercato/shared/modules/messages/types'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { MarkdownContent } from '@open-mercato/ui/backend/markdown/MarkdownContent'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport {\n getMessageUiComponentRegistry,\n} from '../../../../components/utils/typeUiRegistry'\nimport { getMessageObjectType } from '../../../../lib/message-objects-registry'\nimport { getMessageTypeOrDefault } from '../../../../lib/message-types-registry'\n\ntype TokenMessageObject = {\n id: string\n entityModule: string\n entityType: string\n entityId: string\n actionRequired: boolean\n actionType?: string | null\n actionLabel?: string | null\n snapshot?: Record<string, unknown> | null\n}\n\ntype MessageTokenDetailResponse = {\n id: string\n type: string\n subject: string\n body: string\n bodyFormat: 'text' | 'markdown'\n priority: 'low' | 'normal' | 'high' | 'urgent'\n senderUserId: string\n sentAt?: string | null\n actionData?: {\n actions: Array<{\n id: string\n label: string\n labelKey?: string\n variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost'\n icon?: string\n commandId?: string\n href?: string\n isTerminal?: boolean\n confirmRequired?: boolean\n confirmMessage?: string\n }>\n primaryActionId?: string\n expiresAt?: string\n } | null\n actionTaken?: string | null\n actionTakenAt?: string | null\n actionTakenByUserId?: string | null\n objects: TokenMessageObject[]\n requiresAuth: boolean\n recipientUserId: string\n}\n\ntype MessageTokenPreflightResponse = {\n requiresAuth: true\n}\n\ntype MessageTokenResponse = MessageTokenDetailResponse | MessageTokenPreflightResponse\n\nfunction isProtectedPreflight(data: MessageTokenResponse): data is MessageTokenPreflightResponse {\n return data.requiresAuth && !('id' in data)\n}\n\nfunction toErrorMessage(payload: unknown): string | null {\n if (!payload) return null\n if (typeof payload === 'string') return payload\n if (Array.isArray(payload)) {\n for (const entry of payload) {\n const nested = toErrorMessage(entry)\n if (nested) return nested\n }\n return null\n }\n if (typeof payload === 'object') {\n const record = payload as Record<string, unknown>\n return (\n toErrorMessage(record.error)\n ?? toErrorMessage(record.message)\n ?? toErrorMessage(record.detail)\n ?? toErrorMessage(record.details)\n ?? null\n )\n }\n return null\n}\n\nfunction formatDateTime(value: string | null | undefined, unknownDate: string): string {\n if (!value) return unknownDate\n const date = new Date(value)\n if (Number.isNaN(date.getTime())) return unknownDate\n return date.toLocaleString()\n}\n\nfunction toObjectActions(\n objectActions: Array<{\n id: string\n labelKey: string\n variant?: 'default' | 'secondary' | 'destructive' | 'outline'\n icon?: string\n commandId?: string\n href?: string\n isTerminal?: boolean\n confirmRequired?: boolean\n confirmMessage?: string\n }>,\n): MessageObjectAction[] {\n return objectActions.map((action) => ({\n id: action.id,\n labelKey: action.labelKey,\n variant: action.variant,\n icon: action.icon,\n commandId: action.commandId,\n href: action.href,\n isTerminal: action.isTerminal,\n confirmRequired: action.confirmRequired,\n confirmMessage: action.confirmMessage,\n }))\n}\n\nfunction mergeTokenActions(\n messageActions: MessageTokenDetailResponse['actionData'],\n defaultActions: Array<{\n id: string\n label: string\n labelKey?: string\n variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost'\n icon?: string\n commandId?: string\n href?: string\n isTerminal?: boolean\n confirmRequired?: boolean\n confirmMessage?: string\n }> | undefined,\n): MessageTokenDetailResponse['actionData'] {\n const deduped = new Map<string, NonNullable<MessageTokenDetailResponse['actionData']>['actions'][number]>()\n\n for (const action of messageActions?.actions ?? []) {\n const normalizedId = action.id.trim()\n if (!normalizedId) continue\n deduped.set(normalizedId, { ...action, id: normalizedId })\n }\n\n for (const action of defaultActions ?? []) {\n const normalizedId = action.id.trim()\n if (!normalizedId || deduped.has(normalizedId)) continue\n deduped.set(normalizedId, { ...action, id: normalizedId })\n }\n\n const actions = Array.from(deduped.values())\n if (actions.length === 0 && !messageActions?.expiresAt) return null\n\n return {\n actions,\n primaryActionId: messageActions?.primaryActionId,\n expiresAt: messageActions?.expiresAt,\n }\n}\n\nexport default function MessageTokenPage({ params }: { params: { token: string } }) {\n const t = useT()\n const messageUiRegistry = React.useMemo(() => getMessageUiComponentRegistry(), [])\n const token = params?.token\n\n const [loading, setLoading] = React.useState(true)\n const [data, setData] = React.useState<MessageTokenResponse | null>(null)\n const [errorMessage, setErrorMessage] = React.useState<string | null>(null)\n const [errorStatus, setErrorStatus] = React.useState<number | null>(null)\n\n React.useEffect(() => {\n let mounted = true\n\n async function run() {\n if (!token) return\n\n setLoading(true)\n setData(null)\n setErrorMessage(null)\n setErrorStatus(null)\n\n try {\n const call = await apiCall<MessageTokenResponse>(`/api/messages/token/${encodeURIComponent(token)}`)\n\n if (!mounted) return\n\n if (!call.ok || !call.result) {\n const status = call.status\n setErrorStatus(status)\n\n const fallback = status === 404\n ? t('messages.token.errors.notFound', 'This message link is invalid or has already been used.')\n : status === 409\n ? t('messages.token.errors.limitExceeded', 'This message link reached its usage limit.')\n : status === 410\n ? t('messages.token.errors.expired', 'This message link has expired.')\n : t('messages.token.errors.generic', 'Unable to load this message.')\n\n setErrorMessage(toErrorMessage(call.result) ?? fallback)\n return\n }\n\n setData(call.result)\n } catch (error) {\n if (!mounted) return\n setErrorMessage(\n error instanceof Error\n ? error.message\n : t('messages.token.errors.generic', 'Unable to load this message.'),\n )\n } finally {\n if (mounted) {\n setLoading(false)\n }\n }\n }\n\n void run()\n\n return () => {\n mounted = false\n }\n }, [t, token])\n\n if (loading) {\n return (\n <main className=\"mx-auto max-w-3xl p-6\">\n <p className=\"flex items-center gap-2 text-sm text-muted-foreground\">\n <Spinner className=\"h-4 w-4 animate-spin\" />\n {t('messages.token.loading', 'Loading message...')}\n </p>\n </main>\n )\n }\n\n if (errorMessage || !data) {\n return (\n <main className=\"mx-auto max-w-3xl space-y-3 p-6\">\n <h1 className=\"text-2xl font-semibold\">{t('messages.token.pageTitle', 'Message')}</h1>\n <p className=\"text-sm text-destructive\">{errorMessage ?? t('messages.token.errors.generic', 'Unable to load this message.')}</p>\n {errorStatus ? <p className=\"text-xs text-muted-foreground\">HTTP {errorStatus}</p> : null}\n </main>\n )\n }\n\n if (isProtectedPreflight(data)) {\n return (\n <main className=\"mx-auto max-w-3xl space-y-4 p-6\">\n <h1 className=\"text-2xl font-semibold\">{t('messages.token.authRequired.title', 'Sign in required')}</h1>\n <p className=\"text-sm text-muted-foreground\">\n {t('messages.token.authRequired.description', 'This message includes protected objects. Sign in to continue.')}\n </p>\n <Button asChild>\n <Link href=\"/login\">{t('auth.signIn', 'Sign in')}</Link>\n </Button>\n </main>\n )\n }\n\n const messageType = getMessageTypeOrDefault(data.type)\n const contentComponentKey = messageType.ui?.contentComponent\n const actionsComponentKey = messageType.ui?.actionsComponent\n const ContentComponent = contentComponentKey\n ? messageUiRegistry.contentComponents[contentComponentKey] ?? null\n : null\n const ActionsComponent = actionsComponentKey\n ? messageUiRegistry.actionsComponents[actionsComponentKey] ?? null\n : null\n const resolvedActionData = mergeTokenActions(data.actionData ?? null, messageType.defaultActions)\n\n const contentProps: MessageContentProps = {\n message: {\n id: data.id,\n type: data.type,\n subject: data.subject,\n body: data.body,\n bodyFormat: data.bodyFormat,\n priority: data.priority,\n sentAt: data.sentAt ? new Date(data.sentAt) : null,\n senderUserId: data.senderUserId,\n actionData: resolvedActionData,\n actionTaken: data.actionTaken ?? null,\n actionTakenAt: data.actionTakenAt ? new Date(data.actionTakenAt) : null,\n },\n objects: data.objects.map((objectItem) => ({\n id: objectItem.id,\n entityModule: objectItem.entityModule,\n entityType: objectItem.entityType,\n entityId: objectItem.entityId,\n actionRequired: objectItem.actionRequired,\n snapshot: objectItem.snapshot ?? undefined,\n })),\n attachments: [],\n }\n\n const tokenActions = resolvedActionData?.actions ?? []\n\n return (\n <main className=\"mx-auto max-w-3xl space-y-6 p-6\">\n <header className=\"space-y-1\">\n <h1 className=\"text-2xl font-semibold\">{data.subject}</h1>\n <p className=\"text-sm text-muted-foreground\">\n {t('messages.token.sentAt', 'Sent')}:{' '}\n {formatDateTime(data.sentAt, t('messages.unknownDate', 'Unknown date'))}\n </p>\n </header>\n\n <section className=\"rounded-xl border bg-card p-4\">\n {ContentComponent ? (\n <ContentComponent {...contentProps} />\n ) : (\n <MarkdownContent\n body={data.body}\n format={data.bodyFormat}\n className=\"text-sm whitespace-pre-wrap [&>*]:mb-2 [&>*:last-child]:mb-0 [&_ul]:ml-4 [&_ul]:list-disc [&_ol]:ml-4 [&_ol]:list-decimal [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:text-xs\"\n />\n )}\n </section>\n\n {tokenActions.length > 0 ? (\n <section className=\"space-y-3 rounded-xl border bg-card p-4\">\n <h2 className=\"text-base font-semibold\">{t('messages.actions.title', 'Actions')}</h2>\n {ActionsComponent ? (\n <ActionsComponent\n message={{\n id: data.id,\n type: data.type,\n actionData: resolvedActionData,\n actionTaken: data.actionTaken ?? null,\n }}\n onExecuteAction={async (_actionId) => {\n return\n }}\n isExecuting={false}\n executingActionId={null}\n />\n ) : (\n <div className=\"flex flex-wrap gap-2\">\n {tokenActions.map((action) => (\n <Button\n key={action.id}\n type=\"button\"\n variant={action.variant ?? 'default'}\n disabled\n >\n {t(action.labelKey ?? action.label, action.label)}\n </Button>\n ))}\n </div>\n )}\n </section>\n ) : null}\n\n <section className=\"space-y-2 rounded-xl border bg-card p-4\">\n <h2 className=\"text-base font-semibold\">{t('messages.attachedObjects', 'Attached objects')}</h2>\n {data.objects.length === 0 ? (\n null\n ) : (\n <div className=\"space-y-2\">\n {data.objects.map((objectItem) => {\n const objectType = getMessageObjectType(objectItem.entityModule, objectItem.entityType)\n const objectActions = toObjectActions(objectType?.actions ?? [])\n const detailComponentKey = `${objectItem.entityModule}:${objectItem.entityType}`\n const DetailComponent = messageUiRegistry.objectDetailComponents[detailComponentKey] ?? null\n\n return DetailComponent ? (\n <DetailComponent\n key={objectItem.id}\n entityId={objectItem.entityId}\n entityModule={objectItem.entityModule}\n entityType={objectItem.entityType}\n snapshot={objectItem.snapshot ?? undefined}\n previewData={undefined}\n actionRequired={objectItem.actionRequired}\n actionType={objectItem.actionType ?? undefined}\n actionLabel={objectItem.actionLabel ?? undefined}\n actions={objectActions}\n actionTaken={data.actionTaken ?? null}\n actionTakenAt={data.actionTakenAt ? new Date(data.actionTakenAt) : null}\n actionTakenByUserId={data.actionTakenByUserId ?? null}\n icon={objectType?.icon}\n onAction={async () => {\n return\n }}\n />\n ) : null\n })}\n </div>\n )}\n </section>\n </main>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -90,6 +90,8 @@ async function buildForwardThreadSlice(em, scope, selectedMessage, options = {})
|
|
|
90
90
|
parentMessageId = parentMessage.parentMessageId ?? null;
|
|
91
91
|
}
|
|
92
92
|
const orderedThreadMessages = Array.from(messagesById.values()).sort((a, b) => {
|
|
93
|
+
if (a.sentAt && !b.sentAt) return -1;
|
|
94
|
+
if (!a.sentAt && b.sentAt) return 1;
|
|
93
95
|
const aTime = a.sentAt?.getTime() ?? 0;
|
|
94
96
|
const bTime = b.sentAt?.getTime() ?? 0;
|
|
95
97
|
if (aTime !== bTime) return aTime - bTime;
|
|
@@ -159,7 +161,7 @@ async function buildForwardPreviewFromThreadSlice(em, scope, selectedMessage, th
|
|
|
159
161
|
return [
|
|
160
162
|
FORWARD_MARKER,
|
|
161
163
|
`From: ${formatUserLabel(sender, item.senderUserId)}`,
|
|
162
|
-
`Date: ${formatDateLabel(item.sentAt
|
|
164
|
+
`Date: ${formatDateLabel(item.sentAt)}`,
|
|
163
165
|
`Subject: ${normalizeSubject(item.subject)}`,
|
|
164
166
|
`To: ${toLabel}`,
|
|
165
167
|
"",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/messages/lib/forwarding.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { User } from '../../auth/data/entities'\nimport { Message, MessageRecipient } from '../data/entities'\n\nconst FORWARD_MARKER = '---------- Forwarded message ----------'\nconst MAX_FORWARD_BODY_LENGTH = 50_000\n\ntype MessageScope = {\n tenantId: string\n organizationId: string | null\n userId?: string | null\n}\n\n/**\n * `includePublicThreadMessages` widens the participant filter to every\n * explicitly public message in the slice. Set it only for a caller who reached\n * the thread through channel-thread access (#6355): on a channel conversation\n * the inbound messages have no platform participant, so the participant filter\n * alone would forward an empty body.\n */\ntype ForwardThreadSliceOptions = {\n includePublicThreadMessages?: boolean\n}\n\ntype ForwardMessageBlock = {\n id: string\n senderUserId: string\n subject: string\n body: string\n sentAt: Date | null | undefined\n createdAt: Date\n}\n\nfunction formatForwardSubject(subject: string): string {\n if (/^fwd:\\s*/i.test(subject)) return subject\n return `Fwd: ${subject}`\n}\n\nfunction formatDateLabel(date: Date | null | undefined): string {\n if (!date) return '-'\n return date.toISOString()\n}\n\nfunction normalizeSubject(subject: string | null | undefined): string {\n if (typeof subject !== 'string') return '(no subject)'\n const trimmed = subject.trim()\n return trimmed.length > 0 ? trimmed : '(no subject)'\n}\n\nfunction formatUserLabel(user: { name?: string | null; email?: string | null } | undefined, fallbackUserId: string): string {\n const name = user?.name?.trim()\n const email = user?.email?.trim()\n if (name && email) return `${name} <${email}>`\n if (name) return name\n if (email) return email\n return fallbackUserId\n}\n\nfunction ensureMaxForwardBodyLength(body: string): void {\n if (body.length > MAX_FORWARD_BODY_LENGTH) {\n throw new Error('Forward body exceeds maximum length')\n }\n}\n\nexport async function buildForwardThreadSlice(\n em: EntityManager,\n scope: MessageScope,\n selectedMessage: Message,\n options: ForwardThreadSliceOptions = {},\n): Promise<ForwardMessageBlock[]> {\n const decryptedThreadMessages = await findWithDecryption(\n em,\n Message,\n {\n threadId: selectedMessage.threadId ?? selectedMessage.id,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n isDraft: false,\n },\n {\n orderBy: {\n sentAt: 'ASC',\n createdAt: 'ASC',\n id: 'ASC',\n },\n },\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n\n const messagesById = new Map(decryptedThreadMessages.map((item) => [item.id, item]))\n messagesById.set(selectedMessage.id, selectedMessage)\n\n const threadRootId = selectedMessage.threadId ?? null\n if (threadRootId && !messagesById.has(threadRootId)) {\n const threadRoot = await findOneWithDecryption(\n em,\n Message,\n {\n id: threadRootId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n isDraft: false,\n },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n if (threadRoot) {\n messagesById.set(threadRoot.id, threadRoot)\n }\n }\n\n let parentMessageId = selectedMessage.parentMessageId ?? null\n while (parentMessageId) {\n const parentMessage = await findOneWithDecryption(\n em,\n Message,\n {\n id: parentMessageId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n isDraft: false,\n },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n if (!parentMessage) break\n messagesById.set(parentMessage.id, parentMessage)\n parentMessageId = parentMessage.parentMessageId ?? null\n }\n\n const orderedThreadMessages = Array.from(messagesById.values()).sort((a, b) => {\n const aTime = a.sentAt?.getTime() ?? 0\n const bTime = b.sentAt?.getTime() ?? 0\n if (aTime !== bTime) return aTime - bTime\n\n const aCreatedAt = a.createdAt.getTime()\n const bCreatedAt = b.createdAt.getTime()\n if (aCreatedAt !== bCreatedAt) return aCreatedAt - bCreatedAt\n\n return a.id.localeCompare(b.id)\n })\n\n const selectedIndex = orderedThreadMessages.findIndex((item) => item.id === selectedMessage.id)\n const threadSlice = selectedIndex >= 0 ? orderedThreadMessages.slice(0, selectedIndex + 1) : [selectedMessage]\n\n if (scope.userId) {\n const sliceIds = threadSlice.map((item) => item.id)\n const recipientRows = sliceIds.length > 0\n ? await em.find(MessageRecipient, {\n messageId: { $in: sliceIds },\n recipientUserId: scope.userId,\n deletedAt: null,\n })\n : []\n const recipientMessageIds = new Set(recipientRows.map((row) => row.messageId))\n const visibleSlice = threadSlice.filter(\n (item) => item.senderUserId === scope.userId\n || recipientMessageIds.has(item.id)\n || (options.includePublicThreadMessages === true && item.visibility === 'public'),\n )\n return visibleSlice.map((item) => ({\n id: item.id,\n senderUserId: item.senderUserId,\n subject: item.subject,\n body: item.body,\n sentAt: item.sentAt,\n createdAt: item.createdAt,\n }))\n }\n\n return threadSlice.map((item) => ({\n id: item.id,\n senderUserId: item.senderUserId,\n subject: item.subject,\n body: item.body,\n sentAt: item.sentAt,\n createdAt: item.createdAt,\n }))\n}\n\nexport async function buildForwardPreviewFromThreadSlice(\n em: EntityManager,\n scope: MessageScope,\n selectedMessage: Message,\n threadSlice: ForwardMessageBlock[],\n): Promise<{ subject: string; body: string }> {\n const allMessageIds = threadSlice.map((item) => item.id)\n const allSenderIds = Array.from(new Set(threadSlice.map((item) => item.senderUserId)))\n\n const recipients = allMessageIds.length > 0\n ? await em.find(MessageRecipient, {\n messageId: { $in: allMessageIds },\n deletedAt: null,\n })\n : []\n\n const allRecipientUserIds = Array.from(new Set(recipients.map((item) => item.recipientUserId)))\n const allUserIds = Array.from(new Set([...allSenderIds, ...allRecipientUserIds]))\n const users = allUserIds.length > 0\n ? await findWithDecryption(\n em,\n User,\n { id: { $in: allUserIds } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n : []\n\n const userById = new Map(users.map((user) => [user.id, user]))\n const recipientsByMessageId = new Map<string, MessageRecipient[]>()\n for (const recipient of recipients) {\n const currentRecipients = recipientsByMessageId.get(recipient.messageId) ?? []\n currentRecipients.push(recipient)\n recipientsByMessageId.set(recipient.messageId, currentRecipients)\n }\n\n const body = threadSlice.map((item) => {\n const sender = userById.get(item.senderUserId)\n const toRecipients = recipientsByMessageId.get(item.id) ?? []\n const toLabel = toRecipients.length > 0\n ? toRecipients\n .map((recipient) => formatUserLabel(userById.get(recipient.recipientUserId), recipient.recipientUserId))\n .join(', ')\n : '-'\n\n return [\n FORWARD_MARKER,\n `From: ${formatUserLabel(sender, item.senderUserId)}`,\n `Date: ${formatDateLabel(item.sentAt
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { User } from '../../auth/data/entities'\nimport { Message, MessageRecipient } from '../data/entities'\n\nconst FORWARD_MARKER = '---------- Forwarded message ----------'\nconst MAX_FORWARD_BODY_LENGTH = 50_000\n\ntype MessageScope = {\n tenantId: string\n organizationId: string | null\n userId?: string | null\n}\n\n/**\n * `includePublicThreadMessages` widens the participant filter to every\n * explicitly public message in the slice. Set it only for a caller who reached\n * the thread through channel-thread access (#6355): on a channel conversation\n * the inbound messages have no platform participant, so the participant filter\n * alone would forward an empty body.\n */\ntype ForwardThreadSliceOptions = {\n includePublicThreadMessages?: boolean\n}\n\ntype ForwardMessageBlock = {\n id: string\n senderUserId: string\n subject: string\n body: string\n sentAt: Date | null | undefined\n createdAt: Date\n}\n\nfunction formatForwardSubject(subject: string): string {\n if (/^fwd:\\s*/i.test(subject)) return subject\n return `Fwd: ${subject}`\n}\n\nfunction formatDateLabel(date: Date | null | undefined): string {\n if (!date) return '-'\n return date.toISOString()\n}\n\nfunction normalizeSubject(subject: string | null | undefined): string {\n if (typeof subject !== 'string') return '(no subject)'\n const trimmed = subject.trim()\n return trimmed.length > 0 ? trimmed : '(no subject)'\n}\n\nfunction formatUserLabel(user: { name?: string | null; email?: string | null } | undefined, fallbackUserId: string): string {\n const name = user?.name?.trim()\n const email = user?.email?.trim()\n if (name && email) return `${name} <${email}>`\n if (name) return name\n if (email) return email\n return fallbackUserId\n}\n\nfunction ensureMaxForwardBodyLength(body: string): void {\n if (body.length > MAX_FORWARD_BODY_LENGTH) {\n throw new Error('Forward body exceeds maximum length')\n }\n}\n\nexport async function buildForwardThreadSlice(\n em: EntityManager,\n scope: MessageScope,\n selectedMessage: Message,\n options: ForwardThreadSliceOptions = {},\n): Promise<ForwardMessageBlock[]> {\n const decryptedThreadMessages = await findWithDecryption(\n em,\n Message,\n {\n threadId: selectedMessage.threadId ?? selectedMessage.id,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n isDraft: false,\n },\n {\n orderBy: {\n sentAt: 'ASC',\n createdAt: 'ASC',\n id: 'ASC',\n },\n },\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n\n const messagesById = new Map(decryptedThreadMessages.map((item) => [item.id, item]))\n messagesById.set(selectedMessage.id, selectedMessage)\n\n const threadRootId = selectedMessage.threadId ?? null\n if (threadRootId && !messagesById.has(threadRootId)) {\n const threadRoot = await findOneWithDecryption(\n em,\n Message,\n {\n id: threadRootId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n isDraft: false,\n },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n if (threadRoot) {\n messagesById.set(threadRoot.id, threadRoot)\n }\n }\n\n let parentMessageId = selectedMessage.parentMessageId ?? null\n while (parentMessageId) {\n const parentMessage = await findOneWithDecryption(\n em,\n Message,\n {\n id: parentMessageId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n isDraft: false,\n },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n if (!parentMessage) break\n messagesById.set(parentMessage.id, parentMessage)\n parentMessageId = parentMessage.parentMessageId ?? null\n }\n\n const orderedThreadMessages = Array.from(messagesById.values()).sort((a, b) => {\n if (a.sentAt && !b.sentAt) return -1\n if (!a.sentAt && b.sentAt) return 1\n const aTime = a.sentAt?.getTime() ?? 0\n const bTime = b.sentAt?.getTime() ?? 0\n if (aTime !== bTime) return aTime - bTime\n\n const aCreatedAt = a.createdAt.getTime()\n const bCreatedAt = b.createdAt.getTime()\n if (aCreatedAt !== bCreatedAt) return aCreatedAt - bCreatedAt\n\n return a.id.localeCompare(b.id)\n })\n\n const selectedIndex = orderedThreadMessages.findIndex((item) => item.id === selectedMessage.id)\n const threadSlice = selectedIndex >= 0 ? orderedThreadMessages.slice(0, selectedIndex + 1) : [selectedMessage]\n\n if (scope.userId) {\n const sliceIds = threadSlice.map((item) => item.id)\n const recipientRows = sliceIds.length > 0\n ? await em.find(MessageRecipient, {\n messageId: { $in: sliceIds },\n recipientUserId: scope.userId,\n deletedAt: null,\n })\n : []\n const recipientMessageIds = new Set(recipientRows.map((row) => row.messageId))\n const visibleSlice = threadSlice.filter(\n (item) => item.senderUserId === scope.userId\n || recipientMessageIds.has(item.id)\n || (options.includePublicThreadMessages === true && item.visibility === 'public'),\n )\n return visibleSlice.map((item) => ({\n id: item.id,\n senderUserId: item.senderUserId,\n subject: item.subject,\n body: item.body,\n sentAt: item.sentAt,\n createdAt: item.createdAt,\n }))\n }\n\n return threadSlice.map((item) => ({\n id: item.id,\n senderUserId: item.senderUserId,\n subject: item.subject,\n body: item.body,\n sentAt: item.sentAt,\n createdAt: item.createdAt,\n }))\n}\n\nexport async function buildForwardPreviewFromThreadSlice(\n em: EntityManager,\n scope: MessageScope,\n selectedMessage: Message,\n threadSlice: ForwardMessageBlock[],\n): Promise<{ subject: string; body: string }> {\n const allMessageIds = threadSlice.map((item) => item.id)\n const allSenderIds = Array.from(new Set(threadSlice.map((item) => item.senderUserId)))\n\n const recipients = allMessageIds.length > 0\n ? await em.find(MessageRecipient, {\n messageId: { $in: allMessageIds },\n deletedAt: null,\n })\n : []\n\n const allRecipientUserIds = Array.from(new Set(recipients.map((item) => item.recipientUserId)))\n const allUserIds = Array.from(new Set([...allSenderIds, ...allRecipientUserIds]))\n const users = allUserIds.length > 0\n ? await findWithDecryption(\n em,\n User,\n { id: { $in: allUserIds } },\n undefined,\n { tenantId: scope.tenantId, organizationId: scope.organizationId },\n )\n : []\n\n const userById = new Map(users.map((user) => [user.id, user]))\n const recipientsByMessageId = new Map<string, MessageRecipient[]>()\n for (const recipient of recipients) {\n const currentRecipients = recipientsByMessageId.get(recipient.messageId) ?? []\n currentRecipients.push(recipient)\n recipientsByMessageId.set(recipient.messageId, currentRecipients)\n }\n\n const body = threadSlice.map((item) => {\n const sender = userById.get(item.senderUserId)\n const toRecipients = recipientsByMessageId.get(item.id) ?? []\n const toLabel = toRecipients.length > 0\n ? toRecipients\n .map((recipient) => formatUserLabel(userById.get(recipient.recipientUserId), recipient.recipientUserId))\n .join(', ')\n : '-'\n\n return [\n FORWARD_MARKER,\n `From: ${formatUserLabel(sender, item.senderUserId)}`,\n `Date: ${formatDateLabel(item.sentAt)}`,\n `Subject: ${normalizeSubject(item.subject)}`,\n `To: ${toLabel}`,\n '',\n item.body ?? '',\n ].join('\\n')\n }).join('\\n\\n')\n\n ensureMaxForwardBodyLength(body)\n\n return {\n subject: formatForwardSubject(selectedMessage.subject),\n body,\n }\n}\n\nexport async function buildForwardPreview(\n em: EntityManager,\n scope: MessageScope,\n selectedMessage: Message,\n options: ForwardThreadSliceOptions = {},\n): Promise<{ subject: string; body: string }> {\n const threadSlice = await buildForwardThreadSlice(em, scope, selectedMessage, options)\n return buildForwardPreviewFromThreadSlice(em, scope, selectedMessage, threadSlice)\n}\n\nexport function buildForwardBodyFromLegacyInput(\n generatedBody: string,\n additionalBody?: string,\n): string {\n const legacyPrefix = additionalBody?.trim()\n const finalBody = legacyPrefix\n ? `${legacyPrefix}\\n\\n${generatedBody}`\n : generatedBody\n ensureMaxForwardBodyLength(finalBody)\n return finalBody\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7772.1.9d3d6ce261",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -266,16 +266,16 @@
|
|
|
266
266
|
"zod": "^4.4.3"
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
271
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
269
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7772.1.9d3d6ce261",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7772.1.9d3d6ce261",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7772.1.9d3d6ce261",
|
|
272
272
|
"react": "^19.0.0",
|
|
273
273
|
"react-dom": "^19.0.0"
|
|
274
274
|
},
|
|
275
275
|
"devDependencies": {
|
|
276
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
278
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
276
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7772.1.9d3d6ce261",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7772.1.9d3d6ce261",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7772.1.9d3d6ce261",
|
|
279
279
|
"@testing-library/dom": "^10.4.1",
|
|
280
280
|
"@testing-library/jest-dom": "^7.0.1",
|
|
281
281
|
"@testing-library/react": "^16.3.3",
|
|
@@ -8,7 +8,6 @@ import { htmlToPlainText } from '@open-mercato/shared/lib/html/htmlToPlainText'
|
|
|
8
8
|
import { emitCommunicationChannelsEvent } from '../events'
|
|
9
9
|
import { resolveContact } from '../lib/contact-resolver'
|
|
10
10
|
import type { ChannelAdapterRegistry } from '../lib/registry'
|
|
11
|
-
import type { NormalizedInboundMessage } from '../lib/adapter'
|
|
12
11
|
import { matchThread, type ThreadMatch } from '../lib/thread-matcher'
|
|
13
12
|
import {
|
|
14
13
|
ChannelThreadMapping,
|
|
@@ -213,7 +212,7 @@ const ingestInboundMessageCommand: CommandHandler<IngestInboundMessageInput, Ing
|
|
|
213
212
|
subject: m.subject ?? null,
|
|
214
213
|
tenantId: input.scope.tenantId,
|
|
215
214
|
organizationId: input.scope.organizationId ?? null,
|
|
216
|
-
lastMessageAt: m.timestamp
|
|
215
|
+
lastMessageAt: m.timestamp,
|
|
217
216
|
assignedUserId: channel.userId ?? null,
|
|
218
217
|
})
|
|
219
218
|
em.persist(conversation)
|
|
@@ -281,7 +280,6 @@ const ingestInboundMessageCommand: CommandHandler<IngestInboundMessageInput, Ing
|
|
|
281
280
|
ccAddresses: extractStringArrayFromMeta(metaForMatcher, 'cc'),
|
|
282
281
|
bodyPlain: m.bodyFormat === 'html' ? null : m.body ?? null,
|
|
283
282
|
bodyHtml: m.bodyFormat === 'html' ? m.body ?? null : null,
|
|
284
|
-
receivedAt: m.timestamp ?? new Date(),
|
|
285
283
|
},
|
|
286
284
|
{ em },
|
|
287
285
|
)
|
|
@@ -422,8 +420,8 @@ const ingestInboundMessageCommand: CommandHandler<IngestInboundMessageInput, Ing
|
|
|
422
420
|
// mail, not when this worker ran. Otherwise a history import lands every
|
|
423
421
|
// message on the import minute and the inbox (sorted on `sent_at`) shows
|
|
424
422
|
// a 90-day mailbox as one block in import order. Adapters without a
|
|
425
|
-
// timestamp
|
|
426
|
-
sentAt: m.timestamp
|
|
423
|
+
// timestamp retain an explicitly unknown occurrence time.
|
|
424
|
+
sentAt: m.timestamp,
|
|
427
425
|
// Stable dedup key so a retried ingest (after a transient failure between
|
|
428
426
|
// compose and the ExternalMessage anchor insert) reuses the message
|
|
429
427
|
// composed by the first attempt instead of duplicating it. Mirrors the
|
|
@@ -490,7 +488,7 @@ const ingestInboundMessageCommand: CommandHandler<IngestInboundMessageInput, Ing
|
|
|
490
488
|
direction: 'inbound',
|
|
491
489
|
senderIdentifier: m.senderIdentifier,
|
|
492
490
|
senderDisplayName: m.senderDisplayName ?? null,
|
|
493
|
-
providerTimestamp: m.
|
|
491
|
+
providerTimestamp: m.providerTimestamp ?? null,
|
|
494
492
|
tenantId: input.scope.tenantId,
|
|
495
493
|
organizationId: input.scope.organizationId ?? null,
|
|
496
494
|
})
|
|
@@ -588,7 +586,7 @@ const ingestInboundMessageCommand: CommandHandler<IngestInboundMessageInput, Ing
|
|
|
588
586
|
// this worker ran. Carried here so no consumer has to read the
|
|
589
587
|
// ExternalMessage row across the module boundary. ISO string because a
|
|
590
588
|
// persistent event is serialized onto the queue; null when the adapter
|
|
591
|
-
// supplied no timestamp
|
|
589
|
+
// supplied no provider-native timestamp.
|
|
592
590
|
providerTimestamp:
|
|
593
591
|
externalMessage.providerTimestamp instanceof Date
|
|
594
592
|
? externalMessage.providerTimestamp.toISOString()
|
|
@@ -121,7 +121,8 @@ export const normalizedInboundMessageSchema = z.object({
|
|
|
121
121
|
body: z.string(),
|
|
122
122
|
bodyFormat: z.enum(['text', 'markdown', 'html']),
|
|
123
123
|
attachments: z.array(normalizedAttachmentSchema).optional(),
|
|
124
|
-
timestamp: z.date(),
|
|
124
|
+
timestamp: z.date().nullable(),
|
|
125
|
+
providerTimestamp: z.date().nullable().optional(),
|
|
125
126
|
replyToExternalId: z.string().optional(),
|
|
126
127
|
channelPayload: z.record(z.string(), z.unknown()),
|
|
127
128
|
channelContentType: z.string().min(1),
|
|
@@ -174,7 +174,10 @@ export interface NormalizedInboundMessage {
|
|
|
174
174
|
body: string
|
|
175
175
|
bodyFormat: 'text' | 'markdown' | 'html'
|
|
176
176
|
attachments?: NormalizedAttachment[]
|
|
177
|
-
|
|
177
|
+
/** Best meaningful occurrence time selected by the adapter; null when unknown. */
|
|
178
|
+
timestamp: Date | null
|
|
179
|
+
/** Provider-native transport or storage time when separately available. */
|
|
180
|
+
providerTimestamp?: Date | null
|
|
178
181
|
replyToExternalId?: string
|
|
179
182
|
channelPayload: Record<string, unknown>
|
|
180
183
|
channelContentType: string
|
|
@@ -426,13 +426,19 @@ export interface NormalizeMimeInboundOptions {
|
|
|
426
426
|
* Fallback timestamp when neither `receivedAt` nor a usable Date header is
|
|
427
427
|
* available.
|
|
428
428
|
*/
|
|
429
|
-
fallbackDate?: Date
|
|
429
|
+
fallbackDate?: Date | null
|
|
430
430
|
/** Provider-specific fields merged into `channelMetadata`. */
|
|
431
431
|
channelMetadata?: (parsed: ParsedMail) => Record<string, unknown>
|
|
432
432
|
/** Provider-specific fields merged into `channelPayload`. */
|
|
433
433
|
channelPayload?: (parsed: ParsedMail) => Record<string, unknown>
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
+
function validDate(value: Date | string | null | undefined): Date | null {
|
|
437
|
+
if (value == null) return null
|
|
438
|
+
const date = value instanceof Date ? new Date(value.getTime()) : new Date(value)
|
|
439
|
+
return Number.isNaN(date.getTime()) ? null : date
|
|
440
|
+
}
|
|
441
|
+
|
|
436
442
|
/**
|
|
437
443
|
* A MIME `Date` header this far ahead of the clock is treated as bogus. Clock
|
|
438
444
|
* skew between real mail servers is seconds to minutes; a day of tolerance
|
|
@@ -450,21 +456,30 @@ function isValidDate(value: unknown): value is Date {
|
|
|
450
456
|
*
|
|
451
457
|
* Order: the provider's receipt time; then the sender's MIME `Date` header,
|
|
452
458
|
* unless it is unparsable or more than a day in the future; then the caller's
|
|
453
|
-
* fallback;
|
|
459
|
+
* fallback; unknown remains null.
|
|
454
460
|
*/
|
|
455
|
-
|
|
461
|
+
function resolveKnownInboundTimestamp(input: {
|
|
456
462
|
receivedAt?: Date
|
|
457
463
|
mimeDate?: Date
|
|
458
|
-
fallbackDate?: Date
|
|
464
|
+
fallbackDate?: Date | null
|
|
459
465
|
now?: Date
|
|
460
|
-
}): Date {
|
|
466
|
+
}): Date | null {
|
|
461
467
|
if (isValidDate(input.receivedAt)) return input.receivedAt
|
|
462
468
|
const now = input.now ?? new Date()
|
|
463
469
|
if (isValidDate(input.mimeDate) && input.mimeDate.getTime() <= now.getTime() + MAX_MIME_DATE_FUTURE_SKEW_MS) {
|
|
464
470
|
return input.mimeDate
|
|
465
471
|
}
|
|
466
472
|
if (isValidDate(input.fallbackDate)) return input.fallbackDate
|
|
467
|
-
return
|
|
473
|
+
return null
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function resolveInboundTimestamp(input: {
|
|
477
|
+
receivedAt?: Date
|
|
478
|
+
mimeDate?: Date
|
|
479
|
+
fallbackDate?: Date | null
|
|
480
|
+
now?: Date
|
|
481
|
+
}): Date {
|
|
482
|
+
return resolveKnownInboundTimestamp(input) ?? input.now ?? new Date()
|
|
468
483
|
}
|
|
469
484
|
|
|
470
485
|
/**
|
|
@@ -475,6 +490,7 @@ export function resolveInboundTimestamp(input: {
|
|
|
475
490
|
*/
|
|
476
491
|
export function normalizeMimeInbound(options: NormalizeMimeInboundOptions): NormalizedInboundMessage {
|
|
477
492
|
const { parsed } = options
|
|
493
|
+
const providerTimestamp = validDate(options.receivedAt) ?? validDate(options.fallbackDate)
|
|
478
494
|
|
|
479
495
|
const messageId = stripBrackets(parsed.messageId) ?? options.fallbackMessageId
|
|
480
496
|
const inReplyTo = stripBrackets(parsed.inReplyTo)
|
|
@@ -519,11 +535,12 @@ export function normalizeMimeInbound(options: NormalizeMimeInboundOptions): Norm
|
|
|
519
535
|
body,
|
|
520
536
|
bodyFormat,
|
|
521
537
|
attachments,
|
|
522
|
-
timestamp:
|
|
538
|
+
timestamp: resolveKnownInboundTimestamp({
|
|
523
539
|
receivedAt: options.receivedAt,
|
|
524
540
|
mimeDate: parsed.date ? new Date(parsed.date) : undefined,
|
|
525
541
|
fallbackDate: options.fallbackDate,
|
|
526
542
|
}),
|
|
543
|
+
providerTimestamp,
|
|
527
544
|
replyToExternalId: inReplyTo ?? undefined,
|
|
528
545
|
channelPayload,
|
|
529
546
|
channelContentType: 'email/mime',
|
|
@@ -44,7 +44,8 @@ export type ThreadMatchInput = {
|
|
|
44
44
|
bodyPlain: string | null
|
|
45
45
|
bodyHtml: string | null
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
/** @deprecated Ignored by the matcher; retained for backward compatibility. */
|
|
48
|
+
receivedAt?: Date
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export type ThreadMatchStrategy =
|
|
@@ -30,6 +30,7 @@ import { resolveCustomersRequestContext } from '../../lib/interactionRequestCont
|
|
|
30
30
|
import { hydrateCanonicalInteractions } from '../../lib/interactionReadModel'
|
|
31
31
|
import { resolveCanonicalActivityTargetId } from '../../lib/legacyActivityBridge'
|
|
32
32
|
import { buildEmailVisibilityMikroFilter } from '../../lib/visibilityFilter'
|
|
33
|
+
import { buildInteractionOccurredAtOrderBy } from '../../lib/interactionOrderBy'
|
|
33
34
|
import { listGrantsForViewer, listSharedChannelIds } from '../../lib/conversationShares'
|
|
34
35
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
35
36
|
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
@@ -148,25 +149,25 @@ async function legacyAdaptersDisabledResponse(): Promise<Response> {
|
|
|
148
149
|
|
|
149
150
|
function buildLegacyOrderBy(sortField: string | undefined, sortDir: 'asc' | 'desc') {
|
|
150
151
|
if (sortField === 'createdAt') {
|
|
151
|
-
return { createdAt: sortDir }
|
|
152
|
+
return { createdAt: sortDir, id: sortDir }
|
|
152
153
|
}
|
|
153
|
-
return
|
|
154
|
+
return buildInteractionOccurredAtOrderBy(sortDir)
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
function buildCanonicalOrderBy(sortField: string | undefined, sortDir: 'asc' | 'desc') {
|
|
157
158
|
if (sortField === 'createdAt') {
|
|
158
|
-
return { createdAt: sortDir }
|
|
159
|
+
return { createdAt: sortDir, id: sortDir }
|
|
159
160
|
}
|
|
160
|
-
return
|
|
161
|
+
return buildInteractionOccurredAtOrderBy(sortDir)
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
function resolveActivitySortValue(item: ActivityItem, sortField: string | undefined): number {
|
|
164
|
+
function resolveActivitySortValue(item: ActivityItem, sortField: string | undefined): number | null {
|
|
164
165
|
const raw =
|
|
165
166
|
sortField === 'createdAt'
|
|
166
167
|
? item.createdAt
|
|
167
|
-
: item.occurredAt
|
|
168
|
+
: item.occurredAt
|
|
168
169
|
const timestamp = raw ? new Date(raw).getTime() : Number.NaN
|
|
169
|
-
return Number.isNaN(timestamp) ?
|
|
170
|
+
return Number.isNaN(timestamp) ? null : timestamp
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
function sortActivityItems(
|
|
@@ -178,10 +179,19 @@ function sortActivityItems(
|
|
|
178
179
|
const leftValue = resolveActivitySortValue(left, sortField)
|
|
179
180
|
const rightValue = resolveActivitySortValue(right, sortField)
|
|
180
181
|
if (leftValue === rightValue) {
|
|
182
|
+
const leftCreatedAt = resolveActivitySortValue(left, 'createdAt')
|
|
183
|
+
const rightCreatedAt = resolveActivitySortValue(right, 'createdAt')
|
|
184
|
+
if (leftCreatedAt !== rightCreatedAt) {
|
|
185
|
+
if (leftCreatedAt === null) return 1
|
|
186
|
+
if (rightCreatedAt === null) return -1
|
|
187
|
+
return sortDir === 'asc' ? leftCreatedAt - rightCreatedAt : rightCreatedAt - leftCreatedAt
|
|
188
|
+
}
|
|
181
189
|
return sortDir === 'asc'
|
|
182
190
|
? left.id.localeCompare(right.id)
|
|
183
191
|
: right.id.localeCompare(left.id)
|
|
184
192
|
}
|
|
193
|
+
if (leftValue === null) return 1
|
|
194
|
+
if (rightValue === null) return -1
|
|
185
195
|
return sortDir === 'asc' ? leftValue - rightValue : rightValue - leftValue
|
|
186
196
|
})
|
|
187
197
|
}
|
|
@@ -95,17 +95,6 @@ export default async function handler(
|
|
|
95
95
|
const metaJson = (link.channelMetadata ?? null) as Record<string, unknown> | null
|
|
96
96
|
const payloadJson = (link.channelPayload ?? null) as Record<string, unknown> | null
|
|
97
97
|
|
|
98
|
-
// ── (1b) Resolve when the email actually happened ─────────────────────
|
|
99
|
-
//
|
|
100
|
-
// `link.createdAt` is when the hub ingested the message, which for a history
|
|
101
|
-
// import is the import minute, not the day the mail arrived (#6095). The hub
|
|
102
|
-
// carries the provider's own timestamp on the event payload, so take it from
|
|
103
|
-
// there: reading the communication_channels ExternalMessage row here would
|
|
104
|
-
// cross the storage boundary (Cross-Module Coupling in AGENTS.md) and add a
|
|
105
|
-
// peer-table query per event. Resolved once and threaded through every branch
|
|
106
|
-
// below so the address-match and threading-inheritance paths agree.
|
|
107
|
-
const occurredAt = resolveOccurredAt(payload, link)
|
|
108
|
-
|
|
109
98
|
// ── (2) Resolve the channel to get its owner userId ───────────────────
|
|
110
99
|
//
|
|
111
100
|
// The channel.userId is needed for two purposes:
|
|
@@ -130,6 +119,8 @@ export default async function handler(
|
|
|
130
119
|
channelUserId = channel?.userId ?? null
|
|
131
120
|
}
|
|
132
121
|
|
|
122
|
+
const occurredAt = await resolveEmailOccurredAt(em, link, tenantId, organizationId, payload)
|
|
123
|
+
|
|
133
124
|
// ── (3) Collect recipient addresses ───────────────────────────────────
|
|
134
125
|
//
|
|
135
126
|
// The channel metadata shape differs by provider and direction:
|
|
@@ -281,7 +272,7 @@ async function handleThreadingInheritance(
|
|
|
281
272
|
channelId: string | null,
|
|
282
273
|
metaJson: Record<string, unknown> | null,
|
|
283
274
|
payloadJson: Record<string, unknown> | null,
|
|
284
|
-
occurredAt: Date,
|
|
275
|
+
occurredAt: Date | null,
|
|
285
276
|
): Promise<void> {
|
|
286
277
|
// ── Primary: inherit Person(s) from the hub's authoritative thread ──────
|
|
287
278
|
//
|
|
@@ -474,35 +465,33 @@ async function handleThreadingInheritance(
|
|
|
474
465
|
)
|
|
475
466
|
}
|
|
476
467
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* When the linked email happened, for `CustomerInteraction.occurredAt` (#6095).
|
|
481
|
-
*
|
|
482
|
-
* Preference order:
|
|
483
|
-
* 1. `payload.providerTimestamp` — the provider's own receive/send time, put
|
|
484
|
-
* on the hub event by ingest / delivery;
|
|
485
|
-
* 2. `MessageChannelLink.createdAt` — the ingest time, the only date this
|
|
486
|
-
* handler knew before #6095, and the compatibility path for events that
|
|
487
|
-
* were enqueued before the field existed;
|
|
488
|
-
* 3. now, for a link row with no usable `createdAt` (test stubs).
|
|
489
|
-
*/
|
|
490
|
-
function resolveOccurredAt(
|
|
491
|
-
payload: LinkChannelMessagePayload,
|
|
468
|
+
async function resolveEmailOccurredAt(
|
|
469
|
+
em: EntityManager,
|
|
492
470
|
link: Record<string, unknown>,
|
|
493
|
-
|
|
494
|
-
|
|
471
|
+
tenantId: string,
|
|
472
|
+
organizationId: string | null,
|
|
473
|
+
payload: LinkChannelMessagePayload,
|
|
474
|
+
): Promise<Date | null> {
|
|
475
|
+
const message = typeof link.messageId === 'string'
|
|
476
|
+
? await findOneWithDecryption(
|
|
477
|
+
em,
|
|
478
|
+
'Message' as never,
|
|
479
|
+
{ id: link.messageId, tenantId, organizationId, deletedAt: null } as never,
|
|
480
|
+
undefined,
|
|
481
|
+
{ tenantId, organizationId },
|
|
482
|
+
) as { sentAt?: Date | null } | null
|
|
483
|
+
: null
|
|
484
|
+
|
|
485
|
+
if (message) return message.sentAt ?? null
|
|
495
486
|
const carried = payload.providerTimestamp
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const parsed = new Date(carried)
|
|
501
|
-
if (!Number.isNaN(parsed.getTime())) return parsed
|
|
502
|
-
}
|
|
503
|
-
return ingestedAt
|
|
487
|
+
const providerTimestamp = carried instanceof Date
|
|
488
|
+
? carried
|
|
489
|
+
: typeof carried === 'string' && carried ? new Date(carried) : null
|
|
490
|
+
return providerTimestamp && !Number.isNaN(providerTimestamp.getTime()) ? providerTimestamp : null
|
|
504
491
|
}
|
|
505
492
|
|
|
493
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
494
|
+
|
|
506
495
|
interface InteractionData {
|
|
507
496
|
linkId: string
|
|
508
497
|
tenantId: string
|
|
@@ -511,7 +500,7 @@ interface InteractionData {
|
|
|
511
500
|
title: string | null
|
|
512
501
|
body: string | null
|
|
513
502
|
authorUserId: string | null
|
|
514
|
-
occurredAt: Date
|
|
503
|
+
occurredAt: Date | null
|
|
515
504
|
visibility: 'private' | 'shared'
|
|
516
505
|
channelProviderKey: string | null
|
|
517
506
|
/**
|