@planningcenter/chat-react-native 3.18.0-rc.0 → 3.18.0-rc.10

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 (129) hide show
  1. package/build/components/conversation/message.d.ts +2 -1
  2. package/build/components/conversation/message.d.ts.map +1 -1
  3. package/build/components/conversation/message.js +28 -17
  4. package/build/components/conversation/message.js.map +1 -1
  5. package/build/components/conversation/message_form.d.ts.map +1 -1
  6. package/build/components/conversation/message_form.js +7 -6
  7. package/build/components/conversation/message_form.js.map +1 -1
  8. package/build/components/conversation/messages_disabled_banners.d.ts +3 -0
  9. package/build/components/conversation/messages_disabled_banners.d.ts.map +1 -0
  10. package/build/components/conversation/messages_disabled_banners.js +53 -0
  11. package/build/components/conversation/messages_disabled_banners.js.map +1 -0
  12. package/build/components/conversation/reply_connectors.d.ts.map +1 -1
  13. package/build/components/conversation/reply_connectors.js +0 -5
  14. package/build/components/conversation/reply_connectors.js.map +1 -1
  15. package/build/components/conversation/typing_indicator.d.ts +1 -5
  16. package/build/components/conversation/typing_indicator.d.ts.map +1 -1
  17. package/build/components/conversation/typing_indicator.js +2 -2
  18. package/build/components/conversation/typing_indicator.js.map +1 -1
  19. package/build/components/conversations/conversations.d.ts.map +1 -1
  20. package/build/components/conversations/conversations.js +2 -3
  21. package/build/components/conversations/conversations.js.map +1 -1
  22. package/build/contexts/conversation_context.d.ts +13 -0
  23. package/build/contexts/conversation_context.d.ts.map +1 -0
  24. package/build/contexts/conversation_context.js +14 -0
  25. package/build/contexts/conversation_context.js.map +1 -0
  26. package/build/hooks/use_broadcast_typing_status.d.ts +1 -1
  27. package/build/hooks/use_broadcast_typing_status.d.ts.map +1 -1
  28. package/build/hooks/use_broadcast_typing_status.js +7 -3
  29. package/build/hooks/use_broadcast_typing_status.js.map +1 -1
  30. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  31. package/build/hooks/use_conversation_messages.js +2 -1
  32. package/build/hooks/use_conversation_messages.js.map +1 -1
  33. package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
  34. package/build/hooks/use_conversation_messages_jolt_events.js +23 -70
  35. package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
  36. package/build/hooks/use_features.d.ts +9 -0
  37. package/build/hooks/use_features.d.ts.map +1 -0
  38. package/build/hooks/use_features.js +35 -0
  39. package/build/hooks/use_features.js.map +1 -0
  40. package/build/hooks/use_message_create_or_update.d.ts +0 -2
  41. package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
  42. package/build/hooks/use_message_create_or_update.js +10 -8
  43. package/build/hooks/use_message_create_or_update.js.map +1 -1
  44. package/build/hooks/use_typing_indicators.d.ts +1 -1
  45. package/build/hooks/use_typing_indicators.d.ts.map +1 -1
  46. package/build/hooks/use_typing_indicators.js +16 -3
  47. package/build/hooks/use_typing_indicators.js.map +1 -1
  48. package/build/screens/conversation_details_screen.d.ts.map +1 -1
  49. package/build/screens/conversation_details_screen.js +9 -6
  50. package/build/screens/conversation_details_screen.js.map +1 -1
  51. package/build/screens/conversation_new/components/form_list.d.ts +2 -2
  52. package/build/screens/conversation_new/components/form_list.d.ts.map +1 -1
  53. package/build/screens/conversation_new/components/form_list.js +2 -3
  54. package/build/screens/conversation_new/components/form_list.js.map +1 -1
  55. package/build/screens/conversation_screen.d.ts +2 -1
  56. package/build/screens/conversation_screen.d.ts.map +1 -1
  57. package/build/screens/conversation_screen.js +41 -18
  58. package/build/screens/conversation_screen.js.map +1 -1
  59. package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.d.ts.map +1 -1
  60. package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.js +2 -3
  61. package/build/screens/conversation_select_recipients/conversation_select_group_recipients_screen.js.map +1 -1
  62. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.d.ts.map +1 -1
  63. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js +2 -3
  64. package/build/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.js.map +1 -1
  65. package/build/screens/message_actions_screen.js +4 -2
  66. package/build/screens/message_actions_screen.js.map +1 -1
  67. package/build/types/jolt_events/reaction_events.d.ts +1 -0
  68. package/build/types/jolt_events/reaction_events.d.ts.map +1 -1
  69. package/build/types/jolt_events/reaction_events.js.map +1 -1
  70. package/build/types/jolt_events/typing_events.d.ts +1 -0
  71. package/build/types/jolt_events/typing_events.d.ts.map +1 -1
  72. package/build/types/jolt_events/typing_events.js.map +1 -1
  73. package/build/types/resources/feature_resource.d.ts +7 -0
  74. package/build/types/resources/feature_resource.d.ts.map +1 -0
  75. package/build/types/resources/feature_resource.js +2 -0
  76. package/build/types/resources/feature_resource.js.map +1 -0
  77. package/build/utils/cache/messages_cache.d.ts +9 -0
  78. package/build/utils/cache/messages_cache.d.ts.map +1 -0
  79. package/build/utils/cache/messages_cache.js +89 -0
  80. package/build/utils/cache/messages_cache.js.map +1 -0
  81. package/build/utils/cache/optimistically_create_message.d.ts +2 -1
  82. package/build/utils/cache/optimistically_create_message.d.ts.map +1 -1
  83. package/build/utils/cache/optimistically_create_message.js +6 -3
  84. package/build/utils/cache/optimistically_create_message.js.map +1 -1
  85. package/build/utils/index.d.ts +0 -1
  86. package/build/utils/index.d.ts.map +1 -1
  87. package/build/utils/index.js +0 -1
  88. package/build/utils/index.js.map +1 -1
  89. package/build/utils/request/get_features.d.ts +11 -0
  90. package/build/utils/request/get_features.d.ts.map +1 -0
  91. package/build/utils/request/get_features.js +18 -0
  92. package/build/utils/request/get_features.js.map +1 -0
  93. package/package.json +2 -3
  94. package/src/components/conversation/message.tsx +42 -20
  95. package/src/components/conversation/message_form.tsx +6 -11
  96. package/src/components/conversation/messages_disabled_banners.tsx +69 -0
  97. package/src/components/conversation/reply_connectors.tsx +0 -3
  98. package/src/components/conversation/typing_indicator.tsx +2 -6
  99. package/src/components/conversations/conversations.tsx +7 -9
  100. package/src/contexts/conversation_context.tsx +34 -0
  101. package/src/hooks/use_broadcast_typing_status.ts +7 -3
  102. package/src/hooks/use_conversation_messages.ts +3 -1
  103. package/src/hooks/use_conversation_messages_jolt_events.ts +39 -81
  104. package/src/hooks/use_features.ts +47 -0
  105. package/src/hooks/use_message_create_or_update.ts +10 -9
  106. package/src/hooks/use_typing_indicators.ts +15 -3
  107. package/src/screens/conversation_details_screen.tsx +9 -6
  108. package/src/screens/conversation_new/components/form_list.tsx +3 -5
  109. package/src/screens/conversation_screen.tsx +58 -20
  110. package/src/screens/conversation_select_recipients/conversation_select_group_recipients_screen.tsx +2 -4
  111. package/src/screens/conversation_select_recipients/conversation_select_teams_i_lead_recipients_screen.tsx +2 -4
  112. package/src/screens/message_actions_screen.tsx +4 -2
  113. package/src/types/jolt_events/reaction_events.ts +1 -0
  114. package/src/types/jolt_events/typing_events.ts +1 -0
  115. package/src/types/resources/feature_resource.ts +6 -0
  116. package/src/utils/cache/messages_cache.ts +113 -0
  117. package/src/utils/cache/optimistically_create_message.ts +7 -2
  118. package/src/utils/index.ts +0 -1
  119. package/src/utils/request/get_features.ts +20 -0
  120. package/build/components/conversation/disabled_replies_banners.d.ts +0 -3
  121. package/build/components/conversation/disabled_replies_banners.d.ts.map +0 -1
  122. package/build/components/conversation/disabled_replies_banners.js +0 -41
  123. package/build/components/conversation/disabled_replies_banners.js.map +0 -1
  124. package/build/utils/replies_local_feature_flag.d.ts +0 -2
  125. package/build/utils/replies_local_feature_flag.d.ts.map +0 -1
  126. package/build/utils/replies_local_feature_flag.js +0 -3
  127. package/build/utils/replies_local_feature_flag.js.map +0 -1
  128. package/src/components/conversation/disabled_replies_banners.tsx +0 -58
  129. package/src/utils/replies_local_feature_flag.ts +0 -2
