@planningcenter/chat-react-native 4.0.1-qa-staging.2 → 4.0.1-rc.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 (85) hide show
  1. package/build/components/conversation/message.d.ts +2 -3
  2. package/build/components/conversation/message.d.ts.map +1 -1
  3. package/build/components/conversation/message.js +22 -23
  4. package/build/components/conversation/message.js.map +1 -1
  5. package/build/components/conversation/message_read_receipts.d.ts +4 -2
  6. package/build/components/conversation/message_read_receipts.d.ts.map +1 -1
  7. package/build/components/conversation/message_read_receipts.js +3 -2
  8. package/build/components/conversation/message_read_receipts.js.map +1 -1
  9. package/build/components/conversation/reply_shadow_message.d.ts +1 -2
  10. package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
  11. package/build/components/conversation/reply_shadow_message.js +46 -47
  12. package/build/components/conversation/reply_shadow_message.js.map +1 -1
  13. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  14. package/build/hooks/use_conversation_messages.js +28 -40
  15. package/build/hooks/use_conversation_messages.js.map +1 -1
  16. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  17. package/build/hooks/use_conversation_messages_jolt_events.js +1 -6
  18. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  19. package/build/hooks/use_message_create_or_update.d.ts +4 -99
  20. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  21. package/build/hooks/use_message_create_or_update.js +6 -47
  22. package/build/hooks/use_message_create_or_update.js.map +1 -1
  23. package/build/screens/conversation_new/components/groups_form.js +80 -71
  24. package/build/screens/conversation_new/components/groups_form.js.map +1 -1
  25. package/build/screens/conversation_screen.d.ts.map +1 -1
  26. package/build/screens/conversation_screen.js +365 -184
  27. package/build/screens/conversation_screen.js.map +1 -1
  28. package/build/types/resources/message.d.ts +0 -2
  29. package/build/types/resources/message.d.ts.map +1 -1
  30. package/build/types/resources/message.js.map +1 -1
  31. package/build/utils/cache/messages_cache.d.ts +0 -1
  32. package/build/utils/cache/messages_cache.d.ts.map +1 -1
  33. package/build/utils/cache/messages_cache.js +7 -14
  34. package/build/utils/cache/messages_cache.js.map +1 -1
  35. package/build/utils/cache/optimistically_create_message.d.ts +1 -2
  36. package/build/utils/cache/optimistically_create_message.d.ts.map +1 -1
  37. package/build/utils/cache/optimistically_create_message.js +2 -4
  38. package/build/utils/cache/optimistically_create_message.js.map +1 -1
  39. package/build/utils/client/transform_response.d.ts.map +1 -1
  40. package/build/utils/client/transform_response.js +5 -9
  41. package/build/utils/client/transform_response.js.map +1 -1
  42. package/build/utils/group_messages.d.ts +0 -1
  43. package/build/utils/group_messages.d.ts.map +1 -1
  44. package/build/utils/group_messages.js +1 -10
  45. package/build/utils/group_messages.js.map +1 -1
  46. package/build/utils/jolt/transform_message_event_data_to_message_resource.d.ts.map +1 -1
  47. package/build/utils/jolt/transform_message_event_data_to_message_resource.js +0 -1
  48. package/build/utils/jolt/transform_message_event_data_to_message_resource.js.map +1 -1
  49. package/build/utils/request/messages_data_options.d.ts.map +1 -1
  50. package/build/utils/request/messages_data_options.js +2 -2
  51. package/build/utils/request/messages_data_options.js.map +1 -1
  52. package/package.json +3 -3
  53. package/src/components/conversation/message.tsx +18 -15
  54. package/src/components/conversation/message_read_receipts.tsx +5 -3
  55. package/src/components/conversation/reply_shadow_message.tsx +7 -9
  56. package/src/hooks/use_conversation_messages.ts +1 -5
  57. package/src/hooks/use_conversation_messages_jolt_events.ts +0 -7
  58. package/src/hooks/use_message_create_or_update.ts +16 -29
  59. package/src/screens/conversation_new/components/groups_form.tsx +7 -2
  60. package/src/screens/conversation_screen.tsx +104 -11
  61. package/src/types/resources/message.ts +0 -2
  62. package/src/utils/__tests__/group_messages.test.ts +0 -48
  63. package/src/utils/cache/messages_cache.ts +11 -27
  64. package/src/utils/cache/optimistically_create_message.ts +0 -3
  65. package/src/utils/client/transform_response.ts +5 -7
  66. package/src/utils/group_messages.ts +1 -18
  67. package/src/utils/jolt/transform_message_event_data_to_message_resource.ts +0 -1
  68. package/src/utils/request/messages_data_options.ts +1 -2
  69. package/build/components/conversation/message_list.d.ts +0 -21
  70. package/build/components/conversation/message_list.d.ts.map +0 -1
  71. package/build/components/conversation/message_list.js +0 -273
  72. package/build/components/conversation/message_list.js.map +0 -1
  73. package/build/utils/replace_equal_by_id.d.ts +0 -6
  74. package/build/utils/replace_equal_by_id.d.ts.map +0 -1
  75. package/build/utils/replace_equal_by_id.js +0 -41
  76. package/build/utils/replace_equal_by_id.js.map +0 -1
  77. package/src/components/conversation/__tests__/message_list.test.tsx +0 -271
  78. package/src/components/conversation/__tests__/reply_shadow_message.test.tsx +0 -119
  79. package/src/components/conversation/message_list.tsx +0 -157
  80. package/src/hooks/__tests__/use_message_create_or_update.test.tsx +0 -110
  81. package/src/utils/__tests__/replace_equal_by_id.test.ts +0 -73
  82. package/src/utils/cache/__tests__/messages_cache.test.ts +0 -89
  83. package/src/utils/cache/__tests__/messages_cache_reply_roots.test.ts +0 -70
  84. package/src/utils/client/__tests__/transform_response.test.ts +0 -62
  85. package/src/utils/replace_equal_by_id.ts +0 -42
