@open-mercato/core 0.10.1-develop.7771.1.615a6c9ba9 → 0.10.1-develop.7773.1.a41b0f6c03
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/backend/customers/companies-v2/[id]/page.js +1 -1
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +1 -1
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +1 -1
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js +3 -2
- package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/DealDetailTabs.js +3 -2
- package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.js +3 -2
- package/dist/modules/customers/components/detail/PersonDetailTabs.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/backend/customers/companies-v2/[id]/page.tsx +1 -1
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +1 -1
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +1 -1
- package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +5 -1
- package/src/modules/customers/components/detail/DealDetailTabs.tsx +6 -2
- package/src/modules/customers/components/detail/PersonDetailTabs.tsx +6 -2
- 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": "AACA,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,YAAY;AACrB,SAAS,SAAS,wBAAwB;AAE1C,MAAM,iBAAiB;AACvB,MAAM,0BAA0B;AA4BhC,SAAS,qBAAqB,SAAyB;AACrD,MAAI,YAAY,KAAK,OAAO,EAAG,QAAO;AACtC,SAAO,QAAQ,OAAO;AACxB;AAEA,SAAS,gBAAgB,MAAuC;AAC9D,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,KAAK,YAAY;AAC1B;AAEA,SAAS,iBAAiB,SAA4C;AACpE,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,QAAM,UAAU,QAAQ,KAAK;AAC7B,SAAO,QAAQ,SAAS,IAAI,UAAU;AACxC;AAEA,SAAS,gBAAgB,MAAmE,gBAAgC;AAC1H,QAAM,OAAO,MAAM,MAAM,KAAK;AAC9B,QAAM,QAAQ,MAAM,OAAO,KAAK;AAChC,MAAI,QAAQ,MAAO,QAAO,GAAG,IAAI,KAAK,KAAK;AAC3C,MAAI,KAAM,QAAO;AACjB,MAAI,MAAO,QAAO;AAClB,SAAO;AACT;AAEA,SAAS,2BAA2B,MAAoB;AACtD,MAAI,KAAK,SAAS,yBAAyB;AACzC,UAAM,IAAI,MAAM,qCAAqC;AAAA,EACvD;AACF;AAEA,eAAsB,wBACpB,IACA,OACA,iBACA,UAAqC,CAAC,GACN;AAChC,QAAM,0BAA0B,MAAM;AAAA,IACpC;AAAA,IACA;AAAA,IACA;AAAA,MACE,UAAU,gBAAgB,YAAY,gBAAgB;AAAA,MACtD,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,MACtB,WAAW;AAAA,MACX,SAAS;AAAA,IACX;AAAA,IACA;AAAA,MACE,SAAS;AAAA,QACP,QAAQ;AAAA,QACR,WAAW;AAAA,QACX,IAAI;AAAA,MACN;AAAA,IACF;AAAA,IACA,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAAA,EACnE;AAEA,QAAM,eAAe,IAAI,IAAI,wBAAwB,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;AACnF,eAAa,IAAI,gBAAgB,IAAI,eAAe;AAEpD,QAAM,eAAe,gBAAgB,YAAY;AACjD,MAAI,gBAAgB,CAAC,aAAa,IAAI,YAAY,GAAG;AACnD,UAAM,aAAa,MAAM;AAAA,MACvB;AAAA,MACA;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,WAAW;AAAA,QACX,SAAS;AAAA,MACX;AAAA,MACA;AAAA,MACA,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAAA,IACnE;AACA,QAAI,YAAY;AACd,mBAAa,IAAI,WAAW,IAAI,UAAU;AAAA,IAC5C;AAAA,EACF;AAEA,MAAI,kBAAkB,gBAAgB,mBAAmB;AACzD,SAAO,iBAAiB;AACtB,UAAM,gBAAgB,MAAM;AAAA,MAC1B;AAAA,MACA;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,WAAW;AAAA,QACX,SAAS;AAAA,MACX;AAAA,MACA;AAAA,MACA,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAAA,IACnE;AACA,QAAI,CAAC,cAAe;AACpB,iBAAa,IAAI,cAAc,IAAI,aAAa;AAChD,sBAAkB,cAAc,mBAAmB;AAAA,EACrD;AAEA,QAAM,wBAAwB,MAAM,KAAK,aAAa,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM;AAC7E,QAAI,EAAE,UAAU,CAAC,EAAE,OAAQ,QAAO;AAClC,QAAI,CAAC,EAAE,UAAU,EAAE,OAAQ,QAAO;AAClC,UAAM,QAAQ,EAAE,QAAQ,QAAQ,KAAK;AACrC,UAAM,QAAQ,EAAE,QAAQ,QAAQ,KAAK;AACrC,QAAI,UAAU,MAAO,QAAO,QAAQ;AAEpC,UAAM,aAAa,EAAE,UAAU,QAAQ;AACvC,UAAM,aAAa,EAAE,UAAU,QAAQ;AACvC,QAAI,eAAe,WAAY,QAAO,aAAa;AAEnD,WAAO,EAAE,GAAG,cAAc,EAAE,EAAE;AAAA,EAChC,CAAC;AAED,QAAM,gBAAgB,sBAAsB,UAAU,CAAC,SAAS,KAAK,OAAO,gBAAgB,EAAE;AAC9F,QAAM,cAAc,iBAAiB,IAAI,sBAAsB,MAAM,GAAG,gBAAgB,CAAC,IAAI,CAAC,eAAe;AAE7G,MAAI,MAAM,QAAQ;AAChB,UAAM,WAAW,YAAY,IAAI,CAAC,SAAS,KAAK,EAAE;AAClD,UAAM,gBAAgB,SAAS,SAAS,IACpC,MAAM,GAAG,KAAK,kBAAkB;AAAA,MAChC,WAAW,EAAE,KAAK,SAAS;AAAA,MAC3B,iBAAiB,MAAM;AAAA,MACvB,WAAW;AAAA,IACb,CAAC,IACC,CAAC;AACL,UAAM,sBAAsB,IAAI,IAAI,cAAc,IAAI,CAAC,QAAQ,IAAI,SAAS,CAAC;AAC7E,UAAM,eAAe,YAAY;AAAA,MAC/B,CAAC,SAAS,KAAK,iBAAiB,MAAM,UACjC,oBAAoB,IAAI,KAAK,EAAE,KAC9B,QAAQ,gCAAgC,QAAQ,KAAK,eAAe;AAAA,IAC5E;AACA,WAAO,aAAa,IAAI,CAAC,UAAU;AAAA,MACjC,IAAI,KAAK;AAAA,MACT,cAAc,KAAK;AAAA,MACnB,SAAS,KAAK;AAAA,MACd,MAAM,KAAK;AAAA,MACX,QAAQ,KAAK;AAAA,MACb,WAAW,KAAK;AAAA,IAClB,EAAE;AAAA,EACJ;AAEA,SAAO,YAAY,IAAI,CAAC,UAAU;AAAA,IAChC,IAAI,KAAK;AAAA,IACT,cAAc,KAAK;AAAA,IACnB,SAAS,KAAK;AAAA,IACd,MAAM,KAAK;AAAA,IACX,QAAQ,KAAK;AAAA,IACb,WAAW,KAAK;AAAA,EAClB,EAAE;AACJ;AAEA,eAAsB,mCACpB,IACA,OACA,iBACA,aAC4C;AAC5C,QAAM,gBAAgB,YAAY,IAAI,CAAC,SAAS,KAAK,EAAE;AACvD,QAAM,eAAe,MAAM,KAAK,IAAI,IAAI,YAAY,IAAI,CAAC,SAAS,KAAK,YAAY,CAAC,CAAC;AAErF,QAAM,aAAa,cAAc,SAAS,IACtC,MAAM,GAAG,KAAK,kBAAkB;AAAA,IAChC,WAAW,EAAE,KAAK,cAAc;AAAA,IAChC,WAAW;AAAA,EACb,CAAC,IACC,CAAC;AAEL,QAAM,sBAAsB,MAAM,KAAK,IAAI,IAAI,WAAW,IAAI,CAAC,SAAS,KAAK,eAAe,CAAC,CAAC;AAC9F,QAAM,aAAa,MAAM,KAAK,oBAAI,IAAI,CAAC,GAAG,cAAc,GAAG,mBAAmB,CAAC,CAAC;AAChF,QAAM,QAAQ,WAAW,SAAS,IAC9B,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,EAAE,IAAI,EAAE,KAAK,WAAW,EAAE;AAAA,IAC1B;AAAA,IACA,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAAA,EACnE,IACE,CAAC;AAEL,QAAM,WAAW,IAAI,IAAI,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;AAC7D,QAAM,wBAAwB,oBAAI,IAAgC;AAClE,aAAW,aAAa,YAAY;AAClC,UAAM,oBAAoB,sBAAsB,IAAI,UAAU,SAAS,KAAK,CAAC;AAC7E,sBAAkB,KAAK,SAAS;AAChC,0BAAsB,IAAI,UAAU,WAAW,iBAAiB;AAAA,EAClE;AAEA,QAAM,OAAO,YAAY,IAAI,CAAC,SAAS;AACrC,UAAM,SAAS,SAAS,IAAI,KAAK,YAAY;AAC7C,UAAM,eAAe,sBAAsB,IAAI,KAAK,EAAE,KAAK,CAAC;AAC5D,UAAM,UAAU,aAAa,SAAS,IAClC,aACC,IAAI,CAAC,cAAc,gBAAgB,SAAS,IAAI,UAAU,eAAe,GAAG,UAAU,eAAe,CAAC,EACtG,KAAK,IAAI,IACV;AAEJ,WAAO;AAAA,MACL;AAAA,MACA,SAAS,gBAAgB,QAAQ,KAAK,YAAY,CAAC;AAAA,MACnD,SAAS,gBAAgB,KAAK,MAAM,CAAC;AAAA,MACrC,YAAY,iBAAiB,KAAK,OAAO,CAAC;AAAA,MAC1C,OAAO,OAAO;AAAA,MACd;AAAA,MACA,KAAK,QAAQ;AAAA,IACf,EAAE,KAAK,IAAI;AAAA,EACb,CAAC,EAAE,KAAK,MAAM;AAEd,6BAA2B,IAAI;AAE/B,SAAO;AAAA,IACL,SAAS,qBAAqB,gBAAgB,OAAO;AAAA,IACrD;AAAA,EACF;AACF;AAEA,eAAsB,oBACpB,IACA,OACA,iBACA,UAAqC,CAAC,GACM;AAC5C,QAAM,cAAc,MAAM,wBAAwB,IAAI,OAAO,iBAAiB,OAAO;AACrF,SAAO,mCAAmC,IAAI,OAAO,iBAAiB,WAAW;AACnF;AAEO,SAAS,gCACd,eACA,gBACQ;AACR,QAAM,eAAe,gBAAgB,KAAK;AAC1C,QAAM,YAAY,eACd,GAAG,YAAY;AAAA;AAAA,EAAO,aAAa,KACnC;AACJ,6BAA2B,SAAS;AACpC,SAAO;AACT;",
|
|
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.7773.1.a41b0f6c03",
|
|
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.7773.1.a41b0f6c03",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7773.1.a41b0f6c03",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7773.1.a41b0f6c03",
|
|
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.7773.1.a41b0f6c03",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7773.1.a41b0f6c03",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7773.1.a41b0f6c03",
|
|
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
|
}
|
|
@@ -530,7 +530,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
|
|
|
530
530
|
activeTab={activeTab}
|
|
531
531
|
onTabChange={handleTabChange}
|
|
532
532
|
isLoadingInjectedTabs={injectedTabsLoading}
|
|
533
|
-
injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label }))}
|
|
533
|
+
injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label, priority: tab.priority }))}
|
|
534
534
|
peopleCount={data.counts?.people ?? 0}
|
|
535
535
|
dealsCount={dealCount}
|
|
536
536
|
activitiesCount={data.counts?.activities ?? 0}
|
|
@@ -667,7 +667,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
|
|
|
667
667
|
isLoadingInjectedTabs={injectedTabsLoading}
|
|
668
668
|
activeTab={activeTab}
|
|
669
669
|
onTabChange={handleTabChange}
|
|
670
|
-
injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label }))}
|
|
670
|
+
injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label, priority: tab.priority }))}
|
|
671
671
|
peopleCount={data.counts.people}
|
|
672
672
|
companiesCount={data.counts.companies}
|
|
673
673
|
>
|
|
@@ -568,7 +568,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
|
|
|
568
568
|
activeTab={activeTab}
|
|
569
569
|
onTabChange={handleTabChange}
|
|
570
570
|
isLoadingInjectedTabs={injectedTabsLoading}
|
|
571
|
-
injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label }))}
|
|
571
|
+
injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label, priority: tab.priority }))}
|
|
572
572
|
activitiesCount={interactionCount}
|
|
573
573
|
dealsCount={dealCount}
|
|
574
574
|
companiesCount={companyCount}
|
|
@@ -32,6 +32,7 @@ type TabDef = {
|
|
|
32
32
|
label: string
|
|
33
33
|
icon?: React.ReactNode
|
|
34
34
|
count?: React.ReactNode
|
|
35
|
+
priority?: number
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export const COMPANY_DETAIL_TABS_COMPONENT_ID = 'section:customers.companies.detailTabs'
|
|
@@ -135,8 +136,11 @@ function DefaultCompanyDetailTabs({
|
|
|
135
136
|
...injectedTabs.filter((tab, index) => !builtInTabs.some((native) => native.id === tab.id) && injectedTabs.findIndex((candidate) => candidate.id === tab.id) === index).map((tab) => ({
|
|
136
137
|
id: tab.id as CompanyTabId,
|
|
137
138
|
label: tab.label,
|
|
139
|
+
priority: tab.priority,
|
|
138
140
|
})),
|
|
139
|
-
]
|
|
141
|
+
]
|
|
142
|
+
.sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0))
|
|
143
|
+
.filter((tab) => !hidden.has(tab.id))
|
|
140
144
|
}, [builtInTabs, hiddenTabIds, injectedTabs])
|
|
141
145
|
|
|
142
146
|
React.useEffect(() => {
|
|
@@ -23,6 +23,7 @@ type TabDef = {
|
|
|
23
23
|
label: string
|
|
24
24
|
icon?: React.ReactNode
|
|
25
25
|
count?: React.ReactNode
|
|
26
|
+
priority?: number
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
export const DEAL_DETAIL_TABS_COMPONENT_ID = 'section:customers.deals.detailTabs'
|
|
@@ -30,7 +31,7 @@ export const DEAL_DETAIL_TABS_COMPONENT_ID = 'section:customers.deals.detailTabs
|
|
|
30
31
|
export type DealDetailTabsProps = {
|
|
31
32
|
activeTab: DealTabId
|
|
32
33
|
onTabChange: (tab: DealTabId) => void
|
|
33
|
-
injectedTabs?: Array<{ id: string; label: string }>
|
|
34
|
+
injectedTabs?: Array<{ id: string; label: string; priority?: number }>
|
|
34
35
|
isLoadingInjectedTabs?: boolean
|
|
35
36
|
hiddenTabIds?: string[]
|
|
36
37
|
peopleCount?: number
|
|
@@ -110,8 +111,11 @@ function DefaultDealDetailTabs({
|
|
|
110
111
|
...injectedTabs.filter((tab, index) => !builtInTabs.some((native) => native.id === tab.id) && injectedTabs.findIndex((candidate) => candidate.id === tab.id) === index).map((tab) => ({
|
|
111
112
|
id: tab.id as DealTabId,
|
|
112
113
|
label: tab.label,
|
|
114
|
+
priority: tab.priority,
|
|
113
115
|
})),
|
|
114
|
-
]
|
|
116
|
+
]
|
|
117
|
+
.sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0))
|
|
118
|
+
.filter((tab) => !hidden.has(tab.id))
|
|
115
119
|
}, [builtInTabs, hiddenTabIds, injectedTabs])
|
|
116
120
|
|
|
117
121
|
React.useEffect(() => {
|
|
@@ -37,6 +37,7 @@ type TabDef = {
|
|
|
37
37
|
label: string
|
|
38
38
|
icon?: React.ReactNode
|
|
39
39
|
count?: React.ReactNode
|
|
40
|
+
priority?: number
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export const PERSON_DETAIL_TABS_COMPONENT_ID = 'section:customers.people.detailTabs'
|
|
@@ -44,7 +45,7 @@ export const PERSON_DETAIL_TABS_COMPONENT_ID = 'section:customers.people.detailT
|
|
|
44
45
|
export type PersonDetailTabsProps = {
|
|
45
46
|
activeTab: PersonTabId
|
|
46
47
|
onTabChange: (tab: PersonTabId) => void
|
|
47
|
-
injectedTabs?: Array<{ id: string; label: string }>
|
|
48
|
+
injectedTabs?: Array<{ id: string; label: string; priority?: number }>
|
|
48
49
|
isLoadingInjectedTabs?: boolean
|
|
49
50
|
hiddenTabIds?: string[]
|
|
50
51
|
activitiesCount?: number
|
|
@@ -148,8 +149,11 @@ function DefaultPersonDetailTabs({
|
|
|
148
149
|
...injectedTabs.filter((tab, index) => !builtInTabs.some((native) => native.id === tab.id) && injectedTabs.findIndex((candidate) => candidate.id === tab.id) === index).map((tab) => ({
|
|
149
150
|
id: tab.id as PersonTabId,
|
|
150
151
|
label: tab.label,
|
|
152
|
+
priority: tab.priority,
|
|
151
153
|
})),
|
|
152
|
-
]
|
|
154
|
+
]
|
|
155
|
+
.sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0))
|
|
156
|
+
.filter((tab) => !hidden.has(tab.id))
|
|
153
157
|
}, [builtInTabs, hiddenTabIds, injectedTabs])
|
|
154
158
|
|
|
155
159
|
React.useEffect(() => {
|