@burdenoff/microfe-housingvista 2026.802.1 → 2026.830.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/housingvista/hooks/index.d.ts +2 -2
  2. package/dist/housingvista/hooks/index.d.ts.map +1 -1
  3. package/dist/housingvista/hooks/index.js.map +1 -1
  4. package/dist/housingvista/hooks/messages.d.ts +18 -11
  5. package/dist/housingvista/hooks/messages.d.ts.map +1 -1
  6. package/dist/housingvista/hooks/messages.js +33 -13
  7. package/dist/housingvista/hooks/messages.js.map +1 -1
  8. package/dist/housingvista/hooks/platform.d.ts +54 -2
  9. package/dist/housingvista/hooks/platform.d.ts.map +1 -1
  10. package/dist/housingvista/hooks/platform.js +63 -18
  11. package/dist/housingvista/hooks/platform.js.map +1 -1
  12. package/dist/housingvista/pages/HomePage.js +140 -142
  13. package/dist/housingvista/pages/HomePage.js.map +1 -1
  14. package/dist/housingvista/pages/chat/ChatPage.d.ts +4 -2
  15. package/dist/housingvista/pages/chat/ChatPage.d.ts.map +1 -1
  16. package/dist/housingvista/pages/chat/ChatPage.js +305 -151
  17. package/dist/housingvista/pages/chat/ChatPage.js.map +1 -1
  18. package/dist/housingvista/pages/properties/PropertiesPage.d.ts.map +1 -1
  19. package/dist/housingvista/pages/properties/PropertiesPage.js +427 -312
  20. package/dist/housingvista/pages/properties/PropertiesPage.js.map +1 -1
  21. package/dist/housingvista/pages/properties/PropertyDetailPage.d.ts.map +1 -1
  22. package/dist/housingvista/pages/properties/PropertyDetailPage.js +491 -374
  23. package/dist/housingvista/pages/properties/PropertyDetailPage.js.map +1 -1
  24. package/dist/housingvista/pages/transactions/TransactionsPage.d.ts.map +1 -1
  25. package/dist/housingvista/pages/transactions/TransactionsPage.js +153 -155
  26. package/dist/housingvista/pages/transactions/TransactionsPage.js.map +1 -1
  27. package/dist/shared/hooks/index.d.ts +2 -0
  28. package/dist/shared/hooks/index.d.ts.map +1 -0
  29. package/dist/shared/hooks/index.js +1 -0
  30. package/dist/shared/hooks/useTr.d.ts +17 -0
  31. package/dist/shared/hooks/useTr.d.ts.map +1 -0
  32. package/dist/shared/hooks/useTr.js +14 -0
  33. package/dist/shared/hooks/useTr.js.map +1 -0
  34. package/package.json +2 -2
@@ -15,6 +15,7 @@ var n = e`
15
15
  lastMessage {
16
16
  id
17
17
  authorId
18
+ type
18
19
  content
19
20
  createdAt
20
21
  }
@@ -22,15 +23,31 @@ var n = e`
22
23
  }
23
24
  }
24
25
  }
