@planningcenter/chat-react-native 3.45.1-qa-staging.5 → 3.45.1-qa-staging.7

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 (56) hide show
  1. package/build/components/conversation/message.d.ts.map +1 -1
  2. package/build/components/conversation/message.js +8 -7
  3. package/build/components/conversation/message.js.map +1 -1
  4. package/build/components/conversation/reply_shadow_message.d.ts +1 -1
  5. package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
  6. package/build/components/conversation/reply_shadow_message.js +44 -43
  7. package/build/components/conversation/reply_shadow_message.js.map +1 -1
  8. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  9. package/build/hooks/use_conversation_messages_jolt_events.js +6 -1
  10. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  11. package/build/hooks/use_message_create_or_update.d.ts +99 -4
  12. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  13. package/build/hooks/use_message_create_or_update.js +47 -6
  14. package/build/hooks/use_message_create_or_update.js.map +1 -1
  15. package/build/types/resources/message.d.ts +2 -0
  16. package/build/types/resources/message.d.ts.map +1 -1
  17. package/build/types/resources/message.js.map +1 -1
  18. package/build/utils/cache/messages_cache.d.ts +1 -0
  19. package/build/utils/cache/messages_cache.d.ts.map +1 -1
  20. package/build/utils/cache/messages_cache.js +14 -7
  21. package/build/utils/cache/messages_cache.js.map +1 -1
  22. package/build/utils/cache/optimistically_create_message.d.ts +2 -1
  23. package/build/utils/cache/optimistically_create_message.d.ts.map +1 -1
  24. package/build/utils/cache/optimistically_create_message.js +4 -2
  25. package/build/utils/cache/optimistically_create_message.js.map +1 -1
  26. package/build/utils/client/transform_response.d.ts.map +1 -1
  27. package/build/utils/client/transform_response.js +9 -5
  28. package/build/utils/client/transform_response.js.map +1 -1
  29. package/build/utils/group_messages.d.ts +1 -0
  30. package/build/utils/group_messages.d.ts.map +1 -1
  31. package/build/utils/group_messages.js +10 -1
  32. package/build/utils/group_messages.js.map +1 -1
  33. package/build/utils/jolt/transform_message_event_data_to_message_resource.d.ts.map +1 -1
  34. package/build/utils/jolt/transform_message_event_data_to_message_resource.js +1 -0
  35. package/build/utils/jolt/transform_message_event_data_to_message_resource.js.map +1 -1
  36. package/build/utils/request/messages_data_options.d.ts.map +1 -1
  37. package/build/utils/request/messages_data_options.js +2 -2
  38. package/build/utils/request/messages_data_options.js.map +1 -1
  39. package/package.json +3 -3
  40. package/src/components/conversation/__tests__/reply_shadow_message.test.tsx +119 -0
  41. package/src/components/conversation/message.tsx +8 -7
  42. package/src/components/conversation/reply_shadow_message.tsx +6 -4
  43. package/src/hooks/__tests__/use_message_create_or_update.test.tsx +110 -0
  44. package/src/hooks/use_conversation_messages_jolt_events.ts +7 -0
  45. package/src/hooks/use_message_create_or_update.ts +29 -16
  46. package/src/types/resources/message.ts +2 -0
  47. package/src/utils/__tests__/group_messages.test.ts +48 -0
  48. package/src/utils/cache/__tests__/messages_cache.test.ts +89 -0
  49. package/src/utils/cache/__tests__/messages_cache_reply_roots.test.ts +71 -0
  50. package/src/utils/cache/messages_cache.ts +27 -11
  51. package/src/utils/cache/optimistically_create_message.ts +3 -0
  52. package/src/utils/client/__tests__/transform_response.test.ts +62 -0
  53. package/src/utils/client/transform_response.ts +7 -5
  54. package/src/utils/group_messages.ts +18 -1
  55. package/src/utils/jolt/transform_message_event_data_to_message_resource.ts +1 -0
  56. package/src/utils/request/messages_data_options.ts +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"transform_response.js","names":["_","inflection","keysToCamelCase","mixin","singular","plural","transformId","id","thru","parseInt","int","transformRelationships","relationships","Object","keys","reduce","acc","key","relationship","isArray","data","ids","map","singularize","get","type","addIncludedRecords","included","seen","parent","records","elem","find","compact","value","set","pluralize","record","transformRecord","at","attributes","links","has","transformed","mapKeys","v","k","charAt","toLowerCase","slice","defaultTransformResponse","response","isEmpty","meta","WeakMap","result","d","transformResponse","flowRight"],"sources":["../../../src/utils/client/transform_response.ts"],"sourcesContent":["// @ts-nocheck\n// TODO: Fix this file but write tests first\n\nimport _ from 'lodash'\nimport inflection from 'lodash-inflection/lib/lodash-inflection'\nimport { keysToCamelCase } from './utils'\n\n_.mixin({ ...inflection })\n\n// Teach some bad grammar\n_.singular(/^(media)$/i, '$1')\n_.plural(/^(media)$/i, '$1')\n\n// eslint-disable-next-line eqeqeq\nconst transformId = id => _.thru(parseInt(id, 10), int => (int == id ? int : id))\n\nconst transformRelationships = relationships => {\n return Object.keys(relationships || {}).reduce((acc, key) => {\n const relationship = relationships[key]\n if (_.isArray(relationship.data)) {\n const ids = _.map(relationship.data, ({ id }) => transformId(id))\n return { ...acc, [`${_.singularize(key)}_ids`]: ids }\n } else {\n const id = _.get(relationship, 'data.id')\n const type = _.get(relationship, 'data.type', null)\n return {\n ...acc,\n [`${key}_id`]: id ? transformId(id) : null,\n [`${key}_type`]: type,\n }\n }\n }, {})\n}\n\nconst addIncludedRecords = (relationships, included, seen, parent) => {\n return Object.keys(relationships || {}).reduce((acc, key) => {\n const relationship = relationships[key]\n if (_.isArray(relationship.data)) {\n const records = _(relationship.data)\n .map(elem => _.find(included, elem))\n .compact() // account for orphans\n .value()\n return _.set(\n acc,\n _.pluralize(key),\n _.map(records, record => transformRecord(record, included, seen))\n )\n } else {\n const [id, type] = _.at(relationship, ['data.id', 'data.type'])\n if (id && type) {\n const record = _.find(included, { id, type })\n return _.set(acc, key, record ? transformRecord(record, included, seen) : null)\n } else {\n return acc\n }\n }\n }, parent)\n}\n\nconst transformRecord = (record, included, seen) => {\n if (!record) return record\n const { id, attributes, links, relationships, type } = record\n\n if (seen.has(record)) {\n return seen.get(record)\n } else {\n const transformed = {\n id: transformId(id),\n type,\n links,\n ...transformRelationships(relationships),\n ..._.mapKeys(\n attributes,\n (v, k) => (k === 'length' ? `${type.charAt(0).toLowerCase()}${type.slice(1)}Length` : k),\n {}\n ),\n }\n\n seen.set(record, transformed)\n\n return addIncludedRecords(relationships, included, seen, transformed)\n }\n}\n\nconst defaultTransformResponse = response => {\n if (_.isEmpty(response)) return response\n const { data, included, meta, links } = response\n const seen = new WeakMap()\n\n if (_.isArray(data)) {\n const result = data.map(d => transformRecord(d, included, seen))\n return { data: result, meta, links, ...data }\n } else {\n return { data: { ...transformRecord(data, included, seen) }, meta, links }\n }\n}\n\nconst transformResponse = _.flowRight(keysToCamelCase, defaultTransformResponse)\n\nexport default transformResponse\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"transform_response.js","names":["_","inflection","keysToCamelCase","mixin","singular","plural","transformId","id","thru","parseInt","int","transformRelationships","relationships","Object","keys","reduce","acc","key","relationship","isArray","data","ids","map","singularize","get","type","addIncludedRecords","record","included","seen","parent","records","elem","find","compact","value","set","pluralize","related","transformRecord","at","isSelfReference","undefined","attributes","links","has","transformed","mapKeys","v","k","charAt","toLowerCase","slice","defaultTransformResponse","response","isEmpty","meta","WeakMap","result","d","transformResponse","flowRight"],"sources":["../../../src/utils/client/transform_response.ts"],"sourcesContent":["// @ts-nocheck\n// TODO: Fix this file but write tests first\n\nimport _ from 'lodash'\nimport inflection from 'lodash-inflection/lib/lodash-inflection'\nimport { keysToCamelCase } from './utils'\n\n_.mixin({ ...inflection })\n\n// Teach some bad grammar\n_.singular(/^(media)$/i, '$1')\n_.plural(/^(media)$/i, '$1')\n\n// eslint-disable-next-line eqeqeq\nconst transformId = id => _.thru(parseInt(id, 10), int => (int == id ? int : id))\n\nconst transformRelationships = relationships => {\n return Object.keys(relationships || {}).reduce((acc, key) => {\n const relationship = relationships[key]\n if (_.isArray(relationship.data)) {\n const ids = _.map(relationship.data, ({ id }) => transformId(id))\n return { ...acc, [`${_.singularize(key)}_ids`]: ids }\n } else {\n const id = _.get(relationship, 'data.id')\n const type = _.get(relationship, 'data.type', null)\n return {\n ...acc,\n [`${key}_id`]: id ? transformId(id) : null,\n [`${key}_type`]: type,\n }\n }\n }, {})\n}\n\nconst addIncludedRecords = (record, included, seen, parent) => {\n const { relationships } = record\n return Object.keys(relationships || {}).reduce((acc, key) => {\n const relationship = relationships[key]\n if (_.isArray(relationship.data)) {\n const records = _(relationship.data)\n .map(elem => _.find(included, elem))\n .compact() // account for orphans\n .value()\n return _.set(\n acc,\n _.pluralize(key),\n _.map(records, related => transformRecord(related, included, seen))\n )\n } else {\n const [id, type] = _.at(relationship, ['data.id', 'data.type'])\n if (id && type) {\n const isSelfReference = id === record.id && type === record.type\n const related = isSelfReference ? undefined : _.find(included, { id, type })\n return _.set(acc, key, related ? transformRecord(related, included, seen) : null)\n } else {\n return acc\n }\n }\n }, parent)\n}\n\nconst transformRecord = (record, included, seen) => {\n if (!record) return record\n const { id, attributes, links, relationships, type } = record\n\n if (seen.has(record)) {\n return seen.get(record)\n } else {\n const transformed = {\n id: transformId(id),\n type,\n links,\n ...transformRelationships(relationships),\n ..._.mapKeys(\n attributes,\n (v, k) => (k === 'length' ? `${type.charAt(0).toLowerCase()}${type.slice(1)}Length` : k),\n {}\n ),\n }\n\n seen.set(record, transformed)\n\n return addIncludedRecords(record, included, seen, transformed)\n }\n}\n\nconst defaultTransformResponse = response => {\n if (_.isEmpty(response)) return response\n const { data, included, meta, links } = response\n const seen = new WeakMap()\n\n if (_.isArray(data)) {\n const result = data.map(d => transformRecord(d, included, seen))\n return { data: result, meta, links, ...data }\n } else {\n return { data: { ...transformRecord(data, included, seen) }, meta, links }\n }\n}\n\nconst transformResponse = _.flowRight(keysToCamelCase, defaultTransformResponse)\n\nexport default transformResponse\n"],"mappings":"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","ignoreList":[]}
@@ -8,6 +8,7 @@ export type ReplyShadowMessage = {
8
8
  type: 'ReplyShadowMessage';
9
9
  id: string;
10
10
  messageId: string;
11
+ replyRoot: MessageResource | null;
11
12
  isReplyShadowMessage: boolean;
12
13
  nextRendersAuthor: boolean;
13
14
  };
@@ -1 +1 @@
1
- {"version":3,"file":"group_messages.d.ts","sourceRoot":"","sources":["../../src/utils/group_messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAMjE,MAAM,MAAM,aAAa,GAAG;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAE/E,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,oBAAoB,CAAA;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,oBAAoB,EAAE,OAAO,CAAA;IAC7B,iBAAiB,EAAE,OAAO,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,eAAe,GAAG,aAAa,GAAG,kBAAkB,CAAA;AAElF,UAAU,kBAAkB;IAC1B,EAAE,EAAE,eAAe,EAAE,CAAA;IACrB,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,kBAAkB,GAAG,eAAe,EAAE,CAkC1F"}
1
+ {"version":3,"file":"group_messages.d.ts","sourceRoot":"","sources":["../../src/utils/group_messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAMjE,MAAM,MAAM,aAAa,GAAG;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAE/E,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,oBAAoB,CAAA;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,eAAe,GAAG,IAAI,CAAA;IACjC,oBAAoB,EAAE,OAAO,CAAA;IAC7B,iBAAiB,EAAE,OAAO,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,eAAe,GAAG,aAAa,GAAG,kBAAkB,CAAA;AAElF,UAAU,kBAAkB;IAC1B,EAAE,EAAE,eAAe,EAAE,CAAA;IACrB,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,kBAAkB,GAAG,eAAe,EAAE,CAyC1F"}
@@ -6,6 +6,7 @@ export function groupMessages({
6
6
  inReplyScreen
7
7
  }) {
8
8
  const items = [];
9
+ const messagesById = new Map(ms.map(message => [message.id, message]));
9
10
  let myLatestSeen = false;
10
11
  let nextNeighborEnriched;
11
12
  ms.forEach((message, i) => {
@@ -22,7 +23,10 @@ export function groupMessages({
22
23
  }
23
24
  const isMyLatest = !myLatestSeen && message.mine;
24
25
  if (isMyLatest) myLatestSeen = true;
25
- const enriched = enrichRegularMessage(message, prev, next, isMyLatest, !!inReplyScreen);
26
+ const enriched = enrichRegularMessage({
27
+ ...message,
28
+ replyRoot: resolveReplyRoot(message, messagesById)
29
+ }, prev, next, isMyLatest, !!inReplyScreen);
26
30
  items.push(enriched);
27
31
  const shadow = replyShadowFor(enriched, prev);
28
32
  if (shadow) items.push(shadow);
@@ -39,6 +43,10 @@ function neighborsOf(arr, i) {
39
43
  next: arr[i - 1]
40
44
  };
41
45
  }
46
+ function resolveReplyRoot(message, messagesById) {
47
+ if (!message.replyRootId || message.replyRootId === message.id) return null;
48
+ return messagesById.get(message.replyRootId) ?? message.replyRoot ?? null;
49
+ }
42
50
  function enrichSystemMessage(message, next) {
43
51
  return {
44
52
  ...message,
@@ -114,6 +122,7 @@ function replyShadowFor(message, prev) {
114
122
  type: 'ReplyShadowMessage',
115
123
  id: `${message.id}-${message.replyRootId}`,
116
124
  messageId: message.replyRootId,
125
+ replyRoot: message.replyRoot ?? null,
117
126
  isReplyShadowMessage: true,
118
127
  nextRendersAuthor: message.renderAuthor ?? false
119
128
  };
@@ -1 +1 @@
1
- {"version":3,"file":"group_messages.js","names":["dayjs","isSystemMessage","FIVE_MINUTES_MS","groupMessages","ms","inReplyScreen","items","myLatestSeen","nextNeighborEnriched","forEach","message","i","prev","neighborsOf","next","enriched","enrichSystemMessage","push","datesDifferBetween","dateSeparator","isMyLatest","mine","enrichRegularMessage","shadow","replyShadowFor","arr","myLatestInConversation","lastInGroup","renderAuthor","nextRendersAuthor","isReplyShadowMessage","nextIsReplyShadowMessage","threadPosition","inThread","replyRootId","showThreadDetails","startsNewGroup","nextIntroducesReplyShadow","threadPositionFor","prevIsMyReply","undefined","nextIsMyReply","a","b","author","id","differsByMoreThan5Min","Math","abs","toMillis","createdAt","iso","Date","getTime","dateKey","format","type","date","isThreadRoot","isLast","current","nextInThread","nextIsThreadRoot","differentThread","enteringNewThread","messageId"],"sources":["../../src/utils/group_messages.ts"],"sourcesContent":["import type { MessageResource } from '../types/resources/message'\nimport dayjs from './dayjs'\nimport { isSystemMessage } from './system_messages'\n\nconst FIVE_MINUTES_MS = 5 * 60 * 1000\n\nexport type DateSeparator = { type: 'DateSeparator'; id: string; date: string }\n\nexport type ReplyShadowMessage = {\n type: 'ReplyShadowMessage'\n id: string\n messageId: string\n isReplyShadowMessage: boolean\n nextRendersAuthor: boolean\n}\n\nexport type EnrichedMessage = MessageResource | DateSeparator | ReplyShadowMessage\n\ninterface GroupMessagesProps {\n ms: MessageResource[]\n inReplyScreen?: boolean\n}\n\nexport function groupMessages({ ms, inReplyScreen }: GroupMessagesProps): EnrichedMessage[] {\n const items: EnrichedMessage[] = []\n let myLatestSeen = false\n let nextNeighborEnriched: MessageResource | undefined\n\n ms.forEach((message, i) => {\n const { prev } = neighborsOf(ms, i)\n const next = nextNeighborEnriched\n\n if (isSystemMessage(message)) {\n const enriched = enrichSystemMessage(message, next)\n items.push(enriched)\n if (datesDifferBetween(message, prev)) items.push(dateSeparator(message))\n nextNeighborEnriched = enriched\n return\n }\n\n const isMyLatest = !myLatestSeen && message.mine\n if (isMyLatest) myLatestSeen = true\n\n const enriched = enrichRegularMessage(message, prev, next, isMyLatest, !!inReplyScreen)\n items.push(enriched)\n\n const shadow = replyShadowFor(enriched, prev)\n if (shadow) items.push(shadow)\n\n if (!prev || datesDifferBetween(message, prev)) {\n items.push(dateSeparator(message))\n }\n\n nextNeighborEnriched = enriched\n })\n\n return items\n}\n\nfunction neighborsOf<T>(arr: T[], i: number): { prev: T | undefined; next: T | undefined } {\n return { prev: arr[i + 1], next: arr[i - 1] }\n}\n\nfunction enrichSystemMessage(\n message: MessageResource,\n next: MessageResource | undefined\n): MessageResource {\n return {\n ...message,\n myLatestInConversation: false,\n lastInGroup: true,\n renderAuthor: false,\n nextRendersAuthor: next?.renderAuthor,\n isReplyShadowMessage: false,\n nextIsReplyShadowMessage: false,\n threadPosition: null,\n }\n}\n\nfunction enrichRegularMessage(\n message: MessageResource,\n prev: MessageResource | undefined,\n next: MessageResource | undefined,\n isMyLatest: boolean,\n inReplyScreen: boolean\n): MessageResource {\n const inThread = message.replyRootId !== null\n const showThreadDetails = !inReplyScreen && inThread\n\n return {\n ...message,\n myLatestInConversation: isMyLatest,\n lastInGroup: !next || startsNewGroup(next, message),\n renderAuthor: !message.mine && (!prev || startsNewGroup(message, prev)),\n nextRendersAuthor: next?.renderAuthor,\n isReplyShadowMessage: false,\n nextIsReplyShadowMessage: nextIntroducesReplyShadow(next, message),\n threadPosition: showThreadDetails ? threadPositionFor(message, next) : null,\n prevIsMyReply: showThreadDetails ? prev?.mine : undefined,\n nextIsMyReply: showThreadDetails ? next?.mine : undefined,\n }\n}\n\nfunction startsNewGroup(a: MessageResource, b: MessageResource): boolean {\n return (\n a.author?.id !== b.author?.id ||\n differsByMoreThan5Min(a, b) ||\n a.replyRootId !== b.replyRootId ||\n datesDifferBetween(a, b)\n )\n}\n\nfunction differsByMoreThan5Min(a: MessageResource, b: MessageResource): boolean {\n return Math.abs(toMillis(a.createdAt) - toMillis(b.createdAt)) > FIVE_MINUTES_MS\n}\n\nfunction toMillis(iso: string): number {\n return new Date(iso).getTime()\n}\n\nfunction datesDifferBetween(a: MessageResource, b: MessageResource | undefined): boolean {\n if (!b) return false\n return dateKey(a) !== dateKey(b)\n}\n\nfunction dateKey(message: MessageResource): string {\n return dayjs(message.createdAt).format('YYYY-MM-DD')\n}\n\nfunction dateSeparator(message: MessageResource): DateSeparator {\n return { type: 'DateSeparator', id: `day-divider-${message.id}`, date: dateKey(message) }\n}\n\nfunction threadPositionFor(\n message: MessageResource,\n next: MessageResource | undefined\n): 'first' | 'center' | 'last' | null {\n const isThreadRoot = message.replyRootId === message.id\n const isLast =\n !next || next.replyRootId !== message.replyRootId || datesDifferBetween(next, message)\n\n if (isThreadRoot && isLast) return null\n if (isThreadRoot) return 'first'\n if (isLast) return 'last'\n return 'center'\n}\n\nfunction nextIntroducesReplyShadow(\n next: MessageResource | undefined,\n current: MessageResource\n): boolean {\n if (!next) return false\n const nextInThread = next.replyRootId !== null\n const nextIsThreadRoot = next.replyRootId === next.id\n const differentThread = next.replyRootId !== current.replyRootId\n return nextInThread && !nextIsThreadRoot && (differentThread || datesDifferBetween(next, current))\n}\n\nfunction replyShadowFor(\n message: MessageResource,\n prev: MessageResource | undefined\n): ReplyShadowMessage | undefined {\n if (!message.replyRootId) return undefined\n if (message.replyRootId === message.id) return undefined\n\n const enteringNewThread =\n !prev || prev.replyRootId !== message.replyRootId || datesDifferBetween(prev, message)\n if (!enteringNewThread) return undefined\n\n return {\n type: 'ReplyShadowMessage',\n id: `${message.id}-${message.replyRootId}`,\n messageId: message.replyRootId,\n isReplyShadowMessage: true,\n nextRendersAuthor: message.renderAuthor ?? false,\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"group_messages.js","names":["dayjs","isSystemMessage","FIVE_MINUTES_MS","groupMessages","ms","inReplyScreen","items","messagesById","Map","map","message","id","myLatestSeen","nextNeighborEnriched","forEach","i","prev","neighborsOf","next","enriched","enrichSystemMessage","push","datesDifferBetween","dateSeparator","isMyLatest","mine","enrichRegularMessage","replyRoot","resolveReplyRoot","shadow","replyShadowFor","arr","replyRootId","get","myLatestInConversation","lastInGroup","renderAuthor","nextRendersAuthor","isReplyShadowMessage","nextIsReplyShadowMessage","threadPosition","inThread","showThreadDetails","startsNewGroup","nextIntroducesReplyShadow","threadPositionFor","prevIsMyReply","undefined","nextIsMyReply","a","b","author","differsByMoreThan5Min","Math","abs","toMillis","createdAt","iso","Date","getTime","dateKey","format","type","date","isThreadRoot","isLast","current","nextInThread","nextIsThreadRoot","differentThread","enteringNewThread","messageId"],"sources":["../../src/utils/group_messages.ts"],"sourcesContent":["import type { MessageResource } from '../types/resources/message'\nimport dayjs from './dayjs'\nimport { isSystemMessage } from './system_messages'\n\nconst FIVE_MINUTES_MS = 5 * 60 * 1000\n\nexport type DateSeparator = { type: 'DateSeparator'; id: string; date: string }\n\nexport type ReplyShadowMessage = {\n type: 'ReplyShadowMessage'\n id: string\n messageId: string\n replyRoot: MessageResource | null\n isReplyShadowMessage: boolean\n nextRendersAuthor: boolean\n}\n\nexport type EnrichedMessage = MessageResource | DateSeparator | ReplyShadowMessage\n\ninterface GroupMessagesProps {\n ms: MessageResource[]\n inReplyScreen?: boolean\n}\n\nexport function groupMessages({ ms, inReplyScreen }: GroupMessagesProps): EnrichedMessage[] {\n const items: EnrichedMessage[] = []\n const messagesById = new Map(ms.map(message => [message.id, message]))\n let myLatestSeen = false\n let nextNeighborEnriched: MessageResource | undefined\n\n ms.forEach((message, i) => {\n const { prev } = neighborsOf(ms, i)\n const next = nextNeighborEnriched\n\n if (isSystemMessage(message)) {\n const enriched = enrichSystemMessage(message, next)\n items.push(enriched)\n if (datesDifferBetween(message, prev)) items.push(dateSeparator(message))\n nextNeighborEnriched = enriched\n return\n }\n\n const isMyLatest = !myLatestSeen && message.mine\n if (isMyLatest) myLatestSeen = true\n\n const enriched = enrichRegularMessage(\n { ...message, replyRoot: resolveReplyRoot(message, messagesById) },\n prev,\n next,\n isMyLatest,\n !!inReplyScreen\n )\n items.push(enriched)\n\n const shadow = replyShadowFor(enriched, prev)\n if (shadow) items.push(shadow)\n\n if (!prev || datesDifferBetween(message, prev)) {\n items.push(dateSeparator(message))\n }\n\n nextNeighborEnriched = enriched\n })\n\n return items\n}\n\nfunction neighborsOf<T>(arr: T[], i: number): { prev: T | undefined; next: T | undefined } {\n return { prev: arr[i + 1], next: arr[i - 1] }\n}\n\nfunction resolveReplyRoot(\n message: MessageResource,\n messagesById: Map<string, MessageResource>\n): MessageResource | null {\n if (!message.replyRootId || message.replyRootId === message.id) return null\n return messagesById.get(message.replyRootId) ?? message.replyRoot ?? null\n}\n\nfunction enrichSystemMessage(\n message: MessageResource,\n next: MessageResource | undefined\n): MessageResource {\n return {\n ...message,\n myLatestInConversation: false,\n lastInGroup: true,\n renderAuthor: false,\n nextRendersAuthor: next?.renderAuthor,\n isReplyShadowMessage: false,\n nextIsReplyShadowMessage: false,\n threadPosition: null,\n }\n}\n\nfunction enrichRegularMessage(\n message: MessageResource,\n prev: MessageResource | undefined,\n next: MessageResource | undefined,\n isMyLatest: boolean,\n inReplyScreen: boolean\n): MessageResource {\n const inThread = message.replyRootId !== null\n const showThreadDetails = !inReplyScreen && inThread\n\n return {\n ...message,\n myLatestInConversation: isMyLatest,\n lastInGroup: !next || startsNewGroup(next, message),\n renderAuthor: !message.mine && (!prev || startsNewGroup(message, prev)),\n nextRendersAuthor: next?.renderAuthor,\n isReplyShadowMessage: false,\n nextIsReplyShadowMessage: nextIntroducesReplyShadow(next, message),\n threadPosition: showThreadDetails ? threadPositionFor(message, next) : null,\n prevIsMyReply: showThreadDetails ? prev?.mine : undefined,\n nextIsMyReply: showThreadDetails ? next?.mine : undefined,\n }\n}\n\nfunction startsNewGroup(a: MessageResource, b: MessageResource): boolean {\n return (\n a.author?.id !== b.author?.id ||\n differsByMoreThan5Min(a, b) ||\n a.replyRootId !== b.replyRootId ||\n datesDifferBetween(a, b)\n )\n}\n\nfunction differsByMoreThan5Min(a: MessageResource, b: MessageResource): boolean {\n return Math.abs(toMillis(a.createdAt) - toMillis(b.createdAt)) > FIVE_MINUTES_MS\n}\n\nfunction toMillis(iso: string): number {\n return new Date(iso).getTime()\n}\n\nfunction datesDifferBetween(a: MessageResource, b: MessageResource | undefined): boolean {\n if (!b) return false\n return dateKey(a) !== dateKey(b)\n}\n\nfunction dateKey(message: MessageResource): string {\n return dayjs(message.createdAt).format('YYYY-MM-DD')\n}\n\nfunction dateSeparator(message: MessageResource): DateSeparator {\n return { type: 'DateSeparator', id: `day-divider-${message.id}`, date: dateKey(message) }\n}\n\nfunction threadPositionFor(\n message: MessageResource,\n next: MessageResource | undefined\n): 'first' | 'center' | 'last' | null {\n const isThreadRoot = message.replyRootId === message.id\n const isLast =\n !next || next.replyRootId !== message.replyRootId || datesDifferBetween(next, message)\n\n if (isThreadRoot && isLast) return null\n if (isThreadRoot) return 'first'\n if (isLast) return 'last'\n return 'center'\n}\n\nfunction nextIntroducesReplyShadow(\n next: MessageResource | undefined,\n current: MessageResource\n): boolean {\n if (!next) return false\n const nextInThread = next.replyRootId !== null\n const nextIsThreadRoot = next.replyRootId === next.id\n const differentThread = next.replyRootId !== current.replyRootId\n return nextInThread && !nextIsThreadRoot && (differentThread || datesDifferBetween(next, current))\n}\n\nfunction replyShadowFor(\n message: MessageResource,\n prev: MessageResource | undefined\n): ReplyShadowMessage | undefined {\n if (!message.replyRootId) return undefined\n if (message.replyRootId === message.id) return undefined\n\n const enteringNewThread =\n !prev || prev.replyRootId !== message.replyRootId || datesDifferBetween(prev, message)\n if (!enteringNewThread) return undefined\n\n return {\n type: 'ReplyShadowMessage',\n id: `${message.id}-${message.replyRootId}`,\n messageId: message.replyRootId,\n replyRoot: message.replyRoot ?? null,\n isReplyShadowMessage: true,\n nextRendersAuthor: message.renderAuthor ?? false,\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"transform_message_event_data_to_message_resource.d.ts","sourceRoot":"","sources":["../../../src/utils/jolt/transform_message_event_data_to_message_resource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AAI5E,wBAAgB,0CAA0C,CAAC,EACzD,IAAI,EACJ,eAAe,GAChB,EAAE;IACD,IAAI,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;IACzC,eAAe,EAAE,MAAM,CAAA;CACxB,GAAG,eAAe,CA8BlB"}
1
+ {"version":3,"file":"transform_message_event_data_to_message_resource.d.ts","sourceRoot":"","sources":["../../../src/utils/jolt/transform_message_event_data_to_message_resource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAA;AAI5E,wBAAgB,0CAA0C,CAAC,EACzD,IAAI,EACJ,eAAe,GAChB,EAAE;IACD,IAAI,EAAE,mBAAmB,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAA;IACzC,eAAe,EAAE,MAAM,CAAA;CACxB,GAAG,eAAe,CA+BlB"}
@@ -22,6 +22,7 @@ export function transformMessageEventDataToMessageResource({
22
22
  reactionCounts: [],
23
23
  replyCount: data.reply_count || 0,
24
24
  replyRootId: data.reply_root_id || null,
25
+ idempotentKey: data.idempotent_key ?? null,
25
26
  messageType: data.message_type,
26
27
  personIdsForSystemEvent: data.system_event_person_ids,
27
28
  systemTextParts: data.system_text_parts ? {
@@ -1 +1 @@
1
- {"version":3,"file":"transform_message_event_data_to_message_resource.js","names":["deepCamelCaseKeys","transformMessageEventDataToMessageResource","data","currentPersonId","type","id","sort_key","text","html","createdAt","created_at","deletedAt","deleted_at","textEditedAt","text_edited_at","mine","author_id","attachments","author","name","author_name","avatar","author_avatar","reactionCounts","replyCount","reply_count","replyRootId","reply_root_id","messageType","message_type","personIdsForSystemEvent","system_event_person_ids","systemTextParts","system_text_parts","names","overflowCount","overflow_count","action"],"sources":["../../../src/utils/jolt/transform_message_event_data_to_message_resource.ts"],"sourcesContent":["import { MessageResource } from '../../types'\nimport { MessageCreatedEvent } from '../../types/jolt_events/message_events'\nimport { DenormalizedAttachmentResource } from '../../types/resources/denormalized_attachment_resource'\nimport { deepCamelCaseKeys } from '../deepCamelCaseKeys'\n\nexport function transformMessageEventDataToMessageResource({\n data,\n currentPersonId,\n}: {\n data: MessageCreatedEvent['data']['data']\n currentPersonId: number\n}): MessageResource {\n return {\n type: 'Message',\n id: data.sort_key,\n text: data.text,\n html: data.html,\n createdAt: data.created_at,\n deletedAt: data.deleted_at,\n textEditedAt: data.text_edited_at,\n mine: data.author_id === currentPersonId,\n attachments: deepCamelCaseKeys<DenormalizedAttachmentResource[]>(data.attachments) || [],\n author: {\n type: 'Person',\n id: data.author_id,\n name: data.author_name,\n avatar: data.author_avatar,\n },\n reactionCounts: [],\n replyCount: data.reply_count || 0,\n replyRootId: data.reply_root_id || null,\n messageType: data.message_type,\n personIdsForSystemEvent: data.system_event_person_ids,\n systemTextParts: data.system_text_parts\n ? {\n names: data.system_text_parts.names,\n overflowCount: data.system_text_parts.overflow_count,\n action: data.system_text_parts.action,\n }\n : null,\n }\n}\n"],"mappings":"AAGA,SAASA,iBAAiB,QAAQ,sBAAsB;AAExD,OAAO,SAASC,0CAA0CA,CAAC;EACzDC,IAAI;EACJC;AAIF,CAAC,EAAmB;EAClB,OAAO;IACLC,IAAI,EAAE,SAAS;IACfC,EAAE,EAAEH,IAAI,CAACI,QAAQ;IACjBC,IAAI,EAAEL,IAAI,CAACK,IAAI;IACfC,IAAI,EAAEN,IAAI,CAACM,IAAI;IACfC,SAAS,EAAEP,IAAI,CAACQ,UAAU;IAC1BC,SAAS,EAAET,IAAI,CAACU,UAAU;IAC1BC,YAAY,EAAEX,IAAI,CAACY,cAAc;IACjCC,IAAI,EAAEb,IAAI,CAACc,SAAS,KAAKb,eAAe;IACxCc,WAAW,EAAEjB,iBAAiB,CAAmCE,IAAI,CAACe,WAAW,CAAC,IAAI,EAAE;IACxFC,MAAM,EAAE;MACNd,IAAI,EAAE,QAAQ;MACdC,EAAE,EAAEH,IAAI,CAACc,SAAS;MAClBG,IAAI,EAAEjB,IAAI,CAACkB,WAAW;MACtBC,MAAM,EAAEnB,IAAI,CAACoB;IACf,CAAC;IACDC,cAAc,EAAE,EAAE;IAClBC,UAAU,EAAEtB,IAAI,CAACuB,WAAW,IAAI,CAAC;IACjCC,WAAW,EAAExB,IAAI,CAACyB,aAAa,IAAI,IAAI;IACvCC,WAAW,EAAE1B,IAAI,CAAC2B,YAAY;IAC9BC,uBAAuB,EAAE5B,IAAI,CAAC6B,uBAAuB;IACrDC,eAAe,EAAE9B,IAAI,CAAC+B,iBAAiB,GACnC;MACEC,KAAK,EAAEhC,IAAI,CAAC+B,iBAAiB,CAACC,KAAK;MACnCC,aAAa,EAAEjC,IAAI,CAAC+B,iBAAiB,CAACG,cAAc;MACpDC,MAAM,EAAEnC,IAAI,CAAC+B,iBAAiB,CAACI;IACjC,CAAC,GACD;EACN,CAAC;AACH","ignoreList":[]}
1
+ {"version":3,"file":"transform_message_event_data_to_message_resource.js","names":["deepCamelCaseKeys","transformMessageEventDataToMessageResource","data","currentPersonId","type","id","sort_key","text","html","createdAt","created_at","deletedAt","deleted_at","textEditedAt","text_edited_at","mine","author_id","attachments","author","name","author_name","avatar","author_avatar","reactionCounts","replyCount","reply_count","replyRootId","reply_root_id","idempotentKey","idempotent_key","messageType","message_type","personIdsForSystemEvent","system_event_person_ids","systemTextParts","system_text_parts","names","overflowCount","overflow_count","action"],"sources":["../../../src/utils/jolt/transform_message_event_data_to_message_resource.ts"],"sourcesContent":["import { MessageResource } from '../../types'\nimport { MessageCreatedEvent } from '../../types/jolt_events/message_events'\nimport { DenormalizedAttachmentResource } from '../../types/resources/denormalized_attachment_resource'\nimport { deepCamelCaseKeys } from '../deepCamelCaseKeys'\n\nexport function transformMessageEventDataToMessageResource({\n data,\n currentPersonId,\n}: {\n data: MessageCreatedEvent['data']['data']\n currentPersonId: number\n}): MessageResource {\n return {\n type: 'Message',\n id: data.sort_key,\n text: data.text,\n html: data.html,\n createdAt: data.created_at,\n deletedAt: data.deleted_at,\n textEditedAt: data.text_edited_at,\n mine: data.author_id === currentPersonId,\n attachments: deepCamelCaseKeys<DenormalizedAttachmentResource[]>(data.attachments) || [],\n author: {\n type: 'Person',\n id: data.author_id,\n name: data.author_name,\n avatar: data.author_avatar,\n },\n reactionCounts: [],\n replyCount: data.reply_count || 0,\n replyRootId: data.reply_root_id || null,\n idempotentKey: data.idempotent_key ?? null,\n messageType: data.message_type,\n personIdsForSystemEvent: data.system_event_person_ids,\n systemTextParts: data.system_text_parts\n ? {\n names: data.system_text_parts.names,\n overflowCount: data.system_text_parts.overflow_count,\n action: data.system_text_parts.action,\n }\n : null,\n }\n}\n"],"mappings":"AAGA,SAASA,iBAAiB,QAAQ,sBAAsB;AAExD,OAAO,SAASC,0CAA0CA,CAAC;EACzDC,IAAI;EACJC;AAIF,CAAC,EAAmB;EAClB,OAAO;IACLC,IAAI,EAAE,SAAS;IACfC,EAAE,EAAEH,IAAI,CAACI,QAAQ;IACjBC,IAAI,EAAEL,IAAI,CAACK,IAAI;IACfC,IAAI,EAAEN,IAAI,CAACM,IAAI;IACfC,SAAS,EAAEP,IAAI,CAACQ,UAAU;IAC1BC,SAAS,EAAET,IAAI,CAACU,UAAU;IAC1BC,YAAY,EAAEX,IAAI,CAACY,cAAc;IACjCC,IAAI,EAAEb,IAAI,CAACc,SAAS,KAAKb,eAAe;IACxCc,WAAW,EAAEjB,iBAAiB,CAAmCE,IAAI,CAACe,WAAW,CAAC,IAAI,EAAE;IACxFC,MAAM,EAAE;MACNd,IAAI,EAAE,QAAQ;MACdC,EAAE,EAAEH,IAAI,CAACc,SAAS;MAClBG,IAAI,EAAEjB,IAAI,CAACkB,WAAW;MACtBC,MAAM,EAAEnB,IAAI,CAACoB;IACf,CAAC;IACDC,cAAc,EAAE,EAAE;IAClBC,UAAU,EAAEtB,IAAI,CAACuB,WAAW,IAAI,CAAC;IACjCC,WAAW,EAAExB,IAAI,CAACyB,aAAa,IAAI,IAAI;IACvCC,aAAa,EAAE1B,IAAI,CAAC2B,cAAc,IAAI,IAAI;IAC1CC,WAAW,EAAE5B,IAAI,CAAC6B,YAAY;IAC9BC,uBAAuB,EAAE9B,IAAI,CAAC+B,uBAAuB;IACrDC,eAAe,EAAEhC,IAAI,CAACiC,iBAAiB,GACnC;MACEC,KAAK,EAAElC,IAAI,CAACiC,iBAAiB,CAACC,KAAK;MACnCC,aAAa,EAAEnC,IAAI,CAACiC,iBAAiB,CAACG,cAAc;MACpDC,MAAM,EAAErC,IAAI,CAACiC,iBAAiB,CAACI;IACjC,CAAC,GACD;EACN,CAAC;AACH","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"messages_data_options.d.ts","sourceRoot":"","sources":["../../../src/utils/request/messages_data_options.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB;;;;CAkB5B,CAAA;AAED,eAAO,MAAM,kBAAkB,UAAgC,CAAA"}
1
+ {"version":3,"file":"messages_data_options.d.ts","sourceRoot":"","sources":["../../../src/utils/request/messages_data_options.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB;;;;CAmB5B,CAAA;AAED,eAAO,MAAM,kBAAkB,UAAmE,CAAA"}
@@ -1,7 +1,7 @@
1
1
  export const getMessageFields = {
2
- Message: ['text', 'text_edited_at', 'mine', 'attachments', 'created_at', 'deleted_at', 'author', 'reaction_counts', 'reply_count', 'reply_root', 'message_type', 'person_ids_for_system_event', 'system_text_parts'],
2
+ Message: ['text', 'text_edited_at', 'mine', 'attachments', 'created_at', 'deleted_at', 'author', 'reaction_counts', 'reply_count', 'reply_root', 'idempotent_key', 'message_type', 'person_ids_for_system_event', 'system_text_parts'],
3
3
  Person: ['name', 'avatar'],
4
4
  ReactionCount: ['value', 'count', 'mine', 'message_id', 'author_ids']
5
5
  };
6
- export const getMessagesInclude = ['author', 'reaction_counts'];
6
+ export const getMessagesInclude = ['author', 'reaction_counts', 'reply_root', 'reply_root.author'];
7
7
  //# sourceMappingURL=messages_data_options.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"messages_data_options.js","names":["getMessageFields","Message","Person","ReactionCount","getMessagesInclude"],"sources":["../../../src/utils/request/messages_data_options.ts"],"sourcesContent":["export const getMessageFields = {\n Message: [\n 'text',\n 'text_edited_at',\n 'mine',\n 'attachments',\n 'created_at',\n 'deleted_at',\n 'author',\n 'reaction_counts',\n 'reply_count',\n 'reply_root',\n 'message_type',\n 'person_ids_for_system_event',\n 'system_text_parts',\n ],\n Person: ['name', 'avatar'],\n ReactionCount: ['value', 'count', 'mine', 'message_id', 'author_ids'],\n}\n\nexport const getMessagesInclude = ['author', 'reaction_counts']\n"],"mappings":"AAAA,OAAO,MAAMA,gBAAgB,GAAG;EAC9BC,OAAO,EAAE,CACP,MAAM,EACN,gBAAgB,EAChB,MAAM,EACN,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,cAAc,EACd,6BAA6B,EAC7B,mBAAmB,CACpB;EACDC,MAAM,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC;EAC1BC,aAAa,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY;AACtE,CAAC;AAED,OAAO,MAAMC,kBAAkB,GAAG,CAAC,QAAQ,EAAE,iBAAiB,CAAC","ignoreList":[]}
1
+ {"version":3,"file":"messages_data_options.js","names":["getMessageFields","Message","Person","ReactionCount","getMessagesInclude"],"sources":["../../../src/utils/request/messages_data_options.ts"],"sourcesContent":["export const getMessageFields = {\n Message: [\n 'text',\n 'text_edited_at',\n 'mine',\n 'attachments',\n 'created_at',\n 'deleted_at',\n 'author',\n 'reaction_counts',\n 'reply_count',\n 'reply_root',\n 'idempotent_key',\n 'message_type',\n 'person_ids_for_system_event',\n 'system_text_parts',\n ],\n Person: ['name', 'avatar'],\n ReactionCount: ['value', 'count', 'mine', 'message_id', 'author_ids'],\n}\n\nexport const getMessagesInclude = ['author', 'reaction_counts', 'reply_root', 'reply_root.author']\n"],"mappings":"AAAA,OAAO,MAAMA,gBAAgB,GAAG;EAC9BC,OAAO,EAAE,CACP,MAAM,EACN,gBAAgB,EAChB,MAAM,EACN,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,6BAA6B,EAC7B,mBAAmB,CACpB;EACDC,MAAM,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC;EAC1BC,aAAa,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY;AACtE,CAAC;AAED,OAAO,MAAMC,kBAAkB,GAAG,CAAC,QAAQ,EAAE,iBAAiB,EAAE,YAAY,EAAE,mBAAmB,CAAC","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.45.1-qa-staging.5",
3
+ "version": "3.45.1-qa-staging.7",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "@fortawesome/fontawesome-svg-core": "^7.2.0",
35
35
  "@fortawesome/react-native-fontawesome": "^0.3.2",
36
- "@planningcenter/emoji-keyboard": "3.45.1-qa-staging.5",
36
+ "@planningcenter/emoji-keyboard": "3.45.1-qa-staging.7",
37
37
  "lodash-inflection": "^1.5.0"
38
38
  },
39
39
  "peerDependencies": {
@@ -84,5 +84,5 @@
84
84
  "sf-symbols-typescript": "^2.2.0",
85
85
  "typescript": "~6.0.3"
86
86
  },
87
- "gitHead": "6ce98f51a1ccd0fddda103caac39b5f01f0d1fb4"
87
+ "gitHead": "edb40832f89d7a62f0193d0699e68c817a9bb222"
88
88
  }
@@ -0,0 +1,119 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
2
+ import { render } from '@testing-library/react-native'
3
+ import React from 'react'
4
+ import { buildTestQueryClient } from '../../../__utils__/query_client'
5
+ import { ChatProvider } from '../../../contexts/chat_context'
6
+ import { useConversationMessage } from '../../../hooks/use_conversation_message'
7
+ import type { MessageResource } from '../../../types/resources/message'
8
+ import type { PersonResource } from '../../../types/resources/person'
9
+ import { ReplyShadowMessage } from '../reply_shadow_message'
10
+
11
+ jest.mock('../../../hooks/use_conversation_message')
12
+ jest.mock('@react-navigation/native', () => ({
13
+ useNavigation: () => ({ navigate: jest.fn() }),
14
+ }))
15
+
16
+ const mockedUseConversationMessage = useConversationMessage as jest.MockedFunction<
17
+ typeof useConversationMessage
18
+ >
19
+
20
+ const author = { id: 1, type: 'Person', name: 'A', avatar: null } as unknown as PersonResource
21
+
22
+ const root = (text: string): MessageResource =>
23
+ ({
24
+ type: 'Message',
25
+ id: 'rootA',
26
+ text,
27
+ html: '',
28
+ createdAt: '2026-01-01T00:00:00Z',
29
+ deletedAt: null,
30
+ textEditedAt: null,
31
+ mine: false,
32
+ attachments: [],
33
+ author,
34
+ reactionCounts: [],
35
+ replyCount: 1,
36
+ replyRootId: 'rootA',
37
+ messageType: 'message',
38
+ }) as MessageResource
39
+
40
+ let queryClient: QueryClient
41
+
42
+ const hookResult = (overrides: { message?: MessageResource; isError?: boolean }) =>
43
+ ({ message: undefined, isError: false, isLoading: false, ...overrides }) as ReturnType<
44
+ typeof useConversationMessage
45
+ >
46
+
47
+ const Wrapper = ({ children }: { children: React.ReactNode }) => (
48
+ <QueryClientProvider client={queryClient}>
49
+ <ChatProvider
50
+ value={{
51
+ token: {
52
+ access_token: '',
53
+ created_at: 0,
54
+ expires_in: 0,
55
+ scope: '',
56
+ refresh_token: '',
57
+ token_type: '',
58
+ },
59
+ onUnauthorizedResponse: () => {},
60
+ theme: { theme: { colors: { interaction: 'rgb(0, 0, 255)' } } },
61
+ }}
62
+ >
63
+ {children}
64
+ </ChatProvider>
65
+ </QueryClientProvider>
66
+ )
67
+
68
+ const renderShadow = (replyRoot: MessageResource | null) =>
69
+ render(
70
+ <ReplyShadowMessage
71
+ {...({} as MessageResource)}
72
+ conversation_id={1}
73
+ messageId="rootA"
74
+ replyRoot={replyRoot}
75
+ isReplyShadowMessage
76
+ nextRendersAuthor={false}
77
+ />,
78
+ { wrapper: Wrapper }
79
+ )
80
+
81
+ beforeEach(() => {
82
+ queryClient = buildTestQueryClient()
83
+ mockedUseConversationMessage.mockReturnValue(hookResult({}))
84
+ })
85
+
86
+ afterEach(() => {
87
+ queryClient.clear()
88
+ jest.clearAllMocks()
89
+ })
90
+
91
+ describe('ReplyShadowMessage', () => {
92
+ it('renders the inline root without enabling the root fetch', () => {
93
+ const view = renderShadow(root('inline root'))
94
+
95
+ expect(view.getByText('inline root')).toBeTruthy()
96
+ expect(mockedUseConversationMessage).toHaveBeenCalledWith(
97
+ expect.objectContaining({ enabled: false })
98
+ )
99
+ })
100
+
101
+ it('falls back to fetching the root when none is inline', () => {
102
+ mockedUseConversationMessage.mockReturnValue(hookResult({ message: root('fetched root') }))
103
+
104
+ const view = renderShadow(null)
105
+
106
+ expect(view.getByText('fetched root')).toBeTruthy()
107
+ expect(mockedUseConversationMessage).toHaveBeenCalledWith(
108
+ expect.objectContaining({ messageId: 'rootA', enabled: true })
109
+ )
110
+ })
111
+
112
+ it('shows the deleted placeholder when the fallback fetch finds nothing', () => {
113
+ mockedUseConversationMessage.mockReturnValue(hookResult({ isError: true }))
114
+
115
+ const view = renderShadow(null)
116
+
117
+ expect(view.getByText('Message deleted')).toBeTruthy()
118
+ })
119
+ })
@@ -73,11 +73,15 @@ export function Message({
73
73
  const [messageBubbleHeight, setMessageBubbleHeight] = React.useState(0)
74
74
  const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
75
75
  useAnimatedMessageBackgroundColor()
76
- const { message: replyRootMessage } = useConversationMessage({
76
+ const isReplyRootMessage = message.replyRootId === message.id
77
+ const isDeletedReplyRootMessage = isReplyRootMessage && !!message.deletedAt
78
+ const replyToReplyRootMessage = !isReplyRootMessage && !!message.replyRootId
79
+ const { message: fetchedReplyRoot } = useConversationMessage({
77
80
  conversation_id,
78
81
  messageId: message.replyRootId || '',
79
- enabled: !!message.replyRootId,
82
+ enabled: replyToReplyRootMessage && !message.replyRoot,
80
83
  })
84
+ const replyRoot = message.replyRoot ?? fetchedReplyRoot
81
85
 
82
86
  const metaProps = {
83
87
  authorName: author.name,
@@ -85,14 +89,11 @@ export function Message({
85
89
  }
86
90
 
87
91
  const renderAuthor = (!message.mine && message.renderAuthor) || false
88
- const showReplyCountButton = !inReplyScreen && message.replyRootId === message.id
89
- const isReplyRootMessage = message.replyRootId === message.id
90
- const isDeletedReplyRootMessage = isReplyRootMessage && !!message.deletedAt
91
- const replyToReplyRootMessage = !isReplyRootMessage && !!message.replyRootId
92
+ const showReplyCountButton = !inReplyScreen && isReplyRootMessage
92
93
 
93
94
  const replyCountText = pluralize(message.replyCount, 'reply')
94
95
  const messagePendingLabel = isPersisted ? 'Saving' : 'Sending'
95
- const replyRootAuthorName = message.replyRootId ? replyRootMessage?.author.name : author.name
96
+ const replyRootAuthorName = replyToReplyRootMessage ? replyRoot?.author.name : author.name
96
97
  const messageText = isDeletedReplyRootMessage ? 'Message deleted' : text
97
98
 
98
99
  const messageBottomMargin =
@@ -41,14 +41,16 @@ export function ReplyShadowMessage({
41
41
  conversation_id,
42
42
  inReplyScreen,
43
43
  messageId,
44
+ replyRoot,
44
45
  isReplyShadowMessage,
45
46
  nextRendersAuthor,
46
47
  }: ReplyShadowMessageProps) {
47
- const { message, isError, isLoading } = useConversationMessage({ conversation_id, messageId })
48
+ const fetched = useConversationMessage({ conversation_id, messageId, enabled: !replyRoot })
49
+ const message = replyRoot ?? fetched.message
48
50
 
49
51
  if (inReplyScreen) return null
50
- if (isLoading) return <ShadowMessageFallback text="Loading..." />
51
- if (isError || !message) return <ShadowMessageFallback text="Message deleted" />
52
+ if (!message && fetched.isLoading) return <ShadowMessageFallback text="Loading..." />
53
+ if (!message) return <ShadowMessageFallback text="Message deleted" />
52
54
 
53
55
  const enrichedMessage = {
54
56
  ...message,
@@ -82,7 +84,7 @@ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageCont
82
84
  const handleNavigateToReplies = () => {
83
85
  navigation.navigate('ConversationReply', {
84
86
  conversation_id,
85
- reply_root_id: message.replyRootId,
87
+ reply_root_id: message.replyRootId ?? message.id,
86
88
  reply_root_author_name: message.author.name,
87
89
  })
88
90
  }
@@ -0,0 +1,110 @@
1
+ import { InfiniteData, QueryClientProvider, notifyManager } from '@tanstack/react-query'
2
+ import { act, renderHook } from '@testing-library/react-native'
3
+ import React from 'react'
4
+ import { chatQueryClient } from '../../contexts/api_provider'
5
+ import type { ApiCollection, MessageResource } from '../../types'
6
+ import { getMessagesQueryKey } from '../../utils/request/get_messages'
7
+ import { useMessageCreateOrUpdate } from '../use_message_create_or_update'
8
+
9
+ jest.mock('../../contexts/api_provider', () => {
10
+ const { QueryClient } = require('@tanstack/react-query')
11
+ // gcTime 0 so nothing outlives the test and keeps jest from exiting
12
+ return {
13
+ chatQueryClient: new QueryClient({
14
+ defaultOptions: { queries: { gcTime: 0 }, mutations: { gcTime: 0 } },
15
+ }),
16
+ }
17
+ })
18
+
19
+ const mockPost = jest.fn()
20
+ jest.mock('../use_api_client', () => ({
21
+ useApiClient: () => ({ chat: { post: mockPost, patch: jest.fn() } }),
22
+ }))
23
+
24
+ jest.mock('../use_current_person', () => ({
25
+ useCurrentPerson: () => ({ type: 'Person', id: 1, name: 'Me', avatar: null }),
26
+ }))
27
+
28
+ // Flush TanStack's batched state updates inside act, as its own tests do
29
+ notifyManager.setNotifyFunction(fn => act(fn))
30
+
31
+ type MessagesQueryData = InfiniteData<ApiCollection<MessageResource>>
32
+
33
+ const conversationId = 7
34
+ const queryKey = getMessagesQueryKey({ conversation_id: conversationId })
35
+
36
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
37
+ <QueryClientProvider client={chatQueryClient}>{children}</QueryClientProvider>
38
+ )
39
+
40
+ const pendingMessagesInCache = () =>
41
+ (chatQueryClient.getQueryData<MessagesQueryData>(queryKey)?.pages[0].data ?? []).filter(
42
+ m => m.pending
43
+ )
44
+
45
+ const sentKey = (call: number): string =>
46
+ mockPost.mock.calls[call][0].data.data.attributes.idempotent_key
47
+
48
+ // What the cache held when each request went out, i.e. after onMutate ran
49
+ let pendingAtSend: MessageResource[][]
50
+
51
+ describe('useMessageCreateOrUpdate idempotent key', () => {
52
+ beforeEach(() => {
53
+ chatQueryClient.clear()
54
+ chatQueryClient.setQueryData<MessagesQueryData>(queryKey, {
55
+ pages: [{ data: [], links: {}, meta: { count: 0, totalCount: 0 } }],
56
+ pageParams: [],
57
+ })
58
+ pendingAtSend = []
59
+ mockPost.mockReset()
60
+ mockPost.mockImplementation(async request => {
61
+ pendingAtSend.push(pendingMessagesInCache())
62
+ const key = request.data.data.attributes.idempotent_key
63
+ return { data: { type: 'Message', id: `real-${key}`, idempotentKey: key, mine: true } }
64
+ })
65
+ })
66
+
67
+ it('gives the optimistic message the same key that is sent to the server', async () => {
68
+ const { result } = renderHook(() => useMessageCreateOrUpdate({ conversationId }), { wrapper })
69
+
70
+ await act(() => result.current.mutateAsync({ text: 'ok' }))
71
+
72
+ expect(sentKey(0)).toEqual(expect.any(String))
73
+ expect(pendingAtSend[0].map(m => m.idempotentKey)).toEqual([sentKey(0)])
74
+ })
75
+
76
+ it('mints a distinct key for each send, even with identical text', async () => {
77
+ const { result } = renderHook(() => useMessageCreateOrUpdate({ conversationId }), { wrapper })
78
+
79
+ await act(() =>
80
+ Promise.all([
81
+ result.current.mutateAsync({ text: 'ok' }),
82
+ result.current.mutateAsync({ text: 'ok' }),
83
+ ])
84
+ )
85
+
86
+ expect(sentKey(0)).not.toBe(sentKey(1))
87
+ expect(pendingAtSend[1].map(m => m.idempotentKey).sort()).toEqual(
88
+ [sentKey(0), sentKey(1)].sort()
89
+ )
90
+ })
91
+
92
+ it('reuses the original key when retrying a failed send', async () => {
93
+ const failed = {
94
+ type: 'Message',
95
+ id: '01FAILED-temp',
96
+ text: 'ok',
97
+ idempotentKey: 'original-key',
98
+ error: 'Failed to send message',
99
+ } as MessageResource
100
+ const { result } = renderHook(
101
+ () => useMessageCreateOrUpdate({ conversationId, message: failed }),
102
+ { wrapper }
103
+ )
104
+
105
+ await act(() => result.current.mutateAsync({ text: 'ok' }))
106
+
107
+ expect(sentKey(0)).toBe('original-key')
108
+ expect(pendingAtSend[0].map(m => m.idempotentKey)).toEqual(['original-key'])
109
+ })
110
+ })
@@ -11,6 +11,7 @@ import {
11
11
  updateCacheWithMessage,
12
12
  updateCacheWithIndividualMessage,
13
13
  updateCacheWithReaction,
14
+ updateCacheReplyRoots,
14
15
  getThreadedMessagesQueryKey,
15
16
  } from '../utils/cache/messages_cache'
16
17
  import { transformMessageEventDataToMessageResource } from '../utils/jolt/transform_message_event_data_to_message_resource'
@@ -44,6 +45,7 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
44
45
  data,
45
46
  currentPersonId: currentPerson.id,
46
47
  })
48
+ const isReplyRootUpdate = e.event === 'message.updated' && data.reply_root_id === data.sort_key
47
49
 
48
50
  if (e.event === 'message.created' && data.author_id) {
49
51
  removeAllTypingEventsByAuthorId(data.author_id)
@@ -62,6 +64,11 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
62
64
  data.reply_root_id
63
65
  )
64
66
  updateCacheWithMessage(queryClient, threadedMessagesQueryKey, message, e.event)
67
+
68
+ if (isReplyRootUpdate) {
69
+ updateCacheReplyRoots(queryClient, messagesQueryKey, message)
70
+ updateCacheReplyRoots(queryClient, threadedMessagesQueryKey, message)
71
+ }
65
72
  }
66
73
 
67
74
  // Update individual message query cache so reply shadow messages update immediately