@droppii-org/chat-mobile 0.2.68 → 0.2.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/lib/module/components/GroupItem/index.js +152 -0
  2. package/lib/module/components/GroupItem/index.js.map +1 -0
  3. package/lib/module/core/useChatListener.js +33 -2
  4. package/lib/module/core/useChatListener.js.map +1 -1
  5. package/lib/module/hooks/conversation/useGetConversationDetail.js +2 -2
  6. package/lib/module/hooks/conversation/useGetConversationDetail.js.map +1 -1
  7. package/lib/module/hooks/groups/useFetchPublicGroup.js +39 -0
  8. package/lib/module/hooks/groups/useFetchPublicGroup.js.map +1 -0
  9. package/lib/module/hooks/groups/useJoinGroup.js +42 -0
  10. package/lib/module/hooks/groups/useJoinGroup.js.map +1 -0
  11. package/lib/module/hooks/query-keys.js +6 -0
  12. package/lib/module/hooks/query-keys.js.map +1 -1
  13. package/lib/module/index.js +1 -1
  14. package/lib/module/index.js.map +1 -1
  15. package/lib/module/screens/SearchConversation/TabResult.js +1 -1
  16. package/lib/module/screens/SearchConversation/TabResult.js.map +1 -1
  17. package/lib/module/screens/chat-detail/ChatComposer.js +33 -4
  18. package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
  19. package/lib/module/screens/chat-detail/ChatDetail.js +20 -2
  20. package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
  21. package/lib/module/screens/chat-detail/ChatListLegend.js +16 -17
  22. package/lib/module/screens/chat-detail/ChatListLegend.js.map +1 -1
  23. package/lib/module/screens/chat-detail/types.js.map +1 -1
  24. package/lib/module/screens/inbox/CommunityTab.js +55 -0
  25. package/lib/module/screens/inbox/CommunityTab.js.map +1 -0
  26. package/lib/module/screens/inbox/Inbox.js +51 -40
  27. package/lib/module/screens/inbox/Inbox.js.map +1 -1
  28. package/lib/module/translation/resources/i18n.js +10 -1
  29. package/lib/module/translation/resources/i18n.js.map +1 -1
  30. package/lib/typescript/src/components/GroupItem/index.d.ts +8 -0
  31. package/lib/typescript/src/components/GroupItem/index.d.ts.map +1 -0
  32. package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
  33. package/lib/typescript/src/hooks/groups/useFetchPublicGroup.d.ts +213 -0
  34. package/lib/typescript/src/hooks/groups/useFetchPublicGroup.d.ts.map +1 -0
  35. package/lib/typescript/src/hooks/groups/useJoinGroup.d.ts +5 -0
  36. package/lib/typescript/src/hooks/groups/useJoinGroup.d.ts.map +1 -0
  37. package/lib/typescript/src/hooks/query-keys.d.ts +6 -0
  38. package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
  39. package/lib/typescript/src/index.d.ts +1 -1
  40. package/lib/typescript/src/index.d.ts.map +1 -1
  41. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts +3 -1
  42. package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
  43. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts +1 -1
  44. package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
  45. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts +1 -1
  46. package/lib/typescript/src/screens/chat-detail/ChatListLegend.d.ts.map +1 -1
  47. package/lib/typescript/src/screens/chat-detail/types.d.ts +2 -1
  48. package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
  49. package/lib/typescript/src/screens/inbox/CommunityTab.d.ts +6 -0
  50. package/lib/typescript/src/screens/inbox/CommunityTab.d.ts.map +1 -0
  51. package/lib/typescript/src/screens/inbox/Inbox.d.ts +2 -1
  52. package/lib/typescript/src/screens/inbox/Inbox.d.ts.map +1 -1
  53. package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
  54. package/package.json +1 -1
  55. package/src/components/GroupItem/index.tsx +174 -0
  56. package/src/core/useChatListener.ts +53 -1
  57. package/src/hooks/conversation/useGetConversationDetail.ts +2 -2
  58. package/src/hooks/groups/useFetchPublicGroup.ts +43 -0
  59. package/src/hooks/groups/useJoinGroup.ts +46 -0
  60. package/src/hooks/query-keys.ts +8 -0
  61. package/src/index.tsx +5 -1
  62. package/src/screens/SearchConversation/TabResult.tsx +1 -1
  63. package/src/screens/chat-detail/ChatComposer.tsx +46 -3
  64. package/src/screens/chat-detail/ChatDetail.tsx +25 -1
  65. package/src/screens/chat-detail/ChatListLegend.tsx +11 -13
  66. package/src/screens/chat-detail/types.ts +5 -1
  67. package/src/screens/inbox/CommunityTab.tsx +65 -0
  68. package/src/screens/inbox/Inbox.tsx +51 -39
  69. package/src/translation/resources/i18n.ts +9 -0