25
- `, r = (e) => e && typeof e == "object" && !Array.isArray(e) ? e : null, i = (e) => typeof e == "string" && e.length > 0 ? e : null, a = (e) => {
26
- let t = r(e?.metadata), n = r(t?.sourceContext), a = r(t?.channelLink), o = r(t?.jumpToContext), s = i(n?.product), c = i(a?.channelKey);
26
+ `, r = (e) => e && typeof e == "object" && !Array.isArray(e) ? e : null, i = (e) => typeof e == "string" && e.length > 0 ? e : null, a = new Set([
27
+ "FILE",
28
+ "IMAGE",
29
+ "VIDEO",
30
+ "AUDIO"
31
+ ]), o = (e) => {
32
+ let t = r(e?.metadata), n = r(t?.sourceContext), a = r(t?.channelLink), o = r(t?.jumpToContext), s = i(n?.product), c = i(a?.channelKey), l = s === "housingvista" || c === "housingvista-widget", u = i(o?.href) ?? i(n?.contextHref), d = i(n?.pagePath) ?? i(o?.pagePath), f = i(n?.threadKey) ?? i(a?.threadKey), p = i(n?.widgetKey) ?? i(a?.widgetKey), m = i(n?.entityType), h = i(n?.entityId);
27
33
  return {
28
- isHousingVista: s === "housingvista" || c === "housingvista-widget",
29
- contextHref: i(o?.href) ?? i(n?.contextHref),
30
- entityType: i(n?.entityType)
34
+ isHousingVista: l,
35
+ contextHref: u,
36
+ entityType: m,
37
+ entityId: h,
38
+ sourceContext: s && f && p && u && d ? {
39
+ product: s,
40
+ threadKey: f,
41
+ widgetKey: p,
42
+ contextHref: u,
43
+ pagePath: d,
44
+ contextTitle: i(n?.contextTitle) ?? void 0,
45
+ entityId: h ?? void 0,
46
+ entityType: m ?? void 0
47
+ } : null
31
48
  };
32
- }, o = (e = 50) => {
33
- let { data: r, loading: i, error: o, refetch: s } = t(n, {
49
+ }, s = (e = 50) => {
50
+ let { data: r, loading: i, error: s, refetch: c } = t(n, {
34
51
  variables: {
35
52
  first: e,
36
53
  sort: {
@@ -41,8 +58,8 @@ var n = e`
41
58
  fetchPolicy: "cache-and-network"
42
59
  });
43
60
  return {
44
- data: (r?.conversations.edges ?? []).map((e) => e.node).filter((e) => a(e.settings).isHousingVista).map((e) => {
45
- let t = a(e.settings);
61
+ data: (r?.conversations.edges ?? []).map((e) => e.node).filter((e) => o(e.settings).isHousingVista).map((e) => {
62
+ let t = o(e.settings);
46
63
  return {
47
64
  id: e.id,
48
65
  title: e.title ?? "Conversation",
@@ -51,16 +68,19 @@ var n = e`
51
68
  updatedAt: e.updatedAt,
52
69
  lastMessageText: e.lastMessage?.content?.text ?? "",
53
70
  lastMessageAt: e.lastMessage?.createdAt ?? null,
71
+ lastMessageIsFile: a.has(e.lastMessage?.type ?? ""),
54
72
  contextHref: t.contextHref,
55
- entityType: t.entityType
73
+ entityType: t.entityType,
74
+ entityId: t.entityId,
75
+ sourceContext: t.sourceContext
56
76
  };
57
77
  }),
58
78
  loading: i,
59
- error: o,
60
- refetch: s
79
+ error: s,
80
+ refetch: c
61
81
  };
62
82
  };
63
83
  //#endregion
64
- export { o as useHousingVistaConversations };
84
+ export { s as useHousingVistaConversations };
65
85
 
66
86
  //# sourceMappingURL=messages.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"messages.js","names":[],"sources":["../../../src/housingvista/hooks/messages.ts"],"sourcesContent":["/**\n * HousingVista inbox — the conversation list behind the `/housingvista/chat`\n * (Messages) surface. Reads the current user's workspace conversations from\n * `wspace-conversations-svc` and narrows them to the threads THIS product\n * created: `resolveContextConversation` stamps every HousingVista thread with a\n * `sourceContext.product = \"housingvista\"` (and `channelLink.channelKey =\n * \"housingvista-widget\"`) inside the conversation `settings`, so we filter on\n * that. Thread reads + sends reuse the shared platform hooks\n * (`useConversationMessages` / `useSendMessage`) — no stub, no mock data.\n * @module housingvista\n */\n\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\n\nconst GET_HV_CONVERSATIONS = gql`\n query HvConversations($first: Int, $sort: ConversationSort) {\n conversations(first: $first, sort: $sort) {\n edges {\n node {\n id\n title\n avatar\n unreadCount\n updatedAt\n settings\n lastMessage {\n id\n authorId\n content\n createdAt\n }\n }\n }\n }\n }\n`;\n\n/** A HousingVista thread as shown in the inbox list. */\nexport interface HvConversationSummary {\n id: string;\n title: string;\n avatar: string | null;\n unreadCount: number;\n updatedAt: string;\n lastMessageText: string;\n lastMessageAt: string | null;\n /** Deep link back to the property/service the thread is about, if any. */\n contextHref: string | null;\n /** e.g. `housingvista-property`, `housingvista-marketplace-service`. */\n entityType: string | null;\n}\n\ninterface MessageContentJson {\n text?: string;\n}\n\ninterface ConversationNode {\n id: string;\n title: string | null;\n avatar: string | null;\n unreadCount: number;\n updatedAt: string;\n settings: Record<string, unknown> | null;\n lastMessage: {\n id: string;\n authorId: string;\n content: MessageContentJson | null;\n createdAt: string;\n } | null;\n}\n\ninterface HvConversationsResult {\n conversations: {\n edges: { node: ConversationNode }[];\n };\n}\n\n/** Safely read a nested object off an opaque JSON value. */\nconst asRecord = (value: unknown): Record<string, unknown> | null =>\n value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : null;\n\nconst asString = (value: unknown): string | null => (typeof value === 'string' && value.length > 0 ? value : null);\n\n/**\n * A conversation belongs to HousingVista when its source context (or channel\n * link) was stamped by this product. Reads the shape written by\n * `buildContextConversationSettings` in wspace-conversations-svc.\n */\nconst readHousingVistaContext = (\n settings: Record<string, unknown> | null\n): { isHousingVista: boolean; contextHref: string | null; entityType: string | null } => {\n const metadata = asRecord(settings?.metadata);\n const sourceContext = asRecord(metadata?.sourceContext);\n const channelLink = asRecord(metadata?.channelLink);\n const jumpToContext = asRecord(metadata?.jumpToContext);\n\n const product = asString(sourceContext?.product);\n const channelKey = asString(channelLink?.channelKey);\n const isHousingVista = product === 'housingvista' || channelKey === 'housingvista-widget';\n\n return {\n isHousingVista,\n contextHref: asString(jumpToContext?.href) ?? asString(sourceContext?.contextHref),\n entityType: asString(sourceContext?.entityType),\n };\n};\n\n/**\n * The user's HousingVista conversations, most-recent first. Filtered client-side\n * to product-owned threads (see {@link readHousingVistaContext}); everything\n * else is honestly empty rather than fabricated.\n */\nexport const useHousingVistaConversations = (first = 50) => {\n const { data, loading, error, refetch } = useQuery<HvConversationsResult>(GET_HV_CONVERSATIONS, {\n variables: { first, sort: { field: 'LAST_MESSAGE_AT', direction: 'DESC' } },\n fetchPolicy: 'cache-and-network',\n });\n\n const conversations: HvConversationSummary[] = (data?.conversations.edges ?? [])\n .map((edge) => edge.node)\n .filter((node) => readHousingVistaContext(node.settings).isHousingVista)\n .map((node) => {\n const context = readHousingVistaContext(node.settings);\n return {\n id: node.id,\n title: node.title ?? 'Conversation',\n avatar: node.avatar,\n unreadCount: node.unreadCount,\n updatedAt: node.updatedAt,\n lastMessageText: node.lastMessage?.content?.text ?? '',\n lastMessageAt: node.lastMessage?.createdAt ?? null,\n contextHref: context.contextHref,\n entityType: context.entityType,\n };\n });\n\n return { data: conversations, loading, error, refetch };\n};\n"],"mappings":";;;AAeA,IAAM,IAAuB,CAAG;;;;;;;;;;;;;;;;;;;;;GAgE1B,KAAY,MAChB,KAAS,OAAO,KAAU,YAAY,CAAC,MAAM,QAAQ,EAAM,GAAI,IAAoC,MAE/F,KAAY,MAAmC,OAAO,KAAU,YAAY,EAAM,SAAS,IAAI,IAAQ,MAOvG,KACJ,MACuF;CACvF,IAAM,IAAW,EAAS,GAAU,SAAS,EACvC,IAAgB,EAAS,GAAU,cAAc,EACjD,IAAc,EAAS,GAAU,YAAY,EAC7C,IAAgB,EAAS,GAAU,cAAc,EAEjD,IAAU,EAAS,GAAe,QAAQ,EAC1C,IAAa,EAAS,GAAa,WAAW;AAGpD,QAAO;EACL,gBAHqB,MAAY,kBAAkB,MAAe;EAIlE,aAAa,EAAS,GAAe,KAAK,IAAI,EAAS,GAAe,YAAY;EAClF,YAAY,EAAS,GAAe,WAAW;EAChD;GAQU,KAAgC,IAAQ,OAAO;CAC1D,IAAM,EAAE,SAAM,YAAS,UAAO,eAAY,EAAgC,GAAsB;EAC9F,WAAW;GAAE;GAAO,MAAM;IAAE,OAAO;IAAmB,WAAW;IAAQ;GAAE;EAC3E,aAAa;EACd,CAAC;AAoBF,QAAO;EAAE,OAlBuC,GAAM,cAAc,SAAS,EAAE,EAC5E,KAAK,MAAS,EAAK,KAAK,CACxB,QAAQ,MAAS,EAAwB,EAAK,SAAS,CAAC,eAAe,CACvE,KAAK,MAAS;GACb,IAAM,IAAU,EAAwB,EAAK,SAAS;AACtD,UAAO;IACL,IAAI,EAAK;IACT,OAAO,EAAK,SAAS;IACrB,QAAQ,EAAK;IACb,aAAa,EAAK;IAClB,WAAW,EAAK;IAChB,iBAAiB,EAAK,aAAa,SAAS,QAAQ;IACpD,eAAe,EAAK,aAAa,aAAa;IAC9C,aAAa,EAAQ;IACrB,YAAY,EAAQ;IACrB;IACD;EAE0B;EAAS;EAAO;EAAS"}
1
+ {"version":3,"file":"messages.js","names":[],"sources":["../../../src/housingvista/hooks/messages.ts"],"sourcesContent":["/**\n * HousingVista inbox — the conversation list behind the `/housingvista/chat`\n * (Messages) surface. Reads the current user's workspace conversations from\n * `wspace-conversations-svc` and narrows them to the threads THIS product\n * created: `resolveContextConversation` stamps every HousingVista thread with a\n * `sourceContext.product = \"housingvista\"` (and `channelLink.channelKey =\n * \"housingvista-widget\"`) inside the conversation `settings`, so we filter on\n * that. Thread reads + sends reuse the shared platform hooks\n * (`useConversationMessages` / `useSendMessage`) — no stub, no mock data.\n * @module housingvista\n */\n\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport type { HvConversationSourceContext } from './platform';\n\nconst GET_HV_CONVERSATIONS = gql`\n query HvConversations($first: Int, $sort: ConversationSort) {\n conversations(first: $first, sort: $sort) {\n edges {\n node {\n id\n title\n avatar\n unreadCount\n updatedAt\n settings\n lastMessage {\n id\n authorId\n type\n content\n createdAt\n }\n }\n }\n }\n }\n`;\n\n/** A HousingVista thread as shown in the inbox list. */\nexport interface HvConversationSummary {\n id: string;\n title: string;\n avatar: string | null;\n unreadCount: number;\n updatedAt: string;\n lastMessageText: string;\n lastMessageAt: string | null;\n /**\n * The last message carries files rather than words (`Message.type` is one of\n * the file kinds and it has no text), so the list row can say so instead of\n * falling back to \"no messages yet\".\n */\n lastMessageIsFile: boolean;\n /** Deep link back to the property/service the thread is about, if any. */\n contextHref: string | null;\n /** e.g. `housingvista-property`, `housingvista-marketplace-service`. */\n entityType: string | null;\n /** Id of the property/service the thread is about, if the thread carries one. */\n entityId: string | null;\n /**\n * The full sourceContext this thread was created with, when every field\n * `sendContextMessage` requires is present. `null` for a thread whose\n * settings predate (or are missing) one of them — the caller must then stay\n * on the plain `sendMessage` path.\n */\n sourceContext: HvConversationSourceContext | null;\n}\n\ninterface MessageContentJson {\n text?: string;\n}\n\ninterface ConversationNode {\n id: string;\n title: string | null;\n avatar: string | null;\n unreadCount: number;\n updatedAt: string;\n settings: Record<string, unknown> | null;\n lastMessage: {\n id: string;\n authorId: string;\n type: string;\n content: MessageContentJson | null;\n createdAt: string;\n } | null;\n}\n\ninterface HvConversationsResult {\n conversations: {\n edges: { node: ConversationNode }[];\n };\n}\n\n/** Safely read a nested object off an opaque JSON value. */\nconst asRecord = (value: unknown): Record<string, unknown> | null =>\n value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : null;\n\nconst asString = (value: unknown): string | null => (typeof value === 'string' && value.length > 0 ? value : null);\n\n/** `MessageType` values that carry an attachment rather than prose. */\nconst FILE_MESSAGE_TYPES = new Set(['FILE', 'IMAGE', 'VIDEO', 'AUDIO']);\n\n/**\n * A conversation belongs to HousingVista when its source context (or channel\n * link) was stamped by this product. Reads the shape written by\n * `buildContextConversationSettings` in wspace-conversations-svc.\n */\nconst readHousingVistaContext = (\n settings: Record<string, unknown> | null\n): {\n isHousingVista: boolean;\n contextHref: string | null;\n entityType: string | null;\n entityId: string | null;\n sourceContext: HvConversationSourceContext | null;\n} => {\n const metadata = asRecord(settings?.metadata);\n const sourceContext = asRecord(metadata?.sourceContext);\n const channelLink = asRecord(metadata?.channelLink);\n const jumpToContext = asRecord(metadata?.jumpToContext);\n\n const product = asString(sourceContext?.product);\n const channelKey = asString(channelLink?.channelKey);\n const isHousingVista = product === 'housingvista' || channelKey === 'housingvista-widget';\n\n const contextHref = asString(jumpToContext?.href) ?? asString(sourceContext?.contextHref);\n const pagePath = asString(sourceContext?.pagePath) ?? asString(jumpToContext?.pagePath);\n const threadKey = asString(sourceContext?.threadKey) ?? asString(channelLink?.threadKey);\n const widgetKey = asString(sourceContext?.widgetKey) ?? asString(channelLink?.widgetKey);\n const entityType = asString(sourceContext?.entityType);\n const entityId = asString(sourceContext?.entityId);\n\n // `SendContextConversationMessageInput.sourceContext` marks product /\n // threadKey / widgetKey / contextHref / pagePath non-null — a thread missing\n // any of them cannot use that mutation, so surface null rather than a\n // half-built input the gateway would reject.\n const reusable: HvConversationSourceContext | null =\n product && threadKey && widgetKey && contextHref && pagePath\n ? {\n product,\n threadKey,\n widgetKey,\n contextHref,\n pagePath,\n contextTitle: asString(sourceContext?.contextTitle) ?? undefined,\n entityId: entityId ?? undefined,\n entityType: entityType ?? undefined,\n }\n : null;\n\n return { isHousingVista, contextHref, entityType, entityId, sourceContext: reusable };\n};\n\n/**\n * The user's HousingVista conversations, most-recent first. Filtered client-side\n * to product-owned threads (see {@link readHousingVistaContext}); everything\n * else is honestly empty rather than fabricated.\n */\nexport const useHousingVistaConversations = (first = 50) => {\n const { data, loading, error, refetch } = useQuery<HvConversationsResult>(GET_HV_CONVERSATIONS, {\n variables: { first, sort: { field: 'LAST_MESSAGE_AT', direction: 'DESC' } },\n fetchPolicy: 'cache-and-network',\n });\n\n const conversations: HvConversationSummary[] = (data?.conversations.edges ?? [])\n .map((edge) => edge.node)\n .filter((node) => readHousingVistaContext(node.settings).isHousingVista)\n .map((node) => {\n const context = readHousingVistaContext(node.settings);\n return {\n id: node.id,\n title: node.title ?? 'Conversation',\n avatar: node.avatar,\n unreadCount: node.unreadCount,\n updatedAt: node.updatedAt,\n lastMessageText: node.lastMessage?.content?.text ?? '',\n lastMessageAt: node.lastMessage?.createdAt ?? null,\n lastMessageIsFile: FILE_MESSAGE_TYPES.has(node.lastMessage?.type ?? ''),\n contextHref: context.contextHref,\n entityType: context.entityType,\n entityId: context.entityId,\n sourceContext: context.sourceContext,\n };\n });\n\n return { data: conversations, loading, error, refetch };\n};\n"],"mappings":";;;AAgBA,IAAM,IAAuB,CAAG;;;;;;;;;;;;;;;;;;;;;;GAiF1B,KAAY,MAChB,KAAS,OAAO,KAAU,YAAY,CAAC,MAAM,QAAQ,EAAM,GAAI,IAAoC,MAE/F,KAAY,MAAmC,OAAO,KAAU,YAAY,EAAM,SAAS,IAAI,IAAQ,MAGvG,IAAqB,IAAI,IAAI;CAAC;CAAQ;CAAS;CAAS;CAAQ,CAAC,EAOjE,KACJ,MAOG;CACH,IAAM,IAAW,EAAS,GAAU,SAAS,EACvC,IAAgB,EAAS,GAAU,cAAc,EACjD,IAAc,EAAS,GAAU,YAAY,EAC7C,IAAgB,EAAS,GAAU,cAAc,EAEjD,IAAU,EAAS,GAAe,QAAQ,EAC1C,IAAa,EAAS,GAAa,WAAW,EAC9C,IAAiB,MAAY,kBAAkB,MAAe,uBAE9D,IAAc,EAAS,GAAe,KAAK,IAAI,EAAS,GAAe,YAAY,EACnF,IAAW,EAAS,GAAe,SAAS,IAAI,EAAS,GAAe,SAAS,EACjF,IAAY,EAAS,GAAe,UAAU,IAAI,EAAS,GAAa,UAAU,EAClF,IAAY,EAAS,GAAe,UAAU,IAAI,EAAS,GAAa,UAAU,EAClF,IAAa,EAAS,GAAe,WAAW,EAChD,IAAW,EAAS,GAAe,SAAS;AAoBlD,QAAO;EAAE;EAAgB;EAAa;EAAY;EAAU,eAb1D,KAAW,KAAa,KAAa,KAAe,IAChD;GACE;GACA;GACA;GACA;GACA;GACA,cAAc,EAAS,GAAe,aAAa,IAAI,KAAA;GACvD,UAAU,KAAY,KAAA;GACtB,YAAY,KAAc,KAAA;GAC3B,GACD;EAE+E;GAQ1E,KAAgC,IAAQ,OAAO;CAC1D,IAAM,EAAE,SAAM,YAAS,UAAO,eAAY,EAAgC,GAAsB;EAC9F,WAAW;GAAE;GAAO,MAAM;IAAE,OAAO;IAAmB,WAAW;IAAQ;GAAE;EAC3E,aAAa;EACd,CAAC;AAuBF,QAAO;EAAE,OArBuC,GAAM,cAAc,SAAS,EAAE,EAC5E,KAAK,MAAS,EAAK,KAAK,CACxB,QAAQ,MAAS,EAAwB,EAAK,SAAS,CAAC,eAAe,CACvE,KAAK,MAAS;GACb,IAAM,IAAU,EAAwB,EAAK,SAAS;AACtD,UAAO;IACL,IAAI,EAAK;IACT,OAAO,EAAK,SAAS;IACrB,QAAQ,EAAK;IACb,aAAa,EAAK;IAClB,WAAW,EAAK;IAChB,iBAAiB,EAAK,aAAa,SAAS,QAAQ;IACpD,eAAe,EAAK,aAAa,aAAa;IAC9C,mBAAmB,EAAmB,IAAI,EAAK,aAAa,QAAQ,GAAG;IACvE,aAAa,EAAQ;IACrB,YAAY,EAAQ;IACpB,UAAU,EAAQ;IAClB,eAAe,EAAQ;IACxB;IACD;EAE0B;EAAS;EAAO;EAAS"}
@@ -6,8 +6,9 @@
6
6
  * cross-MFE import (forbidden):
7
7
  *
8
8
  * - `wspace-conversations-svc` — direct messaging (Contact / Message / Apply
9
- * CTAs) via `resolveContextConversation` + `sendMessage`, and per-listing
10
- * Q&A / reviews via `getEntityComments` + `createEntityComment`.
9
+ * CTAs) via `resolveContextConversation` + `sendMessage` (and
10
+ * `sendContextMessage` when a message carries file attachments), and
11
+ * per-listing Q&A / reviews via `getEntityComments` + `createEntityComment`.
11
12
  * - `wspace-files-svc` — real uploads via the presigned `initiateUpload` →
12
13
  * PUT → `completeUpload` flow, returning a durable File id + download URL.
13
14
  *
@@ -26,12 +27,41 @@ export interface HvContextTarget {
26
27
  /** Router path portion of `contextHref`. */
27
28
  pagePath: string;
28
29
  }
30
+ /** A file attached to a message (`Message.attachments` on the conversations subgraph). */
31
+ export interface HvMessageAttachment {
32
+ id: string;
33
+ fileId: string;
34
+ fileName: string;
35
+ fileSize: number;
36
+ mimeType: string;
37
+ /** Null when the attachment row carries no resolvable download metadata. */
38
+ downloadUrl: string | null;
39
+ }
29
40
  export interface HvConversationMessage {
30
41
  id: string;
31
42
  authorId: string;
43
+ /** Resolved display name of the sender (`Message.authorName`). */
44
+ authorName: string;
32
45
  /** Conversations stores content as JSON; the plain text is under `.text`. */
33
46
  text: string;
34
47
  createdAt: string;
48
+ attachments: HvMessageAttachment[];
49
+ }
50
+ /**
51
+ * The `sourceContext` a HousingVista thread was created with, read back off the
52
+ * conversation's persisted `settings.metadata`. Present only when every field
53
+ * `SendContextConversationMessageInput` requires survived; callers must treat a
54
+ * `null` as "this thread cannot take the context-message path".
55
+ */
56
+ export interface HvConversationSourceContext {
57
+ product: string;
58
+ threadKey: string;
59
+ widgetKey: string;
60
+ contextHref: string;
61
+ pagePath: string;
62
+ contextTitle?: string;
63
+ entityId?: string;
64
+ entityType?: string;
35
65
  }
36
66
  interface MessageContentJson {
37
67
  text?: string;
@@ -45,8 +75,10 @@ interface ConversationMessagesResult {
45
75
  node: {
46
76
  id: string;
47
77
  authorId: string;
78
+ authorName: string;
48
79
  content: MessageContentJson | null;
49
80
  createdAt: string;
81
+ attachments: HvMessageAttachment[];
50
82
  };
51
83
  }[];
52
84
  };
@@ -68,6 +100,26 @@ export declare const useSendMessage: () => {
68
100
  sendMessage: (conversationId: string, text: string) => Promise<void>;
69
101
  loading: boolean;
70
102
  };
103
+ /**
104
+ * Send a message WITH file attachments.
105
+ *
106
+ * `sendMessage`'s `attachmentIds` only stores bare file ids — the subgraph's
107
+ * `Attachment.fileName`/`downloadUrl` resolvers read their values out of the
108
+ * attachment row's metadata, so an id-only attachment comes back rendering its
109
+ * own uuid with no download link. `sendContextMessage` is the mutation that
110
+ * accepts full `ContextMessageAttachmentInput` details, so it is the only path
111
+ * that yields a readable attachment. It resolves the conversation from the
112
+ * thread's `sourceContext` (idempotent on `threadKey`), which is why the caller
113
+ * must hand back the SAME sourceContext the thread was created with.
114
+ */
115
+ export declare const useSendContextMessage: () => {
116
+ sendContextMessage: (args: {
117
+ sourceContext: HvConversationSourceContext;
118
+ text: string;
119
+ attachments: HvUploadedFile[];
120
+ }) => Promise<void>;
121
+ loading: boolean;
122
+ };
71
123
  export declare const useConversationMessages: (conversationId: string | undefined, first?: number) => {
72
124
  data: HvConversationMessage[];
73
125
  title: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../../src/housingvista/hooks/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAsDH,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AASD,UAAU,kBAAkB;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,UAAU,0BAA0B;IAClC,YAAY,EAAE;QACZ,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,QAAQ,EAAE;YACR,KAAK,EAAE;gBACL,IAAI,EAAE;oBAAE,EAAE,EAAE,MAAM,CAAC;oBAAC,QAAQ,EAAE,MAAM,CAAC;oBAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAC;oBAAC,SAAS,EAAE,MAAM,CAAA;iBAAE,CAAC;aAC/F,EAAE,CAAC;SACL,CAAC;KACH,GAAG,IAAI,CAAC;CACV;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB;+BAKhB,eAAe,KAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;;CA4B7E,CAAC;AAEF,eAAO,MAAM,cAAc;kCAGA,MAAM,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;;CAW9D,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,gBAAgB,MAAM,GAAG,SAAS,EAAE,cAAU;;;;;;;;;CAYrF,CAAC;AAkCF,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC1C;AAED,UAAU,oBAAoB;IAC5B,iBAAiB,EAAE;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE;YACL,EAAE,EAAE,MAAM,CAAC;YACX,QAAQ,EAAE,MAAM,CAAC;YACjB,UAAU,EAAE,MAAM,CAAC;YACnB,WAAW,EAAE,OAAO,CAAC;YACrB,SAAS,EAAE,MAAM,CAAC;YAClB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;YACzC,OAAO,EAAE;gBAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;aAAE,GAAG,IAAI,CAAC;SACzC,EAAE,CAAC;KACL,CAAC;CACH;AAED,eAAO,MAAM,iBAAiB,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM,GAAG,SAAS,EAAE,aAAS;;;;;;;;;CAa5F,CAAC;AAEF,eAAO,MAAM,sBAAsB;0BAGlB;QACX,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACpC,KAAG,OAAO,CAAC,IAAI,CAAC;;CAiBpB,CAAC;AAgCF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAkBD;;;;GAIG;AACH,eAAO,MAAM,aAAa;uBAKT,IAAI,KAAG,OAAO,CAAC,cAAc,CAAC;CA0C9C,CAAC"}
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../../src/housingvista/hooks/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AA2EH,MAAM,WAAW,eAAe;IAC9B,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,gFAAgF;IAChF,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,0FAA0F;AAC1F,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,mBAAmB,EAAE,CAAC;CACpC;AAED;;;;;GAKG;AACH,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AASD,UAAU,kBAAkB;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,UAAU,0BAA0B;IAClC,YAAY,EAAE;QACZ,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,QAAQ,EAAE;YACR,KAAK,EAAE;gBACL,IAAI,EAAE;oBACJ,EAAE,EAAE,MAAM,CAAC;oBACX,QAAQ,EAAE,MAAM,CAAC;oBACjB,UAAU,EAAE,MAAM,CAAC;oBACnB,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAC;oBACnC,SAAS,EAAE,MAAM,CAAC;oBAClB,WAAW,EAAE,mBAAmB,EAAE,CAAC;iBACpC,CAAC;aACH,EAAE,CAAC;SACL,CAAC;KACH,GAAG,IAAI,CAAC;CACV;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB;+BAKhB,eAAe,KAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;;CA4B7E,CAAC;AAEF,eAAO,MAAM,cAAc;kCAGA,MAAM,QAAQ,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC;;CAW9D,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB;+BAKjB;QACX,aAAa,EAAE,2BAA2B,CAAC;QAC3C,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,cAAc,EAAE,CAAC;KAC/B,KAAG,OAAO,CAAC,IAAI,CAAC;;CA0BpB,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,gBAAgB,MAAM,GAAG,SAAS,EAAE,cAAU;;;;;;;;;CAcrF,CAAC;AAkCF,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC1C;AAED,UAAU,oBAAoB;IAC5B,iBAAiB,EAAE;QACjB,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE;YACL,EAAE,EAAE,MAAM,CAAC;YACX,QAAQ,EAAE,MAAM,CAAC;YACjB,UAAU,EAAE,MAAM,CAAC;YACnB,WAAW,EAAE,OAAO,CAAC;YACrB,SAAS,EAAE,MAAM,CAAC;YAClB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;YACzC,OAAO,EAAE;gBAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;aAAE,GAAG,IAAI,CAAC;SACzC,EAAE,CAAC;KACL,CAAC;CACH;AAED,eAAO,MAAM,iBAAiB,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM,GAAG,SAAS,EAAE,aAAS;;;;;;;;;CAa5F,CAAC;AAEF,eAAO,MAAM,sBAAsB;0BAGlB;QACX,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACpC,KAAG,OAAO,CAAC,IAAI,CAAC;;CAiBpB,CAAC;AAgCF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CACd;AAkBD;;;;GAIG;AACH,eAAO,MAAM,aAAa;uBAKT,IAAI,KAAG,OAAO,CAAC,cAAc,CAAC;CA0C9C,CAAC"}
@@ -24,6 +24,16 @@ var a = n`
24
24
  }
25
25
  }
26
26
  `, s = n`
27
+ mutation HvSendContextMessage($input: SendContextConversationMessageInput!) {
28
+ sendContextMessage(input: $input) {
29
+ success
30
+ message {
31
+ id
32
+ createdAt
33
+ }
34
+ }
35
+ }
36
+ `, c = n`
27
37
  query HvConversationMessages($id: ID!, $first: Int) {
28
38
  conversation(id: $id) {
29
39
  id
@@ -33,14 +43,23 @@ var a = n`
33
43
  node {
34
44
  id
35
45
  authorId
46
+ authorName
36
47
  content
37
48
  createdAt
49
+ attachments {
50
+ id
51
+ fileId
52
+ fileName
53
+ fileSize
54
+ mimeType
55
+ downloadUrl
56
+ }
38
57
  }
39
58
  }
40
59
  }
