@planningcenter/chat-react-native 3.18.0-rc.6 → 3.18.0-rc.8

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 (60) hide show
  1. package/build/components/conversation/message.d.ts.map +1 -1
  2. package/build/components/conversation/message.js +23 -12
  3. package/build/components/conversation/message.js.map +1 -1
  4. package/build/components/conversation/message_form.d.ts.map +1 -1
  5. package/build/components/conversation/message_form.js +2 -4
  6. package/build/components/conversation/message_form.js.map +1 -1
  7. package/build/components/conversation/typing_indicator.d.ts +1 -5
  8. package/build/components/conversation/typing_indicator.d.ts.map +1 -1
  9. package/build/components/conversation/typing_indicator.js +2 -2
  10. package/build/components/conversation/typing_indicator.js.map +1 -1
  11. package/build/contexts/conversation_context.d.ts +13 -0
  12. package/build/contexts/conversation_context.d.ts.map +1 -0
  13. package/build/contexts/conversation_context.js +14 -0
  14. package/build/contexts/conversation_context.js.map +1 -0
  15. package/build/hooks/use_broadcast_typing_status.d.ts +1 -1
  16. package/build/hooks/use_broadcast_typing_status.d.ts.map +1 -1
  17. package/build/hooks/use_broadcast_typing_status.js +7 -3
  18. package/build/hooks/use_broadcast_typing_status.js.map +1 -1
  19. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  20. package/build/hooks/use_conversation_messages_jolt_events.js +23 -70
  21. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  22. package/build/hooks/use_message_create_or_update.d.ts +0 -2
  23. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  24. package/build/hooks/use_message_create_or_update.js +10 -8
  25. package/build/hooks/use_message_create_or_update.js.map +1 -1
  26. package/build/hooks/use_typing_indicators.d.ts +1 -1
  27. package/build/hooks/use_typing_indicators.d.ts.map +1 -1
  28. package/build/hooks/use_typing_indicators.js +16 -3
  29. package/build/hooks/use_typing_indicators.js.map +1 -1
  30. package/build/screens/conversation_screen.d.ts.map +1 -1
  31. package/build/screens/conversation_screen.js +8 -1
  32. package/build/screens/conversation_screen.js.map +1 -1
  33. package/build/types/jolt_events/reaction_events.d.ts +1 -0
  34. package/build/types/jolt_events/reaction_events.d.ts.map +1 -1
  35. package/build/types/jolt_events/reaction_events.js.map +1 -1
  36. package/build/types/jolt_events/typing_events.d.ts +1 -0
  37. package/build/types/jolt_events/typing_events.d.ts.map +1 -1
  38. package/build/types/jolt_events/typing_events.js.map +1 -1
  39. package/build/utils/cache/messages_cache.d.ts +9 -0
  40. package/build/utils/cache/messages_cache.d.ts.map +1 -0
  41. package/build/utils/cache/messages_cache.js +89 -0
  42. package/build/utils/cache/messages_cache.js.map +1 -0
  43. package/build/utils/cache/optimistically_create_message.d.ts +2 -1
  44. package/build/utils/cache/optimistically_create_message.d.ts.map +1 -1
  45. package/build/utils/cache/optimistically_create_message.js +6 -3
  46. package/build/utils/cache/optimistically_create_message.js.map +1 -1
  47. package/package.json +2 -2
  48. package/src/components/conversation/message.tsx +34 -16
  49. package/src/components/conversation/message_form.tsx +2 -9
  50. package/src/components/conversation/typing_indicator.tsx +2 -6
  51. package/src/contexts/conversation_context.tsx +34 -0
  52. package/src/hooks/use_broadcast_typing_status.ts +7 -3
  53. package/src/hooks/use_conversation_messages_jolt_events.ts +39 -81
  54. package/src/hooks/use_message_create_or_update.ts +10 -9
  55. package/src/hooks/use_typing_indicators.ts +15 -3
  56. package/src/screens/conversation_screen.tsx +15 -1
  57. package/src/types/jolt_events/reaction_events.ts +1 -0
  58. package/src/types/jolt_events/typing_events.ts +1 -0
  59. package/src/utils/cache/messages_cache.ts +113 -0
  60. package/src/utils/cache/optimistically_create_message.ts +7 -2