@@ -1 +1 @@
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
+ {"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 +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,CA+BlB"}
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"}
@@ -22,7 +22,6 @@ 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,
26
25
  messageType: data.message_type,
27
26
  personIdsForSystemEvent: data.system_event_person_ids,
28
27
  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","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
+ {"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 +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;;;;CAmB5B,CAAA;AAED,eAAO,MAAM,kBAAkB,UAAmE,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;;;;CAkB5B,CAAA;AAED,eAAO,MAAM,kBAAkB,UAAgC,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', 'idempotent_key', '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', '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', 'reply_root', 'reply_root.author'];
6
+ export const getMessagesInclude = ['author', 'reaction_counts'];
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 '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":[]}
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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "4.0.1-qa-staging.2",
3
+ "version": "4.0.1-rc.1",
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": "4.0.1-qa-staging.2",
36
+ "@planningcenter/emoji-keyboard": "4.0.0",
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": "092bd6133bddee97b0fbdaab954d5a0984988a56"
87
+ "gitHead": "fa30b4ca64df403ad4e1f307404547528b3b8014"
88
88
  }
@@ -1,6 +1,6 @@
1
1
  import { useNavigation } from '@react-navigation/native'
2
2
  import { some } from 'lodash'
3
- import React, { memo, useEffect } from 'react'
3
+ import React, { useEffect } from 'react'
4
4
  import {
5
5
  AccessibilityActionEvent,
6
6
  Pressable,
@@ -44,14 +44,14 @@ import { TheirReplyConnector, MyReplyConnector, AVATAR_CONNECTOR_SPACING } from
44
44
  interface MessageProps extends MessageResource {
45
45
  canDeleteNonAuthoredMessages: boolean
46
46
  conversation_id: number
47
- readByOthers?: boolean
47
+ latestReadMessageSortKey?: string
48
48
  inReplyScreen?: boolean
49
49
  }
50
50
 
51
- export const Message = memo(function MessageRow({
51
+ export function Message({
52
52
  canDeleteNonAuthoredMessages,
53
53
  conversation_id,
54
- readByOthers,
54
+ latestReadMessageSortKey,
55
55
  inReplyScreen,
56
56
  ...message
57
57
  }: MessageProps) {
@@ -73,16 +73,11 @@ export const Message = memo(function MessageRow({
73
73
  const [messageBubbleHeight, setMessageBubbleHeight] = React.useState(0)
74
74
  const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
75
75
  useAnimatedMessageBackgroundColor()
76
- const isReplyRootMessage = message.replyRootId === message.id
77
- const isDeletedReplyRootMessage = isReplyRootMessage && !!message.deletedAt
78
- const showReadReceipts = message.mine && !pending && !error
79
- const replyToReplyRootMessage = !isReplyRootMessage && !!message.replyRootId
80
- const { message: fetchedReplyRoot } = useConversationMessage({
76
+ const { message: replyRootMessage } = useConversationMessage({
81
77
  conversation_id,
82
78
  messageId: message.replyRootId || '',
83
- enabled: replyToReplyRootMessage && !message.replyRoot,
79
+ enabled: !!message.replyRootId,
84
80
  })
85
- const replyRoot = message.replyRoot ?? fetchedReplyRoot
86
81
 
87
82
  const metaProps = {
88
83
  authorName: author.name,
@@ -90,11 +85,14 @@ export const Message = memo(function MessageRow({
90
85
  }
91
86
 
92
87
  const renderAuthor = (!message.mine && message.renderAuthor) || false
93
- const showReplyCountButton = !inReplyScreen && isReplyRootMessage
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
94
92
 
95
93
  const replyCountText = pluralize(message.replyCount, 'reply')
96
94
  const messagePendingLabel = isPersisted ? 'Saving' : 'Sending'
97
- const replyRootAuthorName = replyToReplyRootMessage ? replyRoot?.author.name : author.name
95
+ const replyRootAuthorName = message.replyRootId ? replyRootMessage?.author.name : author.name
98
96
  const messageText = isDeletedReplyRootMessage ? 'Message deleted' : text
99
97
 
100
98
  const messageBottomMargin =
@@ -286,7 +284,12 @@ export const Message = memo(function MessageRow({
286
284
  )}
287
285
  {showMessageMeta && !isDeletedReplyRootMessage && (
288
286
  <View style={styles.messageMeta}>
289
- {showReadReceipts && <MessageReadReceipts read={readByOthers} />}
287
+ {message.mine && !pending && !error && (
288
+ <MessageReadReceipts
289
+ message={message}
290
+ latestReadMessageSortKey={latestReadMessageSortKey}
291
+ />
292
+ )}
290
293
  {error ? (
291
294
  <View style={styles.errorContainer}>
292
295
  {retryFailedSaveMutation.isPending ? (
@@ -335,7 +338,7 @@ export const Message = memo(function MessageRow({
335
338
  </Animated.View>
336
339
  </Pressable>
337
340
  )
338
- })
341
+ }
339
342
 
340
343
  const useMessageStyles = (mine: boolean, isDeleting: boolean) => {
341
344
  const { colors } = useTheme()
@@ -1,15 +1,17 @@
1
1
  import React from 'react'
2
2
  import { StyleSheet, View } from 'react-native'
3
3
  import { useTheme } from '../../hooks'
4
+ import { MessageResource } from '../../types'
4
5
  import { Icon, Text } from '../display'
5
6
 
6
7
  interface Props {
7
- read?: boolean
8
+ message: MessageResource
9
+ latestReadMessageSortKey?: string
8
10
  }
9
11
 
10
- export function MessageReadReceipts({ read }: Props) {
12
+ export function MessageReadReceipts({ message, latestReadMessageSortKey }: Props) {
11
13
  const styles = useStyles()
12
- if (!read) {
14
+ if (!latestReadMessageSortKey || message.id > latestReadMessageSortKey) {
13
15
  return (
14
16
  <View style={styles.badgeContainer}>
15
17
  <Icon name="general.check" style={styles.icon} />
@@ -1,6 +1,6 @@
1
1
  import { useNavigation } from '@react-navigation/native'
2
2
  import { some } from 'lodash'
3
- import React, { memo } from 'react'
3
+ import React from 'react'
4
4
  import { Pressable, StyleSheet, useWindowDimensions, View } from 'react-native'
5
5
  import Animated from 'react-native-reanimated'
6
6
  import {
@@ -37,20 +37,18 @@ interface ReplyShadowMessageProps extends MessageResource {
37
37
  nextRendersAuthor: boolean
38
38
  }
39
39
 
40
- export const ReplyShadowMessage = memo(function ReplyShadowMessageRow({
40
+ export function ReplyShadowMessage({
41
41
  conversation_id,
42
42
  inReplyScreen,
43
43
  messageId,
44
- replyRoot,
45
44
  isReplyShadowMessage,
46
45
  nextRendersAuthor,
47
46
  }: ReplyShadowMessageProps) {
48
- const fetched = useConversationMessage({ conversation_id, messageId, enabled: !replyRoot })
49
- const message = replyRoot ?? fetched.message
47
+ const { message, isError, isLoading } = useConversationMessage({ conversation_id, messageId })
50
48
 
51
49
  if (inReplyScreen) return null
52
- if (!message && fetched.isLoading) return <ShadowMessageFallback text="Loading..." />
53
- if (!message) return <ShadowMessageFallback text="Message deleted" />
50
+ if (isLoading) return <ShadowMessageFallback text="Loading..." />
51
+ if (isError || !message) return <ShadowMessageFallback text="Message deleted" />
54
52
 
55
53
  const enrichedMessage = {
56
54
  ...message,
@@ -59,7 +57,7 @@ export const ReplyShadowMessage = memo(function ReplyShadowMessageRow({
59
57
  }
60
58
 
61
59
  return <ShadowMessageContent conversation_id={conversation_id} {...enrichedMessage} />
62
- })
60
+ }
63
61
 
64
62
  interface ShadowMessageContentProps extends MessageResource {
65
63
  conversation_id: number
@@ -84,7 +82,7 @@ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageCont
84
82
  const handleNavigateToReplies = () => {
85
83
  navigation.navigate('ConversationReply', {
86
84
  conversation_id,
87
- reply_root_id: message.replyRootId ?? message.id,
85
+ reply_root_id: message.replyRootId,
88
86
  reply_root_author_name: message.author.name,
89
87
  })
90
88
  }
@@ -1,6 +1,5 @@
1
1
  import { useMemo } from 'react'
2
2
  import { MessageResource } from '../types'
3
- import { replaceEqualById } from '../utils/replace_equal_by_id'
4
3
  import { getMessagesQueryKey, getMessagesRequestArgs } from '../utils/request/get_messages'
5
4
  import { SuspensePaginatorOptions, useSuspensePaginator } from './use_suspense_api'
6
5
 
@@ -11,10 +10,7 @@ export const useConversationMessages = (
11
10
  const { data, refetch, isRefetching, fetchNextPage, hasNextPage } =
12
11
  useSuspensePaginator<MessageResource>(
13
12
  getMessagesRequestArgs({ conversation_id, reply_root_id }),
14
- {
15
- structuralSharing: replaceEqualById,
16
- ...opts,
17
- }
13
+ opts
18
14
  )
19
15
  const queryKey = getMessagesQueryKey({ conversation_id, reply_root_id })
20
16
  const messages = useMemo(
@@ -11,7 +11,6 @@ import {
11
11
  updateCacheWithMessage,
12
12
  updateCacheWithIndividualMessage,
13
13
  updateCacheWithReaction,
14
- updateCacheReplyRoots,
15
14
  getThreadedMessagesQueryKey,
16
15
  } from '../utils/cache/messages_cache'
17
16
  import { transformMessageEventDataToMessageResource } from '../utils/jolt/transform_message_event_data_to_message_resource'
@@ -45,7 +44,6 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
45
44
  data,
46
45
  currentPersonId: currentPerson.id,
47
46
  })
48
- const isReplyRootUpdate = e.event === 'message.updated' && data.reply_root_id === data.sort_key
49
47
 
50
48
  if (e.event === 'message.created' && data.author_id) {
51
49
  removeAllTypingEventsByAuthorId(data.author_id)
@@ -64,11 +62,6 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
64
62
  data.reply_root_id
65
63
  )
66
64
  updateCacheWithMessage(queryClient, threadedMessagesQueryKey, message, e.event)
67
-
68
- if (isReplyRootUpdate) {
69
- updateCacheReplyRoots(queryClient, messagesQueryKey, message)
70
- updateCacheReplyRoots(queryClient, threadedMessagesQueryKey, message)
71
- }
72
65
  }
73
66
 
74
67
  // Update individual message query cache so reply shadow messages update immediately
@@ -22,14 +22,6 @@ interface Props {
22
22
  replyRootId?: string | null
23
23
  }
24
24
 
25
- interface MessageMutationVariables {
26
- text: string
27
- attachments?: DenormalizedAttachmentResourceForCreate[]
28
- idempotentKey?: string
29
- }
30
-
31
- type ResolvedMessageMutationVariables = MessageMutationVariables & { idempotentKey: string }
32
-
33
25
  export function useMessageCreateOrUpdate({ conversationId, message, replyRootId }: Props) {
34
26
  const messageId = message?.id || null
35
27
  const isEditing = !isNewMessage(message)
@@ -37,7 +29,13 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
37
29
  const currentPerson = useCurrentPerson()
38
30
 
39
31
  const mutation = useMutation({
40
- mutationFn: ({ text, attachments, idempotentKey }: ResolvedMessageMutationVariables) => {
32
+ mutationFn: ({
33
+ text,
34
+ attachments,
35
+ }: {
36
+ text: string
37
+ attachments?: DenormalizedAttachmentResourceForCreate[]
38
+ }) => {
41
39
  const requestParams = getMessagesRequestArgs({ conversation_id: conversationId })
42
40
  const fieldsWithValueJoined = Object.fromEntries(
43
41
  Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')])
@@ -48,6 +46,7 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
48
46
  ...(!isEditing && replyRootId ? { reply_root: { id: replyRootId } } : {}),
49
47
  }
50
48
  if (!isEditing) {
49
+ const idempotentKey = insecureUUID()
51
50
  attributes.idempotent_key = idempotentKey
52
51
  startMessageCreationTracking(idempotentKey)
53
52
  }
@@ -72,7 +71,13 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
72
71
  })
73
72
  }
74
73
  },
75
- onMutate: async ({ text, attachments, idempotentKey }: ResolvedMessageMutationVariables) => {
74
+ onMutate: async ({
75
+ text,
76
+ attachments,
77
+ }: {
78
+ text: string
79
+ attachments?: DenormalizedAttachmentResourceForCreate[]
80
+ }) => {
76
81
  if (message && isEditing) {
77
82
  const optimisticMessage = optimisticallyUpdateMessage({
78
83
  conversationId,
@@ -91,7 +96,6 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
91
96
  attachments,
92
97
  currentPerson,
93
98
  replyRootId,
94
- idempotentKey,
95
99
  })
96
100
 
97
101
  return { message: optimisticMessage }
@@ -151,24 +155,7 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
151
155
  },
152
156
  })
153
157
 
154
- const resolveVariables = (
155
- variables: MessageMutationVariables
156
- ): ResolvedMessageMutationVariables => ({
157
- ...variables,
158
- idempotentKey: variables.idempotentKey ?? message?.idempotentKey ?? insecureUUID(),
159
- })
160
-
161
- const mutate = (
162
- variables: MessageMutationVariables,
163
- options?: Parameters<typeof mutation.mutate>[1]
164
- ) => mutation.mutate(resolveVariables(variables), options)
165
-
166
- const mutateAsync = (
167
- variables: MessageMutationVariables,
168
- options?: Parameters<typeof mutation.mutateAsync>[1]
169
- ) => mutation.mutateAsync(resolveVariables(variables), options)
170
-
171
- return { ...mutation, mutate, mutateAsync }
158
+ return mutation
172
159
  }
173
160
 
174
161
  /**
@@ -212,9 +212,11 @@ function FormContent({
212
212
  </View>
213
213
  <Divider />
214
214
  <Pressable style={styles.titleSection} onPress={handleTitleSectionPress}>
215
- <Heading variant="h3">Title</Heading>
215
+ <Heading variant="h3">
216
+ Title <Text style={styles.required}>*</Text>
217
+ </Heading>
216
218
  <AutoFocusAwareTextInput
217
- placeholder="Topic of conversation (required)"
219
+ placeholder="Topic of conversation"
218
220
  value={title}
219
221
  onChangeText={setTitle}
220
222
  style={styles.titleInput}
@@ -291,6 +293,9 @@ const useStyles = () => {
291
293
  fontSize: 18,
292
294
  color: colors.textColorDefaultPrimary,
293
295
  },
296
+ required: {
297
+ color: colors.statusErrorText,
298
+ },
294
299
  memberSection: {
295
300
  padding: sectionPadding,
296
301
  paddingBottom: 0,
@@ -1,3 +1,4 @@
1
+ import { date as formatDate } from '@planningcenter/datetime-fmt'
1
2
  import { HeaderTitle, HeaderTitleProps, PlatformPressable } from '@react-navigation/elements'
2
3
  import {
3
4
  CommonActions,
@@ -10,21 +11,24 @@ import {
10
11
  import React, { useCallback, useEffect, useRef, useState } from 'react'
11
12
  import { FlatList, Platform, StyleSheet, useWindowDimensions, View } from 'react-native'
12
13
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
13
- import { Badge, Icon } from '../components'
14
+ import { Badge, Icon, Text } from '../components'
14
15
  import { ConversationStartBanner } from '../components/conversation/conversation_start_banner'
15
16
  import { JumpToBottomButton } from '../components/conversation/jump_to_bottom_button'
17
+ import { Message } from '../components/conversation/message'
16
18
  import { MessageForm } from '../components/conversation/message_form'
17
- import { MessageList } from '../components/conversation/message_list'
18
19
  import {
19
20
  ConversationDisabledBanner,
20
21
  LeaderMessagesDisabledBanner,
21
22
  MemberMessagesDisabledBanner,
22
23
  } from '../components/conversation/messages_disabled_banners'
24
+ import { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'
25
+ import { SystemMessage } from '../components/conversation/system_message'
23
26
  import { TypingIndicator } from '../components/conversation/typing_indicator'
24
27
  import { KeyboardView } from '../components/display/keyboard_view'
25
28
  import { useHeaderSlotReserve } from '../components/display/platform_modal_header_buttons'
26
29
  import BlankState from '../components/primitive/blank_state_primitive'
27
30
  import { ConversationContextProvider } from '../contexts/conversation_context'
31
+ import { useTheme } from '../hooks'
28
32
  import { useMessageAvailableMembersAction } from '../hooks/direct_messages/use_message_available_members_action'
29
33
  import { useConversation } from '../hooks/use_conversation'
30
34
  import { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events'
@@ -41,6 +45,10 @@ import { useVideoModerationJoltEvents } from '../hooks/use_video_moderation_jolt
41
45
  import { ConversationResource } from '../types/resources/conversation'
42
46
  import { ConversationBadgeResource } from '../types/resources/conversation_badge'
43
47
  import { MessageResource } from '../types/resources/message'
48
+ import { getRelativeDateStatus } from '../utils/date'
49
+ import { groupMessages, type DateSeparator } from '../utils/group_messages'
50
+ import { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../utils/styles'
51
+ import { isSystemMessage } from '../utils/system_messages'
44
52
 
45
53
  export type ConversationRouteProps = {
46
54
  conversation_id: number
@@ -93,11 +101,16 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
93
101
  const { markHandledByMessageForm } = useVideoModerationJoltEvents()
94
102
  useEnsureConversationsRouteExists()
95
103
  useMarkLatestMessageRead({ conversation, messages })
96
- const noMessages = messages.length === 0
104
+ const messagesWithSeparators = groupMessages({
105
+ ms: messages,
106
+ inReplyScreen: !!reply_root_id,
107
+ })
108
+ const noMessages = messagesWithSeparators.length === 0
97
109
 
98
110
  const { repliesDisabled, memberAbility, badges, title } = conversation
99
111
  const canReply = memberAbility?.canReply
100
112
  const showLeaderDisabledReplyBanner = canReply && repliesDisabled
113
+ const canDeleteNonAuthoredMessages = memberAbility?.canDeleteNonAuthoredMessages ?? false
101
114
  const currentlyEditingMessage = messages.find(m => String(m.id) === String(editing_message_id))
102
115
  const replyRootAuthorFirstName = reply_root_author_name?.split(' ')[0]
103
116
  const replyHeaderTitle = replyRootAuthorFirstName
@@ -168,19 +181,54 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
168
181
  <ConversationStartBanner conversation={conversation} hasMessages={false} />
169
182
  </View>
170
183
  ) : (
171
- <MessageList
172
- listRef={listRef}
173
- conversation={conversation}
174
- conversationId={conversation_id}
175
- messages={messages}
176
- replyRootId={reply_root_id}
177
- hasNextPage={hasNextPage}
178
- onEndReached={() => fetchNextPage()}
184
+ <FlatList
185
+ inverted
186
+ ref={listRef}
187
+ contentContainerStyle={styles.listContainer}
188
+ data={messagesWithSeparators}
189
+ keyExtractor={item => item.id}
179
190
  onScroll={trackScroll}
180
191
  onLayout={onLayout}
181
192
  onContentSizeChange={onContentSizeChange}
182
193
  onScrollEndDrag={onScrollEndDrag}
183
194
  onMomentumScrollEnd={onMomentumScrollEnd}
195
+ scrollEventThrottle={10}
196
+ ListFooterComponent={
197
+ hasNextPage ? null : (
198
+ <ConversationStartBanner conversation={conversation} hasMessages={true} />
199
+ )
200
+ }
201
+ renderItem={({ item }) => {
202
+ if (item.type === 'DateSeparator') {
203
+ return <InlineDateSeparator {...item} />
204
+ }
205
+
206
+ if (item.type === 'ReplyShadowMessage') {
207
+ return (
208
+ <ReplyShadowMessage
209
+ {...(item as any)}
210
+ conversation_id={conversation_id}
211
+ inReplyScreen={!!reply_root_id}
212
+ />
213
+ )
214
+ }
215
+
216
+ if (isSystemMessage(item)) {
217
+ return <SystemMessage message={item} conversationId={conversation_id} />
218
+ }
219
+
220
+ return (
221
+ <Message
222
+ {...item}
223
+ canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages}
224
+ conversation_id={conversation_id}
225
+ latestReadMessageSortKey={conversation?.latestReadMessageSortKey}
226
+ inReplyScreen={!!reply_root_id}
227
+ />
228
+ )
229
+ }}
230
+ onEndReached={() => fetchNextPage()}
231
+ ListHeaderComponent={<View style={styles.listHeader} />}
184
232
  />
185
233
  )}
186
234
  <JumpToBottomButton onPress={handleReturnToBottom} visible={showJumpToBottomButton} />
@@ -247,6 +295,44 @@ function ConversationBottomBar({
247
295
  )
248
296
  }
249
297
 
298
+ function InlineDateSeparator({ date }: DateSeparator) {
299
+ const styles = useDateSeparatorStyles()
300
+ const { isThisYear } = getRelativeDateStatus(date)
301
+ const showYear = !isThisYear
302
+ const dateStamp = formatDate(date, { style: 'long', year: showYear })
303
+
304
+ return (
305
+ <View style={styles.container}>
306
+ <View style={styles.separator} />
307
+ <Text variant="footnote" style={styles.dateText}>
308
+ {dateStamp}
309
+ </Text>
310
+ <View style={styles.separator} />
311
+ </View>
312
+ )
313
+ }
314
+
315
+ const useDateSeparatorStyles = () => {
316
+ const theme = useTheme()
317
+ return StyleSheet.create({
318
+ container: {
319
+ alignItems: 'center',
320
+ flexDirection: 'row',
321
+ paddingHorizontal: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
322
+ paddingVertical: 16,
323
+ },
324
+ separator: {
325
+ flex: 1,
326
+ height: 1,
327
+ borderTopWidth: 1,
328
+ borderTopColor: theme.colors.borderColorDefaultBase,
329
+ },
330
+ dateText: {
331
+ paddingHorizontal: 8,
332
+ },
333
+ })
334
+ }
335
+
250
336
  interface ConversationScreenTitleProps extends HeaderTitleProps {
251
337
  conversation_id: number
252
338
  badge?: ConversationBadgeResource
@@ -343,6 +429,13 @@ const useStyles = () => {
343
429
  flex: 1,
344
430
  justifyContent: 'center',
345
431
  },
432
+ listContainer: {
433
+ paddingVertical: 12,
434
+ },
435
+ listHeader: {
436
+ // Just whitespace to provide space where the typing indicator can be
437
+ height: 16,
438
+ },
346
439
  })
347
440
  }
348
441