@planningcenter/chat-react-native 4.0.1-rc.3 → 4.2.0-rc.0

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 (29) hide show
  1. package/build/hooks/use_message_create_or_update.d.ts +99 -4
  2. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  3. package/build/hooks/use_message_create_or_update.js +47 -6
  4. package/build/hooks/use_message_create_or_update.js.map +1 -1
  5. package/build/types/resources/message.d.ts +1 -0
  6. package/build/types/resources/message.d.ts.map +1 -1
  7. package/build/types/resources/message.js.map +1 -1
  8. package/build/utils/cache/messages_cache.d.ts.map +1 -1
  9. package/build/utils/cache/messages_cache.js +4 -6
  10. package/build/utils/cache/messages_cache.js.map +1 -1
  11. package/build/utils/cache/optimistically_create_message.d.ts +2 -1
  12. package/build/utils/cache/optimistically_create_message.d.ts.map +1 -1
  13. package/build/utils/cache/optimistically_create_message.js +4 -2
  14. package/build/utils/cache/optimistically_create_message.js.map +1 -1
  15. package/build/utils/jolt/transform_message_event_data_to_message_resource.d.ts.map +1 -1
  16. package/build/utils/jolt/transform_message_event_data_to_message_resource.js +1 -0
  17. package/build/utils/jolt/transform_message_event_data_to_message_resource.js.map +1 -1
  18. package/build/utils/request/messages_data_options.d.ts.map +1 -1
  19. package/build/utils/request/messages_data_options.js +1 -1
  20. package/build/utils/request/messages_data_options.js.map +1 -1
  21. package/package.json +2 -2
  22. package/src/hooks/__tests__/use_message_create_or_update.test.tsx +110 -0
  23. package/src/hooks/use_message_create_or_update.ts +29 -16
  24. package/src/types/resources/message.ts +1 -0
  25. package/src/utils/cache/__tests__/messages_cache.test.ts +89 -0
  26. package/src/utils/cache/messages_cache.ts +6 -10
  27. package/src/utils/cache/optimistically_create_message.ts +3 -0
  28. package/src/utils/jolt/transform_message_event_data_to_message_resource.ts +1 -0
  29. package/src/utils/request/messages_data_options.ts +1 -0
@@ -5,11 +5,106 @@ interface Props {
5
5
  message?: MessageResource;
6
6
  replyRootId?: string | null;
7
7
  }
8
- export declare function useMessageCreateOrUpdate({ conversationId, message, replyRootId }: Props): import("@tanstack/react-query").UseMutationResult<ApiResource<MessageResource>, Error, {
8
+ interface MessageMutationVariables {
9
9
  text: string;
10
10
  attachments?: DenormalizedAttachmentResourceForCreate[];
11
- }, {
12
- message: MessageResource;
13
- }>;
11
+ idempotentKey?: string;
12
+ }
13
+ type ResolvedMessageMutationVariables = MessageMutationVariables & {
14
+ idempotentKey: string;
15
+ };
16
+ export declare function useMessageCreateOrUpdate({ conversationId, message, replyRootId }: Props): {
17
+ mutate: (variables: MessageMutationVariables, options?: Parameters<import("@tanstack/react-query").UseMutateFunction<ApiResource<MessageResource>, Error, ResolvedMessageMutationVariables, {
18
+ message: MessageResource;
19
+ }>>[1]) => void;
20
+ mutateAsync: (variables: MessageMutationVariables, options?: Parameters<import("@tanstack/react-query").UseMutateAsyncFunction<ApiResource<MessageResource>, Error, ResolvedMessageMutationVariables, {
21
+ message: MessageResource;
22
+ }>>[1]) => Promise<ApiResource<MessageResource>>;
23
+ data: undefined;
24
+ variables: undefined;
25
+ error: null;
26
+ isError: false;
27
+ isIdle: true;
28
+ isPending: false;
29
+ isSuccess: false;
30
+ status: "idle";
31
+ reset: () => void;
32
+ context: {
33
+ message: MessageResource;
34
+ } | undefined;
35
+ failureCount: number;
36
+ failureReason: Error | null;
37
+ isPaused: boolean;
38
+ submittedAt: number;
39
+ } | {
40
+ mutate: (variables: MessageMutationVariables, options?: Parameters<import("@tanstack/react-query").UseMutateFunction<ApiResource<MessageResource>, Error, ResolvedMessageMutationVariables, {
41
+ message: MessageResource;
42
+ }>>[1]) => void;
43
+ mutateAsync: (variables: MessageMutationVariables, options?: Parameters<import("@tanstack/react-query").UseMutateAsyncFunction<ApiResource<MessageResource>, Error, ResolvedMessageMutationVariables, {
44
+ message: MessageResource;
45
+ }>>[1]) => Promise<ApiResource<MessageResource>>;
46
+ data: undefined;
47
+ variables: ResolvedMessageMutationVariables;
48
+ error: null;
49
+ isError: false;
50
+ isIdle: false;
51
+ isPending: true;
52
+ isSuccess: false;
53
+ status: "pending";
54
+ reset: () => void;
55
+ context: {
56
+ message: MessageResource;
57
+ } | undefined;
58
+ failureCount: number;
59
+ failureReason: Error | null;
60
+ isPaused: boolean;
61
+ submittedAt: number;
62
+ } | {
63
+ mutate: (variables: MessageMutationVariables, options?: Parameters<import("@tanstack/react-query").UseMutateFunction<ApiResource<MessageResource>, Error, ResolvedMessageMutationVariables, {
64
+ message: MessageResource;
65
+ }>>[1]) => void;
66
+ mutateAsync: (variables: MessageMutationVariables, options?: Parameters<import("@tanstack/react-query").UseMutateAsyncFunction<ApiResource<MessageResource>, Error, ResolvedMessageMutationVariables, {
67
+ message: MessageResource;
68
+ }>>[1]) => Promise<ApiResource<MessageResource>>;
69
+ data: undefined;
70
+ error: Error;
71
+ variables: ResolvedMessageMutationVariables;
72
+ isError: true;
73
+ isIdle: false;
74
+ isPending: false;
75
+ isSuccess: false;
76
+ status: "error";
77
+ reset: () => void;
78
+ context: {
79
+ message: MessageResource;
80
+ } | undefined;
81
+ failureCount: number;
82
+ failureReason: Error | null;
83
+ isPaused: boolean;
84
+ submittedAt: number;
85
+ } | {
86
+ mutate: (variables: MessageMutationVariables, options?: Parameters<import("@tanstack/react-query").UseMutateFunction<ApiResource<MessageResource>, Error, ResolvedMessageMutationVariables, {
87
+ message: MessageResource;
88
+ }>>[1]) => void;
89
+ mutateAsync: (variables: MessageMutationVariables, options?: Parameters<import("@tanstack/react-query").UseMutateAsyncFunction<ApiResource<MessageResource>, Error, ResolvedMessageMutationVariables, {
90
+ message: MessageResource;
91
+ }>>[1]) => Promise<ApiResource<MessageResource>>;
92
+ data: ApiResource<MessageResource>;
93
+ error: null;
94
+ variables: ResolvedMessageMutationVariables;
95
+ isError: false;
96
+ isIdle: false;
97
+ isPending: false;
98
+ isSuccess: true;
99
+ status: "success";
100
+ reset: () => void;
101
+ context: {
102
+ message: MessageResource;
103
+ } | undefined;
104
+ failureCount: number;
105
+ failureReason: Error | null;
106
+ isPaused: boolean;
107
+ submittedAt: number;
108
+ };
14
109
  export {};
15
110
  //# sourceMappingURL=use_message_create_or_update.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use_message_create_or_update.d.ts","sourceRoot":"","sources":["../../src/hooks/use_message_create_or_update.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,WAAW,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AACtE,OAAO,EAAE,uCAAuC,EAAE,MAAM,gEAAgE,CAAA;AAexH,UAAU,KAAK;IACb,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,eAAe,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAED,wBAAgB,wBAAwB,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,EAAE,EAAE,KAAK;UAqD5E,MAAM;kBACE,uCAAuC,EAAE;;;GAgF5D"}