@@ -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"]}
@@ -9,5 +9,4 @@ export * from './pluralize';
9
9
  export * from './destructure_chat_group_graph_id';
10
10
  export * from './convert_attachments_for_create';
11
11
  export * from './assert_keys_are_numbers';
12
- export * from './replies_local_feature_flag';
13
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,mCAAmC,CAAA;AACjD,cAAc,kCAAkC,CAAA;AAChD,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,mCAAmC,CAAA;AACjD,cAAc,kCAAkC,CAAA;AAChD,cAAc,2BAA2B,CAAA"}
@@ -9,5 +9,4 @@ export * from './pluralize';
9
9
  export * from './destructure_chat_group_graph_id';
10
10
  export * from './convert_attachments_for_create';
11
11
  export * from './assert_keys_are_numbers';
12
- export * from './replies_local_feature_flag';
13
12
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,mCAAmC,CAAA;AACjD,cAAc,kCAAkC,CAAA;AAChD,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA","sourcesContent":["export * from './session'\nexport * from './theme'\nexport * from './styles'\nexport * from './client'\nexport * from './uri'\nexport * from './cache'\nexport * from './native_adapters'\nexport * from './pluralize'\nexport * from './destructure_chat_group_graph_id'\nexport * from './convert_attachments_for_create'\nexport * from './assert_keys_are_numbers'\nexport * from './replies_local_feature_flag'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,OAAO,CAAA;AACrB,cAAc,SAAS,CAAA;AACvB,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,mCAAmC,CAAA;AACjD,cAAc,kCAAkC,CAAA;AAChD,cAAc,2BAA2B,CAAA","sourcesContent":["export * from './session'\nexport * from './theme'\nexport * from './styles'\nexport * from './client'\nexport * from './uri'\nexport * from './cache'\nexport * from './native_adapters'\nexport * from './pluralize'\nexport * from './destructure_chat_group_graph_id'\nexport * from './convert_attachments_for_create'\nexport * from './assert_keys_are_numbers'\n"]}
@@ -0,0 +1,11 @@
1
+ export declare const getFeaturesRequestArgs: () => {
2
+ url: string;
3
+ data: {
4
+ perPage: number;
5
+ fields: {
6
+ Feature: string[];
7
+ };
8
+ };
9
+ };
10
+ export declare const getFeaturesQueryKey: () => import("../../hooks/use_suspense_api").RequestQueryKey;
11
+ //# sourceMappingURL=get_features.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get_features.d.ts","sourceRoot":"","sources":["../../../src/utils/request/get_features.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,sBAAsB;;;;;;;;CAYlC,CAAA;AAED,eAAO,MAAM,mBAAmB,8DAG/B,CAAA"}
@@ -0,0 +1,18 @@
1
+ import { getRequestQueryKey } from '../../hooks/use_suspense_api';
2
+ export const getFeaturesRequestArgs = () => {
3
+ const url = '/me/features';
4
+ return {
5
+ url,
6
+ data: {
7
+ perPage: 100,
8
+ fields: {
9
+ Feature: ['name', 'enabled'],
10
+ },
11
+ },
12
+ };
13
+ };
14
+ export const getFeaturesQueryKey = () => {
15
+ const requestArgs = getFeaturesRequestArgs();
16
+ return getRequestQueryKey(requestArgs);
17
+ };
18
+ //# sourceMappingURL=get_features.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get_features.js","sourceRoot":"","sources":["../../../src/utils/request/get_features.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAA;AAEjE,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,EAAE;IACzC,MAAM,GAAG,GAAG,cAAc,CAAA;IAE1B,OAAO;QACL,GAAG;QACH,IAAI,EAAE;YACJ,OAAO,EAAE,GAAG;YACZ,MAAM,EAAE;gBACN,OAAO,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC;aAC7B;SACF;KACF,CAAA;AACH,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,EAAE;IACtC,MAAM,WAAW,GAAG,sBAAsB,EAAE,CAAA;IAC5C,OAAO,kBAAkB,CAAC,WAAW,CAAC,CAAA;AACxC,CAAC,CAAA","sourcesContent":["import { getRequestQueryKey } from '../../hooks/use_suspense_api'\n\nexport const getFeaturesRequestArgs = () => {\n const url = '/me/features'\n\n return {\n url,\n data: {\n perPage: 100,\n fields: {\n Feature: ['name', 'enabled'],\n },\n },\n }\n}\n\nexport const getFeaturesQueryKey = () => {\n const requestArgs = getFeaturesRequestArgs()\n return getRequestQueryKey(requestArgs)\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.0",
3
+ "version": "3.18.0-rc.10",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -29,7 +29,6 @@
29
29
  "@react-navigation/elements": "*",