@@ -0,0 +1,174 @@
1
+ import {
2
+ KButton,
3
+ KColors,
4
+ KContainer,
5
+ KLabel,
6
+ KSpacingValue,
7
+ } from '@droppii/libs';
8
+ import {
9
+ GroupJoinSource,
10
+ type SearchPublicGroupInfo,
11
+ type SearchPublicGroupMemberInfo,
12
+ } from '@droppii/openim-rn-client-sdk';
13
+ import { memo, useCallback, useMemo } from 'react';
14
+ import { StyleSheet } from 'react-native';
15
+ import { Avatar } from '../Avatar';
16
+ import type { AvatarProps } from '../Avatar/Avatar.types';
17
+ import { trans } from '../../translation';
18
+ import { useJoinGroup } from '../../hooks/groups/useJoinGroup';
19
+
20
+ interface IGroupItemProps {
21
+ item: SearchPublicGroupInfo;
22
+ onPress?: (groupId: string) => void;
23
+ }
24
+
25
+ const buildAvatarProps = (
26
+ faceURL: string,
27
+ groupName: string,
28
+ members: SearchPublicGroupMemberInfo[],
29
+ memberCount: number
30
+ ): Pick<
31
+ AvatarProps,
32
+ | 'type'
33
+ | 'source'
34
+ | 'secondarySource'
35
+ | 'tertiarySource'
36
+ | 'remainingCount'
37
+ | 'fullName'
38
+ | 'secondaryFullName'
39
+ | 'tertiaryFullName'
40
+ > => {
41
+ if (faceURL) {
42
+ return { type: 'single', source: faceURL, fullName: groupName };
43
+ }
44
+ const [m0, m1, m2] = members;
45
+ if (members.length >= 4) {
46
+ return {
47
+ type: 'quad',
48
+ source: m0?.faceURL,
49
+ secondarySource: m1?.faceURL,
50
+ tertiarySource: m2?.faceURL,
51
+ remainingCount: memberCount - 3,
52
+ fullName: m0?.nickname,
53
+ secondaryFullName: m1?.nickname,
54
+ tertiaryFullName: m2?.nickname,
55
+ };
56
+ }
57
+ if (members.length === 3) {
58
+ return {
59
+ type: 'triple',
60
+ source: m0?.faceURL,
61
+ secondarySource: m1?.faceURL,
62
+ tertiarySource: m2?.faceURL,
63
+ fullName: m0?.nickname,
64
+ secondaryFullName: m1?.nickname,
65
+ tertiaryFullName: m2?.nickname,
66
+ };
67
+ }
68
+ if (members.length === 2) {
69
+ return {
70
+ type: 'double',
71
+ source: m0?.faceURL,
72
+ secondarySource: m1?.faceURL,
73
+ fullName: m0?.nickname,
74
+ secondaryFullName: m1?.nickname,
75
+ };
76
+ }
77
+ return {
78
+ type: 'single',
79
+ source: faceURL,
80
+ fullName: groupName,
81
+ remainingCount: memberCount,
82
+ };
83
+ };
84
+
85
+ export const GroupItem = memo((props: IGroupItemProps) => {
86
+ const { item, onPress } = props || {};
87
+ const {
88
+ groupID,
89
+ groupName = '',
90
+ memberCount = 0,
91
+ introduction = '',
92
+ faceURL = '',
93
+ isJoined = false,
94
+ members = [],
95
+ } = item || {};
96
+
97
+ const { mutate: joinGroup, isPending } = useJoinGroup();
98
+
99
+ const avatarProps = useMemo(
100
+ () => buildAvatarProps(faceURL, groupName, members, memberCount),
101
+ [faceURL, groupName, members, memberCount]
102
+ );
103
+
104
+ const handleActionPress = useCallback(() => {
105
+ if (isJoined) {
106
+ onPress?.(groupID);
107
+ return;
108
+ }
109
+ joinGroup(
110
+ { groupID, reqMsg: '', joinSource: GroupJoinSource.Search, groupName },
111
+ { onSuccess: () => onPress?.(groupID) }
112
+ );
113
+ }, [isJoined, groupID, joinGroup, onPress, groupName]);
114
+
115
+ return (
116
+ <KContainer.Touchable
117
+ style={styles.item}
118
+ onPress={() => onPress?.(groupID)}
119
+ >
120
+ <Avatar size="md" {...avatarProps} />
121
+ <KContainer.View style={styles.itemContent}>
122
+ <KLabel.Text typo="TextMdMedium" numberOfLines={1}>
123
+ {groupName}
124
+ </KLabel.Text>
125
+ <KContainer.View style={styles.itemDescription}>
126
+ <KContainer.View flex>
127
+ <KLabel.Text
128
+ typo="TextNmNormal"
129
+ numberOfLines={1}
130
+ color={KColors.gray.normal}
131
+ >
132
+ {trans('group:member_count', { count: memberCount })}
133
+ </KLabel.Text>
134
+ <KLabel.Text
135
+ typo="TextNmNormal"
136
+ numberOfLines={1}
137
+ color={KColors.gray.normal}
138
+ >
139
+ {introduction || trans('group:public_group')}
140
+ </KLabel.Text>
141
+ </KContainer.View>
142
+ <KButton.Solid
143
+ label={trans(isJoined ? 'group:access' : 'group:join')}
144
+ onPress={handleActionPress}
145
+ size="xs"
146
+ isLightSolid
147
+ kind={isJoined ? 'light' : 'primary'}
148
+ loading={isPending}
149
+ />
150
+ </KContainer.View>
151
+ </KContainer.View>
152
+ </KContainer.Touchable>
153
+ );
154
+ });
155
+
156
+ const styles = StyleSheet.create({
157
+ item: {
158
+ paddingVertical: KSpacingValue['0.5rem'],
159
+ flexDirection: 'row',
160
+ alignItems: 'center',
161
+ gap: KSpacingValue['0.5rem'],
162
+ borderBottomWidth: 1,
163
+ borderBlockColor: KColors.border.light,
164
+ },
165
+ itemContent: {
166
+ flex: 1,
167
+ },
168
+ itemDescription: {
169
+ flexDirection: 'row',
170
+ alignItems: 'center',
171
+ gap: KSpacingValue['0.5rem'],
172
+ justifyContent: 'space-between',
173
+ },
174
+ });
@@ -4,6 +4,7 @@ import OpenIMSDK, {
4
4
  OpenIMEvent,
5
5
  SessionType,
6
6
  type ConversationItem,
7
+ type GroupApplicationItem,
7
8
  type GroupItem,
8
9
  type GroupMemberItem,
9
10
  type MessageItem,
@@ -24,7 +25,7 @@ import { belongsToConversation } from '../utils/message';
24
25
  import { getConversationID } from '../utils/conversation';
25
26
  import { DeviceEventEmitter } from 'react-native';
26
27
  import { ChatEvent } from '../types/events';
27
- import { conversationQueryKeys } from '../hooks/query-keys';
28
+ import { conversationQueryKeys, groupQueryKeys } from '../hooks/query-keys';
28
29
 
29
30
  export const useChatListener = () => {
30
31
  const updateOneMessage = useMessageStore((state) => state.updateOneMessage);
@@ -266,6 +267,48 @@ export const useChatListener = () => {
266
267
  [currentUserId]
267
268
  );
268
269
 
270
+ const markGroupJoined = useCallback(
271
+ (groupID: string) => {
272
+ queryClient.invalidateQueries({
273
+ queryKey: conversationQueryKeys.joinedGroup(groupID),
274
+ });
275
+
276
+ queryClient.setQueriesData<{
277
+ pages: {
278
+ total: number;
279
+ groups: { groupID: string; isJoined: boolean }[];
280
+ }[];
281
+ pageParams: unknown[];
282
+ }>({ queryKey: groupQueryKeys.public(), exact: false }, (old) => {
283
+ if (!old) return old;
284
+ return {
285
+ ...old,
286
+ pages: old.pages.map((page) => ({
287
+ ...page,
288
+ groups: page.groups.map((g) =>
289
+ g.groupID === groupID ? { ...g, isJoined: true } : g
290
+ ),
291
+ })),
292
+ };
293
+ });
294
+ },
295
+ [queryClient]
296
+ );
297
+
298
+ const onJoinedGroupAdded = useCallback(
299
+ (group: GroupItem) => {
300
+ markGroupJoined(group.groupID);
301
+ },
302
+ [markGroupJoined]
303
+ );
304
+
305
+ const onGroupApplicationAdded = useCallback(
306
+ (application: GroupApplicationItem) => {
307
+ markGroupJoined(application.groupID);
308
+ },
309
+ [markGroupJoined]
310
+ );
311
+
269
312
  const onMessageRevoked = useCallback((revokedInfo: RevokedInfo) => {
270
313
  markMessageRevoked(revokedInfo.clientMsgID);
271
314
  }, []);
@@ -327,6 +370,8 @@ export const useChatListener = () => {
327
370
  );
328
371
  OpenIMSDK.on(OpenIMEvent.OnJoinedGroupDeleted, onJoinedGroupDeleted);
329
372
  OpenIMSDK.on(OpenIMEvent.OnGroupDismissed, onGroupDismissed);
373
+ OpenIMSDK.on(OpenIMEvent.OnJoinedGroupAdded, onJoinedGroupAdded);
374
+ OpenIMSDK.on(OpenIMEvent.OnGroupApplicationAdded, onGroupApplicationAdded);
330
375
 
331
376
  // Message status
332
377
  OpenIMSDK.on(OpenIMEvent.SendMessageProgress, onSendMessageProgress);
@@ -353,6 +398,11 @@ export const useChatListener = () => {
353
398
  );
354
399
  OpenIMSDK.off(OpenIMEvent.OnJoinedGroupDeleted, onJoinedGroupDeleted);
355
400
  OpenIMSDK.off(OpenIMEvent.OnGroupDismissed, onGroupDismissed);
401
+ OpenIMSDK.off(OpenIMEvent.OnJoinedGroupAdded, onJoinedGroupAdded);
402
+ OpenIMSDK.off(
403
+ OpenIMEvent.OnGroupApplicationAdded,
404
+ onGroupApplicationAdded
405
+ );
356
406
 
357
407
  // Message status
358
408
  OpenIMSDK.off(OpenIMEvent.SendMessageProgress, onSendMessageProgress);
@@ -378,5 +428,7 @@ export const useChatListener = () => {
378
428
  OnGroupMemberInfoChanged,
379
429
  onJoinedGroupDeleted,
380
430
  onGroupDismissed,
431
+ onJoinedGroupAdded,
432
+ onGroupApplicationAdded,
381
433
  ]);
382
434
  };