41
60
  }
42
61
  }
43
- `, c = () => {
62
+ `, l = () => {
44
63
  let { workspaceId: n } = e(), [i, { loading: o }] = r(a);
45
64
  return {
46
65
  openConversation: t(async (e) => {
@@ -66,7 +85,7 @@ var a = n`
66
85
  }, [i, n]),
67
86
  loading: o
68
87
  };
69
- }, l = () => {
88
+ }, u = () => {
70
89
  let [e, { loading: n }] = r(o);
71
90
  return {
72
91
  sendMessage: t(async (t, n) => {
@@ -81,8 +100,32 @@ var a = n`
81
100
  }, [e]),
82
101
  loading: n
83
102
  };
84
- }, u = (e, t = 50) => {
85
- let { data: n, loading: r, error: a, refetch: o } = i(s, {
103
+ }, d = () => {
104
+ let { workspaceId: n } = e(), [i, { loading: a }] = r(s);
105
+ return {
106
+ sendContextMessage: t(async (e) => {
107
+ if (!(await i({
108
+ variables: { input: {
109
+ workspaceId: n,
110
+ sourceContext: e.sourceContext,
111
+ content: { text: e.text },
112
+ type: e.attachments.length > 0 ? "FILE" : "TEXT",
113
+ attachments: e.attachments.map((e) => ({
114
+ fileId: e.id,
115
+ fileName: e.name,
116
+ fileSize: e.size,
117
+ mimeType: e.mimeType,
118
+ downloadUrl: e.url || void 0
119
+ }))
120
+ } },
121
+ refetchQueries: ["HvConversationMessages"],
122
+ awaitRefetchQueries: !0
123
+ })).data?.sendContextMessage.success) throw Error("Message was not sent");
124
+ }, [i, n]),
125
+ loading: a
126
+ };
127
+ }, f = (e, t = 50) => {
128
+ let { data: n, loading: r, error: a, refetch: o } = i(c, {
86
129
  variables: {
87
130
  id: e ?? "",
88
131
  first: t
@@ -93,15 +136,17 @@ var a = n`
93
136
  data: (n?.conversation?.messages.edges ?? []).map((e) => ({
94
137
  id: e.node.id,
95
138
  authorId: e.node.authorId,
139
+ authorName: e.node.authorName,
96
140
  text: e.node.content?.text ?? "",
97
- createdAt: e.node.createdAt
141
+ createdAt: e.node.createdAt,
142
+ attachments: e.node.attachments ?? []
98
143
  })),
99
144
  title: n?.conversation?.title ?? null,
100
145
  loading: r,
101
146
  error: a,
102
147
  refetch: o
103
148
  };
104
- }, d = n`
149
+ }, p = n`
105
150
  query HvEntityComments($input: GetCommentsInput!) {
106
151
  getEntityComments(input: $input) {
107
152
  total
@@ -118,15 +163,15 @@ var a = n`
118
163
  }
119
164
  }
120
165
  }
121
- `, f = n`
166
+ `, m = n`
122
167
  mutation HvCreateEntityComment($input: CreateEntityCommentInput!) {
123
168
  createEntityComment(input: $input) {
124
169
  id
125
170
  createdAt
126
171
  }
127
172
  }
128
- `, p = (e, t, n = 50) => {
129
- let { data: r, loading: a, error: o, refetch: s } = i(d, {
173
+ `, h = (e, t, n = 50) => {
174
+ let { data: r, loading: a, error: o, refetch: s } = i(p, {
130
175
  variables: { input: {
131
176
  entityType: e,
132
177
  entityId: t ?? "",
@@ -149,8 +194,8 @@ var a = n`
149
194
  error: o,
150
195
  refetch: s
151
196
  };
152
- }, m = () => {
153
- let [e, { loading: n }] = r(f);
197
+ }, g = () => {
198
+ let [e, { loading: n }] = r(m);
154
199
  return {
155
200
  createComment: t(async (t) => {
156
201
  await e({
@@ -166,7 +211,7 @@ var a = n`
166
211
  }, [e]),
167
212
  loading: n
168
213
  };
169
- }, h = n`
214
+ }, _ = n`
170
215
  mutation HvInitiateUpload($input: InitiateUploadInput!) {
171
216
  initiateUpload(input: $input) {
172
217
  uploadUrl
@@ -175,7 +220,7 @@ var a = n`
175
220
  }
176
221
  }
177
222
  }
178
- `, g = n`
223
+ `, v = n`
179
224
  mutation HvCompleteUpload($input: CompleteUploadInput!) {
180
225
  completeUpload(input: $input) {
181
226
  fileId
@@ -188,13 +233,13 @@ var a = n`
188
233
  }
189
234
  }
190
235
  }
191
- `, _ = (e) => {
236
+ `, y = (e) => {
192
237
  let t = e.lastIndexOf(".");
193
238
  return t > 0 && t < e.length - 1 ? e.slice(t + 1).toLowerCase() : "bin";
194
- }, v = () => {
195
- let [e] = r(h), [n] = r(g);
239
+ }, b = () => {
240
+ let [e] = r(_), [n] = r(v);
196
241
  return { uploadFile: t(async (t) => {
197
- let r = _(t.name), i = t.type || "application/octet-stream", a = await e({ variables: { input: {
242
+ let r = y(t.name), i = t.type || "application/octet-stream", a = await e({ variables: { input: {
198
243
  fileName: t.name,
199
244
  extension: r,
200
245
  fileSize: t.size,
@@ -223,6 +268,6 @@ var a = n`
223
268
  }, [e, n]) };
224
269
  };
225
270
  //#endregion
226
- export { c as useContactConversation, u as useConversationMessages, m as useCreateEntityComment, p as useEntityComments, v as useFileUpload, l as useSendMessage };
271
+ export { l as useContactConversation, f as useConversationMessages, g as useCreateEntityComment, h as useEntityComments, b as useFileUpload, d as useSendContextMessage, u as useSendMessage };
227
272
 
228
273
  //# sourceMappingURL=platform.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"platform.js","names":[],"sources":["../../../src/housingvista/hooks/platform.ts"],"sourcesContent":["/**\n * Platform-service reuse hooks for HousingVista.\n *\n * These call *other* wspace subgraphs through the SAME Apollo client / wspace\n * gateway the MFE already uses — cross-SERVICE GraphQL (allowed), never a\n * cross-MFE import (forbidden):\n *\n * - `wspace-conversations-svc` — direct messaging (Contact / Message / Apply\n * CTAs) via `resolveContextConversation` + `sendMessage`, and per-listing\n * Q&A / reviews via `getEntityComments` + `createEntityComment`.\n * - `wspace-files-svc` — real uploads via the presigned `initiateUpload` →\n * PUT → `completeUpload` flow, returning a durable File id + download URL.\n *\n * No `any`, no mock data. `workspaceId` is read from the HousingVista context.\n * @module housingvista\n */\n\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { useCallback } from 'react';\nimport { useHousingVista } from '../context/HousingVistaContext';\n\n// ---------------------------------------------------------------------------\n// Conversations — direct messaging\n// ---------------------------------------------------------------------------\n\nconst RESOLVE_CONTEXT_CONVERSATION = gql`\n mutation HvResolveContextConversation($input: ResolveContextConversationInput!) {\n resolveContextConversation(input: $input) {\n created\n conversation {\n id\n title\n }\n }\n }\n`;\n\nconst SEND_MESSAGE = gql`\n mutation HvSendMessage($input: SendMessageInput!) {\n sendMessage(input: $input) {\n success\n message {\n id\n createdAt\n }\n }\n }\n`;\n\nconst GET_CONVERSATION_MESSAGES = gql`\n query HvConversationMessages($id: ID!, $first: Int) {\n conversation(id: $id) {\n id\n title\n messages(first: $first) {\n edges {\n node {\n id\n authorId\n content\n createdAt\n }\n }\n }\n }\n }\n`;\n\nexport interface HvContextTarget {\n /** Stable identity of the thing being discussed, e.g. a property id. */\n entityId: string;\n /** e.g. `housingvista-property`, `housingvista-marketplace-service`. */\n entityType: string;\n /** Human title used when a conversation is first created. */\n title: string;\n /** Deep link back into the MFE surface, e.g. `/housingvista/properties/:id`. */\n contextHref: string;\n /** Router path portion of `contextHref`. */\n pagePath: string;\n}\n\nexport interface HvConversationMessage {\n id: string;\n authorId: string;\n /** Conversations stores content as JSON; the plain text is under `.text`. */\n text: string;\n createdAt: string;\n}\n\ninterface ResolveContextConversationResult {\n resolveContextConversation: {\n created: boolean;\n conversation: { id: string; title: string | null };\n };\n}\n\ninterface MessageContentJson {\n text?: string;\n}\n\ninterface ConversationMessagesResult {\n conversation: {\n id: string;\n title: string | null;\n messages: {\n edges: {\n node: { id: string; authorId: string; content: MessageContentJson | null; createdAt: string };\n }[];\n };\n } | null;\n}\n\n/**\n * Resolve (get-or-create) the conversation attached to a HousingVista entity,\n * then return its id. Idempotent per (workspace, entityType, entityId) via the\n * derived `threadKey`, so repeated \"Contact\" clicks reuse one thread.\n */\nexport const useContactConversation = () => {\n const { workspaceId } = useHousingVista();\n const [resolve, { loading }] = useMutation<ResolveContextConversationResult>(RESOLVE_CONTEXT_CONVERSATION);\n\n const openConversation = useCallback(\n async (target: HvContextTarget): Promise<{ id: string; created: boolean }> => {\n const threadKey = `housingvista:${target.entityType}:${target.entityId}`;\n const res = await resolve({\n variables: {\n input: {\n workspaceId,\n title: target.title,\n sourceContext: {\n product: 'housingvista',\n entityId: target.entityId,\n entityType: target.entityType,\n threadKey,\n widgetKey: 'housingvista-contact',\n contextHref: target.contextHref,\n pagePath: target.pagePath,\n contextTitle: target.title,\n },\n },\n },\n });\n const conversation = res.data?.resolveContextConversation.conversation;\n if (!conversation) throw new Error('Could not open the conversation');\n return { id: conversation.id, created: res.data?.resolveContextConversation.created ?? false };\n },\n [resolve, workspaceId]\n );\n\n return { openConversation, loading };\n};\n\nexport const useSendMessage = () => {\n const [send, { loading }] = useMutation<{ sendMessage: { success: boolean } }>(SEND_MESSAGE);\n const sendMessage = useCallback(\n async (conversationId: string, text: string): Promise<void> => {\n const res = await send({\n variables: { input: { conversationId, content: { text } } },\n refetchQueries: ['HvConversationMessages'],\n awaitRefetchQueries: true,\n });\n if (!res.data?.sendMessage.success) throw new Error('Message was not sent');\n },\n [send]\n );\n return { sendMessage, loading };\n};\n\nexport const useConversationMessages = (conversationId: string | undefined, first = 50) => {\n const { data, loading, error, refetch } = useQuery<ConversationMessagesResult>(GET_CONVERSATION_MESSAGES, {\n variables: { id: conversationId ?? '', first },\n skip: !conversationId,\n });\n const messages: HvConversationMessage[] = (data?.conversation?.messages.edges ?? []).map((edge) => ({\n id: edge.node.id,\n authorId: edge.node.authorId,\n text: edge.node.content?.text ?? '',\n createdAt: edge.node.createdAt,\n }));\n return { data: messages, title: data?.conversation?.title ?? null, loading, error, refetch };\n};\n\n// ---------------------------------------------------------------------------\n// Entity comments — per-listing Q&A + reviews\n// ---------------------------------------------------------------------------\n\nconst GET_ENTITY_COMMENTS = gql`\n query HvEntityComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) {\n total\n items {\n id\n authorId\n authorType\n isAnonymous\n createdAt\n metadata\n content {\n text\n }\n }\n }\n }\n`;\n\nconst CREATE_ENTITY_COMMENT = gql`\n mutation HvCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) {\n id\n createdAt\n }\n }\n`;\n\nexport interface HvEntityComment {\n id: string;\n authorId: string;\n text: string;\n createdAt: string;\n /** Free-form; reviews carry `{ stars: number }`. */\n metadata: Record<string, unknown> | null;\n}\n\ninterface EntityCommentsResult {\n getEntityComments: {\n total: number;\n items: {\n id: string;\n authorId: string;\n authorType: string;\n isAnonymous: boolean;\n createdAt: string;\n metadata: Record<string, unknown> | null;\n content: { text: string | null } | null;\n }[];\n };\n}\n\nexport const useEntityComments = (entityType: string, entityId: string | undefined, take = 50) => {\n const { data, loading, error, refetch } = useQuery<EntityCommentsResult>(GET_ENTITY_COMMENTS, {\n variables: { input: { entityType, entityId: entityId ?? '', take, sortBy: 'createdAt', sortOrder: 'desc' } },\n skip: !entityId,\n });\n const comments: HvEntityComment[] = (data?.getEntityComments.items ?? []).map((item) => ({\n id: item.id,\n authorId: item.authorId,\n text: item.content?.text ?? '',\n createdAt: item.createdAt,\n metadata: item.metadata,\n }));\n return { data: comments, total: data?.getEntityComments.total ?? 0, loading, error, refetch };\n};\n\nexport const useCreateEntityComment = () => {\n const [mutate, { loading }] = useMutation<{ createEntityComment: { id: string } }>(CREATE_ENTITY_COMMENT);\n const createComment = useCallback(\n async (args: {\n entityType: string;\n entityId: string;\n text: string;\n metadata?: Record<string, unknown>;\n }): Promise<void> => {\n await mutate({\n variables: {\n input: {\n entityType: args.entityType,\n entityId: args.entityId,\n content: { text: args.text },\n metadata: args.metadata,\n },\n },\n refetchQueries: ['HvEntityComments'],\n awaitRefetchQueries: true,\n });\n },\n [mutate]\n );\n return { createComment, loading };\n};\n\n// ---------------------------------------------------------------------------\n// Files — presigned upload (initiateUpload → PUT → completeUpload)\n// ---------------------------------------------------------------------------\n\nconst INITIATE_UPLOAD = gql`\n mutation HvInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session {\n id\n }\n }\n }\n`;\n\nconst COMPLETE_UPLOAD = gql`\n mutation HvCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file {\n id\n name\n downloadUrl\n mimeType\n size\n }\n }\n }\n`;\n\nexport interface HvUploadedFile {\n id: string;\n name: string;\n url: string;\n mimeType: string;\n size: number;\n}\n\ninterface InitiateUploadResult {\n initiateUpload: { uploadUrl: string | null; session: { id: string } };\n}\n\ninterface CompleteUploadResult {\n completeUpload: {\n fileId: string;\n file: { id: string; name: string; downloadUrl: string | null; mimeType: string; size: number };\n };\n}\n\nconst splitExtension = (fileName: string): string => {\n const dot = fileName.lastIndexOf('.');\n return dot > 0 && dot < fileName.length - 1 ? fileName.slice(dot + 1).toLowerCase() : 'bin';\n};\n\n/**\n * Upload a real browser `File` through wspace-files-svc and return the durable\n * File id + download URL. Single-PUT presigned flow (fine for the image /\n * document sizes HousingVista deals with).\n */\nexport const useFileUpload = () => {\n const [initiate] = useMutation<InitiateUploadResult>(INITIATE_UPLOAD);\n const [complete] = useMutation<CompleteUploadResult>(COMPLETE_UPLOAD);\n\n const uploadFile = useCallback(\n async (file: File): Promise<HvUploadedFile> => {\n const extension = splitExtension(file.name);\n const mimeType = file.type || 'application/octet-stream';\n const initiated = await initiate({\n variables: {\n input: {\n fileName: file.name,\n extension,\n fileSize: file.size,\n mimeType,\n visibility: 'WORKSPACE',\n },\n },\n });\n const uploadUrl = initiated.data?.initiateUpload.uploadUrl;\n const sessionId = initiated.data?.initiateUpload.session.id;\n if (!uploadUrl || !sessionId) throw new Error('Upload could not be started');\n\n const putRes = await fetch(uploadUrl, {\n method: 'PUT',\n body: file,\n headers: { 'Content-Type': mimeType },\n });\n if (!putRes.ok) throw new Error(`Upload failed (${putRes.status})`);\n\n const completed = await complete({\n variables: { input: { sessionId, fileSize: file.size } },\n });\n const uploaded = completed.data?.completeUpload.file;\n if (!uploaded) throw new Error('Upload could not be finalized');\n return {\n id: uploaded.id,\n name: uploaded.name,\n url: uploaded.downloadUrl ?? '',\n mimeType: uploaded.mimeType,\n size: Number(uploaded.size),\n };\n },\n [initiate, complete]\n );\n\n return { uploadFile };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"platform.js","names":[],"sources":["../../../src/housingvista/hooks/platform.ts"],"sourcesContent":["/**\n * Platform-service reuse hooks for HousingVista.\n *\n * These call *other* wspace subgraphs through the SAME Apollo client / wspace\n * gateway the MFE already uses — cross-SERVICE GraphQL (allowed), never a\n * cross-MFE import (forbidden):\n *\n * - `wspace-conversations-svc` — direct messaging (Contact / Message / Apply\n * CTAs) via `resolveContextConversation` + `sendMessage` (and\n * `sendContextMessage` when a message carries file attachments), and\n * per-listing Q&A / reviews via `getEntityComments` + `createEntityComment`.\n * - `wspace-files-svc` — real uploads via the presigned `initiateUpload` →\n * PUT → `completeUpload` flow, returning a durable File id + download URL.\n *\n * No `any`, no mock data. `workspaceId` is read from the HousingVista context.\n * @module housingvista\n */\n\nimport { gql } from '@apollo/client';\nimport { useMutation, useQuery } from '@apollo/client/react';\nimport { useCallback } from 'react';\nimport { useHousingVista } from '../context/HousingVistaContext';\n\n// ---------------------------------------------------------------------------\n// Conversations — direct messaging\n// ---------------------------------------------------------------------------\n\nconst RESOLVE_CONTEXT_CONVERSATION = gql`\n mutation HvResolveContextConversation($input: ResolveContextConversationInput!) {\n resolveContextConversation(input: $input) {\n created\n conversation {\n id\n title\n }\n }\n }\n`;\n\nconst SEND_MESSAGE = gql`\n mutation HvSendMessage($input: SendMessageInput!) {\n sendMessage(input: $input) {\n success\n message {\n id\n createdAt\n }\n }\n }\n`;\n\nconst SEND_CONTEXT_MESSAGE = gql`\n mutation HvSendContextMessage($input: SendContextConversationMessageInput!) {\n sendContextMessage(input: $input) {\n success\n message {\n id\n createdAt\n }\n }\n }\n`;\n\nconst GET_CONVERSATION_MESSAGES = gql`\n query HvConversationMessages($id: ID!, $first: Int) {\n conversation(id: $id) {\n id\n title\n messages(first: $first) {\n edges {\n node {\n id\n authorId\n authorName\n content\n createdAt\n attachments {\n id\n fileId\n fileName\n fileSize\n mimeType\n downloadUrl\n }\n }\n }\n }\n }\n }\n`;\n\nexport interface HvContextTarget {\n /** Stable identity of the thing being discussed, e.g. a property id. */\n entityId: string;\n /** e.g. `housingvista-property`, `housingvista-marketplace-service`. */\n entityType: string;\n /** Human title used when a conversation is first created. */\n title: string;\n /** Deep link back into the MFE surface, e.g. `/housingvista/properties/:id`. */\n contextHref: string;\n /** Router path portion of `contextHref`. */\n pagePath: string;\n}\n\n/** A file attached to a message (`Message.attachments` on the conversations subgraph). */\nexport interface HvMessageAttachment {\n id: string;\n fileId: string;\n fileName: string;\n fileSize: number;\n mimeType: string;\n /** Null when the attachment row carries no resolvable download metadata. */\n downloadUrl: string | null;\n}\n\nexport interface HvConversationMessage {\n id: string;\n authorId: string;\n /** Resolved display name of the sender (`Message.authorName`). */\n authorName: string;\n /** Conversations stores content as JSON; the plain text is under `.text`. */\n text: string;\n createdAt: string;\n attachments: HvMessageAttachment[];\n}\n\n/**\n * The `sourceContext` a HousingVista thread was created with, read back off the\n * conversation's persisted `settings.metadata`. Present only when every field\n * `SendContextConversationMessageInput` requires survived; callers must treat a\n * `null` as \"this thread cannot take the context-message path\".\n */\nexport interface HvConversationSourceContext {\n product: string;\n threadKey: string;\n widgetKey: string;\n contextHref: string;\n pagePath: string;\n contextTitle?: string;\n entityId?: string;\n entityType?: string;\n}\n\ninterface ResolveContextConversationResult {\n resolveContextConversation: {\n created: boolean;\n conversation: { id: string; title: string | null };\n };\n}\n\ninterface MessageContentJson {\n text?: string;\n}\n\ninterface ConversationMessagesResult {\n conversation: {\n id: string;\n title: string | null;\n messages: {\n edges: {\n node: {\n id: string;\n authorId: string;\n authorName: string;\n content: MessageContentJson | null;\n createdAt: string;\n attachments: HvMessageAttachment[];\n };\n }[];\n };\n } | null;\n}\n\n/**\n * Resolve (get-or-create) the conversation attached to a HousingVista entity,\n * then return its id. Idempotent per (workspace, entityType, entityId) via the\n * derived `threadKey`, so repeated \"Contact\" clicks reuse one thread.\n */\nexport const useContactConversation = () => {\n const { workspaceId } = useHousingVista();\n const [resolve, { loading }] = useMutation<ResolveContextConversationResult>(RESOLVE_CONTEXT_CONVERSATION);\n\n const openConversation = useCallback(\n async (target: HvContextTarget): Promise<{ id: string; created: boolean }> => {\n const threadKey = `housingvista:${target.entityType}:${target.entityId}`;\n const res = await resolve({\n variables: {\n input: {\n workspaceId,\n title: target.title,\n sourceContext: {\n product: 'housingvista',\n entityId: target.entityId,\n entityType: target.entityType,\n threadKey,\n widgetKey: 'housingvista-contact',\n contextHref: target.contextHref,\n pagePath: target.pagePath,\n contextTitle: target.title,\n },\n },\n },\n });\n const conversation = res.data?.resolveContextConversation.conversation;\n if (!conversation) throw new Error('Could not open the conversation');\n return { id: conversation.id, created: res.data?.resolveContextConversation.created ?? false };\n },\n [resolve, workspaceId]\n );\n\n return { openConversation, loading };\n};\n\nexport const useSendMessage = () => {\n const [send, { loading }] = useMutation<{ sendMessage: { success: boolean } }>(SEND_MESSAGE);\n const sendMessage = useCallback(\n async (conversationId: string, text: string): Promise<void> => {\n const res = await send({\n variables: { input: { conversationId, content: { text } } },\n refetchQueries: ['HvConversationMessages'],\n awaitRefetchQueries: true,\n });\n if (!res.data?.sendMessage.success) throw new Error('Message was not sent');\n },\n [send]\n );\n return { sendMessage, loading };\n};\n\n/**\n * Send a message WITH file attachments.\n *\n * `sendMessage`'s `attachmentIds` only stores bare file ids — the subgraph's\n * `Attachment.fileName`/`downloadUrl` resolvers read their values out of the\n * attachment row's metadata, so an id-only attachment comes back rendering its\n * own uuid with no download link. `sendContextMessage` is the mutation that\n * accepts full `ContextMessageAttachmentInput` details, so it is the only path\n * that yields a readable attachment. It resolves the conversation from the\n * thread's `sourceContext` (idempotent on `threadKey`), which is why the caller\n * must hand back the SAME sourceContext the thread was created with.\n */\nexport const useSendContextMessage = () => {\n const { workspaceId } = useHousingVista();\n const [send, { loading }] = useMutation<{ sendContextMessage: { success: boolean } }>(SEND_CONTEXT_MESSAGE);\n\n const sendContextMessage = useCallback(\n async (args: {\n sourceContext: HvConversationSourceContext;\n text: string;\n attachments: HvUploadedFile[];\n }): Promise<void> => {\n const res = await send({\n variables: {\n input: {\n workspaceId,\n sourceContext: args.sourceContext,\n content: { text: args.text },\n type: args.attachments.length > 0 ? 'FILE' : 'TEXT',\n attachments: args.attachments.map((file) => ({\n fileId: file.id,\n fileName: file.name,\n fileSize: file.size,\n mimeType: file.mimeType,\n downloadUrl: file.url || undefined,\n })),\n },\n },\n refetchQueries: ['HvConversationMessages'],\n awaitRefetchQueries: true,\n });\n if (!res.data?.sendContextMessage.success) throw new Error('Message was not sent');\n },\n [send, workspaceId]\n );\n\n return { sendContextMessage, loading };\n};\n\nexport const useConversationMessages = (conversationId: string | undefined, first = 50) => {\n const { data, loading, error, refetch } = useQuery<ConversationMessagesResult>(GET_CONVERSATION_MESSAGES, {\n variables: { id: conversationId ?? '', first },\n skip: !conversationId,\n });\n const messages: HvConversationMessage[] = (data?.conversation?.messages.edges ?? []).map((edge) => ({\n id: edge.node.id,\n authorId: edge.node.authorId,\n authorName: edge.node.authorName,\n text: edge.node.content?.text ?? '',\n createdAt: edge.node.createdAt,\n attachments: edge.node.attachments ?? [],\n }));\n return { data: messages, title: data?.conversation?.title ?? null, loading, error, refetch };\n};\n\n// ---------------------------------------------------------------------------\n// Entity comments — per-listing Q&A + reviews\n// ---------------------------------------------------------------------------\n\nconst GET_ENTITY_COMMENTS = gql`\n query HvEntityComments($input: GetCommentsInput!) {\n getEntityComments(input: $input) {\n total\n items {\n id\n authorId\n authorType\n isAnonymous\n createdAt\n metadata\n content {\n text\n }\n }\n }\n }\n`;\n\nconst CREATE_ENTITY_COMMENT = gql`\n mutation HvCreateEntityComment($input: CreateEntityCommentInput!) {\n createEntityComment(input: $input) {\n id\n createdAt\n }\n }\n`;\n\nexport interface HvEntityComment {\n id: string;\n authorId: string;\n text: string;\n createdAt: string;\n /** Free-form; reviews carry `{ stars: number }`. */\n metadata: Record<string, unknown> | null;\n}\n\ninterface EntityCommentsResult {\n getEntityComments: {\n total: number;\n items: {\n id: string;\n authorId: string;\n authorType: string;\n isAnonymous: boolean;\n createdAt: string;\n metadata: Record<string, unknown> | null;\n content: { text: string | null } | null;\n }[];\n };\n}\n\nexport const useEntityComments = (entityType: string, entityId: string | undefined, take = 50) => {\n const { data, loading, error, refetch } = useQuery<EntityCommentsResult>(GET_ENTITY_COMMENTS, {\n variables: { input: { entityType, entityId: entityId ?? '', take, sortBy: 'createdAt', sortOrder: 'desc' } },\n skip: !entityId,\n });\n const comments: HvEntityComment[] = (data?.getEntityComments.items ?? []).map((item) => ({\n id: item.id,\n authorId: item.authorId,\n text: item.content?.text ?? '',\n createdAt: item.createdAt,\n metadata: item.metadata,\n }));\n return { data: comments, total: data?.getEntityComments.total ?? 0, loading, error, refetch };\n};\n\nexport const useCreateEntityComment = () => {\n const [mutate, { loading }] = useMutation<{ createEntityComment: { id: string } }>(CREATE_ENTITY_COMMENT);\n const createComment = useCallback(\n async (args: {\n entityType: string;\n entityId: string;\n text: string;\n metadata?: Record<string, unknown>;\n }): Promise<void> => {\n await mutate({\n variables: {\n input: {\n entityType: args.entityType,\n entityId: args.entityId,\n content: { text: args.text },\n metadata: args.metadata,\n },\n },\n refetchQueries: ['HvEntityComments'],\n awaitRefetchQueries: true,\n });\n },\n [mutate]\n );\n return { createComment, loading };\n};\n\n// ---------------------------------------------------------------------------\n// Files — presigned upload (initiateUpload → PUT → completeUpload)\n// ---------------------------------------------------------------------------\n\nconst INITIATE_UPLOAD = gql`\n mutation HvInitiateUpload($input: InitiateUploadInput!) {\n initiateUpload(input: $input) {\n uploadUrl\n session {\n id\n }\n }\n }\n`;\n\nconst COMPLETE_UPLOAD = gql`\n mutation HvCompleteUpload($input: CompleteUploadInput!) {\n completeUpload(input: $input) {\n fileId\n file {\n id\n name\n downloadUrl\n mimeType\n size\n }\n }\n }\n`;\n\nexport interface HvUploadedFile {\n id: string;\n name: string;\n url: string;\n mimeType: string;\n size: number;\n}\n\ninterface InitiateUploadResult {\n initiateUpload: { uploadUrl: string | null; session: { id: string } };\n}\n\ninterface CompleteUploadResult {\n completeUpload: {\n fileId: string;\n file: { id: string; name: string; downloadUrl: string | null; mimeType: string; size: number };\n };\n}\n\nconst splitExtension = (fileName: string): string => {\n const dot = fileName.lastIndexOf('.');\n return dot > 0 && dot < fileName.length - 1 ? fileName.slice(dot + 1).toLowerCase() : 'bin';\n};\n\n/**\n * Upload a real browser `File` through wspace-files-svc and return the durable\n * File id + download URL. Single-PUT presigned flow (fine for the image /\n * document sizes HousingVista deals with).\n */\nexport const useFileUpload = () => {\n const [initiate] = useMutation<InitiateUploadResult>(INITIATE_UPLOAD);\n const [complete] = useMutation<CompleteUploadResult>(COMPLETE_UPLOAD);\n\n const uploadFile = useCallback(\n async (file: File): Promise<HvUploadedFile> => {\n const extension = splitExtension(file.name);\n const mimeType = file.type || 'application/octet-stream';\n const initiated = await initiate({\n variables: {\n input: {\n fileName: file.name,\n extension,\n fileSize: file.size,\n mimeType,\n visibility: 'WORKSPACE',\n },\n },\n });\n const uploadUrl = initiated.data?.initiateUpload.uploadUrl;\n const sessionId = initiated.data?.initiateUpload.session.id;\n if (!uploadUrl || !sessionId) throw new Error('Upload could not be started');\n\n const putRes = await fetch(uploadUrl, {\n method: 'PUT',\n body: file,\n headers: { 'Content-Type': mimeType },\n });\n if (!putRes.ok) throw new Error(`Upload failed (${putRes.status})`);\n\n const completed = await complete({\n variables: { input: { sessionId, fileSize: file.size } },\n });\n const uploaded = completed.data?.completeUpload.file;\n if (!uploaded) throw new Error('Upload could not be finalized');\n return {\n id: uploaded.id,\n name: uploaded.name,\n url: uploaded.downloadUrl ?? '',\n mimeType: uploaded.mimeType,\n size: Number(uploaded.size),\n };\n },\n [initiate, complete]\n );\n\n return { uploadFile };\n};\n"],"mappings":"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"}