@planningcenter/chat-react-native 3.35.0-rc.2 → 3.35.0-rc.3
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/screens/conversation_screen.d.ts +0 -19
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +1 -96
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/utils/group_messages.d.ts +21 -0
- package/build/utils/group_messages.d.ts.map +1 -0
- package/build/utils/group_messages.js +123 -0
- package/build/utils/group_messages.js.map +1 -0
- package/package.json +2 -2
- package/src/screens/conversation_screen.tsx +1 -128
- package/src/utils/__tests__/group_messages.test.ts +143 -0
- package/src/utils/group_messages.ts +177 -0
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { HeaderTitleProps } from '@react-navigation/elements';
|
|
2
2
|
import { StaticScreenProps } from '@react-navigation/native';
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import { ReplyShadowMessage } from '../components/conversation/reply_shadow_message';
|
|
5
|
-
import { MessageResource } from '../types';
|
|
6
4
|
import { ConversationBadgeResource } from '../types/resources/conversation_badge';
|
|
7
5
|
export type ConversationRouteProps = {
|
|
8
6
|
conversation_id: number;
|
|
@@ -20,23 +18,6 @@ export type ConversationRouteProps = {
|
|
|
20
18
|
};
|
|
21
19
|
export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>;
|
|
22
20
|
export declare function ConversationScreen({ route }: ConversationScreenProps): React.JSX.Element;
|
|
23
|
-
export type DateSeparator = {
|
|
24
|
-
type: 'DateSeparator';
|
|
25
|
-
id: string;
|
|
26
|
-
date: string;
|
|
27
|
-
};
|
|
28
|
-
type ReplyShadowMessage = {
|
|
29
|
-
type: 'ReplyShadowMessage';
|
|
30
|
-
id: string;
|
|
31
|
-
messageId: string;
|
|
32
|
-
isReplyShadowMessage: boolean;
|
|
33
|
-
nextRendersAuthor: boolean;
|
|
34
|
-
};
|
|
35
|
-
interface GroupMessagesProps {
|
|
36
|
-
ms: MessageResource[];
|
|
37
|
-
inReplyScreen?: boolean;
|
|
38
|
-
}
|
|
39
|
-
export declare const groupMessages: ({ ms, inReplyScreen }: GroupMessagesProps) => (MessageResource | DateSeparator | ReplyShadowMessage)[];
|
|
40
21
|
interface ConversationScreenTitleProps extends HeaderTitleProps {
|
|
41
22
|
conversation_id: number;
|
|
42
23
|
badge?: ConversationBadgeResource;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation_screen.d.ts","sourceRoot":"","sources":["../../src/screens/conversation_screen.tsx"],"names":[],"mappings":"AACA,OAAO,EAAe,gBAAgB,EAAqB,MAAM,4BAA4B,CAAA;AAC7F,OAAO,EAGL,iBAAiB,EAIlB,MAAM,0BAA0B,CAAA;AACjC,OAAO,KAAmD,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"conversation_screen.d.ts","sourceRoot":"","sources":["../../src/screens/conversation_screen.tsx"],"names":[],"mappings":"AACA,OAAO,EAAe,gBAAgB,EAAqB,MAAM,4BAA4B,CAAA;AAC7F,OAAO,EAGL,iBAAiB,EAIlB,MAAM,0BAA0B,CAAA;AACjC,OAAO,KAAmD,MAAM,OAAO,CAAA;AA6BvE,OAAO,EAAE,yBAAyB,EAAE,MAAM,uCAAuC,CAAA;AAMjF,MAAM,MAAM,sBAAsB,GAAG;IACnC,eAAe,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,sBAAsB,CAAC,EAAE,MAAM,CAAA;IAC/B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,yBAAyB,CAAA;IACjC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG,iBAAiB,CAAC,sBAAsB,CAAC,CAAA;AAE/E,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,EAAE,uBAAuB,qBA0BpE;AAuMD,UAAU,4BAA6B,SAAQ,gBAAgB;IAC7D,eAAe,EAAE,MAAM,CAAA;IACvB,KAAK,CAAC,EAAE,yBAAyB,CAAA;IACjC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,eAAO,MAAM,uBAAuB,GAAI,8DAOrC,4BAA4B,sBAoC9B,CAAA"}
|
|
@@ -25,7 +25,7 @@ import { useFeatures } from '../hooks/use_features';
|
|
|
25
25
|
import { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read';
|
|
26
26
|
import { normalizeAnalyticsMetadata, usePublishProductAnalyticsEvent, } from '../hooks/use_product_analytics';
|
|
27
27
|
import { getRelativeDateStatus } from '../utils/date';
|
|
28
|
-
import
|
|
28
|
+
import { groupMessages } from '../utils/group_messages';
|
|
29
29
|
import { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../utils/styles';
|
|
30
30
|
import { isSystemMessage } from '../utils/system_messages';
|
|
31
31
|
export function ConversationScreen({ route }) {
|
|
@@ -174,101 +174,6 @@ const useDateSeparatorStyles = () => {
|
|
|
174
174
|
},
|
|
175
175
|
});
|
|
176
176
|
};
|
|
177
|
-
export const groupMessages = ({ ms, inReplyScreen }) => {
|
|
178
|
-
let enrichedMessages = [];
|
|
179
|
-
let encounteredOneOfMyMessages = false;
|
|
180
|
-
ms.forEach((message, i) => {
|
|
181
|
-
const prevMessage = ms[i + 1];
|
|
182
|
-
const nextMessage = ms[i - 1];
|
|
183
|
-
const date = dayjs(message.createdAt).format('YYYY-MM-DD');
|
|
184
|
-
const prevMessageIsDateSeparator = prevMessage && date !== dayjs(prevMessage.createdAt).format('YYYY-MM-DD');
|
|
185
|
-
if (isSystemMessage(message)) {
|
|
186
|
-
message.myLatestInConversation = false;
|
|
187
|
-
message.lastInGroup = true;
|
|
188
|
-
message.renderAuthor = false;
|
|
189
|
-
message.nextRendersAuthor = nextMessage?.renderAuthor;
|
|
190
|
-
message.isReplyShadowMessage = false;
|
|
191
|
-
message.nextIsReplyShadowMessage = false;
|
|
192
|
-
message.threadPosition = null;
|
|
193
|
-
enrichedMessages.push(message);
|
|
194
|
-
if (prevMessageIsDateSeparator) {
|
|
195
|
-
enrichedMessages.push({ type: 'DateSeparator', id: `day-divider-${message.id}`, date });
|
|
196
|
-
}
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
const inThread = message.replyRootId !== null;
|
|
200
|
-
const nextMessageInThread = nextMessage?.replyRootId !== null;
|
|
201
|
-
const threadRoot = message.replyRootId === message.id;
|
|
202
|
-
const nextMessageThreadRoot = nextMessage?.replyRootId === nextMessage?.id;
|
|
203
|
-
const prevMessageDifferentThread = message.replyRootId !== prevMessage?.replyRootId;
|
|
204
|
-
const nextMessageDifferentThread = message.replyRootId !== nextMessage?.replyRootId;
|
|
205
|
-
const prevMessageDifferentAuthor = message.author?.id !== prevMessage?.author?.id;
|
|
206
|
-
const nextMessageDifferentAuthor = message.author?.id !== nextMessage?.author?.id;
|
|
207
|
-
const prevMessageMoreThan5Minutes = prevMessage &&
|
|
208
|
-
new Date(message.createdAt).getTime() - new Date(prevMessage.createdAt).getTime() > 60000 * 5;
|
|
209
|
-
const nextMessageMoreThan5Minutes = nextMessage &&
|
|
210
|
-
new Date(nextMessage.createdAt).getTime() - new Date(message.createdAt).getTime() > 60000 * 5;
|
|
211
|
-
const nextMessageIsDateSeparator = nextMessage && date !== dayjs(nextMessage.createdAt).format('YYYY-MM-DD');
|
|
212
|
-
const insertReplyShadowMessage = message.replyRootId &&
|
|
213
|
-
!threadRoot &&
|
|
214
|
-
(prevMessageDifferentThread || prevMessageIsDateSeparator);
|
|
215
|
-
const lastInGroup = !nextMessage ||
|
|
216
|
-
nextMessageDifferentAuthor ||
|
|
217
|
-
nextMessageMoreThan5Minutes ||
|
|
218
|
-
nextMessageDifferentThread ||
|
|
219
|
-
nextMessageIsDateSeparator;
|
|
220
|
-
const renderAuthor = !message.mine &&
|
|
221
|
-
(!prevMessage ||
|
|
222
|
-
prevMessageDifferentAuthor ||
|
|
223
|
-
prevMessageMoreThan5Minutes ||
|
|
224
|
-
prevMessageDifferentThread ||
|
|
225
|
-
prevMessageIsDateSeparator);
|
|
226
|
-
const nextIsReplyShadowMessage = nextMessageInThread &&
|
|
227
|
-
!nextMessageThreadRoot &&
|
|
228
|
-
(nextMessageDifferentThread || nextMessageIsDateSeparator);
|
|
229
|
-
if (message.mine && !encounteredOneOfMyMessages) {
|
|
230
|
-
encounteredOneOfMyMessages = true;
|
|
231
|
-
message.myLatestInConversation = true;
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
message.myLatestInConversation = false;
|
|
235
|
-
}
|
|
236
|
-
message.lastInGroup = lastInGroup;
|
|
237
|
-
message.renderAuthor = renderAuthor;
|
|
238
|
-
message.threadPosition = null;
|
|
239
|
-
message.nextRendersAuthor = nextMessage?.renderAuthor;
|
|
240
|
-
message.isReplyShadowMessage = false;
|
|
241
|
-
message.nextIsReplyShadowMessage = nextIsReplyShadowMessage;
|
|
242
|
-
if (!inReplyScreen && inThread) {
|
|
243
|
-
message.prevIsMyReply = prevMessage?.mine;
|
|
244
|
-
message.nextIsMyReply = nextMessage?.mine;
|
|
245
|
-
const firstInThread = threadRoot;
|
|
246
|
-
const lastInThread = nextMessageDifferentThread || nextMessageIsDateSeparator;
|
|
247
|
-
if (firstInThread && lastInThread)
|
|
248
|
-
message.threadPosition = null; // ensures we don't render a connector for root replies that aren't immediately followed up a reply
|
|
249
|
-
else if (firstInThread)
|
|
250
|
-
message.threadPosition = 'first';
|
|
251
|
-
else if (lastInThread)
|
|
252
|
-
message.threadPosition = 'last';
|
|
253
|
-
else
|
|
254
|
-
message.threadPosition = 'center';
|
|
255
|
-
}
|
|
256
|
-
enrichedMessages.push(message);
|
|
257
|
-
if (insertReplyShadowMessage) {
|
|
258
|
-
enrichedMessages.push({
|
|
259
|
-
type: 'ReplyShadowMessage',
|
|
260
|
-
id: `${message.id}-${message.replyRootId}`,
|
|
261
|
-
messageId: message.replyRootId,
|
|
262
|
-
isReplyShadowMessage: true,
|
|
263
|
-
nextRendersAuthor: message?.renderAuthor,
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
if (!prevMessage || date !== dayjs(prevMessage.createdAt).format('YYYY-MM-DD')) {
|
|
267
|
-
enrichedMessages.push({ type: 'DateSeparator', id: `day-divider-${message.id}`, date });
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
return enrichedMessages;
|
|
271
|
-
};
|
|
272
177
|
export const ConversationScreenTitle = ({ conversation_id, badge, children, style, deleted, muted, }) => {
|
|
273
178
|
const styles = usePressableHeaderStyle();
|
|
274
179
|
const navigation = useNavigation();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation_screen.js","sourceRoot":"","sources":["../../src/screens/conversation_screen.tsx"],"names":[],"mappings":"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","sourcesContent":["import { date as formatDate } from '@planningcenter/datetime-fmt'\nimport { HeaderTitle, HeaderTitleProps, PlatformPressable } from '@react-navigation/elements'\nimport {\n CommonActions,\n RouteProp,\n StaticScreenProps,\n useNavigation,\n useTheme as useNavigationTheme,\n useRoute,\n} from '@react-navigation/native'\nimport React, { useCallback, useEffect, useRef, useState } from 'react'\nimport { FlatList, Platform, StyleSheet, View } from 'react-native'\nimport { useSafeAreaInsets } from 'react-native-safe-area-context'\nimport { Badge, Icon, Text } from '../components'\nimport { EmptyConversationBlankState } from '../components/conversation/empty_conversation_blank_state'\nimport { JumpToBottomButton } from '../components/conversation/jump_to_bottom_button'\nimport { Message } from '../components/conversation/message'\nimport { MessageForm } from '../components/conversation/message_form'\nimport {\n LeaderMessagesDisabledBanner,\n MemberMessagesDisabledBanner,\n} from '../components/conversation/messages_disabled_banners'\nimport { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'\nimport { SystemMessage } from '../components/conversation/system_message'\nimport { TypingIndicator } from '../components/conversation/typing_indicator'\nimport { KeyboardView } from '../components/display/keyboard_view'\nimport BlankState from '../components/primitive/blank_state_primitive'\nimport { ConversationContextProvider } from '../contexts/conversation_context'\nimport { useTheme } from '../hooks'\nimport { useConversation } from '../hooks/use_conversation'\nimport { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events'\nimport { useConversationMessages } from '../hooks/use_conversation_messages'\nimport { useConversationMessagesJoltEvents } from '../hooks/use_conversation_messages_jolt_events'\nimport { useFeatures } from '../hooks/use_features'\nimport { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read'\nimport {\n normalizeAnalyticsMetadata,\n usePublishProductAnalyticsEvent,\n} from '../hooks/use_product_analytics'\nimport { MessageResource } from '../types'\nimport { ConversationBadgeResource } from '../types/resources/conversation_badge'\nimport { getRelativeDateStatus } from '../utils/date'\nimport dayjs from '../utils/dayjs'\nimport { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../utils/styles'\nimport { isSystemMessage } from '../utils/system_messages'\n\nexport type ConversationRouteProps = {\n conversation_id: number\n reply_root_id?: string | null\n reply_root_author_name?: string\n chat_group_graph_id?: string\n clear_input?: boolean\n editing_message_id?: number | null\n message_id?: string\n title?: string\n subtitle?: string\n badge?: ConversationBadgeResource\n deleted?: boolean\n muted?: boolean\n}\n\nexport type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>\n\nexport function ConversationScreen({ route }: ConversationScreenProps) {\n const { conversation_id, message_id, reply_root_id } = route.params\n\n const { data: conversation } = useConversation({ conversation_id })\n const { featureEnabled } = useFeatures()\n\n usePublishProductAnalyticsEvent('chat.mobile.conversations.show.opened', {\n reply_root_id,\n ...normalizeAnalyticsMetadata(conversation),\n })\n\n const lastReadMessageSortKey = conversation.conversationMembership?.lastReadMessageSortKey ?? null\n const jumpToUnreadAnchor = featureEnabled('jump_to_unread') ? lastReadMessageSortKey : null\n const initialMessageId = message_id ?? jumpToUnreadAnchor\n const initialMessageIdIsAnchor = !!initialMessageId && !message_id\n\n return (\n <ConversationContextProvider\n conversationId={conversation_id}\n currentPageReplyRootId={reply_root_id ?? null}\n initialMessageId={initialMessageId}\n initialMessageIdIsAnchor={initialMessageIdIsAnchor}\n >\n <ConversationScreenContent route={route} />\n </ConversationContextProvider>\n )\n}\n\nfunction ConversationScreenContent({ route }: ConversationScreenProps) {\n const styles = useStyles()\n const navigation = useNavigation()\n const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =\n route.params\n const { data: conversation } = useConversation(route.params)\n const { messages, refetch, isRefetching, fetchOlderMessages } = useConversationMessages({\n conversation_id,\n reply_root_id,\n })\n useConversationJoltEvents({ conversationId: conversation_id })\n useConversationMessagesJoltEvents({ conversationId: conversation_id })\n useEnsureConversationsRouteExists()\n useMarkLatestMessageRead({ conversation, messages })\n const messagesWithSeparators = groupMessages({\n ms: messages,\n inReplyScreen: !!reply_root_id,\n })\n const noMessages = messagesWithSeparators.length === 0\n\n const { repliesDisabled, memberAbility, badges, title } = conversation\n const canReply = memberAbility?.canReply\n const showLeaderDisabledReplyBanner = canReply && repliesDisabled\n const canDeleteNonAuthoredMessages = memberAbility?.canDeleteNonAuthoredMessages ?? false\n const currentlyEditingMessage = messages.find(m => String(m.id) === String(editing_message_id))\n const replyRootAuthorFirstName = reply_root_author_name?.split(' ')[0]\n const replyHeaderTitle = replyRootAuthorFirstName\n ? `Reply to ${replyRootAuthorFirstName}`\n : 'Reply'\n // Prefer the membership for optimistic updates.\n const muted = conversation.conversationMembership?.muted ?? conversation.muted\n\n const listRef = useRef<FlatList>(null)\n const [showJumpToBottomButton, setShowJumpToBottomButton] = useState(false)\n\n const trackScroll = (event: any) => {\n const offsetY = event.nativeEvent.contentOffset.y\n setShowJumpToBottomButton(offsetY > 200)\n }\n\n const handleReturnToBottom = useCallback(() => {\n listRef.current?.scrollToOffset({\n offset: 0,\n })\n }, [])\n\n useEffect(() => {\n if (reply_root_id) {\n navigation.setParams({\n title: replyHeaderTitle,\n })\n } else {\n navigation.setParams({\n title: title,\n badge: badges?.[0],\n deleted: conversation?.deleted,\n muted,\n })\n }\n }, [navigation, title, badges, conversation?.deleted, reply_root_id, replyHeaderTitle, muted])\n\n if (!conversation || conversation.deleted) {\n return (\n <View style={styles.container}>\n <BlankState.Root>\n <BlankState.Imagery name=\"general.outlinedTextMessage\" />\n <BlankState.Content>\n <BlankState.Heading>This conversation has been deleted</BlankState.Heading>\n </BlankState.Content>\n <BlankState.Button\n onPress={navigation.goBack}\n title=\"Back to conversations\"\n accessibilityHint=\"Navigates back to the conversations list\"\n accessibilityRole=\"link\"\n />\n </BlankState.Root>\n </View>\n )\n }\n\n return (\n <View style={styles.container}>\n <KeyboardView>\n {noMessages ? (\n <EmptyConversationBlankState />\n ) : (\n <FlatList\n inverted\n ref={listRef}\n contentContainerStyle={styles.listContainer}\n refreshing={isRefetching}\n onRefresh={refetch}\n data={messagesWithSeparators}\n keyExtractor={item => item.id}\n onScroll={trackScroll}\n scrollEventThrottle={10}\n renderItem={({ item }) => {\n if (item.type === 'DateSeparator') {\n return <InlineDateSeparator {...item} />\n }\n\n if (item.type === 'ReplyShadowMessage') {\n return (\n <ReplyShadowMessage\n {...item}\n conversation_id={conversation_id}\n inReplyScreen={!!reply_root_id}\n />\n )\n }\n\n if (isSystemMessage(item)) {\n return <SystemMessage message={item} conversationId={conversation_id} />\n }\n\n return (\n <Message\n {...item}\n canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages}\n conversation_id={conversation_id}\n latestReadMessageSortKey={conversation?.latestReadMessageSortKey}\n inReplyScreen={!!reply_root_id}\n />\n )\n }}\n onEndReached={() => fetchOlderMessages()}\n ListHeaderComponent={<View style={styles.listHeader} />}\n />\n )}\n <JumpToBottomButton onPress={handleReturnToBottom} visible={showJumpToBottomButton} />\n {!noMessages && <TypingIndicator />}\n {showLeaderDisabledReplyBanner && <LeaderMessagesDisabledBanner />}\n {canReply ? (\n <MessageForm.Root\n replyRootAuthorFirstName={replyRootAuthorFirstName}\n conversation={conversation}\n replyRootId={reply_root_id}\n currentlyEditingMessage={currentlyEditingMessage}\n // We use a separate key so that it remounts component when switching between new\n // and edit message. This simplifies internal state handling.\n key={\n currentlyEditingMessage\n ? `edit-message-form-${currentlyEditingMessage.id}`\n : 'new-message-form'\n }\n >\n <MessageForm.AttachmentPicker />\n <MessageForm.Commands />\n <MessageForm.TextInput />\n <MessageForm.SubmitButton />\n </MessageForm.Root>\n ) : (\n <MemberMessagesDisabledBanner />\n )}\n </KeyboardView>\n </View>\n )\n}\n\nexport type DateSeparator = { type: 'DateSeparator'; id: string; date: string }\n\nfunction InlineDateSeparator({ date }: DateSeparator) {\n const styles = useDateSeparatorStyles()\n const { isThisYear } = getRelativeDateStatus(date)\n const showYear = !isThisYear\n const dateStamp = formatDate(date, { style: 'long', year: showYear })\n\n return (\n <View style={styles.container}>\n <View style={styles.separator} />\n <Text variant=\"footnote\" style={styles.dateText}>\n {dateStamp}\n </Text>\n <View style={styles.separator} />\n </View>\n )\n}\n\nconst useDateSeparatorStyles = () => {\n const theme = useTheme()\n return StyleSheet.create({\n container: {\n alignItems: 'center',\n flexDirection: 'row',\n paddingHorizontal: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,\n paddingVertical: 16,\n },\n separator: {\n flex: 1,\n height: 1,\n borderTopWidth: 1,\n borderTopColor: theme.colors.borderColorDefaultBase,\n },\n dateText: {\n paddingHorizontal: 8,\n },\n })\n}\n\ntype ReplyShadowMessage = {\n type: 'ReplyShadowMessage'\n id: string\n messageId: string\n isReplyShadowMessage: boolean\n nextRendersAuthor: boolean\n}\n\ninterface GroupMessagesProps {\n ms: MessageResource[]\n inReplyScreen?: boolean\n}\n\nexport const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {\n let enrichedMessages: (MessageResource | DateSeparator | ReplyShadowMessage)[] = []\n let encounteredOneOfMyMessages = false\n\n ms.forEach((message, i) => {\n const prevMessage = ms[i + 1]\n const nextMessage = ms[i - 1]\n const date = dayjs(message.createdAt).format('YYYY-MM-DD')\n\n const prevMessageIsDateSeparator =\n prevMessage && date !== dayjs(prevMessage.createdAt).format('YYYY-MM-DD')\n\n if (isSystemMessage(message)) {\n message.myLatestInConversation = false\n message.lastInGroup = true\n message.renderAuthor = false\n message.nextRendersAuthor = nextMessage?.renderAuthor\n message.isReplyShadowMessage = false\n message.nextIsReplyShadowMessage = false\n message.threadPosition = null\n enrichedMessages.push(message)\n if (prevMessageIsDateSeparator) {\n enrichedMessages.push({ type: 'DateSeparator', id: `day-divider-${message.id}`, date })\n }\n return\n }\n\n const inThread = message.replyRootId !== null\n const nextMessageInThread = nextMessage?.replyRootId !== null\n const threadRoot = message.replyRootId === message.id\n const nextMessageThreadRoot = nextMessage?.replyRootId === nextMessage?.id\n const prevMessageDifferentThread = message.replyRootId !== prevMessage?.replyRootId\n const nextMessageDifferentThread = message.replyRootId !== nextMessage?.replyRootId\n const prevMessageDifferentAuthor = message.author?.id !== prevMessage?.author?.id\n const nextMessageDifferentAuthor = message.author?.id !== nextMessage?.author?.id\n const prevMessageMoreThan5Minutes =\n prevMessage &&\n new Date(message.createdAt).getTime() - new Date(prevMessage.createdAt).getTime() > 60000 * 5\n const nextMessageMoreThan5Minutes =\n nextMessage &&\n new Date(nextMessage.createdAt).getTime() - new Date(message.createdAt).getTime() > 60000 * 5\n const nextMessageIsDateSeparator =\n nextMessage && date !== dayjs(nextMessage.createdAt).format('YYYY-MM-DD')\n const insertReplyShadowMessage =\n message.replyRootId &&\n !threadRoot &&\n (prevMessageDifferentThread || prevMessageIsDateSeparator)\n const lastInGroup =\n !nextMessage ||\n nextMessageDifferentAuthor ||\n nextMessageMoreThan5Minutes ||\n nextMessageDifferentThread ||\n nextMessageIsDateSeparator\n const renderAuthor =\n !message.mine &&\n (!prevMessage ||\n prevMessageDifferentAuthor ||\n prevMessageMoreThan5Minutes ||\n prevMessageDifferentThread ||\n prevMessageIsDateSeparator)\n const nextIsReplyShadowMessage =\n nextMessageInThread &&\n !nextMessageThreadRoot &&\n (nextMessageDifferentThread || nextMessageIsDateSeparator)\n\n if (message.mine && !encounteredOneOfMyMessages) {\n encounteredOneOfMyMessages = true\n message.myLatestInConversation = true\n } else {\n message.myLatestInConversation = false\n }\n message.lastInGroup = lastInGroup\n message.renderAuthor = renderAuthor\n message.threadPosition = null\n message.nextRendersAuthor = nextMessage?.renderAuthor\n message.isReplyShadowMessage = false\n message.nextIsReplyShadowMessage = nextIsReplyShadowMessage\n\n if (!inReplyScreen && inThread) {\n message.prevIsMyReply = prevMessage?.mine\n message.nextIsMyReply = nextMessage?.mine\n\n const firstInThread = threadRoot\n const lastInThread = nextMessageDifferentThread || nextMessageIsDateSeparator\n\n if (firstInThread && lastInThread)\n message.threadPosition = null // ensures we don't render a connector for root replies that aren't immediately followed up a reply\n else if (firstInThread) message.threadPosition = 'first'\n else if (lastInThread) message.threadPosition = 'last'\n else message.threadPosition = 'center'\n }\n\n enrichedMessages.push(message)\n\n if (insertReplyShadowMessage) {\n enrichedMessages.push({\n type: 'ReplyShadowMessage',\n id: `${message.id}-${message.replyRootId}`,\n messageId: message.replyRootId!,\n isReplyShadowMessage: true,\n nextRendersAuthor: message?.renderAuthor,\n })\n }\n\n if (!prevMessage || date !== dayjs(prevMessage.createdAt).format('YYYY-MM-DD')) {\n enrichedMessages.push({ type: 'DateSeparator', id: `day-divider-${message.id}`, date })\n }\n })\n\n return enrichedMessages\n}\ninterface ConversationScreenTitleProps extends HeaderTitleProps {\n conversation_id: number\n badge?: ConversationBadgeResource\n deleted?: boolean\n muted?: boolean\n}\n\nexport const ConversationScreenTitle = ({\n conversation_id,\n badge,\n children,\n style,\n deleted,\n muted,\n}: ConversationScreenTitleProps) => {\n const styles = usePressableHeaderStyle()\n const navigation = useNavigation()\n const resourceType = badge?.pcoResourceType || ''\n const productName = badge?.appName\n const name = badge?.text || undefined\n\n return (\n <PlatformPressable\n accessibilityHint=\"Opens details about members and settings\"\n style={styles.container}\n onPress={() => {\n if (deleted) return\n\n navigation.navigate('ConversationDetails', { conversation_id })\n }}\n >\n <View style={styles.titleWrapper}>\n <View style={styles.titleTextContainer}>\n <HeaderTitle maxFontSizeMultiplier={1} style={style}>\n {children}\n </HeaderTitle>\n </View>\n {muted && <Icon name=\"general.bellMuted\" size={12} />}\n {!deleted && <Icon name=\"general.downChevron\" size={12} />}\n </View>\n <Badge\n variant=\"metaSubtle\"\n productLogoName={productName}\n label={resourceType}\n metaLabel={name}\n style={styles.badge}\n maxFontSizeMultiplier={1}\n />\n </PlatformPressable>\n )\n}\n\nconst usePressableHeaderStyle = () => {\n return StyleSheet.create({\n container: {\n alignItems: Platform.select({ android: 'flex-start', default: 'center' }),\n marginRight: Platform.select({ ios: 20, default: 16 }),\n flex: 1,\n },\n titleWrapper: {\n alignItems: 'center',\n columnGap: 4,\n flexDirection: 'row',\n flexShrink: 1,\n },\n titleTextContainer: {\n flexShrink: 1,\n minWidth: 0,\n },\n badge: {\n alignSelf: Platform.select({ android: 'flex-start', default: 'center' }),\n marginTop: 2,\n },\n })\n}\n\nconst useStyles = () => {\n const navigationTheme = useNavigationTheme()\n const { bottom } = useSafeAreaInsets()\n\n return StyleSheet.create({\n container: {\n flex: 1,\n justifyContent: 'center',\n backgroundColor: navigationTheme.colors.card,\n paddingBottom: bottom,\n },\n listContainer: {\n paddingVertical: 12,\n },\n listHeader: {\n // Just whitespace to provide space where the typing indicator can be\n height: 16,\n },\n })\n}\n\n/**\n * useEnsureConversationsRouteExists\n */\nconst useEnsureConversationsRouteExists = () => {\n const navigation = useNavigation()\n const { params } = useRoute<RouteProp<ConversationScreenProps['route']>>()\n\n useEffect(() => {\n const navigationState = navigation.getState()\n const routes = navigationState?.routes || []\n const conversationsRoute = routes.find(r => r.name === 'Conversations')\n\n if (conversationsRoute) return\n\n navigation.dispatch(state => {\n return CommonActions.reset({\n ...state,\n routes: [\n { name: 'Conversations', params: { chat_group_graph_id: params?.chat_group_graph_id } },\n ...routes,\n ],\n index: state.index + 1,\n })\n })\n }, [navigation, params?.chat_group_graph_id])\n}\n"]}
|
|
1
|
+
{"version":3,"file":"conversation_screen.js","sourceRoot":"","sources":["../../src/screens/conversation_screen.tsx"],"names":[],"mappings":"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","sourcesContent":["import { date as formatDate } from '@planningcenter/datetime-fmt'\nimport { HeaderTitle, HeaderTitleProps, PlatformPressable } from '@react-navigation/elements'\nimport {\n CommonActions,\n RouteProp,\n StaticScreenProps,\n useNavigation,\n useTheme as useNavigationTheme,\n useRoute,\n} from '@react-navigation/native'\nimport React, { useCallback, useEffect, useRef, useState } from 'react'\nimport { FlatList, Platform, StyleSheet, View } from 'react-native'\nimport { useSafeAreaInsets } from 'react-native-safe-area-context'\nimport { Badge, Icon, Text } from '../components'\nimport { EmptyConversationBlankState } from '../components/conversation/empty_conversation_blank_state'\nimport { JumpToBottomButton } from '../components/conversation/jump_to_bottom_button'\nimport { Message } from '../components/conversation/message'\nimport { MessageForm } from '../components/conversation/message_form'\nimport {\n LeaderMessagesDisabledBanner,\n MemberMessagesDisabledBanner,\n} from '../components/conversation/messages_disabled_banners'\nimport { ReplyShadowMessage } from '../components/conversation/reply_shadow_message'\nimport { SystemMessage } from '../components/conversation/system_message'\nimport { TypingIndicator } from '../components/conversation/typing_indicator'\nimport { KeyboardView } from '../components/display/keyboard_view'\nimport BlankState from '../components/primitive/blank_state_primitive'\nimport { ConversationContextProvider } from '../contexts/conversation_context'\nimport { useTheme } from '../hooks'\nimport { useConversation } from '../hooks/use_conversation'\nimport { useConversationJoltEvents } from '../hooks/use_conversation_jolt_events'\nimport { useConversationMessages } from '../hooks/use_conversation_messages'\nimport { useConversationMessagesJoltEvents } from '../hooks/use_conversation_messages_jolt_events'\nimport { useFeatures } from '../hooks/use_features'\nimport { useMarkLatestMessageRead } from '../hooks/use_mark_latest_message_read'\nimport {\n normalizeAnalyticsMetadata,\n usePublishProductAnalyticsEvent,\n} from '../hooks/use_product_analytics'\nimport { ConversationBadgeResource } from '../types/resources/conversation_badge'\nimport { getRelativeDateStatus } from '../utils/date'\nimport { groupMessages, type DateSeparator } from '../utils/group_messages'\nimport { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../utils/styles'\nimport { isSystemMessage } from '../utils/system_messages'\n\nexport type ConversationRouteProps = {\n conversation_id: number\n reply_root_id?: string | null\n reply_root_author_name?: string\n chat_group_graph_id?: string\n clear_input?: boolean\n editing_message_id?: number | null\n message_id?: string\n title?: string\n subtitle?: string\n badge?: ConversationBadgeResource\n deleted?: boolean\n muted?: boolean\n}\n\nexport type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>\n\nexport function ConversationScreen({ route }: ConversationScreenProps) {\n const { conversation_id, message_id, reply_root_id } = route.params\n\n const { data: conversation } = useConversation({ conversation_id })\n const { featureEnabled } = useFeatures()\n\n usePublishProductAnalyticsEvent('chat.mobile.conversations.show.opened', {\n reply_root_id,\n ...normalizeAnalyticsMetadata(conversation),\n })\n\n const lastReadMessageSortKey = conversation.conversationMembership?.lastReadMessageSortKey ?? null\n const jumpToUnreadAnchor = featureEnabled('jump_to_unread') ? lastReadMessageSortKey : null\n const initialMessageId = message_id ?? jumpToUnreadAnchor\n const initialMessageIdIsAnchor = !!initialMessageId && !message_id\n\n return (\n <ConversationContextProvider\n conversationId={conversation_id}\n currentPageReplyRootId={reply_root_id ?? null}\n initialMessageId={initialMessageId}\n initialMessageIdIsAnchor={initialMessageIdIsAnchor}\n >\n <ConversationScreenContent route={route} />\n </ConversationContextProvider>\n )\n}\n\nfunction ConversationScreenContent({ route }: ConversationScreenProps) {\n const styles = useStyles()\n const navigation = useNavigation()\n const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =\n route.params\n const { data: conversation } = useConversation(route.params)\n const { messages, refetch, isRefetching, fetchOlderMessages } = useConversationMessages({\n conversation_id,\n reply_root_id,\n })\n useConversationJoltEvents({ conversationId: conversation_id })\n useConversationMessagesJoltEvents({ conversationId: conversation_id })\n useEnsureConversationsRouteExists()\n useMarkLatestMessageRead({ conversation, messages })\n const messagesWithSeparators = groupMessages({\n ms: messages,\n inReplyScreen: !!reply_root_id,\n })\n const noMessages = messagesWithSeparators.length === 0\n\n const { repliesDisabled, memberAbility, badges, title } = conversation\n const canReply = memberAbility?.canReply\n const showLeaderDisabledReplyBanner = canReply && repliesDisabled\n const canDeleteNonAuthoredMessages = memberAbility?.canDeleteNonAuthoredMessages ?? false\n const currentlyEditingMessage = messages.find(m => String(m.id) === String(editing_message_id))\n const replyRootAuthorFirstName = reply_root_author_name?.split(' ')[0]\n const replyHeaderTitle = replyRootAuthorFirstName\n ? `Reply to ${replyRootAuthorFirstName}`\n : 'Reply'\n // Prefer the membership for optimistic updates.\n const muted = conversation.conversationMembership?.muted ?? conversation.muted\n\n const listRef = useRef<FlatList>(null)\n const [showJumpToBottomButton, setShowJumpToBottomButton] = useState(false)\n\n const trackScroll = (event: any) => {\n const offsetY = event.nativeEvent.contentOffset.y\n setShowJumpToBottomButton(offsetY > 200)\n }\n\n const handleReturnToBottom = useCallback(() => {\n listRef.current?.scrollToOffset({\n offset: 0,\n })\n }, [])\n\n useEffect(() => {\n if (reply_root_id) {\n navigation.setParams({\n title: replyHeaderTitle,\n })\n } else {\n navigation.setParams({\n title: title,\n badge: badges?.[0],\n deleted: conversation?.deleted,\n muted,\n })\n }\n }, [navigation, title, badges, conversation?.deleted, reply_root_id, replyHeaderTitle, muted])\n\n if (!conversation || conversation.deleted) {\n return (\n <View style={styles.container}>\n <BlankState.Root>\n <BlankState.Imagery name=\"general.outlinedTextMessage\" />\n <BlankState.Content>\n <BlankState.Heading>This conversation has been deleted</BlankState.Heading>\n </BlankState.Content>\n <BlankState.Button\n onPress={navigation.goBack}\n title=\"Back to conversations\"\n accessibilityHint=\"Navigates back to the conversations list\"\n accessibilityRole=\"link\"\n />\n </BlankState.Root>\n </View>\n )\n }\n\n return (\n <View style={styles.container}>\n <KeyboardView>\n {noMessages ? (\n <EmptyConversationBlankState />\n ) : (\n <FlatList\n inverted\n ref={listRef}\n contentContainerStyle={styles.listContainer}\n refreshing={isRefetching}\n onRefresh={refetch}\n data={messagesWithSeparators}\n keyExtractor={item => item.id}\n onScroll={trackScroll}\n scrollEventThrottle={10}\n renderItem={({ item }) => {\n if (item.type === 'DateSeparator') {\n return <InlineDateSeparator {...item} />\n }\n\n if (item.type === 'ReplyShadowMessage') {\n return (\n <ReplyShadowMessage\n {...item}\n conversation_id={conversation_id}\n inReplyScreen={!!reply_root_id}\n />\n )\n }\n\n if (isSystemMessage(item)) {\n return <SystemMessage message={item} conversationId={conversation_id} />\n }\n\n return (\n <Message\n {...item}\n canDeleteNonAuthoredMessages={canDeleteNonAuthoredMessages}\n conversation_id={conversation_id}\n latestReadMessageSortKey={conversation?.latestReadMessageSortKey}\n inReplyScreen={!!reply_root_id}\n />\n )\n }}\n onEndReached={() => fetchOlderMessages()}\n ListHeaderComponent={<View style={styles.listHeader} />}\n />\n )}\n <JumpToBottomButton onPress={handleReturnToBottom} visible={showJumpToBottomButton} />\n {!noMessages && <TypingIndicator />}\n {showLeaderDisabledReplyBanner && <LeaderMessagesDisabledBanner />}\n {canReply ? (\n <MessageForm.Root\n replyRootAuthorFirstName={replyRootAuthorFirstName}\n conversation={conversation}\n replyRootId={reply_root_id}\n currentlyEditingMessage={currentlyEditingMessage}\n // We use a separate key so that it remounts component when switching between new\n // and edit message. This simplifies internal state handling.\n key={\n currentlyEditingMessage\n ? `edit-message-form-${currentlyEditingMessage.id}`\n : 'new-message-form'\n }\n >\n <MessageForm.AttachmentPicker />\n <MessageForm.Commands />\n <MessageForm.TextInput />\n <MessageForm.SubmitButton />\n </MessageForm.Root>\n ) : (\n <MemberMessagesDisabledBanner />\n )}\n </KeyboardView>\n </View>\n )\n}\n\nfunction InlineDateSeparator({ date }: DateSeparator) {\n const styles = useDateSeparatorStyles()\n const { isThisYear } = getRelativeDateStatus(date)\n const showYear = !isThisYear\n const dateStamp = formatDate(date, { style: 'long', year: showYear })\n\n return (\n <View style={styles.container}>\n <View style={styles.separator} />\n <Text variant=\"footnote\" style={styles.dateText}>\n {dateStamp}\n </Text>\n <View style={styles.separator} />\n </View>\n )\n}\n\nconst useDateSeparatorStyles = () => {\n const theme = useTheme()\n return StyleSheet.create({\n container: {\n alignItems: 'center',\n flexDirection: 'row',\n paddingHorizontal: CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL,\n paddingVertical: 16,\n },\n separator: {\n flex: 1,\n height: 1,\n borderTopWidth: 1,\n borderTopColor: theme.colors.borderColorDefaultBase,\n },\n dateText: {\n paddingHorizontal: 8,\n },\n })\n}\n\ninterface ConversationScreenTitleProps extends HeaderTitleProps {\n conversation_id: number\n badge?: ConversationBadgeResource\n deleted?: boolean\n muted?: boolean\n}\n\nexport const ConversationScreenTitle = ({\n conversation_id,\n badge,\n children,\n style,\n deleted,\n muted,\n}: ConversationScreenTitleProps) => {\n const styles = usePressableHeaderStyle()\n const navigation = useNavigation()\n const resourceType = badge?.pcoResourceType || ''\n const productName = badge?.appName\n const name = badge?.text || undefined\n\n return (\n <PlatformPressable\n accessibilityHint=\"Opens details about members and settings\"\n style={styles.container}\n onPress={() => {\n if (deleted) return\n\n navigation.navigate('ConversationDetails', { conversation_id })\n }}\n >\n <View style={styles.titleWrapper}>\n <View style={styles.titleTextContainer}>\n <HeaderTitle maxFontSizeMultiplier={1} style={style}>\n {children}\n </HeaderTitle>\n </View>\n {muted && <Icon name=\"general.bellMuted\" size={12} />}\n {!deleted && <Icon name=\"general.downChevron\" size={12} />}\n </View>\n <Badge\n variant=\"metaSubtle\"\n productLogoName={productName}\n label={resourceType}\n metaLabel={name}\n style={styles.badge}\n maxFontSizeMultiplier={1}\n />\n </PlatformPressable>\n )\n}\n\nconst usePressableHeaderStyle = () => {\n return StyleSheet.create({\n container: {\n alignItems: Platform.select({ android: 'flex-start', default: 'center' }),\n marginRight: Platform.select({ ios: 20, default: 16 }),\n flex: 1,\n },\n titleWrapper: {\n alignItems: 'center',\n columnGap: 4,\n flexDirection: 'row',\n flexShrink: 1,\n },\n titleTextContainer: {\n flexShrink: 1,\n minWidth: 0,\n },\n badge: {\n alignSelf: Platform.select({ android: 'flex-start', default: 'center' }),\n marginTop: 2,\n },\n })\n}\n\nconst useStyles = () => {\n const navigationTheme = useNavigationTheme()\n const { bottom } = useSafeAreaInsets()\n\n return StyleSheet.create({\n container: {\n flex: 1,\n justifyContent: 'center',\n backgroundColor: navigationTheme.colors.card,\n paddingBottom: bottom,\n },\n listContainer: {\n paddingVertical: 12,\n },\n listHeader: {\n // Just whitespace to provide space where the typing indicator can be\n height: 16,\n },\n })\n}\n\n/**\n * useEnsureConversationsRouteExists\n */\nconst useEnsureConversationsRouteExists = () => {\n const navigation = useNavigation()\n const { params } = useRoute<RouteProp<ConversationScreenProps['route']>>()\n\n useEffect(() => {\n const navigationState = navigation.getState()\n const routes = navigationState?.routes || []\n const conversationsRoute = routes.find(r => r.name === 'Conversations')\n\n if (conversationsRoute) return\n\n navigation.dispatch(state => {\n return CommonActions.reset({\n ...state,\n routes: [\n { name: 'Conversations', params: { chat_group_graph_id: params?.chat_group_graph_id } },\n ...routes,\n ],\n index: state.index + 1,\n })\n })\n }, [navigation, params?.chat_group_graph_id])\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { MessageResource } from '../types/resources/message';
|
|
2
|
+
export type DateSeparator = {
|
|
3
|
+
type: 'DateSeparator';
|
|
4
|
+
id: string;
|
|
5
|
+
date: string;
|
|
6
|
+
};
|
|
7
|
+
export type ReplyShadowMessage = {
|
|
8
|
+
type: 'ReplyShadowMessage';
|
|
9
|
+
id: string;
|
|
10
|
+
messageId: string;
|
|
11
|
+
isReplyShadowMessage: boolean;
|
|
12
|
+
nextRendersAuthor: boolean;
|
|
13
|
+
};
|
|
14
|
+
export type EnrichedMessage = MessageResource | DateSeparator | ReplyShadowMessage;
|
|
15
|
+
interface GroupMessagesProps {
|
|
16
|
+
ms: MessageResource[];
|
|
17
|
+
inReplyScreen?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function groupMessages({ ms, inReplyScreen }: GroupMessagesProps): EnrichedMessage[];
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=group_messages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"group_messages.d.ts","sourceRoot":"","sources":["../../src/utils/group_messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAMjE,MAAM,MAAM,aAAa,GAAG;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAE/E,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,oBAAoB,CAAA;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,oBAAoB,EAAE,OAAO,CAAA;IAC7B,iBAAiB,EAAE,OAAO,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,eAAe,GAAG,aAAa,GAAG,kBAAkB,CAAA;AAElF,UAAU,kBAAkB;IAC1B,EAAE,EAAE,eAAe,EAAE,CAAA;IACrB,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,kBAAkB,GAAG,eAAe,EAAE,CAkC1F"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import dayjs from './dayjs';
|
|
2
|
+
import { isSystemMessage } from './system_messages';
|
|
3
|
+
const FIVE_MINUTES_MS = 5 * 60 * 1000;
|
|
4
|
+
export function groupMessages({ ms, inReplyScreen }) {
|
|
5
|
+
const items = [];
|
|
6
|
+
let myLatestSeen = false;
|
|
7
|
+
let nextNeighborEnriched;
|
|
8
|
+
ms.forEach((message, i) => {
|
|
9
|
+
const { prev } = neighborsOf(ms, i);
|
|
10
|
+
const next = nextNeighborEnriched;
|
|
11
|
+
if (isSystemMessage(message)) {
|
|
12
|
+
const enriched = enrichSystemMessage(message, next);
|
|
13
|
+
items.push(enriched);
|
|
14
|
+
if (datesDifferBetween(message, prev))
|
|
15
|
+
items.push(dateSeparator(message));
|
|
16
|
+
nextNeighborEnriched = enriched;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const isMyLatest = !myLatestSeen && message.mine;
|
|
20
|
+
if (isMyLatest)
|
|
21
|
+
myLatestSeen = true;
|
|
22
|
+
const enriched = enrichRegularMessage(message, prev, next, isMyLatest, !!inReplyScreen);
|
|
23
|
+
items.push(enriched);
|
|
24
|
+
const shadow = replyShadowFor(enriched, prev);
|
|
25
|
+
if (shadow)
|
|
26
|
+
items.push(shadow);
|
|
27
|
+
if (!prev || datesDifferBetween(message, prev)) {
|
|
28
|
+
items.push(dateSeparator(message));
|
|
29
|
+
}
|
|
30
|
+
nextNeighborEnriched = enriched;
|
|
31
|
+
});
|
|
32
|
+
return items;
|
|
33
|
+
}
|
|
34
|
+
function neighborsOf(arr, i) {
|
|
35
|
+
return { prev: arr[i + 1], next: arr[i - 1] };
|
|
36
|
+
}
|
|
37
|
+
function enrichSystemMessage(message, next) {
|
|
38
|
+
return {
|
|
39
|
+
...message,
|
|
40
|
+
myLatestInConversation: false,
|
|
41
|
+
lastInGroup: true,
|
|
42
|
+
renderAuthor: false,
|
|
43
|
+
nextRendersAuthor: next?.renderAuthor,
|
|
44
|
+
isReplyShadowMessage: false,
|
|
45
|
+
nextIsReplyShadowMessage: false,
|
|
46
|
+
threadPosition: null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function enrichRegularMessage(message, prev, next, isMyLatest, inReplyScreen) {
|
|
50
|
+
const inThread = message.replyRootId !== null;
|
|
51
|
+
const showThreadDetails = !inReplyScreen && inThread;
|
|
52
|
+
return {
|
|
53
|
+
...message,
|
|
54
|
+
myLatestInConversation: isMyLatest,
|
|
55
|
+
lastInGroup: !next || startsNewGroup(next, message),
|
|
56
|
+
renderAuthor: !message.mine && (!prev || startsNewGroup(message, prev)),
|
|
57
|
+
nextRendersAuthor: next?.renderAuthor,
|
|
58
|
+
isReplyShadowMessage: false,
|
|
59
|
+
nextIsReplyShadowMessage: nextIntroducesReplyShadow(next, message),
|
|
60
|
+
threadPosition: showThreadDetails ? threadPositionFor(message, next) : null,
|
|
61
|
+
prevIsMyReply: showThreadDetails ? prev?.mine : undefined,
|
|
62
|
+
nextIsMyReply: showThreadDetails ? next?.mine : undefined,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function startsNewGroup(a, b) {
|
|
66
|
+
return (a.author?.id !== b.author?.id ||
|
|
67
|
+
differsByMoreThan5Min(a, b) ||
|
|
68
|
+
a.replyRootId !== b.replyRootId ||
|
|
69
|
+
datesDifferBetween(a, b));
|
|
70
|
+
}
|
|
71
|
+
function differsByMoreThan5Min(a, b) {
|
|
72
|
+
return Math.abs(toMillis(a.createdAt) - toMillis(b.createdAt)) > FIVE_MINUTES_MS;
|
|
73
|
+
}
|
|
74
|
+
function toMillis(iso) {
|
|
75
|
+
return new Date(iso).getTime();
|
|
76
|
+
}
|
|
77
|
+
function datesDifferBetween(a, b) {
|
|
78
|
+
if (!b)
|
|
79
|
+
return false;
|
|
80
|
+
return dateKey(a) !== dateKey(b);
|
|
81
|
+
}
|
|
82
|
+
function dateKey(message) {
|
|
83
|
+
return dayjs(message.createdAt).format('YYYY-MM-DD');
|
|
84
|
+
}
|
|
85
|
+
function dateSeparator(message) {
|
|
86
|
+
return { type: 'DateSeparator', id: `day-divider-${message.id}`, date: dateKey(message) };
|
|
87
|
+
}
|
|
88
|
+
function threadPositionFor(message, next) {
|
|
89
|
+
const isThreadRoot = message.replyRootId === message.id;
|
|
90
|
+
const isLast = !next || next.replyRootId !== message.replyRootId || datesDifferBetween(next, message);
|
|
91
|
+
if (isThreadRoot && isLast)
|
|
92
|
+
return null;
|
|
93
|
+
if (isThreadRoot)
|
|
94
|
+
return 'first';
|
|
95
|
+
if (isLast)
|
|
96
|
+
return 'last';
|
|
97
|
+
return 'center';
|
|
98
|
+
}
|
|
99
|
+
function nextIntroducesReplyShadow(next, current) {
|
|
100
|
+
if (!next)
|
|
101
|
+
return false;
|
|
102
|
+
const nextInThread = next.replyRootId !== null;
|
|
103
|
+
const nextIsThreadRoot = next.replyRootId === next.id;
|
|
104
|
+
const differentThread = next.replyRootId !== current.replyRootId;
|
|
105
|
+
return nextInThread && !nextIsThreadRoot && (differentThread || datesDifferBetween(next, current));
|
|
106
|
+
}
|
|
107
|
+
function replyShadowFor(message, prev) {
|
|
108
|
+
if (!message.replyRootId)
|
|
109
|
+
return undefined;
|
|
110
|
+
if (message.replyRootId === message.id)
|
|
111
|
+
return undefined;
|
|
112
|
+
const enteringNewThread = !prev || prev.replyRootId !== message.replyRootId || datesDifferBetween(prev, message);
|
|
113
|
+
if (!enteringNewThread)
|
|
114
|
+
return undefined;
|
|
115
|
+
return {
|
|
116
|
+
type: 'ReplyShadowMessage',
|
|
117
|
+
id: `${message.id}-${message.replyRootId}`,
|
|
118
|
+
messageId: message.replyRootId,
|
|
119
|
+
isReplyShadowMessage: true,
|
|
120
|
+
nextRendersAuthor: message.renderAuthor ?? false,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=group_messages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"group_messages.js","sourceRoot":"","sources":["../../src/utils/group_messages.ts"],"names":[],"mappings":"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","sourcesContent":["import type { MessageResource } from '../types/resources/message'\nimport dayjs from './dayjs'\nimport { isSystemMessage } from './system_messages'\n\nconst FIVE_MINUTES_MS = 5 * 60 * 1000\n\nexport type DateSeparator = { type: 'DateSeparator'; id: string; date: string }\n\nexport type ReplyShadowMessage = {\n type: 'ReplyShadowMessage'\n id: string\n messageId: string\n isReplyShadowMessage: boolean\n nextRendersAuthor: boolean\n}\n\nexport type EnrichedMessage = MessageResource | DateSeparator | ReplyShadowMessage\n\ninterface GroupMessagesProps {\n ms: MessageResource[]\n inReplyScreen?: boolean\n}\n\nexport function groupMessages({ ms, inReplyScreen }: GroupMessagesProps): EnrichedMessage[] {\n const items: EnrichedMessage[] = []\n let myLatestSeen = false\n let nextNeighborEnriched: MessageResource | undefined\n\n ms.forEach((message, i) => {\n const { prev } = neighborsOf(ms, i)\n const next = nextNeighborEnriched\n\n if (isSystemMessage(message)) {\n const enriched = enrichSystemMessage(message, next)\n items.push(enriched)\n if (datesDifferBetween(message, prev)) items.push(dateSeparator(message))\n nextNeighborEnriched = enriched\n return\n }\n\n const isMyLatest = !myLatestSeen && message.mine\n if (isMyLatest) myLatestSeen = true\n\n const enriched = enrichRegularMessage(message, prev, next, isMyLatest, !!inReplyScreen)\n items.push(enriched)\n\n const shadow = replyShadowFor(enriched, prev)\n if (shadow) items.push(shadow)\n\n if (!prev || datesDifferBetween(message, prev)) {\n items.push(dateSeparator(message))\n }\n\n nextNeighborEnriched = enriched\n })\n\n return items\n}\n\nfunction neighborsOf<T>(arr: T[], i: number): { prev: T | undefined; next: T | undefined } {\n return { prev: arr[i + 1], next: arr[i - 1] }\n}\n\nfunction enrichSystemMessage(\n message: MessageResource,\n next: MessageResource | undefined\n): MessageResource {\n return {\n ...message,\n myLatestInConversation: false,\n lastInGroup: true,\n renderAuthor: false,\n nextRendersAuthor: next?.renderAuthor,\n isReplyShadowMessage: false,\n nextIsReplyShadowMessage: false,\n threadPosition: null,\n }\n}\n\nfunction enrichRegularMessage(\n message: MessageResource,\n prev: MessageResource | undefined,\n next: MessageResource | undefined,\n isMyLatest: boolean,\n inReplyScreen: boolean\n): MessageResource {\n const inThread = message.replyRootId !== null\n const showThreadDetails = !inReplyScreen && inThread\n\n return {\n ...message,\n myLatestInConversation: isMyLatest,\n lastInGroup: !next || startsNewGroup(next, message),\n renderAuthor: !message.mine && (!prev || startsNewGroup(message, prev)),\n nextRendersAuthor: next?.renderAuthor,\n isReplyShadowMessage: false,\n nextIsReplyShadowMessage: nextIntroducesReplyShadow(next, message),\n threadPosition: showThreadDetails ? threadPositionFor(message, next) : null,\n prevIsMyReply: showThreadDetails ? prev?.mine : undefined,\n nextIsMyReply: showThreadDetails ? next?.mine : undefined,\n }\n}\n\nfunction startsNewGroup(a: MessageResource, b: MessageResource): boolean {\n return (\n a.author?.id !== b.author?.id ||\n differsByMoreThan5Min(a, b) ||\n a.replyRootId !== b.replyRootId ||\n datesDifferBetween(a, b)\n )\n}\n\nfunction differsByMoreThan5Min(a: MessageResource, b: MessageResource): boolean {\n return Math.abs(toMillis(a.createdAt) - toMillis(b.createdAt)) > FIVE_MINUTES_MS\n}\n\nfunction toMillis(iso: string): number {\n return new Date(iso).getTime()\n}\n\nfunction datesDifferBetween(a: MessageResource, b: MessageResource | undefined): boolean {\n if (!b) return false\n return dateKey(a) !== dateKey(b)\n}\n\nfunction dateKey(message: MessageResource): string {\n return dayjs(message.createdAt).format('YYYY-MM-DD')\n}\n\nfunction dateSeparator(message: MessageResource): DateSeparator {\n return { type: 'DateSeparator', id: `day-divider-${message.id}`, date: dateKey(message) }\n}\n\nfunction threadPositionFor(\n message: MessageResource,\n next: MessageResource | undefined\n): 'first' | 'center' | 'last' | null {\n const isThreadRoot = message.replyRootId === message.id\n const isLast =\n !next || next.replyRootId !== message.replyRootId || datesDifferBetween(next, message)\n\n if (isThreadRoot && isLast) return null\n if (isThreadRoot) return 'first'\n if (isLast) return 'last'\n return 'center'\n}\n\nfunction nextIntroducesReplyShadow(\n next: MessageResource | undefined,\n current: MessageResource\n): boolean {\n if (!next) return false\n const nextInThread = next.replyRootId !== null\n const nextIsThreadRoot = next.replyRootId === next.id\n const differentThread = next.replyRootId !== current.replyRootId\n return nextInThread && !nextIsThreadRoot && (differentThread || datesDifferBetween(next, current))\n}\n\nfunction replyShadowFor(\n message: MessageResource,\n prev: MessageResource | undefined\n): ReplyShadowMessage | undefined {\n if (!message.replyRootId) return undefined\n if (message.replyRootId === message.id) return undefined\n\n const enteringNewThread =\n !prev || prev.replyRootId !== message.replyRootId || datesDifferBetween(prev, message)\n if (!enteringNewThread) return undefined\n\n return {\n type: 'ReplyShadowMessage',\n id: `${message.id}-${message.replyRootId}`,\n messageId: message.replyRootId,\n isReplyShadowMessage: true,\n nextRendersAuthor: message.renderAuthor ?? false,\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/chat-react-native",
|
|
3
|
-
"version": "3.35.0-rc.
|
|
3
|
+
"version": "3.35.0-rc.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -65,5 +65,5 @@
|
|
|
65
65
|
"react-native-url-polyfill": "^2.0.0",
|
|
66
66
|
"typescript": "~5.9.2"
|
|
67
67
|
},
|
|
68
|
-
"gitHead": "
|
|
68
|
+
"gitHead": "1955eec824c4a2ca101004e0dbbca5a6f862fa3b"
|
|
69
69
|
}
|
|
@@ -37,10 +37,9 @@ import {
|
|
|
37
37
|
normalizeAnalyticsMetadata,
|
|
38
38
|
usePublishProductAnalyticsEvent,
|
|
39
39
|
} from '../hooks/use_product_analytics'
|
|
40
|
-
import { MessageResource } from '../types'
|
|
41
40
|
import { ConversationBadgeResource } from '../types/resources/conversation_badge'
|
|
42
41
|
import { getRelativeDateStatus } from '../utils/date'
|
|
43
|
-
import
|
|
42
|
+
import { groupMessages, type DateSeparator } from '../utils/group_messages'
|
|
44
43
|
import { CONVERSATION_MESSAGE_LIST_PADDING_HORIZONTAL } from '../utils/styles'
|
|
45
44
|
import { isSystemMessage } from '../utils/system_messages'
|
|
46
45
|
|
|
@@ -248,8 +247,6 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
|
|
|
248
247
|
)
|
|
249
248
|
}
|
|
250
249
|
|
|
251
|
-
export type DateSeparator = { type: 'DateSeparator'; id: string; date: string }
|
|
252
|
-
|
|
253
250
|
function InlineDateSeparator({ date }: DateSeparator) {
|
|
254
251
|
const styles = useDateSeparatorStyles()
|
|
255
252
|
const { isThisYear } = getRelativeDateStatus(date)
|
|
@@ -288,130 +285,6 @@ const useDateSeparatorStyles = () => {
|
|
|
288
285
|
})
|
|
289
286
|
}
|
|
290
287
|
|
|
291
|
-
type ReplyShadowMessage = {
|
|
292
|
-
type: 'ReplyShadowMessage'
|
|
293
|
-
id: string
|
|
294
|
-
messageId: string
|
|
295
|
-
isReplyShadowMessage: boolean
|
|
296
|
-
nextRendersAuthor: boolean
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
interface GroupMessagesProps {
|
|
300
|
-
ms: MessageResource[]
|
|
301
|
-
inReplyScreen?: boolean
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
export const groupMessages = ({ ms, inReplyScreen }: GroupMessagesProps) => {
|
|
305
|
-
let enrichedMessages: (MessageResource | DateSeparator | ReplyShadowMessage)[] = []
|
|
306
|
-
let encounteredOneOfMyMessages = false
|
|
307
|
-
|
|
308
|
-
ms.forEach((message, i) => {
|
|
309
|
-
const prevMessage = ms[i + 1]
|
|
310
|
-
const nextMessage = ms[i - 1]
|
|
311
|
-
const date = dayjs(message.createdAt).format('YYYY-MM-DD')
|
|
312
|
-
|
|
313
|
-
const prevMessageIsDateSeparator =
|
|
314
|
-
prevMessage && date !== dayjs(prevMessage.createdAt).format('YYYY-MM-DD')
|
|
315
|
-
|
|
316
|
-
if (isSystemMessage(message)) {
|
|
317
|
-
message.myLatestInConversation = false
|
|
318
|
-
message.lastInGroup = true
|
|
319
|
-
message.renderAuthor = false
|
|
320
|
-
message.nextRendersAuthor = nextMessage?.renderAuthor
|
|
321
|
-
message.isReplyShadowMessage = false
|
|
322
|
-
message.nextIsReplyShadowMessage = false
|
|
323
|
-
message.threadPosition = null
|
|
324
|
-
enrichedMessages.push(message)
|
|
325
|
-
if (prevMessageIsDateSeparator) {
|
|
326
|
-
enrichedMessages.push({ type: 'DateSeparator', id: `day-divider-${message.id}`, date })
|
|
327
|
-
}
|
|
328
|
-
return
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const inThread = message.replyRootId !== null
|
|
332
|
-
const nextMessageInThread = nextMessage?.replyRootId !== null
|
|
333
|
-
const threadRoot = message.replyRootId === message.id
|
|
334
|
-
const nextMessageThreadRoot = nextMessage?.replyRootId === nextMessage?.id
|
|
335
|
-
const prevMessageDifferentThread = message.replyRootId !== prevMessage?.replyRootId
|
|
336
|
-
const nextMessageDifferentThread = message.replyRootId !== nextMessage?.replyRootId
|
|
337
|
-
const prevMessageDifferentAuthor = message.author?.id !== prevMessage?.author?.id
|
|
338
|
-
const nextMessageDifferentAuthor = message.author?.id !== nextMessage?.author?.id
|
|
339
|
-
const prevMessageMoreThan5Minutes =
|
|
340
|
-
prevMessage &&
|
|
341
|
-
new Date(message.createdAt).getTime() - new Date(prevMessage.createdAt).getTime() > 60000 * 5
|
|
342
|
-
const nextMessageMoreThan5Minutes =
|
|
343
|
-
nextMessage &&
|
|
344
|
-
new Date(nextMessage.createdAt).getTime() - new Date(message.createdAt).getTime() > 60000 * 5
|
|
345
|
-
const nextMessageIsDateSeparator =
|
|
346
|
-
nextMessage && date !== dayjs(nextMessage.createdAt).format('YYYY-MM-DD')
|
|
347
|
-
const insertReplyShadowMessage =
|
|
348
|
-
message.replyRootId &&
|
|
349
|
-
!threadRoot &&
|
|
350
|
-
(prevMessageDifferentThread || prevMessageIsDateSeparator)
|
|
351
|
-
const lastInGroup =
|
|
352
|
-
!nextMessage ||
|
|
353
|
-
nextMessageDifferentAuthor ||
|
|
354
|
-
nextMessageMoreThan5Minutes ||
|
|
355
|
-
nextMessageDifferentThread ||
|
|
356
|
-
nextMessageIsDateSeparator
|
|
357
|
-
const renderAuthor =
|
|
358
|
-
!message.mine &&
|
|
359
|
-
(!prevMessage ||
|
|
360
|
-
prevMessageDifferentAuthor ||
|
|
361
|
-
prevMessageMoreThan5Minutes ||
|
|
362
|
-
prevMessageDifferentThread ||
|
|
363
|
-
prevMessageIsDateSeparator)
|
|
364
|
-
const nextIsReplyShadowMessage =
|
|
365
|
-
nextMessageInThread &&
|
|
366
|
-
!nextMessageThreadRoot &&
|
|
367
|
-
(nextMessageDifferentThread || nextMessageIsDateSeparator)
|
|
368
|
-
|
|
369
|
-
if (message.mine && !encounteredOneOfMyMessages) {
|
|
370
|
-
encounteredOneOfMyMessages = true
|
|
371
|
-
message.myLatestInConversation = true
|
|
372
|
-
} else {
|
|
373
|
-
message.myLatestInConversation = false
|
|
374
|
-
}
|
|
375
|
-
message.lastInGroup = lastInGroup
|
|
376
|
-
message.renderAuthor = renderAuthor
|
|
377
|
-
message.threadPosition = null
|
|
378
|
-
message.nextRendersAuthor = nextMessage?.renderAuthor
|
|
379
|
-
message.isReplyShadowMessage = false
|
|
380
|
-
message.nextIsReplyShadowMessage = nextIsReplyShadowMessage
|
|
381
|
-
|
|
382
|
-
if (!inReplyScreen && inThread) {
|
|
383
|
-
message.prevIsMyReply = prevMessage?.mine
|
|
384
|
-
message.nextIsMyReply = nextMessage?.mine
|
|
385
|
-
|
|
386
|
-
const firstInThread = threadRoot
|
|
387
|
-
const lastInThread = nextMessageDifferentThread || nextMessageIsDateSeparator
|
|
388
|
-
|
|
389
|
-
if (firstInThread && lastInThread)
|
|
390
|
-
message.threadPosition = null // ensures we don't render a connector for root replies that aren't immediately followed up a reply
|
|
391
|
-
else if (firstInThread) message.threadPosition = 'first'
|
|
392
|
-
else if (lastInThread) message.threadPosition = 'last'
|
|
393
|
-
else message.threadPosition = 'center'
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
enrichedMessages.push(message)
|
|
397
|
-
|
|
398
|
-
if (insertReplyShadowMessage) {
|
|
399
|
-
enrichedMessages.push({
|
|
400
|
-
type: 'ReplyShadowMessage',
|
|
401
|
-
id: `${message.id}-${message.replyRootId}`,
|
|
402
|
-
messageId: message.replyRootId!,
|
|
403
|
-
isReplyShadowMessage: true,
|
|
404
|
-
nextRendersAuthor: message?.renderAuthor,
|
|
405
|
-
})
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (!prevMessage || date !== dayjs(prevMessage.createdAt).format('YYYY-MM-DD')) {
|
|
409
|
-
enrichedMessages.push({ type: 'DateSeparator', id: `day-divider-${message.id}`, date })
|
|
410
|
-
}
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
return enrichedMessages
|
|
414
|
-
}
|
|
415
288
|
interface ConversationScreenTitleProps extends HeaderTitleProps {
|
|
416
289
|
conversation_id: number
|
|
417
290
|
badge?: ConversationBadgeResource
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { MessageResource } from '../../types/resources/message'
|
|
2
|
+
import type { PersonResource } from '../../types/resources/person'
|
|
3
|
+
import { groupMessages } from '../group_messages'
|
|
4
|
+
|
|
5
|
+
const author = { id: 1, type: 'Person', name: 'A', avatar: null } as unknown as PersonResource
|
|
6
|
+
const otherAuthor = {
|
|
7
|
+
id: 2,
|
|
8
|
+
type: 'Person',
|
|
9
|
+
name: 'B',
|
|
10
|
+
avatar: null,
|
|
11
|
+
} as unknown as PersonResource
|
|
12
|
+
|
|
13
|
+
const message = (id: string, overrides: Partial<MessageResource> = {}): MessageResource =>
|
|
14
|
+
({
|
|
15
|
+
type: 'Message',
|
|
16
|
+
id,
|
|
17
|
+
text: `msg ${id}`,
|
|
18
|
+
html: `<p>msg ${id}</p>`,
|
|
19
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
20
|
+
deletedAt: null,
|
|
21
|
+
textEditedAt: null,
|
|
22
|
+
mine: false,
|
|
23
|
+
attachments: [],
|
|
24
|
+
author,
|
|
25
|
+
reactionCounts: [],
|
|
26
|
+
replyCount: 0,
|
|
27
|
+
replyRootId: null,
|
|
28
|
+
messageType: 'message',
|
|
29
|
+
personIdsForSystemEvent: null,
|
|
30
|
+
systemTextParts: null,
|
|
31
|
+
...overrides,
|
|
32
|
+
}) as MessageResource
|
|
33
|
+
|
|
34
|
+
const findEnriched = (items: ReturnType<typeof groupMessages>, id: string) =>
|
|
35
|
+
items.find(item => 'id' in item && item.id === id && !('type' in item && item.type !== 'Message'))
|
|
36
|
+
|
|
37
|
+
describe('groupMessages — immutability', () => {
|
|
38
|
+
it('does not mutate the input messages', () => {
|
|
39
|
+
const input = [message('02'), message('01')]
|
|
40
|
+
const snapshot = JSON.parse(JSON.stringify(input))
|
|
41
|
+
|
|
42
|
+
groupMessages({ ms: input })
|
|
43
|
+
|
|
44
|
+
expect(input).toEqual(snapshot)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('groupMessages — grouping', () => {
|
|
49
|
+
it('breaks groups across a >5min gap (lastInGroup flips on the older message)', () => {
|
|
50
|
+
const messages = [
|
|
51
|
+
message('02', { createdAt: '2026-01-01T00:06:00Z' }),
|
|
52
|
+
message('01', { createdAt: '2026-01-01T00:00:00Z' }),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
const enriched = groupMessages({ ms: messages })
|
|
56
|
+
|
|
57
|
+
expect((findEnriched(enriched, '01') as MessageResource).lastInGroup).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('breaks groups when authors differ (renderAuthor flips on the newer message)', () => {
|
|
61
|
+
const messages = [message('02', { author }), message('01', { author: otherAuthor })]
|
|
62
|
+
|
|
63
|
+
const enriched = groupMessages({ ms: messages })
|
|
64
|
+
|
|
65
|
+
expect((findEnriched(enriched, '02') as MessageResource).renderAuthor).toBe(true)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('marks the first own message as myLatestInConversation and not later ones', () => {
|
|
69
|
+
const messages = [
|
|
70
|
+
message('03', { mine: true, createdAt: '2026-01-01T00:02:00Z' }),
|
|
71
|
+
message('02', { mine: true, createdAt: '2026-01-01T00:01:00Z' }),
|
|
72
|
+
message('01', { mine: false, createdAt: '2026-01-01T00:00:00Z' }),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
const enriched = groupMessages({ ms: messages })
|
|
76
|
+
|
|
77
|
+
expect((findEnriched(enriched, '03') as MessageResource).myLatestInConversation).toBe(true)
|
|
78
|
+
expect((findEnriched(enriched, '02') as MessageResource).myLatestInConversation).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('groupMessages — render order at thread + date boundaries', () => {
|
|
83
|
+
it('reply shadow precedes date separator so the date renders above', () => {
|
|
84
|
+
const messages = [
|
|
85
|
+
message('02', { createdAt: '2026-01-02T12:00:00Z', replyRootId: 'rootB' }),
|
|
86
|
+
message('01', { createdAt: '2026-01-01T12:00:00Z', replyRootId: 'rootA' }),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
const enriched = groupMessages({ ms: messages })
|
|
90
|
+
|
|
91
|
+
const shadowIdx = enriched.findIndex(
|
|
92
|
+
item => 'type' in item && item.type === 'ReplyShadowMessage' && item.id === '02-rootB'
|
|
93
|
+
)
|
|
94
|
+
const dateSepIdx = enriched.findIndex(
|
|
95
|
+
item => 'type' in item && item.type === 'DateSeparator' && item.id === 'day-divider-02'
|
|
96
|
+
)
|
|
97
|
+
expect(shadowIdx).toBeGreaterThan(-1)
|
|
98
|
+
expect(dateSepIdx).toBeGreaterThan(shadowIdx)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('skips reply-shadow insertion when inReplyScreen is false but threadPosition still resolves', () => {
|
|
102
|
+
const messages = [
|
|
103
|
+
message('02', { replyRootId: 'rootA', createdAt: '2026-01-01T00:01:00Z' }),
|
|
104
|
+
message('01', { id: 'rootA', replyRootId: 'rootA', createdAt: '2026-01-01T00:00:00Z' }),
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
const enriched = groupMessages({ ms: messages })
|
|
108
|
+
|
|
109
|
+
expect((findEnriched(enriched, '02') as MessageResource).threadPosition).toBe('last')
|
|
110
|
+
expect((findEnriched(enriched, 'rootA') as MessageResource).threadPosition).toBe('first')
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('groupMessages — nextRendersAuthor mirrors the newer enriched neighbor', () => {
|
|
115
|
+
it('propagates the newer neighbor’s computed renderAuthor across an author-change boundary', () => {
|
|
116
|
+
const messages = [message('02', { author: otherAuthor }), message('01', { author })]
|
|
117
|
+
|
|
118
|
+
const enriched = groupMessages({ ms: messages })
|
|
119
|
+
|
|
120
|
+
const newer = findEnriched(enriched, '02') as MessageResource
|
|
121
|
+
const older = findEnriched(enriched, '01') as MessageResource
|
|
122
|
+
expect(newer.renderAuthor).toBe(true)
|
|
123
|
+
expect(older.nextRendersAuthor).toBe(true)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('groupMessages — system messages', () => {
|
|
128
|
+
it('flags lastInGroup true and renderAuthor false on system messages', () => {
|
|
129
|
+
const messages = [
|
|
130
|
+
message('sys', {
|
|
131
|
+
messageType: 'user_joined',
|
|
132
|
+
systemTextParts: { names: ['A'], overflowCount: 0, action: 'joined' },
|
|
133
|
+
personIdsForSystemEvent: [1],
|
|
134
|
+
}),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
const enriched = groupMessages({ ms: messages })
|
|
138
|
+
|
|
139
|
+
const sys = findEnriched(enriched, 'sys') as MessageResource
|
|
140
|
+
expect(sys.lastInGroup).toBe(true)
|
|
141
|
+
expect(sys.renderAuthor).toBe(false)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { MessageResource } from '../types/resources/message'
|
|
2
|
+
import dayjs from './dayjs'
|
|
3
|
+
import { isSystemMessage } from './system_messages'
|
|
4
|
+
|
|
5
|
+
const FIVE_MINUTES_MS = 5 * 60 * 1000
|
|
6
|
+
|
|
7
|
+
export type DateSeparator = { type: 'DateSeparator'; id: string; date: string }
|
|
8
|
+
|
|
9
|
+
export type ReplyShadowMessage = {
|
|
10
|
+
type: 'ReplyShadowMessage'
|
|
11
|
+
id: string
|
|
12
|
+
messageId: string
|
|
13
|
+
isReplyShadowMessage: boolean
|
|
14
|
+
nextRendersAuthor: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type EnrichedMessage = MessageResource | DateSeparator | ReplyShadowMessage
|
|
18
|
+
|
|
19
|
+
interface GroupMessagesProps {
|
|
20
|
+
ms: MessageResource[]
|
|
21
|
+
inReplyScreen?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function groupMessages({ ms, inReplyScreen }: GroupMessagesProps): EnrichedMessage[] {
|
|
25
|
+
const items: EnrichedMessage[] = []
|
|
26
|
+
let myLatestSeen = false
|
|
27
|
+
let nextNeighborEnriched: MessageResource | undefined
|
|
28
|
+
|
|
29
|
+
ms.forEach((message, i) => {
|
|
30
|
+
const { prev } = neighborsOf(ms, i)
|
|
31
|
+
const next = nextNeighborEnriched
|
|
32
|
+
|
|
33
|
+
if (isSystemMessage(message)) {
|
|
34
|
+
const enriched = enrichSystemMessage(message, next)
|
|
35
|
+
items.push(enriched)
|
|
36
|
+
if (datesDifferBetween(message, prev)) items.push(dateSeparator(message))
|
|
37
|
+
nextNeighborEnriched = enriched
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isMyLatest = !myLatestSeen && message.mine
|
|
42
|
+
if (isMyLatest) myLatestSeen = true
|
|
43
|
+
|
|
44
|
+
const enriched = enrichRegularMessage(message, prev, next, isMyLatest, !!inReplyScreen)
|
|
45
|
+
items.push(enriched)
|
|
46
|
+
|
|
47
|
+
const shadow = replyShadowFor(enriched, prev)
|
|
48
|
+
if (shadow) items.push(shadow)
|
|
49
|
+
|
|
50
|
+
if (!prev || datesDifferBetween(message, prev)) {
|
|
51
|
+
items.push(dateSeparator(message))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
nextNeighborEnriched = enriched
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return items
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function neighborsOf<T>(arr: T[], i: number): { prev: T | undefined; next: T | undefined } {
|
|
61
|
+
return { prev: arr[i + 1], next: arr[i - 1] }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function enrichSystemMessage(
|
|
65
|
+
message: MessageResource,
|
|
66
|
+
next: MessageResource | undefined
|
|
67
|
+
): MessageResource {
|
|
68
|
+
return {
|
|
69
|
+
...message,
|
|
70
|
+
myLatestInConversation: false,
|
|
71
|
+
lastInGroup: true,
|
|
72
|
+
renderAuthor: false,
|
|
73
|
+
nextRendersAuthor: next?.renderAuthor,
|
|
74
|
+
isReplyShadowMessage: false,
|
|
75
|
+
nextIsReplyShadowMessage: false,
|
|
76
|
+
threadPosition: null,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function enrichRegularMessage(
|
|
81
|
+
message: MessageResource,
|
|
82
|
+
prev: MessageResource | undefined,
|
|
83
|
+
next: MessageResource | undefined,
|
|
84
|
+
isMyLatest: boolean,
|
|
85
|
+
inReplyScreen: boolean
|
|
86
|
+
): MessageResource {
|
|
87
|
+
const inThread = message.replyRootId !== null
|
|
88
|
+
const showThreadDetails = !inReplyScreen && inThread
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
...message,
|
|
92
|
+
myLatestInConversation: isMyLatest,
|
|
93
|
+
lastInGroup: !next || startsNewGroup(next, message),
|
|
94
|
+
renderAuthor: !message.mine && (!prev || startsNewGroup(message, prev)),
|
|
95
|
+
nextRendersAuthor: next?.renderAuthor,
|
|
96
|
+
isReplyShadowMessage: false,
|
|
97
|
+
nextIsReplyShadowMessage: nextIntroducesReplyShadow(next, message),
|
|
98
|
+
threadPosition: showThreadDetails ? threadPositionFor(message, next) : null,
|
|
99
|
+
prevIsMyReply: showThreadDetails ? prev?.mine : undefined,
|
|
100
|
+
nextIsMyReply: showThreadDetails ? next?.mine : undefined,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function startsNewGroup(a: MessageResource, b: MessageResource): boolean {
|
|
105
|
+
return (
|
|
106
|
+
a.author?.id !== b.author?.id ||
|
|
107
|
+
differsByMoreThan5Min(a, b) ||
|
|
108
|
+
a.replyRootId !== b.replyRootId ||
|
|
109
|
+
datesDifferBetween(a, b)
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function differsByMoreThan5Min(a: MessageResource, b: MessageResource): boolean {
|
|
114
|
+
return Math.abs(toMillis(a.createdAt) - toMillis(b.createdAt)) > FIVE_MINUTES_MS
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function toMillis(iso: string): number {
|
|
118
|
+
return new Date(iso).getTime()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function datesDifferBetween(a: MessageResource, b: MessageResource | undefined): boolean {
|
|
122
|
+
if (!b) return false
|
|
123
|
+
return dateKey(a) !== dateKey(b)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function dateKey(message: MessageResource): string {
|
|
127
|
+
return dayjs(message.createdAt).format('YYYY-MM-DD')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function dateSeparator(message: MessageResource): DateSeparator {
|
|
131
|
+
return { type: 'DateSeparator', id: `day-divider-${message.id}`, date: dateKey(message) }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function threadPositionFor(
|
|
135
|
+
message: MessageResource,
|
|
136
|
+
next: MessageResource | undefined
|
|
137
|
+
): 'first' | 'center' | 'last' | null {
|
|
138
|
+
const isThreadRoot = message.replyRootId === message.id
|
|
139
|
+
const isLast =
|
|
140
|
+
!next || next.replyRootId !== message.replyRootId || datesDifferBetween(next, message)
|
|
141
|
+
|
|
142
|
+
if (isThreadRoot && isLast) return null
|
|
143
|
+
if (isThreadRoot) return 'first'
|
|
144
|
+
if (isLast) return 'last'
|
|
145
|
+
return 'center'
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function nextIntroducesReplyShadow(
|
|
149
|
+
next: MessageResource | undefined,
|
|
150
|
+
current: MessageResource
|
|
151
|
+
): boolean {
|
|
152
|
+
if (!next) return false
|
|
153
|
+
const nextInThread = next.replyRootId !== null
|
|
154
|
+
const nextIsThreadRoot = next.replyRootId === next.id
|
|
155
|
+
const differentThread = next.replyRootId !== current.replyRootId
|
|
156
|
+
return nextInThread && !nextIsThreadRoot && (differentThread || datesDifferBetween(next, current))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function replyShadowFor(
|
|
160
|
+
message: MessageResource,
|
|
161
|
+
prev: MessageResource | undefined
|
|
162
|
+
): ReplyShadowMessage | undefined {
|
|
163
|
+
if (!message.replyRootId) return undefined
|
|
164
|
+
if (message.replyRootId === message.id) return undefined
|
|
165
|
+
|
|
166
|
+
const enteringNewThread =
|
|
167
|
+
!prev || prev.replyRootId !== message.replyRootId || datesDifferBetween(prev, message)
|
|
168
|
+
if (!enteringNewThread) return undefined
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
type: 'ReplyShadowMessage',
|
|
172
|
+
id: `${message.id}-${message.replyRootId}`,
|
|
173
|
+
messageId: message.replyRootId,
|
|
174
|
+
isReplyShadowMessage: true,
|
|
175
|
+
nextRendersAuthor: message.renderAuthor ?? false,
|
|
176
|
+
}
|
|
177
|
+
}
|