@@ -96,10 +96,10 @@ export const useGetConversationDetail = ({
96
96
  targetGroupId,
97
97
  targetBotId,
98
98
  }: UseGetConversationDetailParams): UseGetConversationDetailResult => {
99
- const { data: botData, isLoading: botIdLoading } =
99
+ const { data: botData, isFetching: botIdLoading } =
100
100
  useBotConversationData(targetBotId);
101
101
 
102
- const { data: conversationData, isLoading: conversationLoading } =
102
+ const { data: conversationData, isFetching: conversationLoading } =
103
103
  useGetOneConversation({
104
104
  targetUserId,
105
105
  targetGroupId: targetGroupId || botData?.groupId,
@@ -0,0 +1,43 @@
1
+ import { useInfiniteQuery } from '@tanstack/react-query';
2
+ import { useMemo } from 'react';
3
+ import OpenIMSDK, {
4
+ PublicGroupJoinStatus,
5
+ type SearchPublicGroupInfo,
6
+ } from '@droppii/openim-rn-client-sdk';
7
+ import { PAGE_SIZE } from '../../constants';
8
+ import { groupQueryKeys } from '../query-keys';
9
+
10
+ export const useFetchPublicGroup = (keyword: string) => {
11
+ const { data, ...rest } = useInfiniteQuery({
12
+ queryKey: groupQueryKeys.publicSearch(keyword),
13
+ queryFn: ({ pageParam = 0 }) => {
14
+ return OpenIMSDK.searchPublicGroups(
15
+ keyword,
16
+ PublicGroupJoinStatus.All,
17
+ pageParam,
18
+ PAGE_SIZE
19
+ );
20
+ },
21
+ getNextPageParam: (lastPage, allPages) => {
22
+ const fetched = allPages.reduce((sum, p) => sum + p.groups.length, 0);
23
+ return fetched < lastPage.total ? fetched : undefined;
24
+ },
25
+ initialPageParam: 0,
26
+ maxPages: 1,
27
+ });
28
+
29
+ const dataFlatten: SearchPublicGroupInfo[] = useMemo(() => {
30
+ const seen = new Set<string>();
31
+ return (
32
+ data?.pages.flatMap((page) =>
33
+ page.groups.filter((g) => {
34
+ if (seen.has(g.groupID)) return false;
35
+ seen.add(g.groupID);
36
+ return true;
37
+ })
38
+ ) ?? []
39
+ );
40
+ }, [data]);
41
+
42
+ return { dataFlatten, ...rest };
43
+ };
@@ -0,0 +1,46 @@
1
+ import OpenIMSDK, { type JoinGroupParams } from '@droppii/openim-rn-client-sdk';
2
+ import { useQueryClient } from '@tanstack/react-query';
3
+ import { useMutation } from '@tanstack/react-query';
4
+ import { conversationQueryKeys, groupQueryKeys } from '../query-keys';
5
+ import { UIUtils } from '../../utils/UIUtils';
6
+ import { trans } from '../../translation';
7
+
8
+ export const useJoinGroup = () => {
9
+ const queryClient = useQueryClient();
10
+
11
+ return useMutation({
12
+ mutationKey: groupQueryKeys.join(),
13
+ mutationFn: (params: JoinGroupParams & { groupName: string }) =>
14
+ OpenIMSDK.joinGroup(params),
15
+ onSuccess: (_data, params) => {
16
+ queryClient.invalidateQueries({
17
+ queryKey: conversationQueryKeys.joinedGroup(params.groupID),
18
+ });
19
+
20
+ queryClient.setQueriesData<{
21
+ pages: {
22
+ total: number;
23
+ groups: { groupID: string; isJoined: boolean }[];
24
+ }[];
25
+ pageParams: unknown[];
26
+ }>({ queryKey: groupQueryKeys.public(), exact: false }, (old) => {
27
+ if (!old) return old;
28
+ return {
29
+ ...old,
30
+ pages: old.pages.map((page) => ({
31
+ ...page,
32
+ groups: page.groups.map((g) =>
33
+ g.groupID === params.groupID ? { ...g, isJoined: true } : g
34
+ ),
35
+ })),
36
+ };
37
+ });
38
+
39
+ UIUtils.toast.open({
40
+ title: trans('group:joined_group', {
41
+ groupName: params?.groupName || '',
42
+ }),
43
+ });
44
+ },
45
+ });
46
+ };
@@ -61,3 +61,11 @@ export const messageQueryKeys = {
61
61
  search: (query: string, conversationId?: string) =>
62
62
  [...messageQueryKeys.all, 'search', query, conversationId] as const,
63
63
  };
64
+
65
+ export const groupQueryKeys = {
66
+ all: ['groups'] as const,
67
+ public: () => [...groupQueryKeys.all, 'public'] as const,
68
+ publicSearch: (keyword: string) =>
69
+ [...groupQueryKeys.public(), keyword] as const,
70
+ join: () => [...groupQueryKeys.all, 'join'] as const,
71
+ };
package/src/index.tsx CHANGED
@@ -40,7 +40,11 @@ export type {
40
40
  MessageItem,
41
41
  ConversationItem,
42
42
  } from '@droppii/openim-rn-client-sdk';
43
- export { LoginStatus, MessageType } from '@droppii/openim-rn-client-sdk';
43
+ export {
44
+ LoginStatus,
45
+ MessageType,
46
+ GroupJoinSource,
47
+ } from '@droppii/openim-rn-client-sdk';
44
48
  export * from './screens/chat-detail';
45
49
  export type {
46
50
  DChatActionItem,
@@ -122,7 +122,7 @@ const styles = StyleSheet.create({
122
122
  tabRow: {
123
123
  flexDirection: 'row',
124
124
  borderBottomWidth: 1,
125
- borderBottomColor: KColors.border.normal,
125
+ borderBottomColor: KColors.border.light,
126
126
  },
127
127
  tabItem: {
128
128
  paddingVertical: KSpacingValue['0.5rem'],
@@ -64,7 +64,11 @@ import { useSendMessage } from '../../hooks/message/useSendMessage';
64
64
  import { openBotMenuSheet } from './BotMenuSheet';
65
65
  import { UIUtils } from '../../utils/UIUtils';
66
66
  import { GroupPermission, PeerType } from '@droppii/openim-rn-client-sdk';
67
- import { useGuardedAction } from '../../hooks/useGroupPermission';
67
+ import {
68
+ useGuardedAction,
69
+ useHasGroupPermission,
70
+ } from '../../hooks/useGroupPermission';
71
+ import { useIsJoinedGroup } from '../../hooks/useIsJoinedGroup';
68
72
 
69
73
  interface ChatComposerInternalProps {
70
74
  isEmptyMessage?: boolean;
@@ -72,6 +76,8 @@ interface ChatComposerInternalProps {
72
76
  isComposeDisabled?: boolean;
73
77
  composerRef?: RefObject<View | null>;
74
78
  onComposerLayout?: (event: LayoutChangeEvent) => void;
79
+ onPressJoinGroup?: () => void;
80
+ isJoiningGroup?: boolean;
75
81
  }
76
82
 
77
83
  export const ChatComposer = memo(
@@ -81,6 +87,8 @@ export const ChatComposer = memo(
81
87
  isComposeDisabled = false,
82
88
  composerRef,
83
89
  onComposerLayout,
90
+ onPressJoinGroup,
91
+ isJoiningGroup = false,
84
92
  }: ChatComposerInternalProps) => {
85
93
  const insets = useSafeAreaInsets();
86
94
  const isDisableCompose = isComposeDisabled;
@@ -99,6 +107,15 @@ export const ChatComposer = memo(
99
107
  const { mutateAsync: closeSupportSession } = useCloseSupportSession();
100
108
  const { sendMessage } = useSendMessage();
101
109
  const compose = useChatCompose();
110
+ const isSendMessageAllowed = useHasGroupPermission(
111
+ GroupPermission.SendMessage
112
+ );
113
+
114
+ const { data: isJoinedGroup } = useIsJoinedGroup(conversation?.groupID);
115
+
116
+ const isNotInGroup = useMemo(() => {
117
+ return conversation?.groupID ? !isJoinedGroup : false;
118
+ }, [conversation?.groupID, isJoinedGroup]);
102
119
 
103
120
  const {
104
121
  activePanelType,
@@ -403,7 +420,7 @@ export const ChatComposer = memo(
403
420
  conversationFlow?.flowCurrentStep?.type === ReactFlowsNodeTypes.Start &&
404
421
  isEmptyMessage;
405
422
 
406
- if (isDisableCompose && !isGroupConversation) {
423
+ if ((isDisableCompose && !isGroupConversation) || !isSendMessageAllowed) {
407
424
  content = (
408
425
  <KContainer.View row alignItems justifyContent>
409
426
  <KContainer.View marginR="0.25rem">
@@ -414,7 +431,9 @@ export const ChatComposer = memo(
414
431
  />
415
432
  </KContainer.View>
416
433
  <KLabel.Text typo="TextXsNormal">
417
- {trans('chat:compose_disabled_seller_inactive')}
434
+ {!isSendMessageAllowed
435
+ ? trans('group:cannot_send_message')
436
+ : trans('chat:compose_disabled_seller_inactive')}
418
437
  </KLabel.Text>
419
438
  </KContainer.View>
420
439
  );
@@ -431,6 +450,30 @@ export const ChatComposer = memo(
431
450
  />
432
451
  </KContainer.View>
433
452
  );
453
+ } else if (isNotInGroup) {
454
+ content = (
455
+ <KContainer.View
456
+ paddingH="0.75rem"
457
+ justifyContent="center"
458
+ brTW={1}
459
+ brTC={KColors.border.light}
460
+ >
461
+ <KLabel.Text
462
+ typo="TextSmNormal"
463
+ textAlign="center"
464
+ paddingV={'0.75rem'}
465
+ >
466
+ {trans('group:not_in_group')}
467
+ </KLabel.Text>
468
+ <KButton.Solid
469
+ size="md"
470
+ label={trans('group:join')}
471
+ stretch
472
+ loading={isJoiningGroup}
473
+ onPress={onPressJoinGroup}
474
+ />
475
+ </KContainer.View>
476
+ );
434
477
  } else {
435
478
  content = (
436
479
  <KContainer.View background={'#F9F9FA'}>
@@ -30,10 +30,14 @@ import {
30
30
  useGetConversationDetail,
31
31
  useGetConversationTarget,
32
32
  } from '../../hooks/conversation/useGetConversationDetail';
33
- import type { MessageItem } from '@droppii/openim-rn-client-sdk';
33
+ import {
34
+ GroupJoinSource,
35
+ type MessageItem,
36
+ } from '@droppii/openim-rn-client-sdk';
34
37
  import { useMessages } from '../../hooks/message/useMessages';
35
38
  import { ChatEvent, type GroupRemovedPayload } from '../../types/events';
36
39
  import { useSetIsShowFLashMessage } from '../../context/global';
40
+ import { useJoinGroup } from '../../hooks/groups/useJoinGroup';
37
41
 
38
42
  const ChatDetail = memo(
39
43
  ({
@@ -50,6 +54,7 @@ const ChatDetail = memo(
50
54
  onPressUrl,
51
55
  customMessageItemFactory,
52
56
  pannelActions,
57
+ groupJoinSource,
53
58
  }: ChatDetailProps) => {
54
59
  const navigation = useNavigation();
55
60
  const mediaViewerRef = useRef<MediaViewerModalHandle>(null);
@@ -94,6 +99,8 @@ const ChatDetail = memo(
94
99
  hasMoreNew,
95
100
  } = useMessages({ conversationId, startClientMsgId: targetMessageId });
96
101
 
102
+ const { mutate: joinGroup, isPending: isJoiningGroup } = useJoinGroup();
103
+
97
104
  const setShowFLashMessage = useSetIsShowFLashMessage();
98
105
 
99
106
  const { pinnedMessages, pinMessage, unpinMessage } =
@@ -164,6 +171,21 @@ const ChatDetail = memo(
164
171
  onPressMenu?.(conversationId);
165
172
  }, [conversationId, onPressMenu]);
166
173
 
174
+ const handlePressJoinGroup = useCallback(() => {
175
+ if (!conversation?.groupID) return;
176
+ joinGroup({
177
+ groupID: conversation?.groupID,
178
+ reqMsg: '',
179
+ joinSource: groupJoinSource || GroupJoinSource.Search,
180
+ groupName: conversation?.showName || '',
181
+ });
182
+ }, [
183
+ groupJoinSource,
184
+ conversation?.groupID,
185
+ joinGroup,
186
+ conversation?.showName,
187
+ ]);
188
+
167
189
  return (
168
190
  <KContainer.Page flex edges={['bottom']}>
169
191
  <ChatDetailHeader
@@ -207,6 +229,8 @@ const ChatDetail = memo(
207
229
  isEmptyMessage={messages?.length === 0}
208
230
  pannelActions={pannelActions}
209
231
  isComposeDisabled={isComposeDisabled}
232
+ onPressJoinGroup={handlePressJoinGroup}
233
+ isJoiningGroup={isJoiningGroup}
210
234
  />
211
235
 
212
236
  <MediaViewerModal ref={mediaViewerRef} />
@@ -8,6 +8,7 @@ import {
8
8
  type RefObject,
9
9
  } from 'react';
10
10
  import {
11
+ ActivityIndicator,
11
12
  DeviceEventEmitter,
12
13
  Platform,
13
14
  StyleSheet,
@@ -89,6 +90,7 @@ export const ChatListLegend = memo(
89
90
  freeze,
90
91
  scrollMessageToEnd,
91
92
  keyboardOffset,
93
+ isLoading,
92
94
  }: ChatListLegendInternalProps) => {
93
95
  const currentUserId = useUserStore((s) => s.user?.userID);
94
96
  const windowWidth = useWindowDimensions().width;
@@ -492,16 +494,13 @@ export const ChatListLegend = memo(
492
494
  listRef,
493
495
  ]);
494
496
 
495
- // if (isLoading) {
496
- // return (
497
- // <KContainer.View flex center background={KColors.white}>
498
- // <ActivityIndicator
499
- // color={KColors.palette.primary.w400}
500
- // size="small"
501
- // />
502
- // </KContainer.View>
503
- // );
504
- // }
497
+ if (isLoading) {
498
+ return (
499
+ <KContainer.View flex center background={KColors.white}>
500
+ <ActivityIndicator color={KColors.primary.normal} size="small" />
501
+ </KContainer.View>
502
+ );
503
+ }
505
504
 
506
505
  return (
507
506
  <KContainer.View flex background={KColors.white} style={styles.container}>
@@ -509,7 +508,7 @@ export const ChatListLegend = memo(
509
508
  ref={listRef}
510
509
  data={messages}
511
510
  renderItem={renderItem}
512
- estimatedItemSize={100}
511
+ estimatedItemSize={200}
513
512
  getItemType={getItemType}
514
513
  getFixedItemSize={getFixedItemSize}
515
514
  keyExtractor={(item) => String(item.clientMsgID || item.serverMsgID)}
@@ -517,7 +516,6 @@ export const ChatListLegend = memo(
517
516
  extraData={`${conversationFlow?.flowCurrentStep?.id ?? ''}-${lastMessageId ?? ''}-${highlightedId ?? ''}`}
518
517
  alignItemsAtEnd
519
518
  maintainVisibleContentPosition={maintainVisiblePositionConfig}
520
- maintainScrollAtEnd={true}
521
519
  freeze={freeze}
522
520
  keyboardOffset={keyboardOffset}
523
521
  scrollEventThrottle={16}
@@ -532,7 +530,7 @@ export const ChatListLegend = memo(
532
530
  onEndReached={handleEndReached}
533
531
  onEndReachedThreshold={0.2}
534
532
  showsVerticalScrollIndicator={false}
535
- initialScrollAtEnd
533
+ initialScrollIndex={messages.length - 1}
536
534
  />
537
535
  <Animated.View
538
536
  style={[styles.scrollToBottomButton, animatedButtonStyle]}
@@ -1,7 +1,10 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { MediaItem } from '../../hooks/useMediaViewer';
3
3
  import type { MeasuredRect } from './messageToolbox/types';
4
- import type { MessageItem } from '@droppii/openim-rn-client-sdk';
4
+ import type {
5
+ GroupJoinSource,
6
+ MessageItem,
7
+ } from '@droppii/openim-rn-client-sdk';
5
8
 
6
9
  export type DChatActionIconProvider = 'MaterialCommunityIcons' | 'DroppiiNew';
7
10
 
@@ -100,4 +103,5 @@ export interface ChatDetailProps extends Omit<
100
103
  onPressUrl?: (url: string) => void;
101
104
  customMessageItemFactory?: (item: MessageItem) => ReactNode;
102
105
  pannelActions?: DChatPannelAction[];
106
+ groupJoinSource?: GroupJoinSource;
103
107
  }