@@ -5,6 +5,7 @@ interface BaseReactionEventData extends Record<string, unknown> {
5
5
  author_id: number;
6
6
  conversation_id: number;
7
7
  message_sort_key: string;
8
+ reply_root_id?: string | null;
8
9
  created_at: string;
9
10
  organization_id: number;
10
11
  value: ReactionCountResource['value'];
@@ -1 +1 @@
1
- {"version":3,"file":"reaction_events.d.ts","sourceRoot":"","sources":["../../../src/types/jolt_events/reaction_events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAA;AAC1F,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAE7D,UAAU,qBAAsB,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC7D,IAAI,EAAE;QACJ,SAAS,EAAE,MAAM,CAAA;QACjB,eAAe,EAAE,MAAM,CAAA;QACvB,gBAAgB,EAAE,MAAM,CAAA;QACxB,UAAU,EAAE,MAAM,CAAA;QAClB,eAAe,EAAE,MAAM,CAAA;QACvB,KAAK,EAAE,qBAAqB,CAAC,OAAO,CAAC,CAAA;QACrC,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,aAAa,CAAC,EAAE,MAAM,CAAA;QACtB,KAAK,EAAE,MAAM,CAAA;KACd,CAAA;CACF;AAED,MAAM,WAAW,oBAAqB,SAAQ,aAAa;IACzD,KAAK,EAAE,kBAAkB,CAAA;IACzB,IAAI,EAAE,qBAAqB,CAAA;CAC5B;AAED,MAAM,WAAW,oBAAqB,SAAQ,aAAa;IACzD,KAAK,EAAE,oBAAoB,CAAA;IAC3B,IAAI,EAAE,qBAAqB,CAAA;CAC5B"}
1
+ {"version":3,"file":"reaction_events.d.ts","sourceRoot":"","sources":["../../../src/types/jolt_events/reaction_events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAA;AAC1F,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAE7D,UAAU,qBAAsB,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC7D,IAAI,EAAE;QACJ,SAAS,EAAE,MAAM,CAAA;QACjB,eAAe,EAAE,MAAM,CAAA;QACvB,gBAAgB,EAAE,MAAM,CAAA;QACxB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC7B,UAAU,EAAE,MAAM,CAAA;QAClB,eAAe,EAAE,MAAM,CAAA;QACvB,KAAK,EAAE,qBAAqB,CAAC,OAAO,CAAC,CAAA;QACrC,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,aAAa,CAAC,EAAE,MAAM,CAAA;QACtB,KAAK,EAAE,MAAM,CAAA;KACd,CAAA;CACF;AAED,MAAM,WAAW,oBAAqB,SAAQ,aAAa;IACzD,KAAK,EAAE,kBAAkB,CAAA;IACzB,IAAI,EAAE,qBAAqB,CAAA;CAC5B;AAED,MAAM,WAAW,oBAAqB,SAAQ,aAAa;IACzD,KAAK,EAAE,oBAAoB,CAAA;IAC3B,IAAI,EAAE,qBAAqB,CAAA;CAC5B"}
@@ -1 +1 @@
1
- {"version":3,"file":"reaction_events.js","sourceRoot":"","sources":["../../../src/types/jolt_events/reaction_events.ts"],"names":[],"mappings":"","sourcesContent":["import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'\nimport { ReactionCountResource } from '../resources/reaction'\n\ninterface BaseReactionEventData extends Record<string, unknown> {\n data: {\n author_id: number\n conversation_id: number\n message_sort_key: string\n created_at: string\n organization_id: number\n value: ReactionCountResource['value']\n author_name?: string\n author_avatar?: string\n count: number\n }\n}\n\nexport interface ReactionCreatedEvent extends CustomMessage {\n event: 'reaction.created'\n data: BaseReactionEventData\n}\n\nexport interface ReactionDeletedEvent extends CustomMessage {\n event: 'reaction.destroyed'\n data: BaseReactionEventData\n}\n"]}
1
+ {"version":3,"file":"reaction_events.js","sourceRoot":"","sources":["../../../src/types/jolt_events/reaction_events.ts"],"names":[],"mappings":"","sourcesContent":["import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'\nimport { ReactionCountResource } from '../resources/reaction'\n\ninterface BaseReactionEventData extends Record<string, unknown> {\n data: {\n author_id: number\n conversation_id: number\n message_sort_key: string\n reply_root_id?: string | null\n created_at: string\n organization_id: number\n value: ReactionCountResource['value']\n author_name?: string\n author_avatar?: string\n count: number\n }\n}\n\nexport interface ReactionCreatedEvent extends CustomMessage {\n event: 'reaction.created'\n data: BaseReactionEventData\n}\n\nexport interface ReactionDeletedEvent extends CustomMessage {\n event: 'reaction.destroyed'\n data: BaseReactionEventData\n}\n"]}
@@ -10,6 +10,7 @@ export interface TypingBroadcastDataAttributes {
10
10
  author_id: number;
11
11
  author_name: string;
12
12
  id: string;
13
+ reply_root_id: string | null;
13
14
  }
14
15
  export {};
15
16
  //# sourceMappingURL=typing_events.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"typing_events.d.ts","sourceRoot":"","sources":["../../../src/types/jolt_events/typing_events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAA;AAE1F,UAAU,mBAAoB,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC3D,IAAI,EAAE,6BAA6B,CAAA;CACpC;AAED,MAAM,WAAW,oBAAqB,SAAQ,aAAa;IACzD,KAAK,EAAE,kBAAkB,CAAA;IACzB,IAAI,EAAE,mBAAmB,CAAA;CAC1B;AAED,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,EAAE,EAAE,MAAM,CAAA;CACX"}
1
+ {"version":3,"file":"typing_events.d.ts","sourceRoot":"","sources":["../../../src/types/jolt_events/typing_events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAA;AAE1F,UAAU,mBAAoB,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC3D,IAAI,EAAE,6BAA6B,CAAA;CACpC;AAED,MAAM,WAAW,oBAAqB,SAAQ,aAAa;IACzD,KAAK,EAAE,kBAAkB,CAAA;IACzB,IAAI,EAAE,mBAAmB,CAAA;CAC1B;AAED,MAAM,WAAW,6BAA6B;IAC5C,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,EAAE,EAAE,MAAM,CAAA;IACV,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B"}
@@ -1 +1 @@
1
- {"version":3,"file":"typing_events.js","sourceRoot":"","sources":["../../../src/types/jolt_events/typing_events.ts"],"names":[],"mappings":"","sourcesContent":["import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'\n\ninterface TypingBroadcastData extends Record<string, unknown> {\n data: TypingBroadcastDataAttributes\n}\n\nexport interface TypingBroadcastEvent extends CustomMessage {\n event: 'typing.broadcast'\n data: TypingBroadcastData\n}\n\nexport interface TypingBroadcastDataAttributes {\n author_id: number\n author_name: string\n id: string\n}\n"]}
1
+ {"version":3,"file":"typing_events.js","sourceRoot":"","sources":["../../../src/types/jolt_events/typing_events.ts"],"names":[],"mappings":"","sourcesContent":["import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'\n\ninterface TypingBroadcastData extends Record<string, unknown> {\n data: TypingBroadcastDataAttributes\n}\n\nexport interface TypingBroadcastEvent extends CustomMessage {\n event: 'typing.broadcast'\n data: TypingBroadcastData\n}\n\nexport interface TypingBroadcastDataAttributes {\n author_id: number\n author_name: string\n id: string\n reply_root_id: string | null\n}\n"]}
@@ -0,0 +1,9 @@
1
+ import { QueryClient } from '@tanstack/react-query';
2
+ import { MessageResource } from '../../types';
3
+ import { JoltReactionEvent } from '../../types/jolt_events';
4
+ export declare function updateCacheWithMessage(queryClient: QueryClient, queryKey: unknown[], message: MessageResource, event: 'message.created' | 'message.updated'): void;
5
+ export declare function updateCacheWithReaction(queryClient: QueryClient, queryKey: unknown[], event: JoltReactionEvent, currentPersonId: number): void;
6
+ export declare function isTemporaryMessageId(messageId?: string | null): boolean;
7
+ export declare function isNewMessage(message?: MessageResource): boolean;
8
+ export declare function getThreadedMessagesQueryKey(conversationId: number, replyRootId: string): import("../../hooks/use_suspense_api").RequestQueryKey;
9
+ //# sourceMappingURL=messages_cache.d.ts.map
@@ -0,0 +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;AACjE,OAAO,EAAiB,eAAe,EAAE,MAAM,aAAa,CAAA;AAG5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAK3D,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,eAAe,EACxB,KAAK,EAAE,iBAAiB,GAAG,iBAAiB,QAsC7C;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,0DAMtF"}
@@ -0,0 +1,89 @@
1
+ import { deleteRecordInPagesData } from './page_mutations';
2
+ import { updateOrCreateRecordInPagesData, updateRecordInPagesData } from './page_mutations';
3
+ import { transformReactionEventDataToReactionCountResource } from '../jolt/transform_reaction_event_data_to_reaction_count_resource';
4
+ import { getMessagesRequestArgs } from '../request/get_messages';
5
+ import { getRequestQueryKey } from '../../hooks/use_suspense_api';
6
+ export function updateCacheWithMessage(queryClient, queryKey, message, event) {
7
+ queryClient.setQueryData(queryKey, prev => {
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
11
+ let dataAfterTempRemoval = prev;
12
+ if (prev && message.text && message.mine) {
13
+ dataAfterTempRemoval = deleteRecordInPagesData({
14
+ data: prev,
15
+ record: message,
16
+ matchFn: (existingMessage, _record) => {
17
+ return (isTemporaryMessageId(existingMessage.id) &&
18
+ existingMessage.text === message.text &&
19
+ existingMessage.mine);
20
+ },
21
+ });
22
+ }
23
+ return updateOrCreateRecordInPagesData({
24
+ data: dataAfterTempRemoval,
25
+ record: message,
26
+ processRecord: (record, current) => {
27
+ return { ...current, ...record };
28
+ },
29
+ });
30
+ }
31
+ else {
32
+ return updateRecordInPagesData({
33
+ data: prev,
34
+ record: message,
35
+ processRecord: (record, current) => {
36
+ return { ...current, ...record };
37
+ },
38
+ });
39
+ }
40
+ });
41
+ }
42
+ export function updateCacheWithReaction(queryClient, queryKey, event, currentPersonId) {
43
+ const message = { id: event.data.data.message_sort_key };
44
+ queryClient.setQueryData(queryKey, prev => updateRecordInPagesData({
45
+ data: prev,
46
+ record: message,
47
+ processRecord: (record, oldMessage) => {
48
+ const reactionCounts = oldMessage.reactionCounts || [];
49
+ let foundMatch = false;
50
+ let newReactionCounts = reactionCounts.map(reactionCount => {
51
+ if (reactionCount.value === event.data.data.value) {
52
+ foundMatch = true;
53
+ return transformReactionEventDataToReactionCountResource({
54
+ data: event.data.data,
55
+ oldData: reactionCount,
56
+ event: event.event,
57
+ currentPersonId,
58
+ });
59
+ }
60
+ return reactionCount;
61
+ });
62
+ if (!foundMatch) {
63
+ const newReactionCount = transformReactionEventDataToReactionCountResource({
64
+ data: event.data.data,
65
+ event: event.event,
66
+ currentPersonId,
67
+ });
68
+ if (newReactionCount?.count) {
69
+ newReactionCounts = [...newReactionCounts, newReactionCount];
70
+ }
71
+ }
72
+ return { ...oldMessage, reactionCounts: newReactionCounts };
73
+ },
74
+ }));
75
+ }
76
+ export function isTemporaryMessageId(messageId) {
77
+ return !!messageId && messageId.endsWith('-temp');
78
+ }
79
+ export function isNewMessage(message) {
80
+ return !message?.id || isTemporaryMessageId(message.id);
81
+ }
82
+ export function getThreadedMessagesQueryKey(conversationId, replyRootId) {
83
+ const requestArgs = getMessagesRequestArgs({
84
+ conversation_id: conversationId,
85
+ reply_root_id: replyRootId,
86
+ });
87
+ return getRequestQueryKey(requestArgs);
88
+ }
89
+ //# sourceMappingURL=messages_cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages_cache.js","sourceRoot":"","sources":["../../../src/utils/cache/messages_cache.ts"],"names":[],"mappings":"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","sourcesContent":["import { InfiniteData, QueryClient } from '@tanstack/react-query'\nimport { ApiCollection, MessageResource } from '../../types'\nimport { deleteRecordInPagesData } from './page_mutations'\nimport { updateOrCreateRecordInPagesData, updateRecordInPagesData } from './page_mutations'\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 { getRequestQueryKey } from '../../hooks/use_suspense_api'\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) => {\n return { ...current, ...record }\n },\n })\n } else {\n return updateRecordInPagesData({\n data: prev,\n record: message,\n processRecord: (record, current) => {\n return { ...current, ...record }\n },\n })\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"]}
@@ -1,10 +1,11 @@
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, }: {
3
+ export declare function optimisticallyCreateMessage({ conversationId, text, attachments, currentPerson, message, replyRootId, }: {
4
4
  conversationId: number;
5
5
  text: string;
6
6
  attachments?: DenormalizedAttachmentResourceForCreate[];
7
7
  currentPerson: CurrentPersonResource;
8
8
  message?: MessageResource;
9
+ replyRootId?: string | null;
9
10
  }): MessageResource;
10
11
  //# 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":"AACA,OAAO,EAAiB,qBAAqB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAKnF,OAAO,EAAE,uCAAuC,EAAE,MAAM,mEAAmE,CAAA;AAI3H,wBAAgB,2BAA2B,CAAC,EAC1C,cAAc,EACd,IAAI,EACJ,WAAW,EACX,aAAa,EACb,OAAO,GACR,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;CAC1B,mBA4CA"}
1
+ {"version":3,"file":"optimistically_create_message.d.ts","sourceRoot":"","sources":["../../../src/utils/cache/optimistically_create_message.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,qBAAqB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAKnF,OAAO,EAAE,uCAAuC,EAAE,MAAM,mEAAmE,CAAA;AAI3H,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,mBA+CA"}
@@ -3,7 +3,7 @@ import { chatQueryClient } from '../../contexts/api_provider';
3
3
  import { updateOrCreateRecordInPagesData } from './page_mutations';
4
4
  import { convertAttachmentsForCreate } from '../convert_attachments_for_create';
5
5
  import { generatePlaceholderUlid } from '../generate_placeholder_ulid';
6
- export function optimisticallyCreateMessage({ conversationId, text, attachments, currentPerson, message, }) {
6
+ export function optimisticallyCreateMessage({ conversationId, text, attachments, currentPerson, message, replyRootId, }) {
7
7
  const id = message?.id || generateTempMessageId();
8
8
  // Convert attachments to denormalized format for optimistic UI
9
9
  const decoratedAttachments = convertAttachmentsForCreate(attachments || []);
@@ -28,9 +28,12 @@ export function optimisticallyCreateMessage({ conversationId, text, attachments,
28
28
  lastInGroup: true,
29
29
  pending: true,
30
30
  replyCount: 0,
31
- replyRootId: null,
31
+ replyRootId: replyRootId || null,
32
32
  };
33
- const queryKey = getMessagesQueryKey({ conversation_id: conversationId });
33
+ const queryKey = getMessagesQueryKey({
34
+ conversation_id: conversationId,
35
+ reply_root_id: replyRootId,
36
+ });
34
37
  chatQueryClient.setQueryData(queryKey, data => updateOrCreateRecordInPagesData({
35
38
  data,
36
39
  record: optimisticMessage,
@@ -1 +1 @@
1
- {"version":3,"file":"optimistically_create_message.js","sourceRoot":"","sources":["../../../src/utils/cache/optimistically_create_message.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAC7D,OAAO,EAAE,+BAA+B,EAAE,MAAM,kBAAkB,CAAA;AAClE,OAAO,EAAE,2BAA2B,EAAE,MAAM,mCAAmC,CAAA;AAG/E,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAEtE,MAAM,UAAU,2BAA2B,CAAC,EAC1C,cAAc,EACd,IAAI,EACJ,WAAW,EACX,aAAa,EACb,OAAO,GAOR;IACC,MAAM,EAAE,GAAG,OAAO,EAAE,EAAE,IAAI,qBAAqB,EAAE,CAAA;IAEjD,+DAA+D;IAC/D,MAAM,oBAAoB,GAAqC,2BAA2B,CACxF,WAAW,IAAI,EAAE,CAClB,CAAA;IAED,+BAA+B;IAC/B,MAAM,iBAAiB,GAAoB;QACzC,GAAG,OAAO;QACV,IAAI,EAAE,SAAS;QACf,EAAE;QACF,IAAI;QACJ,IAAI,EAAE,EAAE,EAAE,2BAA2B;QACrC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,SAAS,EAAE,IAAI;QACf,YAAY,EAAE,IAAI;QAClB,IAAI,EAAE,IAAI;QACV,WAAW,EAAE,oBAAoB;QACjC,oBAAoB,EAAE,WAAW,IAAI,EAAE;QACvC,MAAM,EAAE,aAAa;QACrB,cAAc,EAAE,EAAE;QAClB,YAAY,EAAE,KAAK;QACnB,UAAU,EAAE,IAAI;QAChB,sBAAsB,EAAE,IAAI;QAC5B,WAAW,EAAE,IAAI;QACjB,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,CAAC;QACb,WAAW,EAAE,IAAI;KAClB,CAAA;IAID,MAAM,QAAQ,GAAG,mBAAmB,CAAC,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC,CAAA;IAEzE,eAAe,CAAC,YAAY,CAAY,QAAQ,EAAE,IAAI,CAAC,EAAE,CACvD,+BAA+B,CAAC;QAC9B,IAAI;QACJ,MAAM,EAAE,iBAAoC;KAC7C,CAAC,CACH,CAAA;IAED,OAAO,iBAAiB,CAAA;AAC1B,CAAC;AAED,MAAM,qBAAqB,GAAG,GAAG,EAAE;IACjC,0FAA0F;IAC1F,sEAAsE;IACtE,OAAO,GAAG,uBAAuB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,OAAO,CAAA;AAC9D,CAAC,CAAA","sourcesContent":["import { InfiniteData } from '@tanstack/react-query'\nimport { ApiCollection, CurrentPersonResource, MessageResource } from '../../types'\nimport { getMessagesQueryKey } from '../../utils/request/get_messages'\nimport { chatQueryClient } from '../../contexts/api_provider'\nimport { updateOrCreateRecordInPagesData } from './page_mutations'\nimport { convertAttachmentsForCreate } from '../convert_attachments_for_create'\nimport { DenormalizedAttachmentResourceForCreate } from '../../types/resources/denormalized_attachment_resource_for_create'\nimport { DenormalizedAttachmentResource } from '../../types/resources/denormalized_attachment_resource'\nimport { generatePlaceholderUlid } from '../generate_placeholder_ulid'\n\nexport function optimisticallyCreateMessage({\n conversationId,\n text,\n attachments,\n currentPerson,\n message,\n}: {\n conversationId: number\n text: string\n attachments?: DenormalizedAttachmentResourceForCreate[]\n currentPerson: CurrentPersonResource\n message?: MessageResource\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 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: null,\n }\n\n // Add the optimistic message to the cache\n type QueryData = InfiniteData<ApiCollection<MessageResource>>\n const queryKey = getMessagesQueryKey({ conversation_id: conversationId })\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"]}
1
+ {"version":3,"file":"optimistically_create_message.js","sourceRoot":"","sources":["../../../src/utils/cache/optimistically_create_message.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAC7D,OAAO,EAAE,+BAA+B,EAAE,MAAM,kBAAkB,CAAA;AAClE,OAAO,EAAE,2BAA2B,EAAE,MAAM,mCAAmC,CAAA;AAG/E,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAA;AAEtE,MAAM,UAAU,2BAA2B,CAAC,EAC1C,cAAc,EACd,IAAI,EACJ,WAAW,EACX,aAAa,EACb,OAAO,EACP,WAAW,GAQZ;IACC,MAAM,EAAE,GAAG,OAAO,EAAE,EAAE,IAAI,qBAAqB,EAAE,CAAA;IAEjD,+DAA+D;IAC/D,MAAM,oBAAoB,GAAqC,2BAA2B,CACxF,WAAW,IAAI,EAAE,CAClB,CAAA;IAED,+BAA+B;IAC/B,MAAM,iBAAiB,GAAoB;QACzC,GAAG,OAAO;QACV,IAAI,EAAE,SAAS;QACf,EAAE;QACF,IAAI;QACJ,IAAI,EAAE,EAAE,EAAE,2BAA2B;QACrC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,SAAS,EAAE,IAAI;QACf,YAAY,EAAE,IAAI;QAClB,IAAI,EAAE,IAAI;QACV,WAAW,EAAE,oBAAoB;QACjC,oBAAoB,EAAE,WAAW,IAAI,EAAE;QACvC,MAAM,EAAE,aAAa;QACrB,cAAc,EAAE,EAAE;QAClB,YAAY,EAAE,KAAK;QACnB,UAAU,EAAE,IAAI;QAChB,sBAAsB,EAAE,IAAI;QAC5B,WAAW,EAAE,IAAI;QACjB,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,CAAC;QACb,WAAW,EAAE,WAAW,IAAI,IAAI;KACjC,CAAA;IAID,MAAM,QAAQ,GAAG,mBAAmB,CAAC;QACnC,eAAe,EAAE,cAAc;QAC/B,aAAa,EAAE,WAAW;KAC3B,CAAC,CAAA;IAEF,eAAe,CAAC,YAAY,CAAY,QAAQ,EAAE,IAAI,CAAC,EAAE,CACvD,+BAA+B,CAAC;QAC9B,IAAI;QACJ,MAAM,EAAE,iBAAoC;KAC7C,CAAC,CACH,CAAA;IAED,OAAO,iBAAiB,CAAA;AAC1B,CAAC;AAED,MAAM,qBAAqB,GAAG,GAAG,EAAE;IACjC,0FAA0F;IAC1F,sEAAsE;IACtE,OAAO,GAAG,uBAAuB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,OAAO,CAAA;AAC9D,CAAC,CAAA","sourcesContent":["import { InfiniteData } from '@tanstack/react-query'\nimport { ApiCollection, CurrentPersonResource, MessageResource } from '../../types'\nimport { getMessagesQueryKey } from '../../utils/request/get_messages'\nimport { chatQueryClient } from '../../contexts/api_provider'\nimport { updateOrCreateRecordInPagesData } from './page_mutations'\nimport { convertAttachmentsForCreate } from '../convert_attachments_for_create'\nimport { DenormalizedAttachmentResourceForCreate } from '../../types/resources/denormalized_attachment_resource_for_create'\nimport { DenormalizedAttachmentResource } from '../../types/resources/denormalized_attachment_resource'\nimport { generatePlaceholderUlid } from '../generate_placeholder_ulid'\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 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.18.0-rc.6",
3
+ "version": "3.18.0-rc.8",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -58,5 +58,5 @@
58
58
  "react-native-url-polyfill": "^2.0.0",
59
59
  "typescript": "<5.6.0"
60
60
  },
61
- "gitHead": "78fdeec4ebbeb3d824a7e105303831415a2c9441"
61
+ "gitHead": "fff0564227516dace7a52c0cb1896b23b6c0e395"
62
62
  }
@@ -22,11 +22,12 @@ import {
22
22
  import Animated from 'react-native-reanimated'
23
23
  import { useLiveRelativeTime } from '../../hooks/use_live_relative_time'
24
24
  import { MessageReadReceipts } from './message_read_receipts'
25
- import { isNewMessage, useMessageCreateOrUpdate } from '../../hooks/use_message_create_or_update'
25
+ import { useMessageCreateOrUpdate } from '../../hooks/use_message_create_or_update'
26
26
  import { Haptic } from '../../utils/native_adapters'
27
27
  import { TheirReplyConnector, MyReplyConnector, AVATAR_CONNECTOR_SPACING } from './reply_connectors'
28
28
  import { pluralize } from '../../utils'
29
29
  import { useConversationMessage } from '../../hooks/use_conversation_message'
30
+ import { isNewMessage } from '../../utils/cache/messages_cache'
30
31
 
31
32
  /** Message
32
33
  * Component for display of a message within a conversation list
@@ -79,7 +80,6 @@ export function Message({
79
80
  const isReplyRootMessage = message.replyRootId === message.id
80
81
  const isDeletedReplyRootMessage = isReplyRootMessage && !!message.deletedAt
81
82
 
82
- const messageText = isDeletedReplyRootMessage ? 'Message deleted' : text
83
83
  const replyCountText = pluralize(message.replyCount, 'reply')
84
84
  const messagePendingLabel = isPersisted ? 'Saving' : 'Sending'
85
85
  const replyRootAuthorName = message.replyRootId
@@ -117,6 +117,10 @@ export function Message({
117
117
  }
118
118
  }
119
119
 
120
+ const handleMessagePress = () => {
121
+ setShowMessageMetaToggle(!showMessageMetaToggle)
122
+ }
123
+
120
124
  const handleMessageLongPress = () => {
121
125
  if (!isPersisted) return
122
126
 
@@ -129,6 +133,7 @@ export function Message({
129
133
  reply_root_author_name: replyRootAuthorName,
130
134
  })
131
135
  }
136
+
132
137
  const handleReactionLongPress = (reaction: ReactionCountResource) => {
133
138
  Haptic.impactLight()
134
139
  navigation.navigate('Reactions', {
@@ -161,11 +166,12 @@ export function Message({
161
166
  return (
162
167
  <Pressable
163
168
  onLongPress={handleMessageLongPress}
164
- onPress={() => setShowMessageMetaToggle(!showMessageMetaToggle)}
169
+ onPress={handleMessagePress}
165
170
  onPressIn={handleMessagePressIn}
166
171
  onPressOut={handleMessagePressOut}
167
172
  android_ripple={{ color: colors.androidRippleNeutral, borderless: false, foreground: true }}
168
173
  accessibilityHint="Long press to view message actions like reacting and copying."
174
+ disabled={isDeletedReplyRootMessage}
169
175
  >
170
176
  <Animated.View style={[styles.message, animatedBackgroundColor]}>
171
177
  {!message.mine && (
@@ -189,7 +195,7 @@ export function Message({
189
195
  </View>
190
196
  )}
191
197
  <View style={[styles.messageContent, { marginBottom: messageBottomMargin }]}>
192
- {renderAuthor && (
198
+ {renderAuthor && !isDeletedReplyRootMessage && (
193
199
  <Text variant="footnote" style={styles.authorName}>
194
200
  {message.author.name}
195
201
  </Text>
@@ -198,18 +204,26 @@ export function Message({
198
204
  style={styles.messageBubble}
199
205
  onLayout={e => setMessageBubbleHeight(e.nativeEvent.layout.height)}
200
206
  >
201
- <ErrorBoundary>
202
- <MessageAttachments
203
- attachments={message.attachments}
204
- metaProps={metaProps}
205
- onMessageAttachmentLongPress={handleMessageAttachmentLongPress}
206
- onMessageLongPress={handleMessageLongPress}
207
- />
208
- </ErrorBoundary>
209
- {messageText && (
207
+ {isDeletedReplyRootMessage ? (
210
208
  <View style={styles.messageText}>
211
- <MessageMarkdown text={messageText} />
209
+ <Text style={styles.replyRootDeletedText}>Message deleted</Text>
212
210
  </View>
211
+ ) : (
212
+ <>
213
+ <ErrorBoundary>
214
+ <MessageAttachments
215
+ attachments={message.attachments}
216
+ metaProps={metaProps}
217
+ onMessageAttachmentLongPress={handleMessageAttachmentLongPress}
218
+ onMessageLongPress={handleMessageLongPress}
219
+ />
220
+ </ErrorBoundary>
221
+ {text && (
222
+ <View style={styles.messageText}>
223
+ <MessageMarkdown text={text} />
224
+ </View>
225
+ )}
226
+ </>
213
227
  )}
214
228
  </View>
215
229
  {showReplyCountButton && (
@@ -223,7 +237,7 @@ export function Message({
223
237
  {replyCountText}
224
238
  </TextButton>
225
239
  )}
226
- {hasReactions && (
240
+ {hasReactions && !isDeletedReplyRootMessage && (
227
241
  <View style={styles.messageReactions}>
228
242
  {reactionCounts.map(reaction => (
229
243
  <MessageReaction
@@ -236,7 +250,7 @@ export function Message({
236
250
  ))}
237
251
  </View>
238
252
  )}
239
- {showMessageMeta && (
253
+ {showMessageMeta && !isDeletedReplyRootMessage && (
240
254
  <View style={styles.messageMeta}>
241
255
  {message.mine && !pending && !error && (
242
256
  <MessageReadReceipts
@@ -364,5 +378,9 @@ const useMessageStyles = ({ mine }: MessageResource) => {
364
378
  errorText: {
365
379
  color: colors.statusErrorText,
366
380
  },
381
+ replyRootDeletedText: {
382
+ color: colors.textColorDefaultSecondary,
383
+ fontStyle: 'italic',
384
+ },
367
385
  })
368
386
  }
@@ -1,9 +1,4 @@
1
- import {
2
- RouteProp,
3
- useNavigation,
4
- useTheme as useNavigationTheme,
5
- useRoute,
6
- } from '@react-navigation/native'
1
+ import { useNavigation, useTheme as useNavigationTheme, useRoute } from '@react-navigation/native'
7
2
  import React, { useCallback, useContext, useEffect, useState } from 'react'
8
3
  import {
9
4
  Platform,
@@ -319,9 +314,7 @@ function MessageFormInput() {
319
314
  React.useContext(MessageFormContext)
320
315
  const attachmentError = attachmentUploader?.errorMessage
321
316
 
322
- const route = useRoute<RouteProp<ConversationScreenProps['route']>>()
323
- const conversationId = route.params.conversation_id
324
- const broadcastTypingStatus = useBroadcastTypingStatus(conversationId)
317
+ const broadcastTypingStatus = useBroadcastTypingStatus()
325
318
 
326
319
  const handleTextChange = (newText: string) => {
327
320
  setText(newText)
@@ -41,16 +41,12 @@ const TypingDots = () => {
41
41
  )
42
42
  }
43
43
 
44
- interface TypingIndicatorProps {
45
- conversationId: number
46
- }
47
-
48
44
  /**
49
45
  * Component to display typing indicators in a conversation
50
46
  * Shows "X is typing..." with animated dots
51
47
  */
52
- export const TypingIndicator = ({ conversationId }: TypingIndicatorProps) => {
53
- const typingPeople = useTypingIndicators(conversationId)
48
+ export const TypingIndicator = () => {
49
+ const typingPeople = useTypingIndicators()
54
50
  const styles = useStyles()
55
51
  const enabled = typingPeople.length > 0
56
52
 
@@ -0,0 +1,34 @@
1
+ import React, { createContext, PropsWithChildren, useContext, useMemo } from 'react'
2
+
3
+ interface ConversationContextValue {
4
+ conversationId: number
5
+ currentPageReplyRootId: string | null
6
+ }
7
+
8
+ interface ConversationContextProviderProps extends PropsWithChildren {
9
+ conversationId: number
10
+ currentPageReplyRootId: string | null
11
+ }
12
+
13
+ const ConversationContext = createContext<ConversationContextValue>({
14
+ conversationId: 0,
15
+ currentPageReplyRootId: null,
16
+ })
17
+
18
+ export const ConversationContextProvider = ({
19
+ children,
20
+ conversationId,
21
+ currentPageReplyRootId,
22
+ }: ConversationContextProviderProps) => {
23
+ const value = useMemo(
24
+ () => ({
25
+ conversationId,
26
+ currentPageReplyRootId,
27
+ }),
28
+ [conversationId, currentPageReplyRootId]
29
+ )
30
+
31
+ return <ConversationContext.Provider value={value}>{children}</ConversationContext.Provider>
32
+ }
33
+
34
+ export const useConversationContext = () => useContext(ConversationContext)
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useRef } from 'react'
2
2
  import { useApiClient } from './use_api_client'
3
+ import { useConversationContext } from '../contexts/conversation_context'
3
4
 
4
5
  const THROTTLE_INTERVAL = 3000 // 3 seconds
5
6
 
@@ -10,7 +11,8 @@ const THROTTLE_INTERVAL = 3000 // 3 seconds
10
11
  * after receiving a typing event. This is how we can show a steady typing indicator even
11
12
  * if the user types once every 2.9 seconds.
12
13
  */
13
- export const useBroadcastTypingStatus = (conversationId: string | number) => {
14
+ export const useBroadcastTypingStatus = () => {
15
+ const { conversationId, currentPageReplyRootId } = useConversationContext()
14
16
  const apiClient = useApiClient()
15
17
  const lastBroadcastTime = useRef<number>(0)
16
18
 
@@ -26,12 +28,14 @@ export const useBroadcastTypingStatus = (conversationId: string | number) => {
26
28
  apiClient.chat
27
29
  .post({
28
30
  url: `/me/conversations/${conversationId}/broadcast_typing_status`,
29
- data: { data: { type: 'TypingStatus', attributes: {} } },
31
+ data: {
32
+ data: { type: 'TypingStatus', attributes: { reply_root_id: currentPageReplyRootId } },
33
+ },
30
34
  })
31
35
  .catch(error => {
32
36
  console.error('Failed to broadcast typing status:', error)
33
37
  })
34
- }, [apiClient, conversationId])
38
+ }, [apiClient.chat, conversationId, currentPageReplyRootId])
35
39
 
36
40
  return broadcastTypingStatus
37
41
  }
@@ -1,22 +1,21 @@
1
1
  import { ApiCollection, MessageResource } from '../types'
2
2
  import { useJoltChannel, useJoltEvent } from './use_jolt'
3
- import {
4
- deleteRecordInPagesData,
5
- updateOrCreateRecordInPagesData,
6
- updateRecordInPagesData,
7
- } from '../utils'
3
+ import { deleteRecordInPagesData } from '../utils'
8
4
  import { MessageCreatedEvent, MessageDeletedEvent } from '../types/jolt_events/message_events'
9
5
  import { InfiniteData, useQueryClient } from '@tanstack/react-query'
10
6
  import { useCurrentPerson } from './use_current_person'
11
7
  import { transformMessageEventDataToMessageResource } from '../utils/jolt/transform_message_event_data_to_message_resource'
12
8
  import { getRequestQueryKey } from './use_suspense_api'
13
9
  import { JoltReactionEvent, JoltTypingEvent } from '../types/jolt_events'
14
- import { transformReactionEventDataToReactionCountResource } from '../utils/jolt/transform_reaction_event_data_to_reaction_count_resource'
15
10
  import { getMessagesRequestArgs } from '../utils/request/get_messages'
16
11
  import { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache'
17
- import { isTemporaryMessageId } from './use_message_create_or_update'
18
12
  import { completeMessageCreationTracking } from '../utils/performance_tracking'
19
13
  import { useApiClient } from './use_api_client'
14
+ import {
15
+ updateCacheWithMessage,
16
+ updateCacheWithReaction,
17
+ getThreadedMessagesQueryKey,
18
+ } from '../utils/cache/messages_cache'
20
19
 
21
20
  interface Props {
22
21
  conversationId: number
@@ -47,42 +46,17 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
47
46
  }
48
47
  }
49
48
 
50
- queryClient.setQueryData<QueryData>(messagesQueryKey, prev => {
51
- if (e.event === 'message.created') {
52
- // Before adding the new message, remove any pending temporary messages
53
- // with matching text to prevent duplicates from race conditions
54
- let dataAfterTempRemoval = prev
55
- if (prev && message.text && message.mine) {
56
- dataAfterTempRemoval = deleteRecordInPagesData({
57
- data: prev,
58
- record: message,
59
- matchFn: (existingMessage, _record) => {
60
- return (
61
- isTemporaryMessageId(existingMessage.id) &&
62
- existingMessage.text === message.text &&
63
- existingMessage.mine
64
- )
65
- },
66
- })
67
- }
68
-
69
- return updateOrCreateRecordInPagesData({
70
- data: dataAfterTempRemoval,
71
- record: message,
72
- processRecord: (record, current) => {
73
- return { ...current, ...record }
74
- },
75
- })
76
- } else {
77
- return updateRecordInPagesData({
78
- data: prev,
79
- record: message,
80
- processRecord: (record, current) => {
81
- return { ...current, ...record }
82
- },
83
- })
84
- }
85
- })
49
+ // Update the main conversation cache
50
+ updateCacheWithMessage(queryClient, messagesQueryKey, message, e.event)
51
+
52
+ // If message has a reply_root_id, also update the threaded cache
53
+ if (data.reply_root_id) {
54
+ const threadedMessagesQueryKey = getThreadedMessagesQueryKey(
55
+ conversationId,
56
+ data.reply_root_id
57
+ )
58
+ updateCacheWithMessage(queryClient, threadedMessagesQueryKey, message, e.event)
59
+ }
86
60
  }
87
61
 
88
62
  const handleMessageDeleted = async (e: MessageDeletedEvent) => {
@@ -92,50 +66,34 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
92
66
  currentPersonId: currentPerson.id,
93
67
  })
94
68
 
69
+ // Update the main conversation cache
95
70
  queryClient.setQueryData<QueryData>(messagesQueryKey, prev =>
96
71
  deleteRecordInPagesData({ data: prev, record: message })
97
72
  )
73
+
74
+ // If message has a reply_root_id, also update the threaded cache
75
+ if (data.reply_root_id) {
76
+ const threadedMessagesQueryKey = getThreadedMessagesQueryKey(
77
+ conversationId,
78
+ data.reply_root_id
79
+ )
80
+ queryClient.setQueryData<QueryData>(threadedMessagesQueryKey, prev =>
81
+ deleteRecordInPagesData({ data: prev, record: message })
82
+ )
83
+ }
98
84
  }
99
85
 
100
86
  const handleReactionJoltEvent = async (e: JoltReactionEvent) => {
101
- const { data } = e.data
102
- const message = { id: data.message_sort_key } as MessageResource
103
- queryClient.setQueryData<QueryData>(messagesQueryKey, prev =>
104
- updateRecordInPagesData({
105
- data: prev,
106
- record: message,
107
- processRecord: (record, oldMessage) => {
108
- const reactionCounts = oldMessage.reactionCounts || []
109
- let foundMatch = false
110
- let newReactionCounts = reactionCounts.map(reactionCount => {
111
- if (reactionCount.value === data.value) {
112
- foundMatch = true
113
- return transformReactionEventDataToReactionCountResource({
114
- data,
115
- oldData: reactionCount,
116
- event: e.event,
117
- currentPersonId: currentPerson.id,
118
- })
119
- }
120
- return reactionCount
121
- })
122
-
123
- if (!foundMatch) {
124
- const newReactionCount = transformReactionEventDataToReactionCountResource({
125
- data,
126
- event: e.event,
127
- currentPersonId: currentPerson.id,
128
- })
129
-
130
- if (newReactionCount?.count) {
131
- newReactionCounts = [...newReactionCounts, newReactionCount]
132
- }
133
- }
134
-
135
- return { ...oldMessage, reactionCounts: newReactionCounts }
136
- },
137
- })
138
- )
87
+ // Update the main conversation cache and capture the reply_root_id if present
88
+ updateCacheWithReaction(queryClient, messagesQueryKey, e, currentPerson.id)
89
+
90
+ const replyRootId = e.data.data.reply_root_id
91
+
92
+ // If the message has a reply_root_id, also update the threaded cache
93
+ if (replyRootId) {
94
+ const threadedMessagesQueryKey = getThreadedMessagesQueryKey(conversationId, replyRootId)
95
+ updateCacheWithReaction(queryClient, threadedMessagesQueryKey, e, currentPerson.id)
96
+ }
139
97
  }
140
98
 
141
99
  const handleTypingEvent = async (e: JoltTypingEvent) => {