1
+ {"version":3,"file":"use_message_create_or_update.d.ts","sourceRoot":"","sources":["../../src/hooks/use_message_create_or_update.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,WAAW,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AACtE,OAAO,EAAE,uCAAuC,EAAE,MAAM,gEAAgE,CAAA;AAexH,UAAU,KAAK;IACb,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,eAAe,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAED,UAAU,wBAAwB;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,uCAAuC,EAAE,CAAA;IACvD,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,KAAK,gCAAgC,GAAG,wBAAwB,GAAG;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5F,wBAAgB,wBAAwB,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,EAAE,EAAE,KAAK;wBAiIzE,wBAAwB,YACzB,UAAU;;OAAwB,CAAC,CAAC,CAAC;6BAIpC,wBAAwB,YACzB,UAAU;;OAA6B,CAAC,CAAC,CAAC;;;;;;;;;;;;;;;;;;wBANzC,wBAAwB,YACzB,UAAU;;OAAwB,CAAC,CAAC,CAAC;6BAIpC,wBAAwB,YACzB,UAAU;;OAA6B,CAAC,CAAC,CAAC;;;;;;;;;;;;;;;;;;wBANzC,wBAAwB,YACzB,UAAU;;OAAwB,CAAC,CAAC,CAAC;6BAIpC,wBAAwB,YACzB,UAAU;;OAA6B,CAAC,CAAC,CAAC;;;;;;;;;;;;;;;;;;wBANzC,wBAAwB,YACzB,UAAU;;OAAwB,CAAC,CAAC,CAAC;6BAIpC,wBAAwB,YACzB,UAAU;;OAA6B,CAAC,CAAC,CAAC;;;;;;;;;;;;;;;;;EAIvD"}
@@ -10,7 +10,7 @@ import { getMessagesQueryKey, getMessagesRequestArgs } from '../utils/request/ge
10
10
  import { useApiClient } from './use_api_client';
11
11
  import { useCurrentPerson } from './use_current_person';
12
12
  export function useMessageCreateOrUpdate(t0) {
13
- const $ = _c(23);
13
+ const $ = _c(33);
14
14
  const {
15
15
  conversationId,
16
16
  message,
@@ -33,7 +33,8 @@ export function useMessageCreateOrUpdate(t0) {
33
33
  t2 = t3 => {
34
34
  const {
35
35
  text,
36
- attachments
36
+ attachments,
37
+ idempotentKey
37
38
  } = t3;
38
39
  const requestParams = getMessagesRequestArgs({
39
40
  conversation_id: conversationId
@@ -51,7 +52,6 @@ export function useMessageCreateOrUpdate(t0) {
51
52
  } : {})
52
53
  };
53
54
  if (!isEditing) {
54
- const idempotentKey = insecureUUID();
55
55
  attributes.idempotent_key = idempotentKey;
56
56
  startMessageCreationTracking(idempotentKey);
57
57
  }
@@ -89,7 +89,8 @@ export function useMessageCreateOrUpdate(t0) {
89
89
  t3 = async t4 => {
90
90
  const {
91
91
  text: text_0,
92
- attachments: attachments_0
92
+ attachments: attachments_0,
93
+ idempotentKey: idempotentKey_0
93
94
  } = t4;
94
95
  if (message && isEditing) {
95
96
  const optimisticMessage = optimisticallyUpdateMessage({
@@ -108,7 +109,8 @@ export function useMessageCreateOrUpdate(t0) {
108
109
  text: text_0,
109
110
  attachments: attachments_0,
110
111
  currentPerson,
111
- replyRootId
112
+ replyRootId,
113
+ idempotentKey: idempotentKey_0
112
114
  });
113
115
  return {
114
116
  message: optimisticMessage_0
@@ -193,7 +195,46 @@ export function useMessageCreateOrUpdate(t0) {
193
195
  t6 = $[22];
194
196
  }
195
197
  const mutation = useMutation(t6);
196
- return mutation;
198
+ let t7;
199
+ if ($[23] !== message?.idempotentKey) {
200
+ t7 = variables_1 => ({
201
+ ...variables_1,
202
+ idempotentKey: variables_1.idempotentKey ?? message?.idempotentKey ?? insecureUUID()
203
+ });
204
+ $[23] = message?.idempotentKey;
205
+ $[24] = t7;
206
+ } else {
207
+ t7 = $[24];
208
+ }
209
+ const resolveVariables = t7;
210
+ let mutate;
211
+ let mutateAsync;
212
+ if ($[25] !== mutation || $[26] !== resolveVariables) {
213
+ mutate = (variables_2, options) => mutation.mutate(resolveVariables(variables_2), options);
214
+ mutateAsync = (variables_3, options_0) => mutation.mutateAsync(resolveVariables(variables_3), options_0);
215
+ $[25] = mutation;
216
+ $[26] = resolveVariables;
217
+ $[27] = mutate;
218
+ $[28] = mutateAsync;
219
+ } else {
220
+ mutate = $[27];
221
+ mutateAsync = $[28];
222
+ }
223
+ let t8;
224
+ if ($[29] !== mutate || $[30] !== mutateAsync || $[31] !== mutation) {
225
+ t8 = {
226
+ ...mutation,
227
+ mutate,
228
+ mutateAsync
229
+ };
230
+ $[29] = mutate;
231
+ $[30] = mutateAsync;
232
+ $[31] = mutation;
233
+ $[32] = t8;
234
+ } else {
235
+ t8 = $[32];
236
+ }
237
+ return t8;
197
238
  }
198
239
 
199
240
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"use_message_create_or_update.js","names":["useMutation","chatQueryClient","deleteRecordInPagesData","Haptic","updateOrCreateRecordInPagesData","updateRecordInPagesData","isNewMessage","mergeMessageUpdate","optimisticallyCreateMessage","optimisticallyUpdateMessage","startMessageCreationTracking","getMessagesQueryKey","getMessagesRequestArgs","useApiClient","useCurrentPerson","useMessageCreateOrUpdate","t0","$","_c","conversationId","message","replyRootId","messageId","id","t1","isEditing","apiClient","currentPerson","t2","t3","text","attachments","requestParams","conversation_id","fieldsWithValueJoined","Object","fromEntries","entries","data","fields","map","_temp","attributes","reply_root","idempotentKey","insecureUUID","idempotent_key","type","chat","patch","url","post","t4","text_0","attachments_0","optimisticMessage","optimisticMessage_0","t5","error","variables","context","optimisticMessage_1","notificationError","queryKey","reply_root_id","setQueryData","data_0","record","processRecord","_next","prev","pending","result","variables_0","context_0","optimisticMessage_2","updatedMessage","queryKey_0","data_1","data_2","_temp2","t6","mutationFn","onMutate","onError","onSuccess","mutation","current","k","v","join","replace","Math","floor","random","toString"],"sources":["../../src/hooks/use_message_create_or_update.ts"],"sourcesContent":["import { InfiniteData, useMutation } from '@tanstack/react-query'\nimport { chatQueryClient } from '../contexts/api_provider'\nimport { ApiCollection, ApiResource, MessageResource } from '../types'\nimport { DenormalizedAttachmentResourceForCreate } from '../types/resources/denormalized_attachment_resource_for_create'\nimport {\n deleteRecordInPagesData,\n Haptic,\n updateOrCreateRecordInPagesData,\n updateRecordInPagesData,\n} from '../utils'\nimport { isNewMessage, mergeMessageUpdate } from '../utils/cache/messages_cache'\nimport { optimisticallyCreateMessage } from '../utils/cache/optimistically_create_message'\nimport { optimisticallyUpdateMessage } from '../utils/cache/optimistically_update_message'\nimport { startMessageCreationTracking } from '../utils/performance_tracking'\nimport { getMessagesQueryKey, getMessagesRequestArgs } from '../utils/request/get_messages'\nimport { useApiClient } from './use_api_client'\nimport { useCurrentPerson } from './use_current_person'\n\ninterface Props {\n conversationId: number\n message?: MessageResource\n replyRootId?: string | null\n}\n\nexport function useMessageCreateOrUpdate({ conversationId, message, replyRootId }: Props) {\n const messageId = message?.id || null\n const isEditing = !isNewMessage(message)\n const apiClient = useApiClient()\n const currentPerson = useCurrentPerson()\n\n const mutation = useMutation({\n mutationFn: ({\n text,\n attachments,\n }: {\n text: string\n attachments?: DenormalizedAttachmentResourceForCreate[]\n }) => {\n const requestParams = getMessagesRequestArgs({ conversation_id: conversationId })\n const fieldsWithValueJoined = Object.fromEntries(\n Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')])\n )\n let attributes: any = {\n text,\n ...(attachments ? { attachments } : {}),\n ...(!isEditing && replyRootId ? { reply_root: { id: replyRootId } } : {}),\n }\n if (!isEditing) {\n const idempotentKey = insecureUUID()\n attributes.idempotent_key = idempotentKey\n startMessageCreationTracking(idempotentKey)\n }\n const data = {\n ...requestParams.data,\n data: {\n type: 'Message',\n attributes,\n },\n fields: fieldsWithValueJoined,\n }\n\n if (isEditing) {\n return apiClient.chat.patch<ApiResource<MessageResource>>({\n url: `/me/conversations/${conversationId}/messages/${messageId}`,\n data,\n })\n } else {\n return apiClient.chat.post<ApiResource<MessageResource>>({\n url: `/me/conversations/${conversationId}/messages`,\n data,\n })\n }\n },\n onMutate: async ({\n text,\n attachments,\n }: {\n text: string\n attachments?: DenormalizedAttachmentResourceForCreate[]\n }) => {\n if (message && isEditing) {\n const optimisticMessage = optimisticallyUpdateMessage({\n conversationId,\n message,\n text,\n replyRootId,\n })\n\n return { message: optimisticMessage }\n }\n\n const optimisticMessage = optimisticallyCreateMessage({\n conversationId,\n message,\n text,\n attachments,\n currentPerson,\n replyRootId,\n })\n\n return { message: optimisticMessage }\n },\n onError: (error, variables, context) => {\n const { message: optimisticMessage } = context || {}\n Haptic.notificationError()\n\n // Add error to the optimistic message from the cache on error\n if (optimisticMessage) {\n const queryKey = getMessagesQueryKey({\n conversation_id: conversationId,\n reply_root_id: replyRootId,\n })\n chatQueryClient.setQueryData(\n queryKey,\n (data: InfiniteData<ApiCollection<MessageResource>> | undefined) =>\n updateRecordInPagesData({\n data,\n record: optimisticMessage,\n processRecord: (_next, prev) => ({\n ...prev,\n error: error.message || 'Failed to send message',\n pending: false, // Mark as no longer pending\n }),\n })\n )\n }\n },\n onSuccess: (result: ApiResource<MessageResource>, variables, context) => {\n const { message: optimisticMessage } = context || {}\n const updatedMessage = result.data\n type QueryData = InfiniteData<ApiCollection<MessageResource>>\n const queryKey = getMessagesQueryKey({\n conversation_id: conversationId,\n reply_root_id: replyRootId,\n })\n\n // First remove the optimistic message if it exists\n if (optimisticMessage) {\n chatQueryClient.setQueryData<QueryData>(queryKey, data =>\n deleteRecordInPagesData({\n data,\n record: optimisticMessage,\n })\n )\n }\n\n // Then add the real message\n chatQueryClient.setQueryData<QueryData>(queryKey, data =>\n updateOrCreateRecordInPagesData({\n data,\n record: updatedMessage,\n processRecord: (record, current) => mergeMessageUpdate(record, current),\n })\n )\n },\n })\n\n return mutation\n}\n\n/**\n * Generate a random UUID (v4) for idempotent keys.\n * Uses Math.random, which is not cryptographically secure.\n * An actual crypto library requires native dependencies.\n * This is OK for now since idempotent keys are not security-sensitive\n * or need to be guaranteed unique.\n * They are short lived and we use it in combination with the message's creator_id so\n * their impact is scoped only the current user.\n */\nfunction insecureUUID(): string {\n return 'xxxxxxxx-xxxx-4xxx-Nxxx-xxxxxxxxxxxx'\n .replace(/x/g, () => Math.floor(Math.random() * 16).toString(16))\n .replace(/N/g, () => (Math.floor(Math.random() * 4) + 8).toString(16))\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"use_message_create_or_update.js","names":["useMutation","chatQueryClient","deleteRecordInPagesData","Haptic","updateOrCreateRecordInPagesData","updateRecordInPagesData","isNewMessage","mergeMessageUpdate","optimisticallyCreateMessage","optimisticallyUpdateMessage","startMessageCreationTracking","getMessagesQueryKey","getMessagesRequestArgs","useApiClient","useCurrentPerson","useMessageCreateOrUpdate","t0","$","_c","conversationId","message","replyRootId","messageId","id","t1","isEditing","apiClient","currentPerson","t2","t3","text","attachments","idempotentKey","requestParams","conversation_id","fieldsWithValueJoined","Object","fromEntries","entries","data","fields","map","_temp","attributes","reply_root","idempotent_key","type","chat","patch","url","post","t4","text_0","attachments_0","idempotentKey_0","optimisticMessage","optimisticMessage_0","t5","error","variables","context","optimisticMessage_1","notificationError","queryKey","reply_root_id","setQueryData","data_0","record","processRecord","_next","prev","pending","result","variables_0","context_0","optimisticMessage_2","updatedMessage","queryKey_0","data_1","data_2","_temp2","t6","mutationFn","onMutate","onError","onSuccess","mutation","t7","variables_1","insecureUUID","resolveVariables","mutate","mutateAsync","variables_2","options","variables_3","options_0","t8","current","k","v","join","replace","Math","floor","random","toString"],"sources":["../../src/hooks/use_message_create_or_update.ts"],"sourcesContent":["import { InfiniteData, useMutation } from '@tanstack/react-query'\nimport { chatQueryClient } from '../contexts/api_provider'\nimport { ApiCollection, ApiResource, MessageResource } from '../types'\nimport { DenormalizedAttachmentResourceForCreate } from '../types/resources/denormalized_attachment_resource_for_create'\nimport {\n deleteRecordInPagesData,\n Haptic,\n updateOrCreateRecordInPagesData,\n updateRecordInPagesData,\n} from '../utils'\nimport { isNewMessage, mergeMessageUpdate } from '../utils/cache/messages_cache'\nimport { optimisticallyCreateMessage } from '../utils/cache/optimistically_create_message'\nimport { optimisticallyUpdateMessage } from '../utils/cache/optimistically_update_message'\nimport { startMessageCreationTracking } from '../utils/performance_tracking'\nimport { getMessagesQueryKey, getMessagesRequestArgs } from '../utils/request/get_messages'\nimport { useApiClient } from './use_api_client'\nimport { useCurrentPerson } from './use_current_person'\n\ninterface Props {\n conversationId: number\n message?: MessageResource\n replyRootId?: string | null\n}\n\ninterface MessageMutationVariables {\n text: string\n attachments?: DenormalizedAttachmentResourceForCreate[]\n idempotentKey?: string\n}\n\ntype ResolvedMessageMutationVariables = MessageMutationVariables & { idempotentKey: string }\n\nexport function useMessageCreateOrUpdate({ conversationId, message, replyRootId }: Props) {\n const messageId = message?.id || null\n const isEditing = !isNewMessage(message)\n const apiClient = useApiClient()\n const currentPerson = useCurrentPerson()\n\n const mutation = useMutation({\n mutationFn: ({ text, attachments, idempotentKey }: ResolvedMessageMutationVariables) => {\n const requestParams = getMessagesRequestArgs({ conversation_id: conversationId })\n const fieldsWithValueJoined = Object.fromEntries(\n Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')])\n )\n let attributes: any = {\n text,\n ...(attachments ? { attachments } : {}),\n ...(!isEditing && replyRootId ? { reply_root: { id: replyRootId } } : {}),\n }\n if (!isEditing) {\n attributes.idempotent_key = idempotentKey\n startMessageCreationTracking(idempotentKey)\n }\n const data = {\n ...requestParams.data,\n data: {\n type: 'Message',\n attributes,\n },\n fields: fieldsWithValueJoined,\n }\n\n if (isEditing) {\n return apiClient.chat.patch<ApiResource<MessageResource>>({\n url: `/me/conversations/${conversationId}/messages/${messageId}`,\n data,\n })\n } else {\n return apiClient.chat.post<ApiResource<MessageResource>>({\n url: `/me/conversations/${conversationId}/messages`,\n data,\n })\n }\n },\n onMutate: async ({ text, attachments, idempotentKey }: ResolvedMessageMutationVariables) => {\n if (message && isEditing) {\n const optimisticMessage = optimisticallyUpdateMessage({\n conversationId,\n message,\n text,\n replyRootId,\n })\n\n return { message: optimisticMessage }\n }\n\n const optimisticMessage = optimisticallyCreateMessage({\n conversationId,\n message,\n text,\n attachments,\n currentPerson,\n replyRootId,\n idempotentKey,\n })\n\n return { message: optimisticMessage }\n },\n onError: (error, variables, context) => {\n const { message: optimisticMessage } = context || {}\n Haptic.notificationError()\n\n // Add error to the optimistic message from the cache on error\n if (optimisticMessage) {\n const queryKey = getMessagesQueryKey({\n conversation_id: conversationId,\n reply_root_id: replyRootId,\n })\n chatQueryClient.setQueryData(\n queryKey,\n (data: InfiniteData<ApiCollection<MessageResource>> | undefined) =>\n updateRecordInPagesData({\n data,\n record: optimisticMessage,\n processRecord: (_next, prev) => ({\n ...prev,\n error: error.message || 'Failed to send message',\n pending: false, // Mark as no longer pending\n }),\n })\n )\n }\n },\n onSuccess: (result: ApiResource<MessageResource>, variables, context) => {\n const { message: optimisticMessage } = context || {}\n const updatedMessage = result.data\n type QueryData = InfiniteData<ApiCollection<MessageResource>>\n const queryKey = getMessagesQueryKey({\n conversation_id: conversationId,\n reply_root_id: replyRootId,\n })\n\n // First remove the optimistic message if it exists\n if (optimisticMessage) {\n chatQueryClient.setQueryData<QueryData>(queryKey, data =>\n deleteRecordInPagesData({\n data,\n record: optimisticMessage,\n })\n )\n }\n\n // Then add the real message\n chatQueryClient.setQueryData<QueryData>(queryKey, data =>\n updateOrCreateRecordInPagesData({\n data,\n record: updatedMessage,\n processRecord: (record, current) => mergeMessageUpdate(record, current),\n })\n )\n },\n })\n\n const resolveVariables = (\n variables: MessageMutationVariables\n ): ResolvedMessageMutationVariables => ({\n ...variables,\n idempotentKey: variables.idempotentKey ?? message?.idempotentKey ?? insecureUUID(),\n })\n\n const mutate = (\n variables: MessageMutationVariables,\n options?: Parameters<typeof mutation.mutate>[1]\n ) => mutation.mutate(resolveVariables(variables), options)\n\n const mutateAsync = (\n variables: MessageMutationVariables,\n options?: Parameters<typeof mutation.mutateAsync>[1]\n ) => mutation.mutateAsync(resolveVariables(variables), options)\n\n return { ...mutation, mutate, mutateAsync }\n}\n\n/**\n * Generate a random UUID (v4) for idempotent keys.\n * Uses Math.random, which is not cryptographically secure.\n * An actual crypto library requires native dependencies.\n * This is OK for now since idempotent keys are not security-sensitive\n * or need to be guaranteed unique.\n * They are short lived and we use it in combination with the message's creator_id so\n * their impact is scoped only the current user.\n */\nfunction insecureUUID(): string {\n return 'xxxxxxxx-xxxx-4xxx-Nxxx-xxxxxxxxxxxx'\n .replace(/x/g, () => Math.floor(Math.random() * 16).toString(16))\n .replace(/N/g, () => (Math.floor(Math.random() * 4) + 8).toString(16))\n}\n"],"mappings":"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","ignoreList":[]}
@@ -16,6 +16,7 @@ export interface MessageResource {
16
16
  reactionCounts: ReactionCountResource[];
17
17
  replyCount: number;
18
18
  replyRootId?: string | null;
19
+ idempotentKey?: string | null;
19
20
  messageType: string;
20
21
  personIdsForSystemEvent?: number[] | null;
21
22
  systemTextParts?: {
@@ -1 +1 @@
1
- {"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../../src/types/resources/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,8BAA8B,EAAE,MAAM,oCAAoC,CAAA;AACnF,OAAO,EAAE,uCAAuC,EAAE,MAAM,+CAA+C,CAAA;AACvG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAC9C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAEvD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,CAAA;IACf,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,IAAI,EAAE,OAAO,CAAA;IACb,WAAW,EAAE,8BAA8B,EAAE,CAAA;IAC7C,MAAM,EAAE,cAAc,CAAA;IACtB,cAAc,EAAE,qBAAqB,EAAE,CAAA;IACvC,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,uBAAuB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;IACzC,eAAe,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAGnF,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,cAAc,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI,CAAA;IACnD,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,wBAAwB,CAAC,EAAE,OAAO,CAAA;IAGlC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oBAAoB,CAAC,EAAE,uCAAuC,EAAE,CAAA;CACjE"}
1
+ {"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../../src/types/resources/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,8BAA8B,EAAE,MAAM,oCAAoC,CAAA;AACnF,OAAO,EAAE,uCAAuC,EAAE,MAAM,+CAA+C,CAAA;AACvG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAC9C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAEvD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,CAAA;IACf,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,IAAI,EAAE,OAAO,CAAA;IACb,WAAW,EAAE,8BAA8B,EAAE,CAAA;IAC7C,MAAM,EAAE,cAAc,CAAA;IACtB,cAAc,EAAE,qBAAqB,EAAE,CAAA;IACvC,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,WAAW,EAAE,MAAM,CAAA;IACnB,uBAAuB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;IACzC,eAAe,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IAGnF,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,cAAc,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI,CAAA;IACnD,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,wBAAwB,CAAC,EAAE,OAAO,CAAA;IAGlC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oBAAoB,CAAC,EAAE,uCAAuC,EAAE,CAAA;CACjE"}
@@ -1 +1 @@
1
- {"version":3,"file":"message.js","names":[],"sources":["../../../src/types/resources/message.ts"],"sourcesContent":["import { DenormalizedAttachmentResource } from './denormalized_attachment_resource'\nimport { DenormalizedAttachmentResourceForCreate } from './denormalized_attachment_resource_for_create'\nimport type { PersonResource } from './person'\nimport type { ReactionCountResource } from './reaction'\n\nexport interface MessageResource {\n type: 'Message'\n id: string\n text: string | null\n html: string\n createdAt: string\n deletedAt: string | null\n textEditedAt: string | null\n mine: boolean\n attachments: DenormalizedAttachmentResource[]\n author: PersonResource\n reactionCounts: ReactionCountResource[]\n replyCount: number\n replyRootId?: string | null\n messageType: string\n personIdsForSystemEvent?: number[] | null\n systemTextParts?: { names: string[]; overflowCount: number; action: string } | null\n\n // Custom Local Properties we set for rendering\n renderAuthor?: boolean\n renderTime?: boolean\n myLatestInConversation?: boolean\n lastInGroup?: boolean\n threadPosition?: 'first' | 'center' | 'last' | null\n prevIsMyReply?: boolean\n nextIsMyReply?: boolean\n nextRendersAuthor?: boolean\n isReplyShadowMessage?: boolean\n nextIsReplyShadowMessage?: boolean\n\n // Properties for mutation state\n pending?: boolean // Indicates if the message is optimistically created and pending server response\n error?: string // Error message if the message failed to send\n attachmentsForCreate?: DenormalizedAttachmentResourceForCreate[]\n}\n"],"mappings":"","ignoreList":[]}
1
+ {"version":3,"file":"message.js","names":[],"sources":["../../../src/types/resources/message.ts"],"sourcesContent":["import { DenormalizedAttachmentResource } from './denormalized_attachment_resource'\nimport { DenormalizedAttachmentResourceForCreate } from './denormalized_attachment_resource_for_create'\nimport type { PersonResource } from './person'\nimport type { ReactionCountResource } from './reaction'\n\nexport interface MessageResource {\n type: 'Message'\n id: string\n text: string | null\n html: string\n createdAt: string\n deletedAt: string | null\n textEditedAt: string | null\n mine: boolean\n attachments: DenormalizedAttachmentResource[]\n author: PersonResource\n reactionCounts: ReactionCountResource[]\n replyCount: number\n replyRootId?: string | null\n idempotentKey?: string | null\n messageType: string\n personIdsForSystemEvent?: number[] | null\n systemTextParts?: { names: string[]; overflowCount: number; action: string } | null\n\n // Custom Local Properties we set for rendering\n renderAuthor?: boolean\n renderTime?: boolean\n myLatestInConversation?: boolean\n lastInGroup?: boolean\n threadPosition?: 'first' | 'center' | 'last' | null\n prevIsMyReply?: boolean\n nextIsMyReply?: boolean\n nextRendersAuthor?: boolean\n isReplyShadowMessage?: boolean\n nextIsReplyShadowMessage?: boolean\n\n // Properties for mutation state\n pending?: boolean // Indicates if the message is optimistically created and pending server response\n error?: string // Error message if the message failed to send\n attachmentsForCreate?: DenormalizedAttachmentResourceForCreate[]\n}\n"],"mappings":"","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"messages_cache.d.ts","sourceRoot":"","sources":["../../../src/utils/cache/messages_cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAEjE,OAAO,EAA8B,eAAe,EAAE,MAAM,aAAa,CAAA;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAM3D,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,eAAe,EACxB,KAAK,EAAE,iBAAiB,GAAG,iBAAiB,QAkC7C;AAID,wBAAgB,gCAAgC,CAC9C,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,eAAe,QAgBzB;AAED,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,OAAO,EAAE,EACnB,KAAK,EAAE,iBAAiB,EACxB,eAAe,EAAE,MAAM,QAuCxB;AAGD,wBAAgB,oBAAoB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAEvE;AACD,wBAAgB,YAAY,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAE/D;AAED,wBAAgB,2BAA2B,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,mCAMtF;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,eAAe,EACvB,OAAO,CAAC,EAAE,eAAe,GACxB,eAAe,CAWjB"}
1
+ {"version":3,"file":"messages_cache.d.ts","sourceRoot":"","sources":["../../../src/utils/cache/messages_cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAEjE,OAAO,EAA8B,eAAe,EAAE,MAAM,aAAa,CAAA;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAM3D,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,eAAe,EACxB,KAAK,EAAE,iBAAiB,GAAG,iBAAiB,QA8B7C;AAID,wBAAgB,gCAAgC,CAC9C,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,eAAe,QAgBzB;AAED,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,OAAO,EAAE,EACnB,KAAK,EAAE,iBAAiB,EACxB,eAAe,EAAE,MAAM,QAuCxB;AAGD,wBAAgB,oBAAoB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAEvE;AACD,wBAAgB,YAAY,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAE/D;AAED,wBAAgB,2BAA2B,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,mCAMtF;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,eAAe,EACvB,OAAO,CAAC,EAAE,eAAe,GACxB,eAAe,CAWjB"}
@@ -6,16 +6,14 @@ import { deleteRecordInPagesData } from './page_mutations';
6
6
  export function updateCacheWithMessage(queryClient, queryKey, message, event) {
7
7
  queryClient.setQueryData(queryKey, prev => {
8
8
  if (event === 'message.created') {
9
- // Before adding the new message, remove any pending temporary messages
10
- // with matching text to prevent duplicates from race conditions
9
+ // Before adding the new message, remove its pending temporary counterpart.
10
+ // The Jolt echo can arrive before the HTTP response has swapped it out.
11
11
  let dataAfterTempRemoval = prev;
12
- if (prev && message.text && message.mine) {
12
+ if (prev && message.mine && message.idempotentKey) {
13
13
  dataAfterTempRemoval = deleteRecordInPagesData({
14
14
  data: prev,
15
15
  record: message,
16
- matchFn: (existingMessage, _record) => {
17
- return isTemporaryMessageId(existingMessage.id) && existingMessage.text === message.text && existingMessage.mine;
18
- }
16
+ matchFn: existingMessage => isTemporaryMessageId(existingMessage.id) && existingMessage.idempotentKey === message.idempotentKey
19
17
  });
20
18
  }
21
19
  return updateOrCreateRecordInPagesData({
@@ -1 +1 @@
1
- {"version":3,"file":"messages_cache.js","names":["getRequestQueryKey","transformReactionEventDataToReactionCountResource","getMessagesRequestArgs","updateOrCreateRecordInPagesData","updateRecordInPagesData","deleteRecordInPagesData","updateCacheWithMessage","queryClient","queryKey","message","event","setQueryData","prev","dataAfterTempRemoval","text","mine","data","record","matchFn","existingMessage","_record","isTemporaryMessageId","id","processRecord","current","mergeMessageUpdate","updateCacheWithIndividualMessage","links","meta","updateCacheWithReaction","currentPersonId","message_sort_key","oldMessage","reactionCounts","foundMatch","newReactionCounts","map","reactionCount","value","oldData","newReactionCount","count","messageId","endsWith","isNewMessage","getThreadedMessagesQueryKey","conversationId","replyRootId","requestArgs","conversation_id","reply_root_id"],"sources":["../../../src/utils/cache/messages_cache.ts"],"sourcesContent":["import { InfiniteData, QueryClient } from '@tanstack/react-query'\nimport { getRequestQueryKey } from '../../hooks/use_suspense_api'\nimport { ApiCollection, ApiResource, MessageResource } from '../../types'\nimport { JoltReactionEvent } from '../../types/jolt_events'\nimport { transformReactionEventDataToReactionCountResource } from '../jolt/transform_reaction_event_data_to_reaction_count_resource'\nimport { getMessagesRequestArgs } from '../request/get_messages'\nimport { updateOrCreateRecordInPagesData, updateRecordInPagesData } from './page_mutations'\nimport { deleteRecordInPagesData } from './page_mutations'\n\nexport function updateCacheWithMessage(\n queryClient: QueryClient,\n queryKey: unknown[],\n message: MessageResource,\n event: 'message.created' | 'message.updated'\n) {\n queryClient.setQueryData<MessagesQueryData>(queryKey, prev => {\n if (event === 'message.created') {\n // Before adding the new message, remove any pending temporary messages\n // with matching text to prevent duplicates from race conditions\n let dataAfterTempRemoval = prev\n if (prev && message.text && message.mine) {\n dataAfterTempRemoval = deleteRecordInPagesData({\n data: prev,\n record: message,\n matchFn: (existingMessage, _record) => {\n return (\n isTemporaryMessageId(existingMessage.id) &&\n existingMessage.text === message.text &&\n existingMessage.mine\n )\n },\n })\n }\n\n return updateOrCreateRecordInPagesData({\n data: dataAfterTempRemoval,\n record: message,\n processRecord: (record, current) => mergeMessageUpdate(record, current),\n })\n } else {\n return updateRecordInPagesData({\n data: prev,\n record: message,\n processRecord: (record, current) => mergeMessageUpdate(record, current),\n })\n }\n })\n}\n\ntype IndividualMessageQueryData = ApiResource<MessageResource>\n\nexport function updateCacheWithIndividualMessage(\n queryClient: QueryClient,\n queryKey: unknown[],\n message: MessageResource\n) {\n queryClient.setQueryData<IndividualMessageQueryData>(queryKey, prev => {\n if (!prev) {\n return {\n data: message,\n links: {},\n meta: {},\n }\n }\n\n return {\n ...prev,\n data: mergeMessageUpdate(message, prev.data),\n }\n })\n}\n\nexport function updateCacheWithReaction(\n queryClient: QueryClient,\n queryKey: unknown[],\n event: JoltReactionEvent,\n currentPersonId: number\n) {\n const message = { id: event.data.data.message_sort_key } as MessageResource\n queryClient.setQueryData<MessagesQueryData>(queryKey, prev =>\n updateRecordInPagesData({\n data: prev,\n record: message,\n processRecord: (record, oldMessage) => {\n const reactionCounts = oldMessage.reactionCounts || []\n let foundMatch = false\n let newReactionCounts = reactionCounts.map(reactionCount => {\n if (reactionCount.value === event.data.data.value) {\n foundMatch = true\n return transformReactionEventDataToReactionCountResource({\n data: event.data.data,\n oldData: reactionCount,\n event: event.event,\n currentPersonId,\n })\n }\n return reactionCount\n })\n\n if (!foundMatch) {\n const newReactionCount = transformReactionEventDataToReactionCountResource({\n data: event.data.data,\n event: event.event,\n currentPersonId,\n })\n\n if (newReactionCount?.count) {\n newReactionCounts = [...newReactionCounts, newReactionCount]\n }\n }\n\n return { ...oldMessage, reactionCounts: newReactionCounts }\n },\n })\n )\n}\n\ntype MessagesQueryData = InfiniteData<ApiCollection<MessageResource>>\nexport function isTemporaryMessageId(messageId?: string | null): boolean {\n return !!messageId && messageId.endsWith('-temp')\n}\nexport function isNewMessage(message?: MessageResource): boolean {\n return !message?.id || isTemporaryMessageId(message.id)\n}\n\nexport function getThreadedMessagesQueryKey(conversationId: number, replyRootId: string) {\n const requestArgs = getMessagesRequestArgs({\n conversation_id: conversationId,\n reply_root_id: replyRootId,\n })\n return getRequestQueryKey(requestArgs)\n}\n\nexport function mergeMessageUpdate(\n record: MessageResource,\n current?: MessageResource\n): MessageResource {\n if (!current) {\n return record\n }\n\n return {\n ...current,\n ...record,\n // Preserve reactions from cache - they're managed via separate reaction events\n reactionCounts: current.reactionCounts || [],\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"messages_cache.js","names":["getRequestQueryKey","transformReactionEventDataToReactionCountResource","getMessagesRequestArgs","updateOrCreateRecordInPagesData","updateRecordInPagesData","deleteRecordInPagesData","updateCacheWithMessage","queryClient","queryKey","message","event","setQueryData","prev","dataAfterTempRemoval","mine","idempotentKey","data","record","matchFn","existingMessage","isTemporaryMessageId","id","processRecord","current","mergeMessageUpdate","updateCacheWithIndividualMessage","links","meta","updateCacheWithReaction","currentPersonId","message_sort_key","oldMessage","reactionCounts","foundMatch","newReactionCounts","map","reactionCount","value","oldData","newReactionCount","count","messageId","endsWith","isNewMessage","getThreadedMessagesQueryKey","conversationId","replyRootId","requestArgs","conversation_id","reply_root_id"],"sources":["../../../src/utils/cache/messages_cache.ts"],"sourcesContent":["import { InfiniteData, QueryClient } from '@tanstack/react-query'\nimport { getRequestQueryKey } from '../../hooks/use_suspense_api'\nimport { ApiCollection, ApiResource, MessageResource } from '../../types'\nimport { JoltReactionEvent } from '../../types/jolt_events'\nimport { transformReactionEventDataToReactionCountResource } from '../jolt/transform_reaction_event_data_to_reaction_count_resource'\nimport { getMessagesRequestArgs } from '../request/get_messages'\nimport { updateOrCreateRecordInPagesData, updateRecordInPagesData } from './page_mutations'\nimport { deleteRecordInPagesData } from './page_mutations'\n\nexport function updateCacheWithMessage(\n queryClient: QueryClient,\n queryKey: unknown[],\n message: MessageResource,\n event: 'message.created' | 'message.updated'\n) {\n queryClient.setQueryData<MessagesQueryData>(queryKey, prev => {\n if (event === 'message.created') {\n // Before adding the new message, remove its pending temporary counterpart.\n // The Jolt echo can arrive before the HTTP response has swapped it out.\n let dataAfterTempRemoval = prev\n if (prev && message.mine && message.idempotentKey) {\n dataAfterTempRemoval = deleteRecordInPagesData({\n data: prev,\n record: message,\n matchFn: existingMessage =>\n isTemporaryMessageId(existingMessage.id) &&\n existingMessage.idempotentKey === message.idempotentKey,\n })\n }\n\n return updateOrCreateRecordInPagesData({\n data: dataAfterTempRemoval,\n record: message,\n processRecord: (record, current) => mergeMessageUpdate(record, current),\n })\n } else {\n return updateRecordInPagesData({\n data: prev,\n record: message,\n processRecord: (record, current) => mergeMessageUpdate(record, current),\n })\n }\n })\n}\n\ntype IndividualMessageQueryData = ApiResource<MessageResource>\n\nexport function updateCacheWithIndividualMessage(\n queryClient: QueryClient,\n queryKey: unknown[],\n message: MessageResource\n) {\n queryClient.setQueryData<IndividualMessageQueryData>(queryKey, prev => {\n if (!prev) {\n return {\n data: message,\n links: {},\n meta: {},\n }\n }\n\n return {\n ...prev,\n data: mergeMessageUpdate(message, prev.data),\n }\n })\n}\n\nexport function updateCacheWithReaction(\n queryClient: QueryClient,\n queryKey: unknown[],\n event: JoltReactionEvent,\n currentPersonId: number\n) {\n const message = { id: event.data.data.message_sort_key } as MessageResource\n queryClient.setQueryData<MessagesQueryData>(queryKey, prev =>\n updateRecordInPagesData({\n data: prev,\n record: message,\n processRecord: (record, oldMessage) => {\n const reactionCounts = oldMessage.reactionCounts || []\n let foundMatch = false\n let newReactionCounts = reactionCounts.map(reactionCount => {\n if (reactionCount.value === event.data.data.value) {\n foundMatch = true\n return transformReactionEventDataToReactionCountResource({\n data: event.data.data,\n oldData: reactionCount,\n event: event.event,\n currentPersonId,\n })\n }\n return reactionCount\n })\n\n if (!foundMatch) {\n const newReactionCount = transformReactionEventDataToReactionCountResource({\n data: event.data.data,\n event: event.event,\n currentPersonId,\n })\n\n if (newReactionCount?.count) {\n newReactionCounts = [...newReactionCounts, newReactionCount]\n }\n }\n\n return { ...oldMessage, reactionCounts: newReactionCounts }\n },\n })\n )\n}\n\ntype MessagesQueryData = InfiniteData<ApiCollection<MessageResource>>\nexport function isTemporaryMessageId(messageId?: string | null): boolean {\n return !!messageId && messageId.endsWith('-temp')\n}\nexport function isNewMessage(message?: MessageResource): boolean {\n return !message?.id || isTemporaryMessageId(message.id)\n}\n\nexport function getThreadedMessagesQueryKey(conversationId: number, replyRootId: string) {\n const requestArgs = getMessagesRequestArgs({\n conversation_id: conversationId,\n reply_root_id: replyRootId,\n })\n return getRequestQueryKey(requestArgs)\n}\n\nexport function mergeMessageUpdate(\n record: MessageResource,\n current?: MessageResource\n): MessageResource {\n if (!current) {\n return record\n }\n\n return {\n ...current,\n ...record,\n // Preserve reactions from cache - they're managed via separate reaction events\n reactionCounts: current.reactionCounts || [],\n }\n}\n"],"mappings":"AACA,SAASA,kBAAkB,QAAQ,8BAA8B;AAGjE,SAASC,iDAAiD,QAAQ,kEAAkE;AACpI,SAASC,sBAAsB,QAAQ,yBAAyB;AAChE,SAASC,+BAA+B,EAAEC,uBAAuB,QAAQ,kBAAkB;AAC3F,SAASC,uBAAuB,QAAQ,kBAAkB;AAE1D,OAAO,SAASC,sBAAsBA,CACpCC,WAAwB,EACxBC,QAAmB,EACnBC,OAAwB,EACxBC,KAA4C,EAC5C;EACAH,WAAW,CAACI,YAAY,CAAoBH,QAAQ,EAAEI,IAAI,IAAI;IAC5D,IAAIF,KAAK,KAAK,iBAAiB,EAAE;MAC/B;MACA;MACA,IAAIG,oBAAoB,GAAGD,IAAI;MAC/B,IAAIA,IAAI,IAAIH,OAAO,CAACK,IAAI,IAAIL,OAAO,CAACM,aAAa,EAAE;QACjDF,oBAAoB,GAAGR,uBAAuB,CAAC;UAC7CW,IAAI,EAAEJ,IAAI;UACVK,MAAM,EAAER,OAAO;UACfS,OAAO,EAAEC,eAAe,IACtBC,oBAAoB,CAACD,eAAe,CAACE,EAAE,CAAC,IACxCF,eAAe,CAACJ,aAAa,KAAKN,OAAO,CAACM;QAC9C,CAAC,CAAC;MACJ;MAEA,OAAOZ,+BAA+B,CAAC;QACrCa,IAAI,EAAEH,oBAAoB;QAC1BI,MAAM,EAAER,OAAO;QACfa,aAAa,EAAEA,CAACL,MAAM,EAAEM,OAAO,KAAKC,kBAAkB,CAACP,MAAM,EAAEM,OAAO;MACxE,CAAC,CAAC;IACJ,CAAC,MAAM;MACL,OAAOnB,uBAAuB,CAAC;QAC7BY,IAAI,EAAEJ,IAAI;QACVK,MAAM,EAAER,OAAO;QACfa,aAAa,EAAEA,CAACL,MAAM,EAAEM,OAAO,KAAKC,kBAAkB,CAACP,MAAM,EAAEM,OAAO;MACxE,CAAC,CAAC;IACJ;EACF,CAAC,CAAC;AACJ;AAIA,OAAO,SAASE,gCAAgCA,CAC9ClB,WAAwB,EACxBC,QAAmB,EACnBC,OAAwB,EACxB;EACAF,WAAW,CAACI,YAAY,CAA6BH,QAAQ,EAAEI,IAAI,IAAI;IACrE,IAAI,CAACA,IAAI,EAAE;MACT,OAAO;QACLI,IAAI,EAAEP,OAAO;QACbiB,KAAK,EAAE,CAAC,CAAC;QACTC,IAAI,EAAE,CAAC;MACT,CAAC;IACH;IAEA,OAAO;MACL,GAAGf,IAAI;MACPI,IAAI,EAAEQ,kBAAkB,CAACf,OAAO,EAAEG,IAAI,CAACI,IAAI;IAC7C,CAAC;EACH,CAAC,CAAC;AACJ;AAEA,OAAO,SAASY,uBAAuBA,CACrCrB,WAAwB,EACxBC,QAAmB,EACnBE,KAAwB,EACxBmB,eAAuB,EACvB;EACA,MAAMpB,OAAO,GAAG;IAAEY,EAAE,EAAEX,KAAK,CAACM,IAAI,CAACA,IAAI,CAACc;EAAiB,CAAoB;EAC3EvB,WAAW,CAACI,YAAY,CAAoBH,QAAQ,EAAEI,IAAI,IACxDR,uBAAuB,CAAC;IACtBY,IAAI,EAAEJ,IAAI;IACVK,MAAM,EAAER,OAAO;IACfa,aAAa,EAAEA,CAACL,MAAM,EAAEc,UAAU,KAAK;MACrC,MAAMC,cAAc,GAAGD,UAAU,CAACC,cAAc,IAAI,EAAE;MACtD,IAAIC,UAAU,GAAG,KAAK;MACtB,IAAIC,iBAAiB,GAAGF,cAAc,CAACG,GAAG,CAACC,aAAa,IAAI;QAC1D,IAAIA,aAAa,CAACC,KAAK,KAAK3B,KAAK,CAACM,IAAI,CAACA,IAAI,CAACqB,KAAK,EAAE;UACjDJ,UAAU,GAAG,IAAI;UACjB,OAAOhC,iDAAiD,CAAC;YACvDe,IAAI,EAAEN,KAAK,CAACM,IAAI,CAACA,IAAI;YACrBsB,OAAO,EAAEF,aAAa;YACtB1B,KAAK,EAAEA,KAAK,CAACA,KAAK;YAClBmB;UACF,CAAC,CAAC;QACJ;QACA,OAAOO,aAAa;MACtB,CAAC,CAAC;MAEF,IAAI,CAACH,UAAU,EAAE;QACf,MAAMM,gBAAgB,GAAGtC,iDAAiD,CAAC;UACzEe,IAAI,EAAEN,KAAK,CAACM,IAAI,CAACA,IAAI;UACrBN,KAAK,EAAEA,KAAK,CAACA,KAAK;UAClBmB;QACF,CAAC,CAAC;QAEF,IAAIU,gBAAgB,EAAEC,KAAK,EAAE;UAC3BN,iBAAiB,GAAG,CAAC,GAAGA,iBAAiB,EAAEK,gBAAgB,CAAC;QAC9D;MACF;MAEA,OAAO;QAAE,GAAGR,UAAU;QAAEC,cAAc,EAAEE;MAAkB,CAAC;IAC7D;EACF,CAAC,CACH,CAAC;AACH;AAGA,OAAO,SAASd,oBAAoBA,CAACqB,SAAyB,EAAW;EACvE,OAAO,CAAC,CAACA,SAAS,IAAIA,SAAS,CAACC,QAAQ,CAAC,OAAO,CAAC;AACnD;AACA,OAAO,SAASC,YAAYA,CAAClC,OAAyB,EAAW;EAC/D,OAAO,CAACA,OAAO,EAAEY,EAAE,IAAID,oBAAoB,CAACX,OAAO,CAACY,EAAE,CAAC;AACzD;AAEA,OAAO,SAASuB,2BAA2BA,CAACC,cAAsB,EAAEC,WAAmB,EAAE;EACvF,MAAMC,WAAW,GAAG7C,sBAAsB,CAAC;IACzC8C,eAAe,EAAEH,cAAc;IAC/BI,aAAa,EAAEH;EACjB,CAAC,CAAC;EACF,OAAO9C,kBAAkB,CAAC+C,WAAW,CAAC;AACxC;AAEA,OAAO,SAASvB,kBAAkBA,CAChCP,MAAuB,EACvBM,OAAyB,EACR;EACjB,IAAI,CAACA,OAAO,EAAE;IACZ,OAAON,MAAM;EACf;EAEA,OAAO;IACL,GAAGM,OAAO;IACV,GAAGN,MAAM;IACT;IACAe,cAAc,EAAET,OAAO,CAACS,cAAc,IAAI;EAC5C,CAAC;AACH","ignoreList":[]}
@@ -1,11 +1,12 @@
1
1
  import { CurrentPersonResource, MessageResource } from '../../types';
2
2
  import { DenormalizedAttachmentResourceForCreate } from '../../types/resources/denormalized_attachment_resource_for_create';
3
- export declare function optimisticallyCreateMessage({ conversationId, text, attachments, currentPerson, message, replyRootId, }: {
3
+ export declare function optimisticallyCreateMessage({ conversationId, text, attachments, currentPerson, message, replyRootId, idempotentKey, }: {
4
4
  conversationId: number;
5
5
  text: string;
6
6
  attachments?: DenormalizedAttachmentResourceForCreate[];
7
7
  currentPerson: CurrentPersonResource;
8
8
  message?: MessageResource;
9
9
  replyRootId?: string | null;
10
+ idempotentKey?: string;
10
11
  }): MessageResource;
11
12
  //# sourceMappingURL=optimistically_create_message.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"optimistically_create_message.d.ts","sourceRoot":"","sources":["../../../src/utils/cache/optimistically_create_message.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,qBAAqB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAEnF,OAAO,EAAE,uCAAuC,EAAE,MAAM,mEAAmE,CAAA;AAM3H,wBAAgB,2BAA2B,CAAC,EAC1C,cAAc,EACd,IAAI,EACJ,WAAW,EACX,aAAa,EACb,OAAO,EACP,WAAW,GACZ,EAAE;IACD,cAAc,EAAE,MAAM,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,uCAAuC,EAAE,CAAA;IACvD,aAAa,EAAE,qBAAqB,CAAA;IACpC,OAAO,CAAC,EAAE,eAAe,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B,mBAgDA"}
1
+ {"version":3,"file":"optimistically_create_message.d.ts","sourceRoot":"","sources":["../../../src/utils/cache/optimistically_create_message.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,qBAAqB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAEnF,OAAO,EAAE,uCAAuC,EAAE,MAAM,mEAAmE,CAAA;AAM3H,wBAAgB,2BAA2B,CAAC,EAC1C,cAAc,EACd,IAAI,EACJ,WAAW,EACX,aAAa,EACb,OAAO,EACP,WAAW,EACX,aAAa,GACd,EAAE;IACD,cAAc,EAAE,MAAM,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,uCAAuC,EAAE,CAAA;IACvD,aAAa,EAAE,qBAAqB,CAAA;IACpC,OAAO,CAAC,EAAE,eAAe,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,mBAiDA"}
@@ -9,7 +9,8 @@ export function optimisticallyCreateMessage({
9
9
  attachments,
10
10
  currentPerson,
11
11
  message,
12
- replyRootId
12
+ replyRootId,
13
+ idempotentKey
13
14
  }) {
14
15
  const id = message?.id || generateTempMessageId();
15
16
 
@@ -39,7 +40,8 @@ export function optimisticallyCreateMessage({
39
40
  lastInGroup: true,
40
41
  pending: true,
41
42
  replyCount: 0,
42
- replyRootId: replyRootId || null
43
+ replyRootId: replyRootId || null,
44
+ idempotentKey
43
45
  };
44
46
 
45
47
  // Add the optimistic message to the cache
@@ -1 +1 @@
1
- {"version":3,"file":"optimistically_create_message.js","names":["chatQueryClient","getMessagesQueryKey","convertAttachmentsForCreate","generatePlaceholderUlid","updateOrCreateRecordInPagesData","optimisticallyCreateMessage","conversationId","text","attachments","currentPerson","message","replyRootId","id","generateTempMessageId","decoratedAttachments","optimisticMessage","type","messageType","html","createdAt","Date","toISOString","deletedAt","textEditedAt","mine","attachmentsForCreate","author","reactionCounts","renderAuthor","renderTime","myLatestInConversation","lastInGroup","pending","replyCount","queryKey","conversation_id","reply_root_id","setQueryData","data","record","offsetMs"],"sources":["../../../src/utils/cache/optimistically_create_message.ts"],"sourcesContent":["import { InfiniteData } from '@tanstack/react-query'\nimport { chatQueryClient } from '../../contexts/api_provider'\nimport { ApiCollection, CurrentPersonResource, MessageResource } from '../../types'\nimport { DenormalizedAttachmentResource } from '../../types/resources/denormalized_attachment_resource'\nimport { DenormalizedAttachmentResourceForCreate } from '../../types/resources/denormalized_attachment_resource_for_create'\nimport { getMessagesQueryKey } from '../../utils/request/get_messages'\nimport { convertAttachmentsForCreate } from '../convert_attachments_for_create'\nimport { generatePlaceholderUlid } from '../generate_placeholder_ulid'\nimport { updateOrCreateRecordInPagesData } from './page_mutations'\n\nexport function optimisticallyCreateMessage({\n conversationId,\n text,\n attachments,\n currentPerson,\n message,\n replyRootId,\n}: {\n conversationId: number\n text: string\n attachments?: DenormalizedAttachmentResourceForCreate[]\n currentPerson: CurrentPersonResource\n message?: MessageResource\n replyRootId?: string | null\n}) {\n const id = message?.id || generateTempMessageId()\n\n // Convert attachments to denormalized format for optimistic UI\n const decoratedAttachments: DenormalizedAttachmentResource[] = convertAttachmentsForCreate(\n attachments || []\n )\n\n // Create an optimistic message\n const optimisticMessage: MessageResource = {\n ...message,\n type: 'Message',\n messageType: 'user',\n id,\n text,\n html: '', // Will be filled by server\n createdAt: new Date().toISOString(),\n deletedAt: null,\n textEditedAt: null,\n mine: true,\n attachments: decoratedAttachments,\n attachmentsForCreate: attachments || [],\n author: currentPerson,\n reactionCounts: [],\n renderAuthor: false,\n renderTime: true,\n myLatestInConversation: true,\n lastInGroup: true,\n pending: true,\n replyCount: 0,\n replyRootId: replyRootId || null,\n }\n\n // Add the optimistic message to the cache\n type QueryData = InfiniteData<ApiCollection<MessageResource>>\n const queryKey = getMessagesQueryKey({\n conversation_id: conversationId,\n reply_root_id: replyRootId,\n })\n\n chatQueryClient.setQueryData<QueryData>(queryKey, data =>\n updateOrCreateRecordInPagesData({\n data,\n record: optimisticMessage as MessageResource,\n })\n )\n\n return optimisticMessage\n}\n\nconst generateTempMessageId = () => {\n // Put it 5 seconds in the future to account for server lag of previously created messages\n // that are still pending. This gets overwritten by the server anyway.\n return `${generatePlaceholderUlid({ offsetMs: 5000 })}-temp`\n}\n"],"mappings":"AACA,SAASA,eAAe,QAAQ,6BAA6B;AAI7D,SAASC,mBAAmB,QAAQ,kCAAkC;AACtE,SAASC,2BAA2B,QAAQ,mCAAmC;AAC/E,SAASC,uBAAuB,QAAQ,8BAA8B;AACtE,SAASC,+BAA+B,QAAQ,kBAAkB;AAElE,OAAO,SAASC,2BAA2BA,CAAC;EAC1CC,cAAc;EACdC,IAAI;EACJC,WAAW;EACXC,aAAa;EACbC,OAAO;EACPC;AAQF,CAAC,EAAE;EACD,MAAMC,EAAE,GAAGF,OAAO,EAAEE,EAAE,IAAIC,qBAAqB,CAAC,CAAC;;EAEjD;EACA,MAAMC,oBAAsD,GAAGZ,2BAA2B,CACxFM,WAAW,IAAI,EACjB,CAAC;;EAED;EACA,MAAMO,iBAAkC,GAAG;IACzC,GAAGL,OAAO;IACVM,IAAI,EAAE,SAAS;IACfC,WAAW,EAAE,MAAM;IACnBL,EAAE;IACFL,IAAI;IACJW,IAAI,EAAE,EAAE;IAAE;IACVC,SAAS,EAAE,IAAIC,IAAI,CAAC,CAAC,CAACC,WAAW,CAAC,CAAC;IACnCC,SAAS,EAAE,IAAI;IACfC,YAAY,EAAE,IAAI;IAClBC,IAAI,EAAE,IAAI;IACVhB,WAAW,EAAEM,oBAAoB;IACjCW,oBAAoB,EAAEjB,WAAW,IAAI,EAAE;IACvCkB,MAAM,EAAEjB,aAAa;IACrBkB,cAAc,EAAE,EAAE;IAClBC,YAAY,EAAE,KAAK;IACnBC,UAAU,EAAE,IAAI;IAChBC,sBAAsB,EAAE,IAAI;IAC5BC,WAAW,EAAE,IAAI;IACjBC,OAAO,EAAE,IAAI;IACbC,UAAU,EAAE,CAAC;IACbtB,WAAW,EAAEA,WAAW,IAAI;EAC9B,CAAC;;EAED;;EAEA,MAAMuB,QAAQ,GAAGjC,mBAAmB,CAAC;IACnCkC,eAAe,EAAE7B,cAAc;IAC/B8B,aAAa,EAAEzB;EACjB,CAAC,CAAC;EAEFX,eAAe,CAACqC,YAAY,CAAYH,QAAQ,EAAEI,IAAI,IACpDlC,+BAA+B,CAAC;IAC9BkC,IAAI;IACJC,MAAM,EAAExB;EACV,CAAC,CACH,CAAC;EAED,OAAOA,iBAAiB;AAC1B;AAEA,MAAMF,qBAAqB,GAAGA,CAAA,KAAM;EAClC;EACA;EACA,OAAO,GAAGV,uBAAuB,CAAC;IAAEqC,QAAQ,EAAE;EAAK,CAAC,CAAC,OAAO;AAC9D,CAAC","ignoreList":[]}
1
+ {"version":3,"file":"optimistically_create_message.js","names":["chatQueryClient","getMessagesQueryKey","convertAttachmentsForCreate","generatePlaceholderUlid","updateOrCreateRecordInPagesData","optimisticallyCreateMessage","conversationId","text","attachments","currentPerson","message","replyRootId","idempotentKey","id","generateTempMessageId","decoratedAttachments","optimisticMessage","type","messageType","html","createdAt","Date","toISOString","deletedAt","textEditedAt","mine","attachmentsForCreate","author","reactionCounts","renderAuthor","renderTime","myLatestInConversation","lastInGroup","pending","replyCount","queryKey","conversation_id","reply_root_id","setQueryData","data","record","offsetMs"],"sources":["../../../src/utils/cache/optimistically_create_message.ts"],"sourcesContent":["import { InfiniteData } from '@tanstack/react-query'\nimport { chatQueryClient } from '../../contexts/api_provider'\nimport { ApiCollection, CurrentPersonResource, MessageResource } from '../../types'\nimport { DenormalizedAttachmentResource } from '../../types/resources/denormalized_attachment_resource'\nimport { DenormalizedAttachmentResourceForCreate } from '../../types/resources/denormalized_attachment_resource_for_create'\nimport { getMessagesQueryKey } from '../../utils/request/get_messages'\nimport { convertAttachmentsForCreate } from '../convert_attachments_for_create'\nimport { generatePlaceholderUlid } from '../generate_placeholder_ulid'\nimport { updateOrCreateRecordInPagesData } from './page_mutations'\n\nexport function optimisticallyCreateMessage({\n conversationId,\n text,\n attachments,\n currentPerson,\n message,\n replyRootId,\n idempotentKey,\n}: {\n conversationId: number\n text: string\n attachments?: DenormalizedAttachmentResourceForCreate[]\n currentPerson: CurrentPersonResource\n message?: MessageResource\n replyRootId?: string | null\n idempotentKey?: string\n}) {\n const id = message?.id || generateTempMessageId()\n\n // Convert attachments to denormalized format for optimistic UI\n const decoratedAttachments: DenormalizedAttachmentResource[] = convertAttachmentsForCreate(\n attachments || []\n )\n\n // Create an optimistic message\n const optimisticMessage: MessageResource = {\n ...message,\n type: 'Message',\n messageType: 'user',\n id,\n text,\n html: '', // Will be filled by server\n createdAt: new Date().toISOString(),\n deletedAt: null,\n textEditedAt: null,\n mine: true,\n attachments: decoratedAttachments,\n attachmentsForCreate: attachments || [],\n author: currentPerson,\n reactionCounts: [],\n renderAuthor: false,\n renderTime: true,\n myLatestInConversation: true,\n lastInGroup: true,\n pending: true,\n replyCount: 0,\n replyRootId: replyRootId || null,\n idempotentKey,\n }\n\n // Add the optimistic message to the cache\n type QueryData = InfiniteData<ApiCollection<MessageResource>>\n const queryKey = getMessagesQueryKey({\n conversation_id: conversationId,\n reply_root_id: replyRootId,\n })\n\n chatQueryClient.setQueryData<QueryData>(queryKey, data =>\n updateOrCreateRecordInPagesData({\n data,\n record: optimisticMessage as MessageResource,\n })\n )\n\n return optimisticMessage\n}\n\nconst generateTempMessageId = () => {\n // Put it 5 seconds in the future to account for server lag of previously created messages\n // that are still pending. This gets overwritten by the server anyway.\n return `${generatePlaceholderUlid({ offsetMs: 5000 })}-temp`\n}\n"],"mappings":"AACA,SAASA,eAAe,QAAQ,6BAA6B;AAI7D,SAASC,mBAAmB,QAAQ,kCAAkC;AACtE,SAASC,2BAA2B,QAAQ,mCAAmC;AAC/E,SAASC,uBAAuB,QAAQ,8BAA8B;AACtE,SAASC,+BAA+B,QAAQ,kBAAkB;AAElE,OAAO,SAASC,2BAA2BA,CAAC;EAC1CC,cAAc;EACdC,IAAI;EACJC,WAAW;EACXC,aAAa;EACbC,OAAO;EACPC,WAAW;EACXC;AASF,CAAC,EAAE;EACD,MAAMC,EAAE,GAAGH,OAAO,EAAEG,EAAE,IAAIC,qBAAqB,CAAC,CAAC;;EAEjD;EACA,MAAMC,oBAAsD,GAAGb,2BAA2B,CACxFM,WAAW,IAAI,EACjB,CAAC;;EAED;EACA,MAAMQ,iBAAkC,GAAG;IACzC,GAAGN,OAAO;IACVO,IAAI,EAAE,SAAS;IACfC,WAAW,EAAE,MAAM;IACnBL,EAAE;IACFN,IAAI;IACJY,IAAI,EAAE,EAAE;IAAE;IACVC,SAAS,EAAE,IAAIC,IAAI,CAAC,CAAC,CAACC,WAAW,CAAC,CAAC;IACnCC,SAAS,EAAE,IAAI;IACfC,YAAY,EAAE,IAAI;IAClBC,IAAI,EAAE,IAAI;IACVjB,WAAW,EAAEO,oBAAoB;IACjCW,oBAAoB,EAAElB,WAAW,IAAI,EAAE;IACvCmB,MAAM,EAAElB,aAAa;IACrBmB,cAAc,EAAE,EAAE;IAClBC,YAAY,EAAE,KAAK;IACnBC,UAAU,EAAE,IAAI;IAChBC,sBAAsB,EAAE,IAAI;IAC5BC,WAAW,EAAE,IAAI;IACjBC,OAAO,EAAE,IAAI;IACbC,UAAU,EAAE,CAAC;IACbvB,WAAW,EAAEA,WAAW,IAAI,IAAI;IAChCC;EACF,CAAC;;EAED;;EAEA,MAAMuB,QAAQ,GAAGlC,mBAAmB,CAAC;IACnCmC,eAAe,EAAE9B,cAAc;IAC/B+B,aAAa,EAAE1B;EACjB,CAAC,CAAC;EAEFX,eAAe,CAACsC,YAAY,CAAYH,QAAQ,EAAEI,IAAI,IACpDnC,+BAA+B,CAAC;IAC9BmC,IAAI;IACJC,MAAM,EAAExB;EACV,CAAC,CACH,CAAC;EAED,OAAOA,iBAAiB;AAC1B;AAEA,MAAMF,qBAAqB,GAAGA,CAAA,KAAM;EAClC;EACA;EACA,OAAO,GAAGX,uBAAuB,CAAC;IAAEsC,QAAQ,EAAE;EAAK,CAAC,CAAC,OAAO;AAC9D,CAAC","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,UAAgC,CAAA"}
@@ -1,5 +1,5 @@
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
  };
@@ -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']\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,CAAC","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "4.0.1-rc.3",
3
+ "version": "4.2.0-rc.0",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -84,5 +84,5 @@
84
84
  "sf-symbols-typescript": "^2.2.0",
85
85
  "typescript": "~6.0.3"
86
86
  },
87
- "gitHead": "23049d1aecdc6f911765889e4c7fd9296806cdcd"
87
+ "gitHead": "c3c00b88ac61f4be9a12571182d60e01017f8dde"
88
88
  }
@@ -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
+ })
@@ -22,6 +22,14 @@ 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
+
25
33
  export function useMessageCreateOrUpdate({ conversationId, message, replyRootId }: Props) {
26
34
  const messageId = message?.id || null
27
35
  const isEditing = !isNewMessage(message)
@@ -29,13 +37,7 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
29
37
  const currentPerson = useCurrentPerson()
30
38
 
31
39
  const mutation = useMutation({
32
- mutationFn: ({
33
- text,
34
- attachments,
35
- }: {
36
- text: string
37
- attachments?: DenormalizedAttachmentResourceForCreate[]
38
- }) => {
40
+ mutationFn: ({ text, attachments, idempotentKey }: ResolvedMessageMutationVariables) => {
39
41
  const requestParams = getMessagesRequestArgs({ conversation_id: conversationId })
40
42
  const fieldsWithValueJoined = Object.fromEntries(
41
43
  Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')])
@@ -46,7 +48,6 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
46
48
  ...(!isEditing && replyRootId ? { reply_root: { id: replyRootId } } : {}),
47
49
  }
48
50
  if (!isEditing) {
49
- const idempotentKey = insecureUUID()
50
51
  attributes.idempotent_key = idempotentKey
51
52
  startMessageCreationTracking(idempotentKey)
52
53
  }
@@ -71,13 +72,7 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
71
72
  })
72
73
  }
73
74
  },
74
- onMutate: async ({
75
- text,
76
- attachments,
77
- }: {
78
- text: string
79
- attachments?: DenormalizedAttachmentResourceForCreate[]
80
- }) => {
75
+ onMutate: async ({ text, attachments, idempotentKey }: ResolvedMessageMutationVariables) => {
81
76
  if (message && isEditing) {
82
77
  const optimisticMessage = optimisticallyUpdateMessage({
83
78
  conversationId,
@@ -96,6 +91,7 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
96
91
  attachments,
97
92
  currentPerson,
98
93
  replyRootId,
94
+ idempotentKey,
99
95
  })
100
96
 
101
97
  return { message: optimisticMessage }
@@ -155,7 +151,24 @@ export function useMessageCreateOrUpdate({ conversationId, message, replyRootId
155
151
  },
156
152
  })
157
153
 
158
- return mutation
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 }
159
172
  }
160
173
 
161
174
  /**
@@ -17,6 +17,7 @@ export interface MessageResource {
17
17
  reactionCounts: ReactionCountResource[]
18
18
  replyCount: number
19
19
  replyRootId?: string | null
20
+ idempotentKey?: string | null
20
21
  messageType: string
21
22
  personIdsForSystemEvent?: number[] | null
22
23
  systemTextParts?: { names: string[]; overflowCount: number; action: string } | null
@@ -0,0 +1,89 @@
1
+ import { InfiniteData, QueryClient } from '@tanstack/react-query'
2
+ import type { ApiCollection, MessageResource } from '../../../types'
3
+ import type { PersonResource } from '../../../types/resources/person'
4
+ import { updateCacheWithMessage } from '../messages_cache'
5
+
6
+ type MessagesQueryData = InfiniteData<ApiCollection<MessageResource>>
7
+
8
+ const queryKey = ['messages']
9
+ const author = { type: 'Person', id: 1, name: 'Me', avatar: null } as unknown as PersonResource
10
+
11
+ const message = (overrides: Partial<MessageResource>): MessageResource =>
12
+ ({
13
+ type: 'Message',
14
+ text: 'ok',
15
+ html: '',
16
+ createdAt: '2026-01-01T00:00:00Z',
17
+ deletedAt: null,
18
+ textEditedAt: null,
19
+ mine: true,
20
+ attachments: [],
21
+ author,
22
+ reactionCounts: [],
23
+ replyCount: 0,
24
+ replyRootId: null,
25
+ messageType: 'user',
26
+ ...overrides,
27
+ }) as MessageResource
28
+
29
+ const seed = (queryClient: QueryClient, ...messages: MessageResource[]) =>
30
+ queryClient.setQueryData<MessagesQueryData>(queryKey, {
31
+ pages: [
32
+ { data: messages, links: {}, meta: { count: messages.length, totalCount: messages.length } },
33
+ ],
34
+ pageParams: [],
35
+ })
36
+
37
+ const idsInCache = (queryClient: QueryClient) =>
38
+ queryClient.getQueryData<MessagesQueryData>(queryKey)!.pages[0].data.map(m => m.id)
39
+
40
+ const echo = (queryClient: QueryClient, overrides: Partial<MessageResource>) =>
41
+ updateCacheWithMessage(queryClient, queryKey, message(overrides), 'message.created')
42
+
43
+ describe('updateCacheWithMessage on message.created', () => {
44
+ let queryClient: QueryClient
45
+
46
+ beforeEach(() => {
47
+ queryClient = new QueryClient()
48
+ seed(
49
+ queryClient,
50
+ message({ id: '01B-temp', idempotentKey: 'key-b', pending: true }),
51
+ message({ id: '01A-temp', idempotentKey: 'key-a', pending: true })
52
+ )
53
+ })
54
+
55
+ it('swaps out only the pending message with the same idempotent key', () => {
56
+ echo(queryClient, { id: '01A', idempotentKey: 'key-a' })
57
+
58
+ expect(idsInCache(queryClient)).toEqual(['01A', '01B-temp'])
59
+ })
60
+
61
+ it('does not swap out a pending message that only shares the same text', () => {
62
+ echo(queryClient, { id: '01C', idempotentKey: 'key-c', text: 'ok' })
63
+
64
+ expect(idsInCache(queryClient)).toEqual(['01C', '01B-temp', '01A-temp'])
65
+ })
66
+
67
+ it('swaps out an attachment-only pending message, which has no text to match', () => {
68
+ seed(
69
+ queryClient,
70
+ message({ id: '01D-temp', idempotentKey: 'key-d', text: null, pending: true })
71
+ )
72
+
73
+ echo(queryClient, { id: '01D', idempotentKey: 'key-d', text: null })
74
+
75
+ expect(idsInCache(queryClient)).toEqual(['01D'])
76
+ })
77
+
78
+ it('leaves pending messages alone when the echo carries no key', () => {
79
+ echo(queryClient, { id: '01E', idempotentKey: null })
80
+
81
+ expect(idsInCache(queryClient)).toEqual(['01E', '01B-temp', '01A-temp'])
82
+ })
83
+
84
+ it("leaves pending messages alone when the echo is someone else's", () => {
85
+ echo(queryClient, { id: '01F', idempotentKey: 'key-a', mine: false })
86
+
87
+ expect(idsInCache(queryClient)).toEqual(['01F', '01B-temp', '01A-temp'])
88
+ })
89
+ })
@@ -15,20 +15,16 @@ export function updateCacheWithMessage(
15
15
  ) {
16
16
  queryClient.setQueryData<MessagesQueryData>(queryKey, prev => {
17
17
  if (event === 'message.created') {
18
- // Before adding the new message, remove any pending temporary messages
19
- // with matching text to prevent duplicates from race conditions
18
+ // Before adding the new message, remove its pending temporary counterpart.
19
+ // The Jolt echo can arrive before the HTTP response has swapped it out.
20
20
  let dataAfterTempRemoval = prev
21
- if (prev && message.text && message.mine) {
21
+ if (prev && message.mine && message.idempotentKey) {
22
22
  dataAfterTempRemoval = deleteRecordInPagesData({
23
23
  data: prev,
24
24
  record: message,
25
- matchFn: (existingMessage, _record) => {
26
- return (
27
- isTemporaryMessageId(existingMessage.id) &&
28
- existingMessage.text === message.text &&
29
- existingMessage.mine
30
- )
31
- },
25
+ matchFn: existingMessage =>
26
+ isTemporaryMessageId(existingMessage.id) &&
27
+ existingMessage.idempotentKey === message.idempotentKey,
32
28
  })
33
29
  }
34
30
 
@@ -15,6 +15,7 @@ export function optimisticallyCreateMessage({
15
15
  currentPerson,
16
16
  message,
17
17
  replyRootId,
18
+ idempotentKey,
18
19
  }: {
19
20
  conversationId: number
20
21
  text: string
@@ -22,6 +23,7 @@ export function optimisticallyCreateMessage({
22
23
  currentPerson: CurrentPersonResource
23
24
  message?: MessageResource
24
25
  replyRootId?: string | null
26
+ idempotentKey?: string
25
27
  }) {
26
28
  const id = message?.id || generateTempMessageId()
27
29
 
@@ -53,6 +55,7 @@ export function optimisticallyCreateMessage({
53
55
  pending: true,
54
56
  replyCount: 0,
55
57
  replyRootId: replyRootId || null,
58
+ idempotentKey,
56
59
  }
57
60
 
58
61
  // Add the optimistic message to the cache
@@ -29,6 +29,7 @@ export function transformMessageEventDataToMessageResource({
29
29
  reactionCounts: [],
30
30
  replyCount: data.reply_count || 0,
31
31
  replyRootId: data.reply_root_id || null,
32
+ idempotentKey: data.idempotent_key ?? null,
32
33
  messageType: data.message_type,
33
34
  personIdsForSystemEvent: data.system_event_person_ids,
34
35
  systemTextParts: data.system_text_parts
@@ -10,6 +10,7 @@ export const getMessageFields = {
10
10
  'reaction_counts',
11
11
  'reply_count',
12
12
  'reply_root',
13
+ 'idempotent_key',
13
14
  'message_type',
14
15
  'person_ids_for_system_event',
15
16
  'system_text_parts',