@planningcenter/chat-react-native 3.12.2-qa-328.0 → 3.12.2-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/hooks/use_conversation_jolt_events.d.ts.map +1 -1
- package/build/hooks/use_conversation_jolt_events.js +14 -2
- package/build/hooks/use_conversation_jolt_events.js.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.js +6 -0
- package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
- package/build/hooks/use_conversations_jolt_events.d.ts.map +1 -1
- package/build/hooks/use_conversations_jolt_events.js +13 -1
- package/build/hooks/use_conversations_jolt_events.js.map +1 -1
- package/build/hooks/use_message_create_or_update.d.ts.map +1 -1
- package/build/hooks/use_message_create_or_update.js +22 -1
- package/build/hooks/use_message_create_or_update.js.map +1 -1
- package/build/types/jolt_events/conversation_events.d.ts +1 -0
- package/build/types/jolt_events/conversation_events.d.ts.map +1 -1
- package/build/types/jolt_events/conversation_events.js.map +1 -1
- package/build/types/jolt_events/message_events.d.ts +1 -0
- package/build/types/jolt_events/message_events.d.ts.map +1 -1
- package/build/types/jolt_events/message_events.js.map +1 -1
- package/build/utils/performance_tracking.d.ts +26 -0
- package/build/utils/performance_tracking.d.ts.map +1 -0
- package/build/utils/performance_tracking.js +100 -0
- package/build/utils/performance_tracking.js.map +1 -0
- package/package.json +2 -2
- package/src/hooks/use_conversation_jolt_events.ts +19 -3
- package/src/hooks/use_conversation_messages_jolt_events.ts +6 -0
- package/src/hooks/use_conversations_jolt_events.ts +14 -2
- package/src/hooks/use_message_create_or_update.ts +23 -1
- package/src/types/jolt_events/conversation_events.ts +1 -0
- package/src/types/jolt_events/message_events.ts +1 -0
- package/src/utils/performance_tracking.ts +128 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use_conversation_jolt_events.d.ts","sourceRoot":"","sources":["../../src/hooks/use_conversation_jolt_events.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"use_conversation_jolt_events.d.ts","sourceRoot":"","sources":["../../src/hooks/use_conversation_jolt_events.ts"],"names":[],"mappings":"AAWA,UAAU,KAAK;IACb,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,yBAAyB,CAAC,EAAE,cAAc,EAAE,EAAE,KAAK,QAkElE"}
|
|
@@ -3,13 +3,25 @@ import { getConversationRequestArgs } from './use_conversation';
|
|
|
3
3
|
import { useQueryClient } from '@tanstack/react-query';
|
|
4
4
|
import { getRequestQueryKey } from './use_suspense_api';
|
|
5
5
|
import { useCallback, useMemo } from 'react';
|
|
6
|
+
import { completeMessageCreationConversationTracking } from '../utils/performance_tracking';
|
|
7
|
+
import { useCurrentPerson } from './use_current_person';
|
|
8
|
+
import { useApiClient } from './use_api_client';
|
|
6
9
|
export function useConversationJoltEvents({ conversationId }) {
|
|
7
10
|
const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`);
|
|
8
11
|
const queryClient = useQueryClient();
|
|
12
|
+
const currentPerson = useCurrentPerson();
|
|
13
|
+
const apiClient = useApiClient();
|
|
9
14
|
const queryKey = useMemo(() => getRequestQueryKey(getConversationRequestArgs({ conversation_id: conversationId })), [conversationId]);
|
|
10
|
-
const handleUpdatedConversation = useCallback(() => {
|
|
15
|
+
const handleUpdatedConversation = useCallback((e) => {
|
|
16
|
+
const { last_message_idempotent_key, last_message_author_id } = e.data.data;
|
|
17
|
+
if (last_message_idempotent_key && last_message_author_id === currentPerson.id) {
|
|
18
|
+
completeMessageCreationConversationTracking({
|
|
19
|
+
apiClient,
|
|
20
|
+
idempotentKey: last_message_idempotent_key,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
11
23
|
queryClient.invalidateQueries({ queryKey });
|
|
12
|
-
}, [queryClient, queryKey]);
|
|
24
|
+
}, [queryClient, queryKey, currentPerson.id, apiClient]);
|
|
13
25
|
const handleConversationRead = useCallback((e) => {
|
|
14
26
|
const { latest_read_message_sort_key } = e.data.data;
|
|
15
27
|
queryClient.setQueryData(queryKey, prev => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use_conversation_jolt_events.js","sourceRoot":"","sources":["../../src/hooks/use_conversation_jolt_events.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzD,OAAO,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAA;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"use_conversation_jolt_events.js","sourceRoot":"","sources":["../../src/hooks/use_conversation_jolt_events.ts"],"names":[],"mappings":"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","sourcesContent":["import { JoltConversationEvent } from '../types/jolt_events'\nimport { useJoltChannel, useJoltEvent } from './use_jolt'\nimport { getConversationRequestArgs } from './use_conversation'\nimport { useQueryClient } from '@tanstack/react-query'\nimport { ApiResource, ConversationResource } from '../types'\nimport { getRequestQueryKey } from './use_suspense_api'\nimport { useCallback, useMemo } from 'react'\nimport { completeMessageCreationConversationTracking } from '../utils/performance_tracking'\nimport { useCurrentPerson } from './use_current_person'\nimport { useApiClient } from './use_api_client'\n\ninterface Props {\n conversationId: number\n}\n\nexport function useConversationJoltEvents({ conversationId }: Props) {\n const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`)\n const queryClient = useQueryClient()\n const currentPerson = useCurrentPerson()\n const apiClient = useApiClient()\n const queryKey = useMemo(\n () => getRequestQueryKey(getConversationRequestArgs({ conversation_id: conversationId })),\n [conversationId]\n )\n\n const handleUpdatedConversation = useCallback(\n (e: JoltConversationEvent) => {\n const { last_message_idempotent_key, last_message_author_id } = e.data.data\n\n if (last_message_idempotent_key && last_message_author_id === currentPerson.id) {\n completeMessageCreationConversationTracking({\n apiClient,\n idempotentKey: last_message_idempotent_key,\n })\n }\n queryClient.invalidateQueries({ queryKey })\n },\n [queryClient, queryKey, currentPerson.id, apiClient]\n )\n\n const handleConversationRead = useCallback(\n (e: JoltConversationEvent) => {\n const { latest_read_message_sort_key } = e.data.data\n queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, prev => {\n if (!prev?.data || !latest_read_message_sort_key) return prev\n if (\n prev.data.latestReadMessageSortKey &&\n prev.data.latestReadMessageSortKey >= latest_read_message_sort_key\n ) {\n return prev\n }\n\n return {\n ...prev,\n data: {\n ...prev.data,\n latestReadMessageSortKey: latest_read_message_sort_key,\n },\n }\n })\n },\n [queryClient, queryKey]\n )\n\n const handleConversationDestroyed = useCallback(() => {\n queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, prev => {\n if (!prev?.data) return prev\n\n return {\n ...prev,\n data: {\n ...prev.data,\n deleted: true,\n },\n }\n })\n }, [queryClient, queryKey])\n\n useJoltEvent(joltChannel, 'conversation.updated', handleUpdatedConversation)\n useJoltEvent(joltChannel, 'conversation.read', handleConversationRead)\n useJoltEvent(joltChannel, 'conversation.destroyed', handleConversationDestroyed)\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use_conversation_messages_jolt_events.d.ts","sourceRoot":"","sources":["../../src/hooks/use_conversation_messages_jolt_events.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"use_conversation_messages_jolt_events.d.ts","sourceRoot":"","sources":["../../src/hooks/use_conversation_messages_jolt_events.ts"],"names":[],"mappings":"AAoBA,UAAU,KAAK;IACb,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,iCAAiC,CAAC,EAAE,cAAc,EAAE,EAAE,KAAK,QA+H1E"}
|
|
@@ -8,12 +8,15 @@ import { transformReactionEventDataToReactionCountResource } from '../utils/jolt
|
|
|
8
8
|
import { getMessagesRequestArgs } from '../utils/request/messages';
|
|
9
9
|
import { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache';
|
|
10
10
|
import { isTemporaryMessageId } from './use_message_create_or_update';
|
|
11
|
+
import { completeMessageCreationTracking } from '../utils/performance_tracking';
|
|
12
|
+
import { useApiClient } from './use_api_client';
|
|
11
13
|
export function useConversationMessagesJoltEvents({ conversationId }) {
|
|
12
14
|
const queryClient = useQueryClient();
|
|
13
15
|
const currentPerson = useCurrentPerson();
|
|
14
16
|
const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`);
|
|
15
17
|
const messagesRequestArgs = getMessagesRequestArgs({ conversation_id: conversationId });
|
|
16
18
|
const messagesQueryKey = getRequestQueryKey(messagesRequestArgs);
|
|
19
|
+
const apiClient = useApiClient();
|
|
17
20
|
const { addTypingEvent, removeTypingEventById, removeAllTypingEventsByAuthorId } = useTypingStatusCache(conversationId);
|
|
18
21
|
const handleMessageUpdateOrCreate = async (e) => {
|
|
19
22
|
const { data } = e.data;
|
|
@@ -23,6 +26,9 @@ export function useConversationMessagesJoltEvents({ conversationId }) {
|
|
|
23
26
|
});
|
|
24
27
|
if (e.event === 'message.created' && data.author_id) {
|
|
25
28
|
removeAllTypingEventsByAuthorId(data.author_id);
|
|
29
|
+
if (data.idempotent_key && data.author_id === currentPerson.id) {
|
|
30
|
+
completeMessageCreationTracking({ apiClient, idempotentKey: data.idempotent_key });
|
|
31
|
+
}
|
|
26
32
|
}
|
|
27
33
|
queryClient.setQueryData(messagesQueryKey, prev => {
|
|
28
34
|
if (e.event === 'message.created') {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use_conversation_messages_jolt_events.js","sourceRoot":"","sources":["../../src/hooks/use_conversation_messages_jolt_events.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiCollection, MessageResource } from '../types'\nimport { useJoltChannel, useJoltEvent } from './use_jolt'\nimport {\n deleteRecordInPagesData,\n updateOrCreateRecordInPagesData,\n updateRecordInPagesData,\n} from '../utils'\nimport { MessageCreatedEvent, MessageDeletedEvent } from '../types/jolt_events/message_events'\nimport { InfiniteData, useQueryClient } from '@tanstack/react-query'\nimport { useCurrentPerson } from './use_current_person'\nimport { transformMessageEventDataToMessageResource } from '../utils/jolt/transform_message_event_data_to_message_resource'\nimport { getRequestQueryKey } from './use_suspense_api'\nimport { JoltReactionEvent, JoltTypingEvent } from '../types/jolt_events'\nimport { transformReactionEventDataToReactionCountResource } from '../utils/jolt/transform_reaction_event_data_to_reaction_count_resource'\nimport { getMessagesRequestArgs } from '../utils/request/messages'\nimport { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache'\nimport { isTemporaryMessageId } from './use_message_create_or_update'\n\ninterface Props {\n conversationId: number\n}\n\nexport function useConversationMessagesJoltEvents({ conversationId }: Props) {\n const queryClient = useQueryClient()\n const currentPerson = useCurrentPerson()\n const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`)\n const messagesRequestArgs = getMessagesRequestArgs({ conversation_id: conversationId })\n const messagesQueryKey = getRequestQueryKey(messagesRequestArgs)\n\n const { addTypingEvent, removeTypingEventById, removeAllTypingEventsByAuthorId } =\n useTypingStatusCache(conversationId)\n\n const handleMessageUpdateOrCreate = async (e: MessageCreatedEvent) => {\n const { data } = e.data\n const message = transformMessageEventDataToMessageResource({\n data,\n currentPersonId: currentPerson.id,\n })\n\n if (e.event === 'message.created' && data.author_id) {\n removeAllTypingEventsByAuthorId(data.author_id)\n }\n\n queryClient.setQueryData<QueryData>(messagesQueryKey, prev => {\n if (e.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\n const handleMessageDeleted = async (e: MessageDeletedEvent) => {\n const { data } = e.data\n const message = transformMessageEventDataToMessageResource({\n data,\n currentPersonId: currentPerson.id,\n })\n\n queryClient.setQueryData<QueryData>(messagesQueryKey, prev =>\n deleteRecordInPagesData({ data: prev, record: message })\n )\n }\n\n const handleReactionJoltEvent = async (e: JoltReactionEvent) => {\n const { data } = e.data\n const message = { id: data.message_sort_key } as MessageResource\n queryClient.setQueryData<QueryData>(messagesQueryKey, 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 === data.value) {\n foundMatch = true\n return transformReactionEventDataToReactionCountResource({\n data,\n oldData: reactionCount,\n event: e.event,\n currentPersonId: currentPerson.id,\n })\n }\n return reactionCount\n })\n\n if (!foundMatch) {\n const newReactionCount = transformReactionEventDataToReactionCountResource({\n data,\n event: e.event,\n currentPersonId: currentPerson.id,\n })\n\n if (newReactionCount?.count) {\n newReactionCounts = [...newReactionCounts, newReactionCount]\n }\n }\n\n return { ...oldMessage, reactionCounts: newReactionCounts }\n },\n })\n )\n }\n\n const handleTypingEvent = async (e: JoltTypingEvent) => {\n const { data } = e.data\n addTypingEvent(data)\n setTimeout(() => removeTypingEventById(data.id), TYPING_TIMEOUT_INTERVAL)\n }\n\n useJoltEvent(joltChannel, 'message.created', handleMessageUpdateOrCreate)\n useJoltEvent(joltChannel, 'message.updated', handleMessageUpdateOrCreate)\n useJoltEvent(joltChannel, 'message.destroyed', handleMessageDeleted)\n useJoltEvent(joltChannel, 'reaction.*', handleReactionJoltEvent)\n useJoltEvent(joltChannel, 'typing.broadcast', handleTypingEvent)\n}\n\ntype QueryData = InfiniteData<ApiCollection<MessageResource>>\n"]}
|
|
1
|
+
{"version":3,"file":"use_conversation_messages_jolt_events.js","sourceRoot":"","sources":["../../src/hooks/use_conversation_messages_jolt_events.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApiCollection, MessageResource } from '../types'\nimport { useJoltChannel, useJoltEvent } from './use_jolt'\nimport {\n deleteRecordInPagesData,\n updateOrCreateRecordInPagesData,\n updateRecordInPagesData,\n} from '../utils'\nimport { MessageCreatedEvent, MessageDeletedEvent } from '../types/jolt_events/message_events'\nimport { InfiniteData, useQueryClient } from '@tanstack/react-query'\nimport { useCurrentPerson } from './use_current_person'\nimport { transformMessageEventDataToMessageResource } from '../utils/jolt/transform_message_event_data_to_message_resource'\nimport { getRequestQueryKey } from './use_suspense_api'\nimport { JoltReactionEvent, JoltTypingEvent } from '../types/jolt_events'\nimport { transformReactionEventDataToReactionCountResource } from '../utils/jolt/transform_reaction_event_data_to_reaction_count_resource'\nimport { getMessagesRequestArgs } from '../utils/request/messages'\nimport { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache'\nimport { isTemporaryMessageId } from './use_message_create_or_update'\nimport { completeMessageCreationTracking } from '../utils/performance_tracking'\nimport { useApiClient } from './use_api_client'\n\ninterface Props {\n conversationId: number\n}\n\nexport function useConversationMessagesJoltEvents({ conversationId }: Props) {\n const queryClient = useQueryClient()\n const currentPerson = useCurrentPerson()\n const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`)\n const messagesRequestArgs = getMessagesRequestArgs({ conversation_id: conversationId })\n const messagesQueryKey = getRequestQueryKey(messagesRequestArgs)\n const apiClient = useApiClient()\n\n const { addTypingEvent, removeTypingEventById, removeAllTypingEventsByAuthorId } =\n useTypingStatusCache(conversationId)\n\n const handleMessageUpdateOrCreate = async (e: MessageCreatedEvent) => {\n const { data } = e.data\n const message = transformMessageEventDataToMessageResource({\n data,\n currentPersonId: currentPerson.id,\n })\n\n if (e.event === 'message.created' && data.author_id) {\n removeAllTypingEventsByAuthorId(data.author_id)\n if (data.idempotent_key && data.author_id === currentPerson.id) {\n completeMessageCreationTracking({ apiClient, idempotentKey: data.idempotent_key })\n }\n }\n\n queryClient.setQueryData<QueryData>(messagesQueryKey, prev => {\n if (e.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\n const handleMessageDeleted = async (e: MessageDeletedEvent) => {\n const { data } = e.data\n const message = transformMessageEventDataToMessageResource({\n data,\n currentPersonId: currentPerson.id,\n })\n\n queryClient.setQueryData<QueryData>(messagesQueryKey, prev =>\n deleteRecordInPagesData({ data: prev, record: message })\n )\n }\n\n const handleReactionJoltEvent = async (e: JoltReactionEvent) => {\n const { data } = e.data\n const message = { id: data.message_sort_key } as MessageResource\n queryClient.setQueryData<QueryData>(messagesQueryKey, 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 === data.value) {\n foundMatch = true\n return transformReactionEventDataToReactionCountResource({\n data,\n oldData: reactionCount,\n event: e.event,\n currentPersonId: currentPerson.id,\n })\n }\n return reactionCount\n })\n\n if (!foundMatch) {\n const newReactionCount = transformReactionEventDataToReactionCountResource({\n data,\n event: e.event,\n currentPersonId: currentPerson.id,\n })\n\n if (newReactionCount?.count) {\n newReactionCounts = [...newReactionCounts, newReactionCount]\n }\n }\n\n return { ...oldMessage, reactionCounts: newReactionCounts }\n },\n })\n )\n }\n\n const handleTypingEvent = async (e: JoltTypingEvent) => {\n const { data } = e.data\n addTypingEvent(data)\n setTimeout(() => removeTypingEventById(data.id), TYPING_TIMEOUT_INTERVAL)\n }\n\n useJoltEvent(joltChannel, 'message.created', handleMessageUpdateOrCreate)\n useJoltEvent(joltChannel, 'message.updated', handleMessageUpdateOrCreate)\n useJoltEvent(joltChannel, 'message.destroyed', handleMessageDeleted)\n useJoltEvent(joltChannel, 'reaction.*', handleReactionJoltEvent)\n useJoltEvent(joltChannel, 'typing.broadcast', handleTypingEvent)\n}\n\ntype QueryData = InfiniteData<ApiCollection<MessageResource>>\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use_conversations_jolt_events.d.ts","sourceRoot":"","sources":["../../src/hooks/use_conversations_jolt_events.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAA;
|
|
1
|
+
{"version":3,"file":"use_conversations_jolt_events.d.ts","sourceRoot":"","sources":["../../src/hooks/use_conversations_jolt_events.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAA;AAOvE,wBAAgB,0BAA0B,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,QA4BjF"}
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
import { useConversationsCache } from './use_conversations_cache';
|
|
2
2
|
import { useCurrentPerson, useCurrentPersonCache } from './use_current_person';
|
|
3
3
|
import { useJoltChannel, useJoltEvent } from './use_jolt';
|
|
4
|
+
import { completeMessageCreationConversationTracking } from '../utils/performance_tracking';
|
|
5
|
+
import { useApiClient } from './use_api_client';
|
|
4
6
|
export function useConversationsJoltEvents(args) {
|
|
5
7
|
const currentPerson = useCurrentPerson();
|
|
6
8
|
const joltChannel = useJoltChannel(`chat.people.${currentPerson.id}`);
|
|
7
9
|
const cache = useConversationsCache(args);
|
|
8
10
|
const currentPersonCache = useCurrentPersonCache();
|
|
9
|
-
|
|
11
|
+
const apiClient = useApiClient();
|
|
12
|
+
useJoltEvent(joltChannel, 'conversation.updated', (e) => {
|
|
13
|
+
if (e.data.data.last_message_idempotent_key &&
|
|
14
|
+
e.data.data.last_message_author_id === currentPerson.id) {
|
|
15
|
+
completeMessageCreationConversationTracking({
|
|
16
|
+
apiClient,
|
|
17
|
+
idempotentKey: e.data.data.last_message_idempotent_key,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
cache.fetchUpdate({ id: e.data.data.id });
|
|
21
|
+
});
|
|
10
22
|
useJoltEvent(joltChannel, 'conversation.created', (e) => cache.fetchCreate({ id: e.data.data.id }));
|
|
11
23
|
useJoltEvent(joltChannel, 'conversation.destroyed', (e) => cache.destroy({ id: e.data.data.id }));
|
|
12
24
|
useJoltEvent(joltChannel, 'STREAM_USER_UPDATED', currentPersonCache.invalidate); // Would be nice to have a non-stream chat event
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use_conversations_jolt_events.js","sourceRoot":"","sources":["../../src/hooks/use_conversations_jolt_events.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAA;AACjE,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAC9E,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"use_conversations_jolt_events.js","sourceRoot":"","sources":["../../src/hooks/use_conversations_jolt_events.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAA;AACjE,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAC9E,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzD,OAAO,EAAE,2CAA2C,EAAE,MAAM,+BAA+B,CAAA;AAC3F,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAE/C,MAAM,UAAU,0BAA0B,CAAC,IAAuC;IAChF,MAAM,aAAa,GAAG,gBAAgB,EAAE,CAAA;IACxC,MAAM,WAAW,GAAG,cAAc,CAAC,eAAe,aAAa,CAAC,EAAE,EAAE,CAAC,CAAA;IACrE,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAA;IACzC,MAAM,kBAAkB,GAAG,qBAAqB,EAAE,CAAA;IAClD,MAAM,SAAS,GAAG,YAAY,EAAE,CAAA;IAEhC,YAAY,CAAC,WAAW,EAAE,sBAAsB,EAAE,CAAC,CAAwB,EAAE,EAAE;QAC7E,IACE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,2BAA2B;YACvC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,sBAAsB,KAAK,aAAa,CAAC,EAAE,EACvD,CAAC;YACD,2CAA2C,CAAC;gBAC1C,SAAS;gBACT,aAAa,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,2BAA2B;aACvD,CAAC,CAAA;QACJ,CAAC;QACD,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAA;IAC3C,CAAC,CAAC,CAAA;IACF,YAAY,CAAC,WAAW,EAAE,sBAAsB,EAAE,CAAC,CAAwB,EAAE,EAAE,CAC7E,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAC1C,CAAA;IACD,YAAY,CAAC,WAAW,EAAE,wBAAwB,EAAE,CAAC,CAAwB,EAAE,EAAE,CAC/E,KAAK,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CACtC,CAAA;IAED,YAAY,CAAC,WAAW,EAAE,qBAAqB,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAA,CAAC,gDAAgD;IAChI,YAAY,CAAC,WAAW,EAAE,mBAAmB,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAA;AAC/E,CAAC","sourcesContent":["import { JoltConversationEvent } from '../types/jolt_events'\nimport { ConversationRequestArgs } from '../utils/request/conversation'\nimport { useConversationsCache } from './use_conversations_cache'\nimport { useCurrentPerson, useCurrentPersonCache } from './use_current_person'\nimport { useJoltChannel, useJoltEvent } from './use_jolt'\nimport { completeMessageCreationConversationTracking } from '../utils/performance_tracking'\nimport { useApiClient } from './use_api_client'\n\nexport function useConversationsJoltEvents(args?: Partial<ConversationRequestArgs>) {\n const currentPerson = useCurrentPerson()\n const joltChannel = useJoltChannel(`chat.people.${currentPerson.id}`)\n const cache = useConversationsCache(args)\n const currentPersonCache = useCurrentPersonCache()\n const apiClient = useApiClient()\n\n useJoltEvent(joltChannel, 'conversation.updated', (e: JoltConversationEvent) => {\n if (\n e.data.data.last_message_idempotent_key &&\n e.data.data.last_message_author_id === currentPerson.id\n ) {\n completeMessageCreationConversationTracking({\n apiClient,\n idempotentKey: e.data.data.last_message_idempotent_key,\n })\n }\n cache.fetchUpdate({ id: e.data.data.id })\n })\n useJoltEvent(joltChannel, 'conversation.created', (e: JoltConversationEvent) =>\n cache.fetchCreate({ id: e.data.data.id })\n )\n useJoltEvent(joltChannel, 'conversation.destroyed', (e: JoltConversationEvent) =>\n cache.destroy({ id: e.data.data.id })\n )\n\n useJoltEvent(joltChannel, 'STREAM_USER_UPDATED', currentPersonCache.invalidate) // Would be nice to have a non-stream chat event\n useJoltEvent(joltChannel, 'conversation.read', currentPersonCache.invalidate)\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use_message_create_or_update.d.ts","sourceRoot":"","sources":["../../src/hooks/use_message_create_or_update.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,WAAW,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAOtE,OAAO,EAAE,uCAAuC,EAAE,MAAM,gEAAgE,CAAA;
|
|
1
|
+
{"version":3,"file":"use_message_create_or_update.d.ts","sourceRoot":"","sources":["../../src/hooks/use_message_create_or_update.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiB,WAAW,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAOtE,OAAO,EAAE,uCAAuC,EAAE,MAAM,gEAAgE,CAAA;AAMxH,UAAU,KAAK;IACb,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,eAAe,CAAA;CAC1B;AAED,wBAAgB,wBAAwB,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,KAAK;UAiD/D,MAAM;kBACE,uCAAuC,EAAE;;;GAqE5D;AAED,wBAAgB,oBAAoB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAEvE;AACD,wBAAgB,YAAY,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAE/D"}
|
|
@@ -6,6 +6,7 @@ import { deleteRecordInPagesData, updateOrCreateRecordInPagesData, updateRecordI
|
|
|
6
6
|
import { useCurrentPerson } from './use_current_person';
|
|
7
7
|
import { optimisticallyUpdateMessage } from '../utils/cache/optimistically_update_message';
|
|
8
8
|
import { optimisticallyCreateMessage } from '../utils/cache/optimistically_create_message';
|
|
9
|
+
import { startMessageCreationTracking } from '../utils/performance_tracking';
|
|
9
10
|
export function useMessageCreateOrUpdate({ conversationId, message }) {
|
|
10
11
|
const messageId = message?.id || null;
|
|
11
12
|
const isEditing = !isNewMessage(message);
|
|
@@ -15,11 +16,17 @@ export function useMessageCreateOrUpdate({ conversationId, message }) {
|
|
|
15
16
|
mutationFn: ({ text, attachments, }) => {
|
|
16
17
|
const requestParams = getMessagesRequestArgs({ conversation_id: conversationId });
|
|
17
18
|
const fieldsWithValueJoined = Object.fromEntries(Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')]));
|
|
19
|
+
let attributes = { text, ...(attachments ? { attachments } : {}) };
|
|
20
|
+
if (!isEditing) {
|
|
21
|
+
const idempotentKey = insecureUUID();
|
|
22
|
+
attributes.idempotent_key = idempotentKey;
|
|
23
|
+
startMessageCreationTracking(idempotentKey);
|
|
24
|
+
}
|
|
18
25
|
const data = {
|
|
19
26
|
...requestParams.data,
|
|
20
27
|
data: {
|
|
21
28
|
type: 'Message',
|
|
22
|
-
attributes
|
|
29
|
+
attributes,
|
|
23
30
|
},
|
|
24
31
|
fields: fieldsWithValueJoined,
|
|
25
32
|
};
|
|
@@ -96,4 +103,18 @@ export function isTemporaryMessageId(messageId) {
|
|
|
96
103
|
export function isNewMessage(message) {
|
|
97
104
|
return !message?.id || isTemporaryMessageId(message.id);
|
|
98
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Generate a random UUID (v4) for idempotent keys.
|
|
108
|
+
* Uses Math.random, which is not cryptographically secure.
|
|
109
|
+
* An actual crypto library requires native dependencies.
|
|
110
|
+
* This is OK for now since idempotent keys are not security-sensitive
|
|
111
|
+
* or need to be guaranteed unique.
|
|
112
|
+
* They are short lived and we use it in combination with the message's creator_id so
|
|
113
|
+
* their impact is scoped only the current user.
|
|
114
|
+
*/
|
|
115
|
+
function insecureUUID() {
|
|
116
|
+
return 'xxxxxxxx-xxxx-4xxx-Nxxx-xxxxxxxxxxxx'
|
|
117
|
+
.replace(/x/g, () => Math.floor(Math.random() * 16).toString(16))
|
|
118
|
+
.replace(/N/g, () => (Math.floor(Math.random() * 4) + 8).toString(16));
|
|
119
|
+
}
|
|
99
120
|
//# sourceMappingURL=use_message_create_or_update.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use_message_create_or_update.js","sourceRoot":"","sources":["../../src/hooks/use_message_create_or_update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACjE,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AACzF,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAE/C,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC1D,OAAO,EACL,uBAAuB,EACvB,+BAA+B,EAC/B,uBAAuB,GACxB,MAAM,UAAU,CAAA;AAEjB,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,EAAE,2BAA2B,EAAE,MAAM,8CAA8C,CAAA;AAC1F,OAAO,EAAE,2BAA2B,EAAE,MAAM,8CAA8C,CAAA;
|
|
1
|
+
{"version":3,"file":"use_message_create_or_update.js","sourceRoot":"","sources":["../../src/hooks/use_message_create_or_update.ts"],"names":[],"mappings":"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","sourcesContent":["import { InfiniteData, useMutation } from '@tanstack/react-query'\nimport { getMessagesQueryKey, getMessagesRequestArgs } from './use_conversation_messages'\nimport { useApiClient } from './use_api_client'\nimport { ApiCollection, ApiResource, MessageResource } from '../types'\nimport { chatQueryClient } from '../contexts/api_provider'\nimport {\n deleteRecordInPagesData,\n updateOrCreateRecordInPagesData,\n updateRecordInPagesData,\n} from '../utils'\nimport { DenormalizedAttachmentResourceForCreate } from '../types/resources/denormalized_attachment_resource_for_create'\nimport { useCurrentPerson } from './use_current_person'\nimport { optimisticallyUpdateMessage } from '../utils/cache/optimistically_update_message'\nimport { optimisticallyCreateMessage } from '../utils/cache/optimistically_create_message'\nimport { startMessageCreationTracking } from '../utils/performance_tracking'\n\ninterface Props {\n conversationId: number\n message?: MessageResource\n}\n\nexport function useMessageCreateOrUpdate({ conversationId, message }: Props) {\n const messageId = message?.id || null\n const isEditing = !isNewMessage(message)\n const apiClient = useApiClient()\n const currentPerson = useCurrentPerson()\n\n const mutation = useMutation({\n mutationFn: ({\n text,\n attachments,\n }: {\n text: string\n attachments?: DenormalizedAttachmentResourceForCreate[]\n }) => {\n const requestParams = getMessagesRequestArgs({ conversation_id: conversationId })\n const fieldsWithValueJoined = Object.fromEntries(\n Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')])\n )\n let attributes: any = { text, ...(attachments ? { attachments } : {}) }\n if (!isEditing) {\n const idempotentKey = insecureUUID()\n attributes.idempotent_key = idempotentKey\n startMessageCreationTracking(idempotentKey)\n }\n const data = {\n ...requestParams.data,\n data: {\n type: 'Message',\n attributes,\n },\n fields: fieldsWithValueJoined,\n }\n\n if (isEditing) {\n return apiClient.chat.patch<ApiResource<MessageResource>>({\n url: `/me/conversations/${conversationId}/messages/${messageId}`,\n data,\n })\n } else {\n return apiClient.chat.post<ApiResource<MessageResource>>({\n url: `/me/conversations/${conversationId}/messages`,\n data,\n })\n }\n },\n onMutate: async ({\n text,\n attachments,\n }: {\n text: string\n attachments?: DenormalizedAttachmentResourceForCreate[]\n }) => {\n if (message && isEditing) {\n const optimisticMessage = optimisticallyUpdateMessage({\n conversationId,\n message,\n text,\n })\n\n return { message: optimisticMessage }\n }\n\n const optimisticMessage = optimisticallyCreateMessage({\n conversationId,\n message,\n text,\n attachments,\n currentPerson,\n })\n\n return { message: optimisticMessage }\n },\n onError: (error, variables, context) => {\n const { message: optimisticMessage } = context || {}\n // Add error to the optimistic message from the cache on error\n if (optimisticMessage) {\n const queryKey = getMessagesQueryKey({ conversation_id: conversationId })\n chatQueryClient.setQueryData(\n queryKey,\n (data: InfiniteData<ApiCollection<MessageResource>> | undefined) =>\n updateRecordInPagesData({\n data,\n record: optimisticMessage,\n processRecord: (_next, prev) => ({\n ...prev,\n error: error.message || 'Failed to send message',\n pending: false, // Mark as no longer pending\n }),\n })\n )\n }\n },\n onSuccess: (result: ApiResource<MessageResource>, variables, context) => {\n const { message: optimisticMessage } = context || {}\n const updatedMessage = result.data\n type QueryData = InfiniteData<ApiCollection<MessageResource>>\n const queryKey = getMessagesQueryKey({ conversation_id: conversationId })\n\n // First remove the optimistic message if it exists\n if (optimisticMessage) {\n chatQueryClient.setQueryData<QueryData>(queryKey, data =>\n deleteRecordInPagesData({\n data,\n record: optimisticMessage,\n })\n )\n }\n\n // Then add the real message\n chatQueryClient.setQueryData<QueryData>(queryKey, data =>\n updateOrCreateRecordInPagesData({\n data,\n record: updatedMessage,\n })\n )\n },\n })\n\n return mutation\n}\n\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\n/**\n * Generate a random UUID (v4) for idempotent keys.\n * Uses Math.random, which is not cryptographically secure.\n * An actual crypto library requires native dependencies.\n * This is OK for now since idempotent keys are not security-sensitive\n * or need to be guaranteed unique.\n * They are short lived and we use it in combination with the message's creator_id so\n * their impact is scoped only the current user.\n */\nfunction insecureUUID(): string {\n return 'xxxxxxxx-xxxx-4xxx-Nxxx-xxxxxxxxxxxx'\n .replace(/x/g, () => Math.floor(Math.random() * 16).toString(16))\n .replace(/N/g, () => (Math.floor(Math.random() * 4) + 8).toString(16))\n}\n"]}
|
|
@@ -9,6 +9,7 @@ interface BaseConversationEventData extends Record<string, unknown> {
|
|
|
9
9
|
last_message_author_name?: string;
|
|
10
10
|
last_message_created_at?: DateString;
|
|
11
11
|
last_message_sort_key?: string;
|
|
12
|
+
last_message_idempotent_key?: string | null;
|
|
12
13
|
last_message_text_preview?: string;
|
|
13
14
|
latest_read_message_sort_key?: string;
|
|
14
15
|
organization_id: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation_events.d.ts","sourceRoot":"","sources":["../../../src/types/jolt_events/conversation_events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAA;AAE1F,KAAK,UAAU,GAAG,MAAM,CAAA;AACxB,UAAU,yBAA0B,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACjE,IAAI,EAAE;QACJ,EAAE,EAAE,MAAM,CAAA;QACV,WAAW,EAAE,UAAU,CAAA;QACvB,UAAU,EAAE,UAAU,CAAA;QACtB,sBAAsB,CAAC,EAAE,MAAM,CAAA;QAC/B,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,uBAAuB,CAAC,EAAE,UAAU,CAAA;QACpC,qBAAqB,CAAC,EAAE,MAAM,CAAA;QAC9B,yBAAyB,CAAC,EAAE,MAAM,CAAA;QAClC,4BAA4B,CAAC,EAAE,MAAM,CAAA;QACrC,eAAe,EAAE,MAAM,CAAA;QACvB,gBAAgB,EAAE,OAAO,CAAA;QACzB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,KAAK,EAAE,MAAM,CAAA;QACb,UAAU,EAAE,UAAU,CAAA;KACvB,CAAA;CACF;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,sBAAsB,CAAA;IAC7B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,sBAAsB,CAAA;IAC7B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,wBAAwB,CAAA;IAC/B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,KAAK,EAAE,mBAAmB,CAAA;IAC1B,IAAI,EAAE,yBAAyB,CAAA;CAChC"}
|
|
1
|
+
{"version":3,"file":"conversation_events.d.ts","sourceRoot":"","sources":["../../../src/types/jolt_events/conversation_events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAA;AAE1F,KAAK,UAAU,GAAG,MAAM,CAAA;AACxB,UAAU,yBAA0B,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACjE,IAAI,EAAE;QACJ,EAAE,EAAE,MAAM,CAAA;QACV,WAAW,EAAE,UAAU,CAAA;QACvB,UAAU,EAAE,UAAU,CAAA;QACtB,sBAAsB,CAAC,EAAE,MAAM,CAAA;QAC/B,wBAAwB,CAAC,EAAE,MAAM,CAAA;QACjC,uBAAuB,CAAC,EAAE,UAAU,CAAA;QACpC,qBAAqB,CAAC,EAAE,MAAM,CAAA;QAC9B,2BAA2B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;QAC3C,yBAAyB,CAAC,EAAE,MAAM,CAAA;QAClC,4BAA4B,CAAC,EAAE,MAAM,CAAA;QACrC,eAAe,EAAE,MAAM,CAAA;QACvB,gBAAgB,EAAE,OAAO,CAAA;QACzB,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,KAAK,EAAE,MAAM,CAAA;QACb,UAAU,EAAE,UAAU,CAAA;KACvB,CAAA;CACF;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,sBAAsB,CAAA;IAC7B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,sBAAsB,CAAA;IAC7B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,wBAAyB,SAAQ,aAAa;IAC7D,KAAK,EAAE,wBAAwB,CAAA;IAC/B,IAAI,EAAE,yBAAyB,CAAA;CAChC;AAED,MAAM,WAAW,qBAAsB,SAAQ,aAAa;IAC1D,KAAK,EAAE,mBAAmB,CAAA;IAC1B,IAAI,EAAE,yBAAyB,CAAA;CAChC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation_events.js","sourceRoot":"","sources":["../../../src/types/jolt_events/conversation_events.ts"],"names":[],"mappings":"","sourcesContent":["import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'\n\ntype DateString = string\ninterface BaseConversationEventData extends Record<string, unknown> {\n data: {\n id: number\n archived_at: DateString\n created_at: DateString\n last_message_author_id?: number\n last_message_author_name?: string\n last_message_created_at?: DateString\n last_message_sort_key?: string\n last_message_text_preview?: string\n latest_read_message_sort_key?: string\n organization_id: number\n replies_disabled: boolean\n subtitle?: string\n title: string\n updated_at: DateString\n }\n}\n\nexport interface ConversationCreatedEvent extends CustomMessage {\n event: 'conversation.created'\n data: BaseConversationEventData\n}\n\nexport interface ConversationUpdatedEvent extends CustomMessage {\n event: 'conversation.updated'\n data: BaseConversationEventData\n}\n\nexport interface ConversationDeletedEvent extends CustomMessage {\n event: 'conversation.destroyed'\n data: BaseConversationEventData\n}\n\nexport interface ConversationReadEvent extends CustomMessage {\n event: 'conversation.read'\n data: BaseConversationEventData\n}\n"]}
|
|
1
|
+
{"version":3,"file":"conversation_events.js","sourceRoot":"","sources":["../../../src/types/jolt_events/conversation_events.ts"],"names":[],"mappings":"","sourcesContent":["import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'\n\ntype DateString = string\ninterface BaseConversationEventData extends Record<string, unknown> {\n data: {\n id: number\n archived_at: DateString\n created_at: DateString\n last_message_author_id?: number\n last_message_author_name?: string\n last_message_created_at?: DateString\n last_message_sort_key?: string\n last_message_idempotent_key?: string | null\n last_message_text_preview?: string\n latest_read_message_sort_key?: string\n organization_id: number\n replies_disabled: boolean\n subtitle?: string\n title: string\n updated_at: DateString\n }\n}\n\nexport interface ConversationCreatedEvent extends CustomMessage {\n event: 'conversation.created'\n data: BaseConversationEventData\n}\n\nexport interface ConversationUpdatedEvent extends CustomMessage {\n event: 'conversation.updated'\n data: BaseConversationEventData\n}\n\nexport interface ConversationDeletedEvent extends CustomMessage {\n event: 'conversation.destroyed'\n data: BaseConversationEventData\n}\n\nexport interface ConversationReadEvent extends CustomMessage {\n event: 'conversation.read'\n data: BaseConversationEventData\n}\n"]}
|
|
@@ -14,6 +14,7 @@ interface BaseMessageEventData extends Record<string, unknown> {
|
|
|
14
14
|
text_edited_at: string | null;
|
|
15
15
|
html: string;
|
|
16
16
|
attachments: DenormalizedAttachmentResource[];
|
|
17
|
+
idempotent_key?: string | null;
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
export interface MessageCreatedEvent extends CustomMessage {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message_events.d.ts","sourceRoot":"","sources":["../../../src/types/jolt_events/message_events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAA;AAC1F,OAAO,EAAE,8BAA8B,EAAE,MAAM,+CAA+C,CAAA;AAE9F,UAAU,oBAAqB,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC5D,IAAI,EAAE;QACJ,aAAa,EAAE,MAAM,CAAA;QACrB,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,eAAe,EAAE,MAAM,CAAA;QACvB,UAAU,EAAE,MAAM,CAAA;QAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;QAC9B,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;QAC7B,IAAI,EAAE,MAAM,CAAA;QACZ,WAAW,EAAE,8BAA8B,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"message_events.d.ts","sourceRoot":"","sources":["../../../src/types/jolt_events/message_events.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAA;AAC1F,OAAO,EAAE,8BAA8B,EAAE,MAAM,+CAA+C,CAAA;AAE9F,UAAU,oBAAqB,SAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC5D,IAAI,EAAE;QACJ,aAAa,EAAE,MAAM,CAAA;QACrB,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,eAAe,EAAE,MAAM,CAAA;QACvB,UAAU,EAAE,MAAM,CAAA;QAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;QACzB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;QAC9B,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;QAC7B,IAAI,EAAE,MAAM,CAAA;QACZ,WAAW,EAAE,8BAA8B,EAAE,CAAA;QAC7C,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAC/B,CAAA;CACF;AAED,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,KAAK,EAAE,iBAAiB,CAAA;IACxB,IAAI,EAAE,oBAAoB,CAAA;CAC3B;AAED,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,KAAK,EAAE,iBAAiB,CAAA;IACxB,IAAI,EAAE,oBAAoB,CAAA;CAC3B;AAED,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,KAAK,EAAE,mBAAmB,CAAA;IAC1B,IAAI,EAAE,oBAAoB,CAAA;CAC3B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message_events.js","sourceRoot":"","sources":["../../../src/types/jolt_events/message_events.ts"],"names":[],"mappings":"","sourcesContent":["import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'\nimport { DenormalizedAttachmentResource } from '../resources/denormalized_attachment_resource'\n\ninterface BaseMessageEventData extends Record<string, unknown> {\n data: {\n author_avatar: string\n author_id: number\n author_name: string\n conversation_id: number\n created_at: string\n deleted_at: string | null\n organization_id: number | null\n sort_key: string\n text: string\n text_edited_at: string | null\n html: string\n attachments: DenormalizedAttachmentResource[]\n }\n}\n\nexport interface MessageCreatedEvent extends CustomMessage {\n event: 'message.created'\n data: BaseMessageEventData\n}\n\nexport interface MessageUpdatedEvent extends CustomMessage {\n event: 'message.updated'\n data: BaseMessageEventData\n}\n\nexport interface MessageDeletedEvent extends CustomMessage {\n event: 'message.destroyed'\n data: BaseMessageEventData\n}\n"]}
|
|
1
|
+
{"version":3,"file":"message_events.js","sourceRoot":"","sources":["../../../src/types/jolt_events/message_events.ts"],"names":[],"mappings":"","sourcesContent":["import type { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'\nimport { DenormalizedAttachmentResource } from '../resources/denormalized_attachment_resource'\n\ninterface BaseMessageEventData extends Record<string, unknown> {\n data: {\n author_avatar: string\n author_id: number\n author_name: string\n conversation_id: number\n created_at: string\n deleted_at: string | null\n organization_id: number | null\n sort_key: string\n text: string\n text_edited_at: string | null\n html: string\n attachments: DenormalizedAttachmentResource[]\n idempotent_key?: string | null\n }\n}\n\nexport interface MessageCreatedEvent extends CustomMessage {\n event: 'message.created'\n data: BaseMessageEventData\n}\n\nexport interface MessageUpdatedEvent extends CustomMessage {\n event: 'message.updated'\n data: BaseMessageEventData\n}\n\nexport interface MessageDeletedEvent extends CustomMessage {\n event: 'message.destroyed'\n data: BaseMessageEventData\n}\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance tracking utilities for client-side metrics (React Native)
|
|
3
|
+
*
|
|
4
|
+
* This module provides functionality to track performance metrics and send them
|
|
5
|
+
* to Datadog via the backend API for aggregation.
|
|
6
|
+
*/
|
|
7
|
+
import { ApiClient } from '../hooks';
|
|
8
|
+
/**
|
|
9
|
+
* Start tracking message creation performance
|
|
10
|
+
*/
|
|
11
|
+
export declare function startMessageCreationTracking(idempotentKey: string): void;
|
|
12
|
+
/**
|
|
13
|
+
* Complete message creation tracking when the message.created event is received
|
|
14
|
+
*/
|
|
15
|
+
export declare function completeMessageCreationTracking({ apiClient, idempotentKey, }: {
|
|
16
|
+
apiClient: ApiClient;
|
|
17
|
+
idempotentKey: string;
|
|
18
|
+
}): void;
|
|
19
|
+
/**
|
|
20
|
+
* Complete message creation tracking when the conversation.updated event is received
|
|
21
|
+
*/
|
|
22
|
+
export declare function completeMessageCreationConversationTracking({ apiClient, idempotentKey, }: {
|
|
23
|
+
apiClient: ApiClient;
|
|
24
|
+
idempotentKey: string;
|
|
25
|
+
}): void;
|
|
26
|
+
//# sourceMappingURL=performance_tracking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performance_tracking.d.ts","sourceRoot":"","sources":["../../src/utils/performance_tracking.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAgBpC;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAMxE;AAED;;GAEG;AACH,wBAAgB,+BAA+B,CAAC,EAC9C,SAAS,EACT,aAAa,GACd,EAAE;IACD,SAAS,EAAE,SAAS,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;CACtB,GAAG,IAAI,CASP;AAED;;GAEG;AACH,wBAAgB,2CAA2C,CAAC,EAC1D,SAAS,EACT,aAAa,GACd,EAAE;IACD,SAAS,EAAE,SAAS,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;CACtB,GAAG,IAAI,CASP"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance tracking utilities for client-side metrics (React Native)
|
|
3
|
+
*
|
|
4
|
+
* This module provides functionality to track performance metrics and send them
|
|
5
|
+
* to Datadog via the backend API for aggregation.
|
|
6
|
+
*/
|
|
7
|
+
import DeviceInfo from 'react-native-device-info';
|
|
8
|
+
import { Platform } from 'react-native';
|
|
9
|
+
const appName = DeviceInfo.getApplicationName();
|
|
10
|
+
const pendingMetrics = new Map();
|
|
11
|
+
/**
|
|
12
|
+
* Start tracking message creation performance
|
|
13
|
+
*/
|
|
14
|
+
export function startMessageCreationTracking(idempotentKey) {
|
|
15
|
+
if (!idempotentKey)
|
|
16
|
+
return;
|
|
17
|
+
const startTime = Date.now();
|
|
18
|
+
pendingMetrics.set(`message:${idempotentKey}`, { idempotentKey, startTime });
|
|
19
|
+
pendingMetrics.set(`conversation:${idempotentKey}`, { idempotentKey, startTime });
|
|
20
|
+
cleanupStaleMetrics();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Complete message creation tracking when the message.created event is received
|
|
24
|
+
*/
|
|
25
|
+
export function completeMessageCreationTracking({ apiClient, idempotentKey, }) {
|
|
26
|
+
if (!idempotentKey)
|
|
27
|
+
return;
|
|
28
|
+
const duration = durationForIdempotentKey(`message:${idempotentKey}`);
|
|
29
|
+
if (!duration)
|
|
30
|
+
return;
|
|
31
|
+
sendPerformanceMetric(apiClient, {
|
|
32
|
+
name: 'chat.message.creation.message_roundtrip_time',
|
|
33
|
+
value: duration,
|
|
34
|
+
unit: 'milliseconds',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Complete message creation tracking when the conversation.updated event is received
|
|
39
|
+
*/
|
|
40
|
+
export function completeMessageCreationConversationTracking({ apiClient, idempotentKey, }) {
|
|
41
|
+
if (!idempotentKey)
|
|
42
|
+
return;
|
|
43
|
+
const duration = durationForIdempotentKey(`conversation:${idempotentKey}`);
|
|
44
|
+
if (!duration)
|
|
45
|
+
return;
|
|
46
|
+
sendPerformanceMetric(apiClient, {
|
|
47
|
+
name: 'chat.message.creation.conversation_roundtrip_time',
|
|
48
|
+
value: duration,
|
|
49
|
+
unit: 'milliseconds',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function durationForIdempotentKey(idempotentKey) {
|
|
53
|
+
const metric = pendingMetrics.get(idempotentKey);
|
|
54
|
+
if (!metric)
|
|
55
|
+
return undefined;
|
|
56
|
+
pendingMetrics.delete(idempotentKey);
|
|
57
|
+
const endTime = Date.now();
|
|
58
|
+
return endTime - metric.startTime;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Send performance metric to backend for Datadog submission
|
|
62
|
+
*/
|
|
63
|
+
async function sendPerformanceMetric(apiClient, metric) {
|
|
64
|
+
try {
|
|
65
|
+
await apiClient.chat.post({
|
|
66
|
+
url: `/me/metrics`,
|
|
67
|
+
data: {
|
|
68
|
+
data: {
|
|
69
|
+
type: 'Metric',
|
|
70
|
+
attributes: {
|
|
71
|
+
metric: {
|
|
72
|
+
name: metric.name,
|
|
73
|
+
value: metric.value,
|
|
74
|
+
unit: metric.unit,
|
|
75
|
+
platform: Platform.OS,
|
|
76
|
+
app: appName,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
// Ignore errors
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Clean up any stale metrics (older than 5 minutes)
|
|
89
|
+
* This prevents memory leaks if messages fail to complete for any reason
|
|
90
|
+
*/
|
|
91
|
+
function cleanupStaleMetrics() {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
const staleThreshold = 5 * 60 * 1000; // 5 minutes in milliseconds
|
|
94
|
+
for (const [key, metric] of pendingMetrics.entries()) {
|
|
95
|
+
if (now - metric.startTime > staleThreshold) {
|
|
96
|
+
pendingMetrics.delete(key);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=performance_tracking.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performance_tracking.js","sourceRoot":"","sources":["../../src/utils/performance_tracking.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Performance tracking utilities for client-side metrics (React Native)\n *\n * This module provides functionality to track performance metrics and send them\n * to Datadog via the backend API for aggregation.\n */\n\nimport DeviceInfo from 'react-native-device-info'\nimport { Platform } from 'react-native'\nimport { ApiClient } from '../hooks'\nconst appName = DeviceInfo.getApplicationName()\n\ninterface PerformanceMetric {\n name: string\n value: number\n unit: 'milliseconds' | 'seconds' | 'count'\n}\n\ninterface PendingMessageMetric {\n idempotentKey: string\n startTime: number\n}\n\nconst pendingMetrics = new Map<string, PendingMessageMetric>()\n\n/**\n * Start tracking message creation performance\n */\nexport function startMessageCreationTracking(idempotentKey: string): void {\n if (!idempotentKey) return\n const startTime = Date.now()\n pendingMetrics.set(`message:${idempotentKey}`, { idempotentKey, startTime })\n pendingMetrics.set(`conversation:${idempotentKey}`, { idempotentKey, startTime })\n cleanupStaleMetrics()\n}\n\n/**\n * Complete message creation tracking when the message.created event is received\n */\nexport function completeMessageCreationTracking({\n apiClient,\n idempotentKey,\n}: {\n apiClient: ApiClient\n idempotentKey: string\n}): void {\n if (!idempotentKey) return\n const duration = durationForIdempotentKey(`message:${idempotentKey}`)\n if (!duration) return\n sendPerformanceMetric(apiClient, {\n name: 'chat.message.creation.message_roundtrip_time',\n value: duration,\n unit: 'milliseconds',\n })\n}\n\n/**\n * Complete message creation tracking when the conversation.updated event is received\n */\nexport function completeMessageCreationConversationTracking({\n apiClient,\n idempotentKey,\n}: {\n apiClient: ApiClient\n idempotentKey: string\n}): void {\n if (!idempotentKey) return\n const duration = durationForIdempotentKey(`conversation:${idempotentKey}`)\n if (!duration) return\n sendPerformanceMetric(apiClient, {\n name: 'chat.message.creation.conversation_roundtrip_time',\n value: duration,\n unit: 'milliseconds',\n })\n}\n\nfunction durationForIdempotentKey(idempotentKey: string): number | undefined {\n const metric = pendingMetrics.get(idempotentKey)\n if (!metric) return undefined\n pendingMetrics.delete(idempotentKey)\n const endTime = Date.now()\n return endTime - metric.startTime\n}\n\n/**\n * Send performance metric to backend for Datadog submission\n */\nasync function sendPerformanceMetric(\n apiClient: ApiClient,\n metric: PerformanceMetric\n): Promise<void> {\n try {\n await apiClient.chat.post({\n url: `/me/metrics`,\n data: {\n data: {\n type: 'Metric',\n attributes: {\n metric: {\n name: metric.name,\n value: metric.value,\n unit: metric.unit,\n platform: Platform.OS,\n app: appName,\n },\n },\n },\n },\n })\n } catch (e) {\n // Ignore errors\n }\n}\n\n/**\n * Clean up any stale metrics (older than 5 minutes)\n * This prevents memory leaks if messages fail to complete for any reason\n */\nfunction cleanupStaleMetrics(): void {\n const now = Date.now()\n const staleThreshold = 5 * 60 * 1000 // 5 minutes in milliseconds\n\n for (const [key, metric] of pendingMetrics.entries()) {\n if (now - metric.startTime > staleThreshold) {\n pendingMetrics.delete(key)\n }\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/chat-react-native",
|
|
3
|
-
"version": "3.12.2-
|
|
3
|
+
"version": "3.12.2-rc.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"react-native-url-polyfill": "^2.0.0",
|
|
56
56
|
"typescript": "<5.6.0"
|
|
57
57
|
},
|
|
58
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "cb8a06d84561aae9fdaf50fea7943e6b8e857067"
|
|
59
59
|
}
|
|
@@ -5,6 +5,9 @@ import { useQueryClient } from '@tanstack/react-query'
|
|
|
5
5
|
import { ApiResource, ConversationResource } from '../types'
|
|
6
6
|
import { getRequestQueryKey } from './use_suspense_api'
|
|
7
7
|
import { useCallback, useMemo } from 'react'
|
|
8
|
+
import { completeMessageCreationConversationTracking } from '../utils/performance_tracking'
|
|
9
|
+
import { useCurrentPerson } from './use_current_person'
|
|
10
|
+
import { useApiClient } from './use_api_client'
|
|
8
11
|
|
|
9
12
|
interface Props {
|
|
10
13
|
conversationId: number
|
|
@@ -13,14 +16,27 @@ interface Props {
|
|
|
13
16
|
export function useConversationJoltEvents({ conversationId }: Props) {
|
|
14
17
|
const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`)
|
|
15
18
|
const queryClient = useQueryClient()
|
|
19
|
+
const currentPerson = useCurrentPerson()
|
|
20
|
+
const apiClient = useApiClient()
|
|
16
21
|
const queryKey = useMemo(
|
|
17
22
|
() => getRequestQueryKey(getConversationRequestArgs({ conversation_id: conversationId })),
|
|
18
23
|
[conversationId]
|
|
19
24
|
)
|
|
20
25
|
|
|
21
|
-
const handleUpdatedConversation = useCallback(
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
const handleUpdatedConversation = useCallback(
|
|
27
|
+
(e: JoltConversationEvent) => {
|
|
28
|
+
const { last_message_idempotent_key, last_message_author_id } = e.data.data
|
|
29
|
+
|
|
30
|
+
if (last_message_idempotent_key && last_message_author_id === currentPerson.id) {
|
|
31
|
+
completeMessageCreationConversationTracking({
|
|
32
|
+
apiClient,
|
|
33
|
+
idempotentKey: last_message_idempotent_key,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
queryClient.invalidateQueries({ queryKey })
|
|
37
|
+
},
|
|
38
|
+
[queryClient, queryKey, currentPerson.id, apiClient]
|
|
39
|
+
)
|
|
24
40
|
|
|
25
41
|
const handleConversationRead = useCallback(
|
|
26
42
|
(e: JoltConversationEvent) => {
|
|
@@ -15,6 +15,8 @@ import { transformReactionEventDataToReactionCountResource } from '../utils/jolt
|
|
|
15
15
|
import { getMessagesRequestArgs } from '../utils/request/messages'
|
|
16
16
|
import { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache'
|
|
17
17
|
import { isTemporaryMessageId } from './use_message_create_or_update'
|
|
18
|
+
import { completeMessageCreationTracking } from '../utils/performance_tracking'
|
|
19
|
+
import { useApiClient } from './use_api_client'
|
|
18
20
|
|
|
19
21
|
interface Props {
|
|
20
22
|
conversationId: number
|
|
@@ -26,6 +28,7 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
|
|
|
26
28
|
const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`)
|
|
27
29
|
const messagesRequestArgs = getMessagesRequestArgs({ conversation_id: conversationId })
|
|
28
30
|
const messagesQueryKey = getRequestQueryKey(messagesRequestArgs)
|
|
31
|
+
const apiClient = useApiClient()
|
|
29
32
|
|
|
30
33
|
const { addTypingEvent, removeTypingEventById, removeAllTypingEventsByAuthorId } =
|
|
31
34
|
useTypingStatusCache(conversationId)
|
|
@@ -39,6 +42,9 @@ export function useConversationMessagesJoltEvents({ conversationId }: Props) {
|
|
|
39
42
|
|
|
40
43
|
if (e.event === 'message.created' && data.author_id) {
|
|
41
44
|
removeAllTypingEventsByAuthorId(data.author_id)
|
|
45
|
+
if (data.idempotent_key && data.author_id === currentPerson.id) {
|
|
46
|
+
completeMessageCreationTracking({ apiClient, idempotentKey: data.idempotent_key })
|
|
47
|
+
}
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
queryClient.setQueryData<QueryData>(messagesQueryKey, prev => {
|
|
@@ -3,16 +3,28 @@ import { ConversationRequestArgs } from '../utils/request/conversation'
|
|
|
3
3
|
import { useConversationsCache } from './use_conversations_cache'
|
|
4
4
|
import { useCurrentPerson, useCurrentPersonCache } from './use_current_person'
|
|
5
5
|
import { useJoltChannel, useJoltEvent } from './use_jolt'
|
|
6
|
+
import { completeMessageCreationConversationTracking } from '../utils/performance_tracking'
|
|
7
|
+
import { useApiClient } from './use_api_client'
|
|
6
8
|
|
|
7
9
|
export function useConversationsJoltEvents(args?: Partial<ConversationRequestArgs>) {
|
|
8
10
|
const currentPerson = useCurrentPerson()
|
|
9
11
|
const joltChannel = useJoltChannel(`chat.people.${currentPerson.id}`)
|
|
10
12
|
const cache = useConversationsCache(args)
|
|
11
13
|
const currentPersonCache = useCurrentPersonCache()
|
|
14
|
+
const apiClient = useApiClient()
|
|
12
15
|
|
|
13
|
-
useJoltEvent(joltChannel, 'conversation.updated', (e: JoltConversationEvent) =>
|
|
16
|
+
useJoltEvent(joltChannel, 'conversation.updated', (e: JoltConversationEvent) => {
|
|
17
|
+
if (
|
|
18
|
+
e.data.data.last_message_idempotent_key &&
|
|
19
|
+
e.data.data.last_message_author_id === currentPerson.id
|
|
20
|
+
) {
|
|
21
|
+
completeMessageCreationConversationTracking({
|
|
22
|
+
apiClient,
|
|
23
|
+
idempotentKey: e.data.data.last_message_idempotent_key,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
14
26
|
cache.fetchUpdate({ id: e.data.data.id })
|
|
15
|
-
)
|
|
27
|
+
})
|
|
16
28
|
useJoltEvent(joltChannel, 'conversation.created', (e: JoltConversationEvent) =>
|
|
17
29
|
cache.fetchCreate({ id: e.data.data.id })
|
|
18
30
|
)
|
|
@@ -12,6 +12,7 @@ import { DenormalizedAttachmentResourceForCreate } from '../types/resources/deno
|
|
|
12
12
|
import { useCurrentPerson } from './use_current_person'
|
|
13
13
|
import { optimisticallyUpdateMessage } from '../utils/cache/optimistically_update_message'
|
|
14
14
|
import { optimisticallyCreateMessage } from '../utils/cache/optimistically_create_message'
|
|
15
|
+
import { startMessageCreationTracking } from '../utils/performance_tracking'
|
|
15
16
|
|
|
16
17
|
interface Props {
|
|
17
18
|
conversationId: number
|
|
@@ -36,11 +37,17 @@ export function useMessageCreateOrUpdate({ conversationId, message }: Props) {
|
|
|
36
37
|
const fieldsWithValueJoined = Object.fromEntries(
|
|
37
38
|
Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')])
|
|
38
39
|
)
|
|
40
|
+
let attributes: any = { text, ...(attachments ? { attachments } : {}) }
|
|
41
|
+
if (!isEditing) {
|
|
42
|
+
const idempotentKey = insecureUUID()
|
|
43
|
+
attributes.idempotent_key = idempotentKey
|
|
44
|
+
startMessageCreationTracking(idempotentKey)
|
|
45
|
+
}
|
|
39
46
|
const data = {
|
|
40
47
|
...requestParams.data,
|
|
41
48
|
data: {
|
|
42
49
|
type: 'Message',
|
|
43
|
-
attributes
|
|
50
|
+
attributes,
|
|
44
51
|
},
|
|
45
52
|
fields: fieldsWithValueJoined,
|
|
46
53
|
}
|
|
@@ -139,3 +146,18 @@ export function isTemporaryMessageId(messageId?: string | null): boolean {
|
|
|
139
146
|
export function isNewMessage(message?: MessageResource): boolean {
|
|
140
147
|
return !message?.id || isTemporaryMessageId(message.id)
|
|
141
148
|
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Generate a random UUID (v4) for idempotent keys.
|
|
152
|
+
* Uses Math.random, which is not cryptographically secure.
|
|
153
|
+
* An actual crypto library requires native dependencies.
|
|
154
|
+
* This is OK for now since idempotent keys are not security-sensitive
|
|
155
|
+
* or need to be guaranteed unique.
|
|
156
|
+
* They are short lived and we use it in combination with the message's creator_id so
|
|
157
|
+
* their impact is scoped only the current user.
|
|
158
|
+
*/
|
|
159
|
+
function insecureUUID(): string {
|
|
160
|
+
return 'xxxxxxxx-xxxx-4xxx-Nxxx-xxxxxxxxxxxx'
|
|
161
|
+
.replace(/x/g, () => Math.floor(Math.random() * 16).toString(16))
|
|
162
|
+
.replace(/N/g, () => (Math.floor(Math.random() * 4) + 8).toString(16))
|
|
163
|
+
}
|
|
@@ -10,6 +10,7 @@ interface BaseConversationEventData extends Record<string, unknown> {
|
|
|
10
10
|
last_message_author_name?: string
|
|
11
11
|
last_message_created_at?: DateString
|
|
12
12
|
last_message_sort_key?: string
|
|
13
|
+
last_message_idempotent_key?: string | null
|
|
13
14
|
last_message_text_preview?: string
|
|
14
15
|
latest_read_message_sort_key?: string
|
|
15
16
|
organization_id: number
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance tracking utilities for client-side metrics (React Native)
|
|
3
|
+
*
|
|
4
|
+
* This module provides functionality to track performance metrics and send them
|
|
5
|
+
* to Datadog via the backend API for aggregation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import DeviceInfo from 'react-native-device-info'
|
|
9
|
+
import { Platform } from 'react-native'
|
|
10
|
+
import { ApiClient } from '../hooks'
|
|
11
|
+
const appName = DeviceInfo.getApplicationName()
|
|
12
|
+
|
|
13
|
+
interface PerformanceMetric {
|
|
14
|
+
name: string
|
|
15
|
+
value: number
|
|
16
|
+
unit: 'milliseconds' | 'seconds' | 'count'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface PendingMessageMetric {
|
|
20
|
+
idempotentKey: string
|
|
21
|
+
startTime: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pendingMetrics = new Map<string, PendingMessageMetric>()
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Start tracking message creation performance
|
|
28
|
+
*/
|
|
29
|
+
export function startMessageCreationTracking(idempotentKey: string): void {
|
|
30
|
+
if (!idempotentKey) return
|
|
31
|
+
const startTime = Date.now()
|
|
32
|
+
pendingMetrics.set(`message:${idempotentKey}`, { idempotentKey, startTime })
|
|
33
|
+
pendingMetrics.set(`conversation:${idempotentKey}`, { idempotentKey, startTime })
|
|
34
|
+
cleanupStaleMetrics()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Complete message creation tracking when the message.created event is received
|
|
39
|
+
*/
|
|
40
|
+
export function completeMessageCreationTracking({
|
|
41
|
+
apiClient,
|
|
42
|
+
idempotentKey,
|
|
43
|
+
}: {
|
|
44
|
+
apiClient: ApiClient
|
|
45
|
+
idempotentKey: string
|
|
46
|
+
}): void {
|
|
47
|
+
if (!idempotentKey) return
|
|
48
|
+
const duration = durationForIdempotentKey(`message:${idempotentKey}`)
|
|
49
|
+
if (!duration) return
|
|
50
|
+
sendPerformanceMetric(apiClient, {
|
|
51
|
+
name: 'chat.message.creation.message_roundtrip_time',
|
|
52
|
+
value: duration,
|
|
53
|
+
unit: 'milliseconds',
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Complete message creation tracking when the conversation.updated event is received
|
|
59
|
+
*/
|
|
60
|
+
export function completeMessageCreationConversationTracking({
|
|
61
|
+
apiClient,
|
|
62
|
+
idempotentKey,
|
|
63
|
+
}: {
|
|
64
|
+
apiClient: ApiClient
|
|
65
|
+
idempotentKey: string
|
|
66
|
+
}): void {
|
|
67
|
+
if (!idempotentKey) return
|
|
68
|
+
const duration = durationForIdempotentKey(`conversation:${idempotentKey}`)
|
|
69
|
+
if (!duration) return
|
|
70
|
+
sendPerformanceMetric(apiClient, {
|
|
71
|
+
name: 'chat.message.creation.conversation_roundtrip_time',
|
|
72
|
+
value: duration,
|
|
73
|
+
unit: 'milliseconds',
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function durationForIdempotentKey(idempotentKey: string): number | undefined {
|
|
78
|
+
const metric = pendingMetrics.get(idempotentKey)
|
|
79
|
+
if (!metric) return undefined
|
|
80
|
+
pendingMetrics.delete(idempotentKey)
|
|
81
|
+
const endTime = Date.now()
|
|
82
|
+
return endTime - metric.startTime
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Send performance metric to backend for Datadog submission
|
|
87
|
+
*/
|
|
88
|
+
async function sendPerformanceMetric(
|
|
89
|
+
apiClient: ApiClient,
|
|
90
|
+
metric: PerformanceMetric
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
try {
|
|
93
|
+
await apiClient.chat.post({
|
|
94
|
+
url: `/me/metrics`,
|
|
95
|
+
data: {
|
|
96
|
+
data: {
|
|
97
|
+
type: 'Metric',
|
|
98
|
+
attributes: {
|
|
99
|
+
metric: {
|
|
100
|
+
name: metric.name,
|
|
101
|
+
value: metric.value,
|
|
102
|
+
unit: metric.unit,
|
|
103
|
+
platform: Platform.OS,
|
|
104
|
+
app: appName,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
} catch (e) {
|
|
111
|
+
// Ignore errors
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Clean up any stale metrics (older than 5 minutes)
|
|
117
|
+
* This prevents memory leaks if messages fail to complete for any reason
|
|
118
|
+
*/
|
|
119
|
+
function cleanupStaleMetrics(): void {
|
|
120
|
+
const now = Date.now()
|
|
121
|
+
const staleThreshold = 5 * 60 * 1000 // 5 minutes in milliseconds
|
|
122
|
+
|
|
123
|
+
for (const [key, metric] of pendingMetrics.entries()) {
|
|
124
|
+
if (now - metric.startTime > staleThreshold) {
|
|
125
|
+
pendingMetrics.delete(key)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|