30
30
  "@react-navigation/native": ">=7.0.0",
31
31
  "@react-navigation/native-stack": ">=7.0.0",
32
- "@shopify/flash-list": "<2.0.0",
33
32
  "@tanstack/react-query": "^5.0.0",
34
33
  "color": "^3.1.2",
35
34
  "lodash": "*",
@@ -59,5 +58,5 @@
59
58
  "react-native-url-polyfill": "^2.0.0",
60
59
  "typescript": "<5.6.0"
61
60
  },
62
- "gitHead": "287575911f3f115e39925babf52624759860167a"
61
+ "gitHead": "20b1107e3efd3b9e108ccaada901e34ab49546fa"
63
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
- import { pluralize, REPLIES_FEATURE_ENABLED } from '../../utils'
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
@@ -36,6 +37,7 @@ interface MessageProps extends MessageResource {
36
37
  conversation_id: number
37
38
  latestReadMessageSortKey?: string
38
39
  inReplyScreen?: boolean
40
+ repliesEnabled?: boolean
39
41
  }
40
42
 
41
43
  export function Message({
@@ -43,6 +45,7 @@ export function Message({
43
45
  conversation_id,
44
46
  latestReadMessageSortKey,
45
47
  inReplyScreen,
48
+ repliesEnabled,
46
49
  ...message
47
50
  }: MessageProps) {
48
51
  const { text, reactionCounts, pending, error } = message
@@ -73,11 +76,10 @@ export function Message({
73
76
 
74
77
  const renderAuthor = (!message.mine && message.renderAuthor) || false
75
78
  const showReplyCountButton =
76
- !inReplyScreen && message.replyRootId === message.id && REPLIES_FEATURE_ENABLED
79
+ !inReplyScreen && message.replyRootId === message.id && repliesEnabled
77
80
  const isReplyRootMessage = message.replyRootId === message.id
78
81
  const isDeletedReplyRootMessage = isReplyRootMessage && !!message.deletedAt
79
82
 
80
- const messageText = isDeletedReplyRootMessage ? 'Message deleted' : text
81
83
  const replyCountText = pluralize(message.replyCount, 'reply')
82
84
  const messagePendingLabel = isPersisted ? 'Saving' : 'Sending'
83
85
  const replyRootAuthorName = message.replyRootId
@@ -115,6 +117,10 @@ export function Message({
115
117
  }
116
118
  }
117
119
 
120
+ const handleMessagePress = () => {
121
+ setShowMessageMetaToggle(!showMessageMetaToggle)
122
+ }
123
+
118
124
  const handleMessageLongPress = () => {
119
125
  if (!isPersisted) return
120
126
 
@@ -127,6 +133,7 @@ export function Message({
127
133
  reply_root_author_name: replyRootAuthorName,
128
134
  })
129
135
  }
136
+
130
137
  const handleReactionLongPress = (reaction: ReactionCountResource) => {
131
138
  Haptic.impactLight()
132
139
  navigation.navigate('Reactions', {
@@ -159,11 +166,12 @@ export function Message({
159
166
  return (
160
167
  <Pressable
161
168
  onLongPress={handleMessageLongPress}
162
- onPress={() => setShowMessageMetaToggle(!showMessageMetaToggle)}
169
+ onPress={handleMessagePress}
163
170
  onPressIn={handleMessagePressIn}
164
171
  onPressOut={handleMessagePressOut}
165
172
  android_ripple={{ color: colors.androidRippleNeutral, borderless: false, foreground: true }}
166
173
  accessibilityHint="Long press to view message actions like reacting and copying."
174
+ disabled={isDeletedReplyRootMessage}
167
175
  >
168
176
  <Animated.View style={[styles.message, animatedBackgroundColor]}>
169
177
  {!message.mine && (
@@ -181,11 +189,13 @@ export function Message({
181
189
  ) : (
182
190
  <View style={styles.avatarPlaceholder} />
183
191
  )}
184
- <TheirReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
192
+ {repliesEnabled && (
193
+ <TheirReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
194
+ )}
185
195
  </View>
186
196
  )}
187
197
  <View style={[styles.messageContent, { marginBottom: messageBottomMargin }]}>
188
- {renderAuthor && (
198
+ {renderAuthor && !isDeletedReplyRootMessage && (
189
199
  <Text variant="footnote" style={styles.authorName}>
190
200
  {message.author.name}
191
201
  </Text>
@@ -194,18 +204,26 @@ export function Message({
194
204
  style={styles.messageBubble}
195
205
  onLayout={e => setMessageBubbleHeight(e.nativeEvent.layout.height)}
196
206
  >
197
- <ErrorBoundary>
198
- <MessageAttachments
199
- attachments={message.attachments}
200
- metaProps={metaProps}
201
- onMessageAttachmentLongPress={handleMessageAttachmentLongPress}
202
- onMessageLongPress={handleMessageLongPress}
203
- />
204
- </ErrorBoundary>
205
- {messageText && (
207
+ {isDeletedReplyRootMessage ? (
206
208
  <View style={styles.messageText}>
207
- <MessageMarkdown text={messageText} />
209
+ <Text style={styles.replyRootDeletedText}>Message deleted</Text>
208
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
+ </>
209
227
  )}
210
228
  </View>
211
229
  {showReplyCountButton && (
@@ -219,7 +237,7 @@ export function Message({
219
237
  {replyCountText}
220
238
  </TextButton>
221
239
  )}
222
- {hasReactions && (
240
+ {hasReactions && !isDeletedReplyRootMessage && (
223
241
  <View style={styles.messageReactions}>
224
242
  {reactionCounts.map(reaction => (
225
243
  <MessageReaction
@@ -232,7 +250,7 @@ export function Message({
232
250
  ))}
233
251
  </View>
234
252
  )}
235
- {showMessageMeta && (
253
+ {showMessageMeta && !isDeletedReplyRootMessage && (
236
254
  <View style={styles.messageMeta}>
237
255
  {message.mine && !pending && !error && (
238
256
  <MessageReadReceipts
@@ -282,7 +300,7 @@ export function Message({
282
300
  </View>
283
301
  )}
284
302
  </View>
285
- {message.mine && (
303
+ {repliesEnabled && message.mine && (
286
304
  <MyReplyConnector message={message} messageBubbleHeight={messageBubbleHeight} />
287
305
  )}
288
306
  </Animated.View>
@@ -360,5 +378,9 @@ const useMessageStyles = ({ mine }: MessageResource) => {
360
378
  errorText: {
361
379
  color: colors.statusErrorText,
362
380
  },
381
+ replyRootDeletedText: {
382
+ color: colors.textColorDefaultSecondary,
383
+ fontStyle: 'italic',
384
+ },
363
385
  })
364
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,
@@ -30,10 +25,10 @@ import { ChatContext } from '../../contexts/chat_context'
30
25
  import { Haptic, ImagePicker, ImagePickerResult } from '../../utils/native_adapters'
31
26
  import {
32
27
  MAX_FONT_SIZE_MULTIPLIER_LANDMARK,
33
- REPLIES_FEATURE_ENABLED,
34
28
  platformFontWeightMedium,
35
29
  platformPressedOpacityStyle,
36
30
  } from '../../utils'
31
+ import { availableFeatures, useFeatures } from '../../hooks/use_features'
37
32
  import { useAttachmentUploader } from '../../hooks/use_attachment_uploader'
38
33
  import { useMessageDraft } from '../../hooks/use_message_draft'
39
34
  import {
@@ -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)
@@ -566,9 +559,11 @@ function EditingIndicator() {
566
559
  function ReplyIndicator() {
567
560
  const { replyRootId, replyRootAuthorFirstName } = React.useContext(MessageFormContext)
568
561
  const navigation = useNavigation()
562
+ const { featureEnabled } = useFeatures()
563
+ const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
569
564
  const title = replyRootAuthorFirstName ? `Reply to ${replyRootAuthorFirstName}` : 'Reply'
570
565
 
571
- if (!REPLIES_FEATURE_ENABLED || !replyRootId) return null
566
+ if (!repliesEnabled || !replyRootId) return null
572
567
 
573
568
  return (
574
569
  <FormIndicatorRow
@@ -0,0 +1,69 @@
1
+ import { StyleSheet, View, type ViewStyle } from 'react-native'
2
+ import { useTheme } from '../../hooks'
3
+ import { Text } from '../display'
4
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
5
+ import { useFeatures } from '../../hooks/use_features'
6
+ import { availableFeatures } from '../../hooks/use_features'
7
+
8
+ export const LeaderMessagesDisabledBanner = () => {
9
+ const { featureEnabled } = useFeatures()
10
+ const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
11
+
12
+ const description = repliesEnabled
13
+ ? 'Only leaders can send messages in this conversation.'
14
+ : 'Replies are frozen for everyone else.'
15
+
16
+ return <MessagesDisabledBanner description={description} />
17
+ }
18
+
19
+ export const MemberMessagesDisabledBanner = () => {
20
+ const styles = useStyles()
21
+ const { featureEnabled } = useFeatures()
22
+ const repliesEnabled = featureEnabled(availableFeatures.threaded_replies)
23
+
24
+ const description = repliesEnabled
25
+ ? 'Only leaders can send messages, but you can still add reactions.'
26
+ : 'Replies have been disabled by a leader, but you can still add reactions.'
27
+
28
+ return <MessagesDisabledBanner description={description} style={styles.memberBanner} />
29
+ }
30
+
31
+ interface MessagesDisabledBannerProps {
32
+ description: string
33
+ style?: ViewStyle
34
+ }
35
+
36
+ const MessagesDisabledBanner = ({ description, style }: MessagesDisabledBannerProps) => {
37
+ const styles = useStyles()
38
+
39
+ return (
40
+ <View style={[styles.baseBanner, style]}>
41
+ <Text style={styles.text} variant="tertiary">
42
+ {description}
43
+ </Text>
44
+ </View>
45
+ )
46
+ }
47
+
48
+ const useStyles = () => {
49
+ const { colors } = useTheme()
50
+ const { bottom } = useSafeAreaInsets()
51
+
52
+ return StyleSheet.create({
53
+ baseBanner: {
54
+ paddingHorizontal: 16,
55
+ paddingVertical: 4,
56
+ backgroundColor: colors.statusNeutralComposedBackground,
57
+ borderTopWidth: 1,
58
+ borderTopColor: colors.borderColorDefaultBase,
59
+ },
60
+ text: {
61
+ textAlign: 'center',
62
+ },
63
+ memberBanner: {
64
+ paddingTop: 16,
65
+ paddingBottom: 16 + bottom,
66
+ marginBottom: -bottom,
67
+ },
68
+ })
69
+ }
@@ -6,7 +6,6 @@ import {
6
6
  CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,
7
7
  MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
8
8
  } from '../../utils/styles'
9
- import { REPLIES_FEATURE_ENABLED } from '../../utils'
10
9
 
11
10
  const MY_REPLY_CONNECTOR_WIDTH = 38
12
11
  const CONNECTOR_BORDER_WIDTH = 4
@@ -22,7 +21,6 @@ export function TheirReplyConnector({ message, messageBubbleHeight }: ReplyConne
22
21
  const styles = useStyles()
23
22
  const { nextRendersAuthor, threadPosition, renderAuthor, isReplyShadowMessage } = message
24
23
 
25
- if (!REPLIES_FEATURE_ENABLED) return null
26
24
  if (messageBubbleHeight === 0) return null // Prevents UI shifting
27
25
 
28
26
  const connectorMap: Record<string, React.ReactNode | null> = {
@@ -59,7 +57,6 @@ export function MyReplyConnector({ message, messageBubbleHeight }: ReplyConnecto
59
57
  const { nextRendersAuthor, threadPosition, prevIsMyReply, nextIsMyReply, isReplyShadowMessage } =
60
58
  message
61
59
 
62
- if (!REPLIES_FEATURE_ENABLED) return null
63
60
  if (messageBubbleHeight === 0) return null // Prevents UI shifting
64
61
 
65
62
  const connectorMap: Record<string, React.ReactNode | null> = {
@@ -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
 
@@ -1,7 +1,6 @@
1
1
  import { useNavigation } from '@react-navigation/native'
2
- import { FlashList } from '@shopify/flash-list'
3
2
  import React, { useMemo } from 'react'
4
- import { StyleSheet, View } from 'react-native'
3
+ import { FlatList, StyleSheet, View } from 'react-native'
5
4
  import { useConversationsContext } from '../../contexts/conversations_context'
6
5
  import { useTheme } from '../../hooks'
7
6
  import { ConversationResource } from '../../types'
@@ -35,7 +34,7 @@ export const Conversations = ({ ListHeaderComponent }: ConversationsProps) => {
35
34
 
36
35
  const showBadges = !chat_group_graph_id
37
36
 
38
- const data: FlashListItem[] = useMemo(() => {
37
+ const data: FlatListItem[] = useMemo(() => {
39
38
  if (isLoading) {
40
39
  return loadingPlaceholder
41
40
  }
@@ -54,9 +53,8 @@ export const Conversations = ({ ListHeaderComponent }: ConversationsProps) => {
54
53
 
55
54
  return (
56
55
  <View style={styles.container}>
57
- <FlashList
56
+ <FlatList
58
57
  data={data}
59
- estimatedItemSize={97}
60
58
  keyExtractor={item => item.id.toString()}
61
59
  contentContainerStyle={styles.contentContainer}
62
60
  onRefresh={refetch}
@@ -114,18 +112,18 @@ const useStyles = () => {
114
112
  })
115
113
  }
116
114
 
117
- interface FlashListLoadingItem {
115
+ interface FlatListLoadingItem {
118
116
  type: 'loading'
119
117
  id: string
120
118
  }
121
- interface FlashListConversationItem {
119
+ interface FlatListConversationItem {
122
120
  type: 'conversation'
123
121
  resource: ConversationResource
124
122
  id: number
125
123
  }
126
- type FlashListItem = FlashListLoadingItem | FlashListConversationItem
124
+ type FlatListItem = FlatListLoadingItem | FlatListConversationItem
127
125
 
128
- const loadingPlaceholder: FlashListItem[] = Array.from({ length: 5 }, (_, i) => ({
126
+ const loadingPlaceholder: FlatListItem[] = Array.from({ length: 5 }, (_, i) => ({
129
127
  type: 'loading',
130
128
  id: `loading${i}`,
131
129
  }))