@droppii-org/chat-mobile 0.2.77 → 0.2.79
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/lib/module/components/messages/groupNotificationMessage/index.js +5 -3
- package/lib/module/components/messages/groupNotificationMessage/index.js.map +1 -1
- package/lib/module/core/useChatListener.js +19 -5
- package/lib/module/core/useChatListener.js.map +1 -1
- package/lib/module/hooks/groups/useGroupAccessGuard.js +145 -0
- package/lib/module/hooks/groups/useGroupAccessGuard.js.map +1 -0
- package/lib/module/hooks/groups/useLeaveGroup.js +93 -0
- package/lib/module/hooks/groups/useLeaveGroup.js.map +1 -0
- package/lib/module/hooks/query-keys.js +2 -1
- package/lib/module/hooks/query-keys.js.map +1 -1
- package/lib/module/hooks/useConversationList.js +6 -9
- package/lib/module/hooks/useConversationList.js.map +1 -1
- package/lib/module/screens/ChatInfo/ChatInfo.js +17 -2
- package/lib/module/screens/ChatInfo/ChatInfo.js.map +1 -1
- package/lib/module/screens/ChatInfo/ChatInfoDescriptionRow.js +4 -1
- package/lib/module/screens/ChatInfo/ChatInfoDescriptionRow.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetail.js +12 -10
- package/lib/module/screens/chat-detail/ChatDetail.js.map +1 -1
- package/lib/module/screens/chat-detail/ChatDetailHeader.js +3 -9
- package/lib/module/screens/chat-detail/ChatDetailHeader.js.map +1 -1
- package/lib/module/screens/inbox/CommunityTab.js +5 -2
- package/lib/module/screens/inbox/CommunityTab.js.map +1 -1
- package/lib/module/screens/inbox/MessagesTab.js +2 -2
- package/lib/module/screens/inbox/MessagesTab.js.map +1 -1
- package/lib/module/store/conversation.js +18 -20
- package/lib/module/store/conversation.js.map +1 -1
- package/lib/module/translation/resources/i18n.js +24 -2
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/utils/conversation.js +4 -1
- package/lib/module/utils/conversation.js.map +1 -1
- package/lib/module/utils/legendListMessage.js +5 -1
- package/lib/module/utils/legendListMessage.js.map +1 -1
- package/lib/module/utils/messageUtils.js +118 -9
- package/lib/module/utils/messageUtils.js.map +1 -1
- package/lib/module/utils/toastFlashMessage.js +6 -0
- package/lib/module/utils/toastFlashMessage.js.map +1 -1
- package/lib/typescript/src/components/messages/groupNotificationMessage/index.d.ts.map +1 -1
- package/lib/typescript/src/core/useChatListener.d.ts.map +1 -1
- package/lib/typescript/src/hooks/groups/useGroupAccessGuard.d.ts +10 -0
- package/lib/typescript/src/hooks/groups/useGroupAccessGuard.d.ts.map +1 -0
- package/lib/typescript/src/hooks/groups/useLeaveGroup.d.ts +6 -0
- package/lib/typescript/src/hooks/groups/useLeaveGroup.d.ts.map +1 -0
- package/lib/typescript/src/hooks/query-keys.d.ts +1 -0
- package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useConversationList.d.ts.map +1 -1
- package/lib/typescript/src/screens/ChatInfo/ChatInfo.d.ts.map +1 -1
- package/lib/typescript/src/screens/ChatInfo/ChatInfoDescriptionRow.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetail.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts +1 -1
- package/lib/typescript/src/screens/chat-detail/ChatDetailHeader.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/types.d.ts +0 -1
- package/lib/typescript/src/screens/chat-detail/types.d.ts.map +1 -1
- package/lib/typescript/src/store/conversation.d.ts.map +1 -1
- package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
- package/lib/typescript/src/utils/conversation.d.ts.map +1 -1
- package/lib/typescript/src/utils/legendListMessage.d.ts.map +1 -1
- package/lib/typescript/src/utils/messageUtils.d.ts +6 -0
- package/lib/typescript/src/utils/messageUtils.d.ts.map +1 -1
- package/lib/typescript/src/utils/toastFlashMessage.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/components/messages/groupNotificationMessage/index.tsx +8 -4
- package/src/core/useChatListener.ts +29 -4
- package/src/hooks/groups/useGroupAccessGuard.ts +182 -0
- package/src/hooks/groups/useLeaveGroup.ts +104 -0
- package/src/hooks/query-keys.ts +1 -0
- package/src/hooks/useConversationList.ts +8 -17
- package/src/screens/ChatInfo/ChatInfo.tsx +27 -2
- package/src/screens/ChatInfo/ChatInfoDescriptionRow.tsx +5 -1
- package/src/screens/chat-detail/ChatDetail.tsx +11 -13
- package/src/screens/chat-detail/ChatDetailHeader.tsx +2 -6
- package/src/screens/chat-detail/types.ts +0 -1
- package/src/screens/inbox/CommunityTab.tsx +2 -2
- package/src/screens/inbox/MessagesTab.tsx +1 -1
- package/src/store/conversation.ts +14 -18
- package/src/translation/resources/i18n.ts +31 -1
- package/src/utils/conversation.ts +6 -3
- package/src/utils/legendListMessage.ts +5 -1
- package/src/utils/messageUtils.ts +128 -8
- package/src/utils/toastFlashMessage.ts +8 -2
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import OpenIMSDK, {
|
|
2
|
+
GroupMemberRole,
|
|
3
|
+
GroupVisibility,
|
|
4
|
+
} from '@droppii/openim-rn-client-sdk';
|
|
5
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
6
|
+
import { conversationQueryKeys, groupQueryKeys } from '../query-keys';
|
|
7
|
+
import { UIUtils } from '../../utils/UIUtils';
|
|
8
|
+
import { trans } from '../../translation';
|
|
9
|
+
|
|
10
|
+
const showOwnerBlockedModal = () => {
|
|
11
|
+
UIUtils.alert.open({
|
|
12
|
+
title: trans('chat:leave_group_blocked_title'),
|
|
13
|
+
message: trans('chat:leave_group_blocked_desc'),
|
|
14
|
+
primaryIndex: 0,
|
|
15
|
+
buttonType: 'horizontal-button',
|
|
16
|
+
buttons: [
|
|
17
|
+
{ label: trans('chat:leave_group_blocked_button'), kind: 'primary' },
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const confirmLeaveGroup = (visibility?: GroupVisibility) =>
|
|
23
|
+
new Promise<void>((resolve, reject) => {
|
|
24
|
+
console.log('confirmLeaveGroup', visibility);
|
|
25
|
+
UIUtils.alert.open({
|
|
26
|
+
title: trans('chat:leave_group_confirm_title'),
|
|
27
|
+
message: trans(
|
|
28
|
+
visibility === GroupVisibility.Public
|
|
29
|
+
? 'chat:leave_group_confirm_desc_public'
|
|
30
|
+
: 'chat:leave_group_confirm_desc_private'
|
|
31
|
+
),
|
|
32
|
+
primaryIndex: 1,
|
|
33
|
+
buttonType: 'horizontal-button',
|
|
34
|
+
buttons: [
|
|
35
|
+
{
|
|
36
|
+
label: trans('chat:leave_group_confirm_secondary'),
|
|
37
|
+
kind: 'light',
|
|
38
|
+
onPress: reject,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
label: trans('chat:leave_group_confirm_primary'),
|
|
42
|
+
kind: 'danger',
|
|
43
|
+
onPress: resolve,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const useLeaveGroup = () => {
|
|
50
|
+
const queryClient = useQueryClient();
|
|
51
|
+
|
|
52
|
+
const mutation = useMutation({
|
|
53
|
+
mutationKey: groupQueryKeys.leave(),
|
|
54
|
+
mutationFn: (groupID: string) => OpenIMSDK.quitGroup(groupID),
|
|
55
|
+
onSuccess: (_data, groupID) => {
|
|
56
|
+
queryClient.invalidateQueries({
|
|
57
|
+
queryKey: conversationQueryKeys.joinedGroup(groupID),
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
onError: () => {
|
|
61
|
+
UIUtils.toast.open({
|
|
62
|
+
title: trans('chat:generic_error'),
|
|
63
|
+
theme: 'error',
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const leaveGroup = async (groupID: string, visibility?: GroupVisibility) => {
|
|
69
|
+
try {
|
|
70
|
+
// The owner must transfer ownership before leaving — block instead of
|
|
71
|
+
// showing the confirm modal.
|
|
72
|
+
const selfInfo = await OpenIMSDK.getSelfUserInfo(groupID);
|
|
73
|
+
if (selfInfo?.roleLevel === GroupMemberRole.Owner) {
|
|
74
|
+
showOwnerBlockedModal();
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
await confirmLeaveGroup(visibility);
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Revalidate right before submitting — role/membership may have
|
|
84
|
+
// changed while the confirm modal was open.
|
|
85
|
+
const latestSelfInfo = await OpenIMSDK.getSelfUserInfo(groupID);
|
|
86
|
+
if (latestSelfInfo?.roleLevel === GroupMemberRole.Owner) {
|
|
87
|
+
showOwnerBlockedModal();
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await mutation.mutateAsync(groupID);
|
|
92
|
+
return true;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('leaveGroup failed', error);
|
|
95
|
+
UIUtils.toast.open({
|
|
96
|
+
title: trans('chat:generic_error'),
|
|
97
|
+
theme: 'error',
|
|
98
|
+
});
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return { leaveGroup, isLeavingGroup: mutation.isPending };
|
|
104
|
+
};
|
package/src/hooks/query-keys.ts
CHANGED
|
@@ -23,26 +23,17 @@ export function useConversationList({
|
|
|
23
23
|
const query = useQuery({
|
|
24
24
|
queryKey: [...conversationQueryKeys.list(applicationType), page],
|
|
25
25
|
enabled: enabled !== false,
|
|
26
|
+
staleTime: 1000 * 60 * 1, // 5 minutes
|
|
26
27
|
queryFn: async () => {
|
|
27
28
|
const offset = (page - 1) * 500;
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}),
|
|
34
|
-
OpenIMSDK.getJoinedGroupList(),
|
|
35
|
-
]);
|
|
29
|
+
const conversations = await OpenIMSDK.getConversationListSplitApp({
|
|
30
|
+
offset,
|
|
31
|
+
count: 500,
|
|
32
|
+
applicationType,
|
|
33
|
+
});
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
conversation.groupID && !joinedGroupIds.has(conversation.groupID)
|
|
40
|
-
? { ...conversation, isNotInGroup: true }
|
|
41
|
-
: conversation
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
useConversationStore.getState().updateConversations(resolved);
|
|
45
|
-
return resolved;
|
|
35
|
+
useConversationStore.getState().updateConversations(conversations);
|
|
36
|
+
return conversations;
|
|
46
37
|
},
|
|
47
38
|
});
|
|
48
39
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { memo } from 'react';
|
|
1
|
+
import { memo, useCallback } from 'react';
|
|
2
2
|
import { KContainer, KNavigation } from '@droppii/libs';
|
|
3
3
|
import { useNavigation } from '@react-navigation/native';
|
|
4
4
|
import { useGetConversationTarget } from '../../hooks/conversation/useGetConversationDetail';
|
|
5
|
+
import { useGetGroupInfo } from '../../hooks/conversation/useGetGroupInfo';
|
|
6
|
+
import { useLeaveGroup } from '../../hooks/groups/useLeaveGroup';
|
|
5
7
|
import { useConversation } from '../../store';
|
|
6
8
|
import ChatInfoProfile from './ChatInfoProfile';
|
|
7
9
|
import ChatInfoMediaRow from './ChatInfoMediaRow';
|
|
@@ -31,6 +33,29 @@ const ChatInfo = memo(
|
|
|
31
33
|
const isTargetActive = target ? target.isActive : true;
|
|
32
34
|
const isGroup = useChatDetailContext().isGroupConversation;
|
|
33
35
|
|
|
36
|
+
const { data: groupInfo } = useGetGroupInfo(
|
|
37
|
+
isGroup ? conversation?.groupID : undefined
|
|
38
|
+
);
|
|
39
|
+
const { leaveGroup } = useLeaveGroup();
|
|
40
|
+
|
|
41
|
+
const handleLeaveGroup = useCallback(async () => {
|
|
42
|
+
if (!conversation?.groupID) return;
|
|
43
|
+
|
|
44
|
+
const didLeave = await leaveGroup(
|
|
45
|
+
conversation.groupID,
|
|
46
|
+
groupInfo?.visibility
|
|
47
|
+
);
|
|
48
|
+
if (didLeave) {
|
|
49
|
+
(onPressLeaveGroup ?? navigation.goBack)();
|
|
50
|
+
}
|
|
51
|
+
}, [
|
|
52
|
+
conversation?.groupID,
|
|
53
|
+
groupInfo?.visibility,
|
|
54
|
+
leaveGroup,
|
|
55
|
+
navigation.goBack,
|
|
56
|
+
onPressLeaveGroup,
|
|
57
|
+
]);
|
|
58
|
+
|
|
34
59
|
return (
|
|
35
60
|
<KContainer.Page flex edges={['bottom']} background="#EBEDF2">
|
|
36
61
|
<KNavigation.Header
|
|
@@ -61,7 +86,7 @@ const ChatInfo = memo(
|
|
|
61
86
|
onPressOrders={onPressOrders}
|
|
62
87
|
onPressCreateGroup={onPressCreateGroup}
|
|
63
88
|
onPressAddToGroup={onPressAddToGroup}
|
|
64
|
-
onPressLeaveGroup={
|
|
89
|
+
onPressLeaveGroup={handleLeaveGroup}
|
|
65
90
|
onDismissGroup={onDismissGroup}
|
|
66
91
|
/>
|
|
67
92
|
</KContainer.ScrollView>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { memo, useCallback, useState } from 'react';
|
|
1
|
+
import { memo, useCallback, useEffect, useState } from 'react';
|
|
2
2
|
import { StyleSheet, type TextLayoutEvent } from 'react-native';
|
|
3
3
|
import { KContainer, KLabel, KColors } from '@droppii/libs';
|
|
4
4
|
import { trans } from '../../translation';
|
|
@@ -47,6 +47,10 @@ const ChatInfoDescriptionRow = memo(
|
|
|
47
47
|
paddingH: '0.75rem' as const,
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
setDisplayedText(introduction);
|
|
52
|
+
}, [introduction]);
|
|
53
|
+
|
|
50
54
|
if (!introduction) {
|
|
51
55
|
return null;
|
|
52
56
|
}
|
|
@@ -35,9 +35,10 @@ import {
|
|
|
35
35
|
type MessageItem,
|
|
36
36
|
} from '@droppii/openim-rn-client-sdk';
|
|
37
37
|
import { useMessages } from '../../hooks/message/useMessages';
|
|
38
|
-
import { ChatEvent
|
|
38
|
+
import { ChatEvent } from '../../types/events';
|
|
39
39
|
import { useSetIsShowFLashMessage } from '../../context/global';
|
|
40
40
|
import { useJoinGroup } from '../../hooks/groups/useJoinGroup';
|
|
41
|
+
import { useGroupAccessGuard } from '../../hooks/groups/useGroupAccessGuard';
|
|
41
42
|
|
|
42
43
|
const ChatDetail = memo(
|
|
43
44
|
({
|
|
@@ -119,16 +120,14 @@ const ChatDetail = memo(
|
|
|
119
120
|
};
|
|
120
121
|
}, [conversationId, targetUserId, targetGroupId, setShowFLashMessage]);
|
|
121
122
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return () => sub.remove();
|
|
131
|
-
}, [targetGroupId, onBack, handleBackDefault]);
|
|
123
|
+
const handleAccessBlocked = useCallback(() => {
|
|
124
|
+
(onBack ?? handleBackDefault)();
|
|
125
|
+
}, [onBack, handleBackDefault]);
|
|
126
|
+
|
|
127
|
+
const { isCheckingAccess } = useGroupAccessGuard({
|
|
128
|
+
groupID: conversation?.groupID || targetGroupId,
|
|
129
|
+
onBlocked: handleAccessBlocked,
|
|
130
|
+
});
|
|
132
131
|
|
|
133
132
|
const resolvedShowAddMember =
|
|
134
133
|
showAddMember ?? shouldShowAddMember(conversation);
|
|
@@ -197,7 +196,6 @@ const ChatDetail = memo(
|
|
|
197
196
|
onPressAddMember={onPressAddMember}
|
|
198
197
|
onPressMenu={handlePressMenu}
|
|
199
198
|
onPressAvatar={handlePressMenu}
|
|
200
|
-
targetGroupId={targetGroupId}
|
|
201
199
|
/>
|
|
202
200
|
|
|
203
201
|
<PinnedMessageBar
|
|
@@ -210,7 +208,7 @@ const ChatDetail = memo(
|
|
|
210
208
|
messages={messages}
|
|
211
209
|
conversationId={conversationId}
|
|
212
210
|
customMessageItemFactory={customMessageItemFactory}
|
|
213
|
-
isLoading={initLoading || isLoadingConversation}
|
|
211
|
+
isLoading={initLoading || isLoadingConversation || isCheckingAccess}
|
|
214
212
|
onLoadMoreOld={getMoreOldMessages}
|
|
215
213
|
hasMoreOld={hasMoreOld}
|
|
216
214
|
moreOldLoading={moreOldLoading}
|
|
@@ -11,8 +11,8 @@ import { NamePrefixIcon } from '../../components/ThreadCard/NamePrefixIcon';
|
|
|
11
11
|
import type { ChatDetailHeaderProps } from './types';
|
|
12
12
|
import { getConversationSubtitle } from './conversationHeader.utils';
|
|
13
13
|
import { useGetUsersInfo } from '../../hooks/users/useGetUsersInfo';
|
|
14
|
-
import { useGetConversationDetail } from '../../hooks/conversation/useGetConversationDetail';
|
|
15
14
|
import { useChatDetailContext } from '../../context';
|
|
15
|
+
import { useConversation } from '../../store';
|
|
16
16
|
|
|
17
17
|
const ChatDetailHeader = memo(
|
|
18
18
|
({
|
|
@@ -24,12 +24,8 @@ const ChatDetailHeader = memo(
|
|
|
24
24
|
onPressAddMember,
|
|
25
25
|
onPressMenu,
|
|
26
26
|
onPressAvatar,
|
|
27
|
-
targetGroupId,
|
|
28
27
|
}: ChatDetailHeaderProps) => {
|
|
29
|
-
const
|
|
30
|
-
targetGroupId,
|
|
31
|
-
targetUserId: targetUserId,
|
|
32
|
-
});
|
|
28
|
+
const conversation = useConversation(conversationId);
|
|
33
29
|
const { data: usersInfo } = useGetUsersInfo(
|
|
34
30
|
targetUserId ? [targetUserId] : []
|
|
35
31
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KContainer, KSpacingValue } from '@droppii/libs';
|
|
1
|
+
import { KColors, KContainer, KSpacingValue } from '@droppii/libs';
|
|
2
2
|
import { memo, useCallback } from 'react';
|
|
3
3
|
import { ActivityIndicator, StyleSheet } from 'react-native';
|
|
4
4
|
import type { SearchPublicGroupInfo } from '@droppii/openim-rn-client-sdk';
|
|
@@ -41,7 +41,7 @@ export const CommunityTab = memo(({ onPressGroup }: CommunityTabProps) => {
|
|
|
41
41
|
<KContainer.Page flex>
|
|
42
42
|
{isFetching && !isFetchingNextPage && !isRefetching ? (
|
|
43
43
|
<KContainer.View flex center>
|
|
44
|
-
<ActivityIndicator />
|
|
44
|
+
<ActivityIndicator color={KColors.primary.normal} size="small" />
|
|
45
45
|
</KContainer.View>
|
|
46
46
|
) : (
|
|
47
47
|
<KContainer.FlatList
|
|
@@ -39,7 +39,7 @@ export const MessagesTab = memo(({ onPressThread }: MessagesTabProps) => {
|
|
|
39
39
|
|
|
40
40
|
const LoadingComponent = (
|
|
41
41
|
<KContainer.View center flex paddingV="3rem">
|
|
42
|
-
<ActivityIndicator
|
|
42
|
+
<ActivityIndicator color={KColors.primary.normal} size="small" />
|
|
43
43
|
</KContainer.View>
|
|
44
44
|
);
|
|
45
45
|
|
|
@@ -52,23 +52,14 @@ export const useConversationStore = create<ConversationStore>()((set, get) => ({
|
|
|
52
52
|
const existing = get().map[id];
|
|
53
53
|
if (!existing) return;
|
|
54
54
|
|
|
55
|
-
const merged = { ...existing, ...data };
|
|
56
55
|
const nextMap = { ...get().map };
|
|
57
|
-
|
|
58
|
-
delete nextMap[id];
|
|
59
|
-
} else {
|
|
60
|
-
nextMap[id] = merged;
|
|
61
|
-
}
|
|
56
|
+
nextMap[id] = { ...existing, ...data };
|
|
62
57
|
set(sortConversation(nextMap));
|
|
63
58
|
},
|
|
64
59
|
updateConversations(conversations) {
|
|
65
60
|
const nextMap = conversations.reduce(
|
|
66
61
|
(pre, curr) => {
|
|
67
|
-
|
|
68
|
-
delete pre[curr.conversationID];
|
|
69
|
-
} else {
|
|
70
|
-
pre[curr.conversationID] = curr;
|
|
71
|
-
}
|
|
62
|
+
pre[curr.conversationID] = curr;
|
|
72
63
|
return pre;
|
|
73
64
|
},
|
|
74
65
|
{ ...get().map }
|
|
@@ -86,14 +77,19 @@ export const useConversationStore = create<ConversationStore>()((set, get) => ({
|
|
|
86
77
|
},
|
|
87
78
|
removeGroupConversation(groupID) {
|
|
88
79
|
const conversationID = getConversationID(SessionType.Group, groupID);
|
|
80
|
+
const existing = get().map[conversationID];
|
|
81
|
+
if (!existing) return;
|
|
89
82
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
83
|
+
// Mark rather than delete: the conversation leaves the inbox and loses its
|
|
84
|
+
// pin, but stays addressable so a public group is still readable. Access
|
|
85
|
+
// itself is decided by useGroupAccessGuard, not by the store.
|
|
86
|
+
const nextMap = { ...get().map };
|
|
87
|
+
nextMap[conversationID] = {
|
|
88
|
+
...existing,
|
|
89
|
+
isNotInGroup: true,
|
|
90
|
+
isPinned: false,
|
|
91
|
+
};
|
|
92
|
+
set(sortConversation(nextMap));
|
|
97
93
|
},
|
|
98
94
|
reset() {
|
|
99
95
|
set({
|
|
@@ -75,16 +75,31 @@ const i18nKey: Record<string, string> = {
|
|
|
75
75
|
'chat:group_created_by': '{{sender}} đã tạo nhóm',
|
|
76
76
|
'msg_type_group_name_updated': 'Đã đổi tên nhóm',
|
|
77
77
|
'chat:group_name_updated_by': '{{sender}} đã đổi tên nhóm',
|
|
78
|
+
'chat:group_name_updated_by_named':
|
|
79
|
+
'{{sender}} đã đổi tên nhóm thành {{newGroupName}}',
|
|
78
80
|
'msg_type_group_info_updated': 'Đã cập nhật thông tin nhóm',
|
|
79
81
|
'chat:group_info_updated_by': '{{sender}} đã cập nhật thông tin nhóm',
|
|
80
82
|
'msg_type_member_quit': 'Đã rời khỏi nhóm',
|
|
81
|
-
'chat:member_quit_by': '{{sender}} đã rời
|
|
83
|
+
'chat:member_quit_by': '{{sender}} đã rời nhóm',
|
|
82
84
|
'msg_type_group_owner_transferred': 'Đã chuyển quyền trưởng nhóm',
|
|
83
85
|
'chat:group_owner_transferred_by': '{{sender}} đã chuyển quyền trưởng nhóm',
|
|
86
|
+
'chat:group_owner_transferred_by_named':
|
|
87
|
+
'{{sender}} đã chuyển quyền trưởng nhóm cho {{memberName}}',
|
|
84
88
|
'msg_type_member_kicked': 'Đã xoá thành viên khỏi nhóm',
|
|
85
89
|
'chat:member_kicked_by': '{{sender}} đã xoá thành viên khỏi nhóm',
|
|
90
|
+
'chat:member_kicked_by_named': '{{sender}} đã xoá {{names}} khỏi nhóm',
|
|
86
91
|
'msg_type_member_invited': 'Đã thêm thành viên vào nhóm',
|
|
87
92
|
'chat:member_invited_by': '{{sender}} đã thêm thành viên vào nhóm',
|
|
93
|
+
'chat:member_invited_by_named': '{{sender}} đã thêm {{names}} vào nhóm',
|
|
94
|
+
'msg_type_group_member_set_admin': 'Đã chỉ định quản trị viên mới',
|
|
95
|
+
'chat:group_member_set_admin_by':
|
|
96
|
+
'{{sender}} đã đặt {{memberName}} làm quản trị viên',
|
|
97
|
+
'chat:group_member_set_admin_by_generic':
|
|
98
|
+
'{{sender}} đã đặt một thành viên làm quản trị viên',
|
|
99
|
+
'msg_type_message_pinned': 'Đã ghim một tin nhắn',
|
|
100
|
+
'chat:group_message_pinned_by': '{{sender}} đã ghim một tin nhắn',
|
|
101
|
+
'msg_type_message_unpinned': 'Đã bỏ ghim một tin nhắn',
|
|
102
|
+
'chat:group_message_unpinned_by': '{{sender}} đã bỏ ghim một tin nhắn',
|
|
88
103
|
'msg_type_member_enter': 'Đã tham gia nhóm',
|
|
89
104
|
'chat:member_enter_by': '{{sender}} đã tham gia nhóm',
|
|
90
105
|
'msg_type_group_dismissed': 'Nhóm đã bị giải tán',
|
|
@@ -124,6 +139,17 @@ const i18nKey: Record<string, string> = {
|
|
|
124
139
|
'chat:info_unpin_message': 'Bỏ ghim trò chuyện',
|
|
125
140
|
'chat:info_delete_history': 'Xoá lịch sử trò chuyện',
|
|
126
141
|
'chat:info_leave_group': 'Rời nhóm',
|
|
142
|
+
'chat:leave_group_confirm_title': 'Rời nhóm?',
|
|
143
|
+
'chat:leave_group_confirm_desc_private':
|
|
144
|
+
'Sau khi rời nhóm, bạn sẽ không thể xem tin nhắn từ cuộc trò chuyện này nữa. Bạn có chắc chắn muốn rời khỏi nhóm?',
|
|
145
|
+
'chat:leave_group_confirm_desc_public':
|
|
146
|
+
'Sau khi rời nhóm, bạn sẽ không thể nhắn tin hay nhận thông báo từ cuộc trò chuyện này nữa. Bạn có chắc chắn muốn rời khỏi nhóm?',
|
|
147
|
+
'chat:leave_group_confirm_primary': 'Rời nhóm',
|
|
148
|
+
'chat:leave_group_confirm_secondary': 'Ở lại nhóm',
|
|
149
|
+
'chat:leave_group_blocked_title': 'Không thể rời nhóm',
|
|
150
|
+
'chat:leave_group_blocked_desc':
|
|
151
|
+
'Bạn không thể rời nhóm khi đang là chủ nhóm, vui lòng chuyển quyền chủ nhóm cho thành viên khác và thử lại!',
|
|
152
|
+
'chat:leave_group_blocked_button': 'Đã hiểu',
|
|
127
153
|
'chat:dismiss_group': 'Giải tán nhóm',
|
|
128
154
|
'chat:info_view_more': 'Xem thêm',
|
|
129
155
|
'chat:group_description_title': 'Mô tả nhóm',
|
|
@@ -183,6 +209,10 @@ const i18nKey: Record<string, string> = {
|
|
|
183
209
|
'group:dismiss_confirm_primary': 'Giải tán',
|
|
184
210
|
'group:dismiss_confirm_secondary': 'Quay lại',
|
|
185
211
|
'group:dismiss_toast_success': 'Đã giải tán nhóm',
|
|
212
|
+
'group:access_blocked_title': 'Không thể xem cuộc trò chuyện này',
|
|
213
|
+
'group:access_blocked_desc':
|
|
214
|
+
'Bạn hiện không có quyền truy cập vào cuộc trò chuyện hoặc tin nhắn này.',
|
|
215
|
+
'group:access_blocked_button': 'Đã hiểu',
|
|
186
216
|
};
|
|
187
217
|
|
|
188
218
|
export default {
|
|
@@ -38,9 +38,12 @@ export const getConversationID = (
|
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
export const sortConversation = (map: Record<string, ConversationItem>) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
// `map` holds every conversation that can still be opened; `list` is the
|
|
42
|
+
// inbox. A group the user is no longer in stays addressable — a public group
|
|
43
|
+
// remains readable via deeplink or search — but must not show in the inbox.
|
|
44
|
+
const list = Object.keys(map)
|
|
45
|
+
.filter((id) => !map[id]?.isNotInGroup)
|
|
46
|
+
.sort((a, b) => conversationCompare(map[a]!, map[b]!));
|
|
44
47
|
return { map, list };
|
|
45
48
|
};
|
|
46
49
|
|
|
@@ -28,8 +28,12 @@ export const getMessageText = (message: MessageItem): string => {
|
|
|
28
28
|
case MessageType.FaceMessage:
|
|
29
29
|
return '[Sticker]';
|
|
30
30
|
default:
|
|
31
|
+
// Never surface the raw notification payload as message text — it
|
|
32
|
+
// contains other users' userID/nickname/faceURL/permissions and would
|
|
33
|
+
// render verbatim as a normal chat bubble for any content type that
|
|
34
|
+
// isn't (yet) mapped in GROUP_NOTIFICATION_TEXT.
|
|
31
35
|
if (message.notificationElem?.detail) {
|
|
32
|
-
return
|
|
36
|
+
return '';
|
|
33
37
|
}
|
|
34
38
|
return message.content || '';
|
|
35
39
|
}
|
|
@@ -79,6 +79,120 @@ export const GROUP_NOTIFICATION_TEXT: Partial<
|
|
|
79
79
|
previewKey: 'msg_type_group_name_updated',
|
|
80
80
|
bySenderKey: 'chat:group_name_updated_by',
|
|
81
81
|
},
|
|
82
|
+
[MessageType.GroupMemberSetToAdmin]: {
|
|
83
|
+
previewKey: 'msg_type_group_member_set_admin',
|
|
84
|
+
bySenderKey: 'chat:group_member_set_admin_by_generic',
|
|
85
|
+
},
|
|
86
|
+
// One content type, two texts (pinned vs unpinned) — the base entry here is
|
|
87
|
+
// just the "unpinned" fallback; resolveNotificationText() below overrides
|
|
88
|
+
// it to the "pinned" keys when notificationElem.detail.isPinned is true.
|
|
89
|
+
[MessageType.PinMsg]: {
|
|
90
|
+
previewKey: 'msg_type_message_unpinned',
|
|
91
|
+
bySenderKey: 'chat:group_message_unpinned_by',
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const parseNotificationDetail = (message: MessageItem): any => {
|
|
96
|
+
if (!message.notificationElem?.detail) return undefined;
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(message.notificationElem.detail);
|
|
99
|
+
} catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// PM-approved rule for "N members added/removed at once": 1-2 names are
|
|
105
|
+
// listed in full, 3+ collapse to the first two names plus a trailing count.
|
|
106
|
+
// Renders identically to the spec either way — e.g. 4 invites become
|
|
107
|
+
// "B, C và 2 người khác".
|
|
108
|
+
const formatMemberNames = (
|
|
109
|
+
members: Array<{ nickname?: string }> | undefined
|
|
110
|
+
): string | undefined => {
|
|
111
|
+
const names = (members ?? [])
|
|
112
|
+
.map((member) => member?.nickname)
|
|
113
|
+
.filter((nickname): nickname is string => !!nickname);
|
|
114
|
+
|
|
115
|
+
if (names.length === 0) return undefined;
|
|
116
|
+
if (names.length <= 2) return names.join(', ');
|
|
117
|
+
return `${names[0]}, ${names[1]} và ${names.length - 2} người khác`;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export interface ResolvedNotificationText {
|
|
121
|
+
previewKey: string;
|
|
122
|
+
bySenderKey: string;
|
|
123
|
+
params: Record<string, string>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// GROUP_NOTIFICATION_TEXT above only covers the static "no extra data needed"
|
|
127
|
+
// shape. This resolver layers in the per-event params (affected member name,
|
|
128
|
+
// new group name, pinned/unpinned) confirmed against real captured payloads —
|
|
129
|
+
// upgrading to a `_named`/`_by` key when the data is present, and silently
|
|
130
|
+
// falling back to the generic base key when it's missing or malformed so the
|
|
131
|
+
// UI never shows a raw `{{placeholder}}` or throws on a bad payload.
|
|
132
|
+
export const resolveNotificationText = (
|
|
133
|
+
message: MessageItem
|
|
134
|
+
): ResolvedNotificationText | undefined => {
|
|
135
|
+
const base = GROUP_NOTIFICATION_TEXT[message.contentType];
|
|
136
|
+
if (!base) return undefined;
|
|
137
|
+
|
|
138
|
+
let previewKey = base.previewKey;
|
|
139
|
+
let bySenderKey = base.bySenderKey;
|
|
140
|
+
const params: Record<string, string> = {};
|
|
141
|
+
const detail = parseNotificationDetail(message);
|
|
142
|
+
|
|
143
|
+
switch (message.contentType) {
|
|
144
|
+
case MessageType.GroupNameUpdated: {
|
|
145
|
+
const newGroupName = detail?.group?.groupName;
|
|
146
|
+
if (newGroupName) {
|
|
147
|
+
params.newGroupName = newGroupName;
|
|
148
|
+
bySenderKey = 'chat:group_name_updated_by_named';
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case MessageType.MemberInvited: {
|
|
153
|
+
const names = formatMemberNames(detail?.invitedUserList);
|
|
154
|
+
if (names) {
|
|
155
|
+
params.names = names;
|
|
156
|
+
bySenderKey = 'chat:member_invited_by_named';
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case MessageType.MemberKicked: {
|
|
161
|
+
const names = formatMemberNames(detail?.kickedUserList);
|
|
162
|
+
if (names) {
|
|
163
|
+
params.names = names;
|
|
164
|
+
bySenderKey = 'chat:member_kicked_by_named';
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
case MessageType.GroupOwnerTransferred: {
|
|
169
|
+
const memberName = detail?.newGroupOwner?.nickname;
|
|
170
|
+
if (memberName) {
|
|
171
|
+
params.memberName = memberName;
|
|
172
|
+
bySenderKey = 'chat:group_owner_transferred_by_named';
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case MessageType.GroupMemberSetToAdmin: {
|
|
177
|
+
const memberName = detail?.changedUser?.nickname;
|
|
178
|
+
if (memberName) {
|
|
179
|
+
params.memberName = memberName;
|
|
180
|
+
bySenderKey = 'chat:group_member_set_admin_by';
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
case MessageType.PinMsg: {
|
|
185
|
+
if (detail?.isPinned === true) {
|
|
186
|
+
previewKey = 'msg_type_message_pinned';
|
|
187
|
+
bySenderKey = 'chat:group_message_pinned_by';
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
default:
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { previewKey, bySenderKey, params };
|
|
82
196
|
};
|
|
83
197
|
|
|
84
198
|
export const extractTextContent = (message: MessageItem): string => {
|
|
@@ -118,7 +232,7 @@ export const getMessagePreview = (message?: MessageItem): string => {
|
|
|
118
232
|
case MessageType.FileMessage: {
|
|
119
233
|
const fileName = message?.fileElem?.fileName;
|
|
120
234
|
if (fileName) {
|
|
121
|
-
return `[File] ${fileName}`;
|
|
235
|
+
return `[File] ${truncateMiddle(fileName, 20)}`;
|
|
122
236
|
}
|
|
123
237
|
return '[File]';
|
|
124
238
|
}
|
|
@@ -177,9 +291,9 @@ export const getMessagePreview = (message?: MessageItem): string => {
|
|
|
177
291
|
return trans('msg_type_burn_message_change');
|
|
178
292
|
|
|
179
293
|
default: {
|
|
180
|
-
const
|
|
181
|
-
if (
|
|
182
|
-
return trans(
|
|
294
|
+
const resolved = resolveNotificationText(message);
|
|
295
|
+
if (resolved) {
|
|
296
|
+
return trans(resolved.previewKey, resolved.params);
|
|
183
297
|
}
|
|
184
298
|
return '[Tin nhắn không được hỗ trợ]';
|
|
185
299
|
}
|
|
@@ -305,8 +419,11 @@ export const getNotificationOpUserNickname = (
|
|
|
305
419
|
try {
|
|
306
420
|
const parsed = JSON.parse(message.notificationElem.detail) as {
|
|
307
421
|
opUser?: { nickname?: string };
|
|
422
|
+
quitUser?: { nickname?: string };
|
|
308
423
|
};
|
|
309
|
-
|
|
424
|
+
// MemberQuit puts the leaver in `quitUser`, not `opUser` — every other
|
|
425
|
+
// group-notification content type uses `opUser` for the actor.
|
|
426
|
+
return parsed?.opUser?.nickname ?? parsed?.quitUser?.nickname;
|
|
310
427
|
} catch {
|
|
311
428
|
return undefined;
|
|
312
429
|
}
|
|
@@ -353,9 +470,12 @@ export const getLastMessageText = ({
|
|
|
353
470
|
return trans('chat:revoked_by_sender', { sender });
|
|
354
471
|
}
|
|
355
472
|
|
|
356
|
-
const
|
|
357
|
-
if (
|
|
358
|
-
return trans(
|
|
473
|
+
const resolvedNotification = resolveNotificationText(message);
|
|
474
|
+
if (resolvedNotification) {
|
|
475
|
+
return trans(resolvedNotification.bySenderKey, {
|
|
476
|
+
sender,
|
|
477
|
+
...resolvedNotification.params,
|
|
478
|
+
});
|
|
359
479
|
}
|
|
360
480
|
|
|
361
481
|
const hideSenderPrefix =
|