@droppii-org/chat-mobile 0.2.51 → 0.2.53
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/assets/images/index.js +2 -1
- package/lib/module/assets/images/index.js.map +1 -1
- package/lib/module/assets/images/resource-center-empty-box.png +0 -0
- package/lib/module/components/MediaViewerModal.js +2 -1
- package/lib/module/components/MediaViewerModal.js.map +1 -1
- package/lib/module/components/MergedImageGrid.js +1 -0
- package/lib/module/components/MergedImageGrid.js.map +1 -1
- package/lib/module/config/api-endpoints.js +2 -1
- package/lib/module/config/api-endpoints.js.map +1 -1
- package/lib/module/context/ChatDetailContext.js +2 -1
- package/lib/module/context/ChatDetailContext.js.map +1 -1
- package/lib/module/core/notification/pushToken.js +7 -7
- package/lib/module/core/notification/pushToken.js.map +1 -1
- package/lib/module/hooks/flows/useFlow.js +10 -0
- package/lib/module/hooks/flows/useFlow.js.map +1 -1
- package/lib/module/hooks/query-keys.js +5 -0
- package/lib/module/hooks/query-keys.js.map +1 -1
- package/lib/module/hooks/resourceCenter/useThreadResources.js +116 -0
- package/lib/module/hooks/resourceCenter/useThreadResources.js.map +1 -0
- package/lib/module/hooks/useImageAttachment.js +0 -2
- package/lib/module/hooks/useImageAttachment.js.map +1 -1
- package/lib/module/hooks/useMediaViewer.js +4 -1
- package/lib/module/hooks/useMediaViewer.js.map +1 -1
- package/lib/module/hooks/useSendAttachment.js +4 -1
- package/lib/module/hooks/useSendAttachment.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/screens/MediaView/index.js +104 -42
- package/lib/module/screens/MediaView/index.js.map +1 -1
- package/lib/module/screens/ResourceCenter/FileResourceRow.js +80 -0
- package/lib/module/screens/ResourceCenter/FileResourceRow.js.map +1 -0
- package/lib/module/screens/ResourceCenter/GridRenderer.js +83 -0
- package/lib/module/screens/ResourceCenter/GridRenderer.js.map +1 -0
- package/lib/module/screens/ResourceCenter/GridThumbnail.js +97 -0
- package/lib/module/screens/ResourceCenter/GridThumbnail.js.map +1 -0
- package/lib/module/screens/ResourceCenter/LinkResourceRow.js +81 -0
- package/lib/module/screens/ResourceCenter/LinkResourceRow.js.map +1 -0
- package/lib/module/screens/ResourceCenter/ListRenderer.js +60 -0
- package/lib/module/screens/ResourceCenter/ListRenderer.js.map +1 -0
- package/lib/module/screens/ResourceCenter/ResourceCenterTabBar.js +75 -0
- package/lib/module/screens/ResourceCenter/ResourceCenterTabBar.js.map +1 -0
- package/lib/module/screens/ResourceCenter/ResourceCenterTabScene.js +122 -0
- package/lib/module/screens/ResourceCenter/ResourceCenterTabScene.js.map +1 -0
- package/lib/module/screens/ResourceCenter/ResourceEmptyState.js +36 -0
- package/lib/module/screens/ResourceCenter/ResourceEmptyState.js.map +1 -0
- package/lib/module/screens/ResourceCenter/index.js +104 -0
- package/lib/module/screens/ResourceCenter/index.js.map +1 -0
- package/lib/module/screens/ResourceCenter/types.js +25 -0
- package/lib/module/screens/ResourceCenter/types.js.map +1 -0
- package/lib/module/screens/chat-detail/BotMenuSheet.js +73 -0
- package/lib/module/screens/chat-detail/BotMenuSheet.js.map +1 -0
- package/lib/module/screens/chat-detail/ChatComposer.js +78 -3
- package/lib/module/screens/chat-detail/ChatComposer.js.map +1 -1
- package/lib/module/screens/chat-detail/components/ChatMessageInput.js +11 -6
- package/lib/module/screens/chat-detail/components/ChatMessageInput.js.map +1 -1
- package/lib/module/services/attachmentOrchestrator.js +19 -4
- package/lib/module/services/attachmentOrchestrator.js.map +1 -1
- package/lib/module/services/endpoints.js +1 -0
- package/lib/module/services/endpoints.js.map +1 -1
- package/lib/module/services/resourceCenter.js +102 -0
- package/lib/module/services/resourceCenter.js.map +1 -0
- package/lib/module/translation/resources/i18n.js +13 -1
- package/lib/module/translation/resources/i18n.js.map +1 -1
- package/lib/module/types/attachment.js +2 -0
- package/lib/module/types/chat.js +20 -0
- package/lib/module/types/chat.js.map +1 -1
- package/lib/module/utils/url.js +11 -0
- package/lib/module/utils/url.js.map +1 -1
- package/lib/typescript/src/assets/images/index.d.ts +1 -0
- package/lib/typescript/src/assets/images/index.d.ts.map +1 -1
- package/lib/typescript/src/components/MediaViewerModal.d.ts +3 -1
- package/lib/typescript/src/components/MediaViewerModal.d.ts.map +1 -1
- package/lib/typescript/src/components/MergedImageGrid.d.ts.map +1 -1
- package/lib/typescript/src/config/api-endpoints.d.ts +1 -0
- package/lib/typescript/src/config/api-endpoints.d.ts.map +1 -1
- package/lib/typescript/src/context/ChatDetailContext.d.ts.map +1 -1
- package/lib/typescript/src/hooks/flows/useFlow.d.ts +1 -0
- package/lib/typescript/src/hooks/flows/useFlow.d.ts.map +1 -1
- package/lib/typescript/src/hooks/query-keys.d.ts +5 -0
- package/lib/typescript/src/hooks/query-keys.d.ts.map +1 -1
- package/lib/typescript/src/hooks/resourceCenter/useThreadResources.d.ts +21 -0
- package/lib/typescript/src/hooks/resourceCenter/useThreadResources.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useImageAttachment.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useMediaViewer.d.ts +5 -1
- package/lib/typescript/src/hooks/useMediaViewer.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useSendAttachment.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/MediaView/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/ResourceCenter/FileResourceRow.d.ts +7 -0
- package/lib/typescript/src/screens/ResourceCenter/FileResourceRow.d.ts.map +1 -0
- package/lib/typescript/src/screens/ResourceCenter/GridRenderer.d.ts +14 -0
- package/lib/typescript/src/screens/ResourceCenter/GridRenderer.d.ts.map +1 -0
- package/lib/typescript/src/screens/ResourceCenter/GridThumbnail.d.ts +10 -0
- package/lib/typescript/src/screens/ResourceCenter/GridThumbnail.d.ts.map +1 -0
- package/lib/typescript/src/screens/ResourceCenter/LinkResourceRow.d.ts +8 -0
- package/lib/typescript/src/screens/ResourceCenter/LinkResourceRow.d.ts.map +1 -0
- package/lib/typescript/src/screens/ResourceCenter/ListRenderer.d.ts +16 -0
- package/lib/typescript/src/screens/ResourceCenter/ListRenderer.d.ts.map +1 -0
- package/lib/typescript/src/screens/ResourceCenter/ResourceCenterTabBar.d.ts +3 -0
- package/lib/typescript/src/screens/ResourceCenter/ResourceCenterTabBar.d.ts.map +1 -0
- package/lib/typescript/src/screens/ResourceCenter/ResourceCenterTabScene.d.ts +16 -0
- package/lib/typescript/src/screens/ResourceCenter/ResourceCenterTabScene.d.ts.map +1 -0
- package/lib/typescript/src/screens/ResourceCenter/ResourceEmptyState.d.ts +6 -0
- package/lib/typescript/src/screens/ResourceCenter/ResourceEmptyState.d.ts.map +1 -0
- package/lib/typescript/src/screens/ResourceCenter/index.d.ts +4 -0
- package/lib/typescript/src/screens/ResourceCenter/index.d.ts.map +1 -0
- package/lib/typescript/src/screens/ResourceCenter/types.d.ts +28 -0
- package/lib/typescript/src/screens/ResourceCenter/types.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/BotMenuSheet.d.ts +7 -0
- package/lib/typescript/src/screens/chat-detail/BotMenuSheet.d.ts.map +1 -0
- package/lib/typescript/src/screens/chat-detail/ChatComposer.d.ts.map +1 -1
- package/lib/typescript/src/screens/chat-detail/components/ChatMessageInput.d.ts +2 -1
- package/lib/typescript/src/screens/chat-detail/components/ChatMessageInput.d.ts.map +1 -1
- package/lib/typescript/src/services/attachmentOrchestrator.d.ts.map +1 -1
- package/lib/typescript/src/services/endpoints.d.ts +1 -0
- package/lib/typescript/src/services/endpoints.d.ts.map +1 -1
- package/lib/typescript/src/services/resourceCenter.d.ts +9 -0
- package/lib/typescript/src/services/resourceCenter.d.ts.map +1 -0
- package/lib/typescript/src/translation/resources/i18n.d.ts.map +1 -1
- package/lib/typescript/src/types/attachment.d.ts +2 -0
- package/lib/typescript/src/types/attachment.d.ts.map +1 -1
- package/lib/typescript/src/types/chat.d.ts +60 -1
- package/lib/typescript/src/types/chat.d.ts.map +1 -1
- package/lib/typescript/src/utils/url.d.ts +7 -0
- package/lib/typescript/src/utils/url.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/assets/images/index.ts +1 -0
- package/src/assets/images/resource-center-empty-box.png +0 -0
- package/src/components/MediaViewerModal.tsx +8 -3
- package/src/components/MergedImageGrid.tsx +1 -0
- package/src/config/api-endpoints.ts +2 -0
- package/src/context/ChatDetailContext.tsx +1 -0
- package/src/core/notification/pushToken.ts +7 -7
- package/src/hooks/flows/useFlow.ts +14 -0
- package/src/hooks/query-keys.ts +10 -0
- package/src/hooks/resourceCenter/useThreadResources.ts +124 -0
- package/src/hooks/useImageAttachment.ts +1 -2
- package/src/hooks/useMediaViewer.ts +21 -7
- package/src/hooks/useSendAttachment.ts +4 -1
- package/src/index.tsx +2 -0
- package/src/screens/MediaView/index.tsx +117 -54
- package/src/screens/ResourceCenter/FileResourceRow.tsx +99 -0
- package/src/screens/ResourceCenter/GridRenderer.tsx +104 -0
- package/src/screens/ResourceCenter/GridThumbnail.tsx +98 -0
- package/src/screens/ResourceCenter/LinkResourceRow.tsx +96 -0
- package/src/screens/ResourceCenter/ListRenderer.tsx +81 -0
- package/src/screens/ResourceCenter/ResourceCenterTabBar.tsx +89 -0
- package/src/screens/ResourceCenter/ResourceCenterTabScene.tsx +142 -0
- package/src/screens/ResourceCenter/ResourceEmptyState.tsx +39 -0
- package/src/screens/ResourceCenter/index.tsx +129 -0
- package/src/screens/ResourceCenter/types.ts +56 -0
- package/src/screens/chat-detail/BotMenuSheet.tsx +94 -0
- package/src/screens/chat-detail/ChatComposer.tsx +100 -3
- package/src/screens/chat-detail/components/ChatMessageInput.tsx +11 -7
- package/src/services/attachmentOrchestrator.ts +15 -2
- package/src/services/endpoints.ts +2 -0
- package/src/services/resourceCenter.ts +137 -0
- package/src/translation/resources/i18n.ts +13 -0
- package/src/types/attachment.ts +3 -0
- package/src/types/chat.ts +75 -0
- package/src/utils/url.ts +11 -0
|
Binary file
|
|
@@ -4,7 +4,11 @@ import MediaViewerScreen from '../screens/MediaView';
|
|
|
4
4
|
import { useMediaViewer, type MediaItem } from '../hooks/useMediaViewer';
|
|
5
5
|
|
|
6
6
|
export interface MediaViewerModalHandle {
|
|
7
|
-
show: (
|
|
7
|
+
show: (
|
|
8
|
+
items: MediaItem[],
|
|
9
|
+
index?: number,
|
|
10
|
+
options?: { pagination?: boolean }
|
|
11
|
+
) => void;
|
|
8
12
|
}
|
|
9
13
|
|
|
10
14
|
interface MediaViewerModalProps {
|
|
@@ -15,7 +19,8 @@ export const MediaViewerModal = forwardRef<
|
|
|
15
19
|
MediaViewerModalHandle,
|
|
16
20
|
MediaViewerModalProps
|
|
17
21
|
>(({ onDismiss }, ref) => {
|
|
18
|
-
const { visible, mediaList, initIndex, show, hide } =
|
|
22
|
+
const { visible, mediaList, initIndex, pagination, show, hide } =
|
|
23
|
+
useMediaViewer();
|
|
19
24
|
|
|
20
25
|
// Expose show method to parent via ref
|
|
21
26
|
useImperativeHandle(
|
|
@@ -53,7 +58,7 @@ export const MediaViewerModal = forwardRef<
|
|
|
53
58
|
<MediaViewerScreen
|
|
54
59
|
images={images}
|
|
55
60
|
initIndex={initIndex}
|
|
56
|
-
pagination={images.length > 1}
|
|
61
|
+
pagination={pagination && images.length > 1}
|
|
57
62
|
onDismiss={handleDismiss}
|
|
58
63
|
download
|
|
59
64
|
/>
|
|
@@ -20,6 +20,7 @@ export interface ApiEndpoints {
|
|
|
20
20
|
stickerCategories: string;
|
|
21
21
|
stickersByCategory: string;
|
|
22
22
|
stickerHistory: string;
|
|
23
|
+
messagesResources: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const DEFAULT_ENDPOINTS: ApiEndpoints = {
|
|
@@ -31,6 +32,7 @@ const DEFAULT_ENDPOINTS: ApiEndpoints = {
|
|
|
31
32
|
stickerCategories: '/chat-service/v1/app/sticker-categories',
|
|
32
33
|
stickersByCategory: '/chat-service/v1/app/sticker-categories',
|
|
33
34
|
stickerHistory: '/chat-service/v1/app/stickers/history',
|
|
35
|
+
messagesResources: '/chat-service/v1/app/messages/resources',
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
let endpoints: ApiEndpoints = { ...DEFAULT_ENDPOINTS };
|
|
@@ -16,13 +16,13 @@ const uploadFcmToken = async (
|
|
|
16
16
|
project: DChatProject,
|
|
17
17
|
expireTime: number
|
|
18
18
|
): Promise<void> => {
|
|
19
|
-
console.log('[chat-mobile] uploadFcmToken', {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
19
|
+
// console.log('[chat-mobile] uploadFcmToken', {
|
|
20
|
+
// platformID,
|
|
21
|
+
// fcmToken,
|
|
22
|
+
// account: userID,
|
|
23
|
+
// project,
|
|
24
|
+
// expireTime,
|
|
25
|
+
// });
|
|
26
26
|
const token = useUserStore.getState().token;
|
|
27
27
|
if (!token) {
|
|
28
28
|
console.warn(
|
|
@@ -48,6 +48,20 @@ export const useResetFlow = () => {
|
|
|
48
48
|
});
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
export const useCloseSupportSession = () => {
|
|
52
|
+
const conversationId =
|
|
53
|
+
useConversationStore((state) => state.currentConversationId) || '';
|
|
54
|
+
return useMutation({
|
|
55
|
+
mutationKey: conversationQueryKeys.closeSession(conversationId),
|
|
56
|
+
mutationFn: async () => {
|
|
57
|
+
const res = await apiInstance?.post<BaseResponse<unknown>>(
|
|
58
|
+
ENDPOINTS.chatService.closeConversationSession(conversationId)
|
|
59
|
+
);
|
|
60
|
+
return res?.data?.data;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
51
65
|
export const useRunFlow = () => {
|
|
52
66
|
const conversationId =
|
|
53
67
|
useConversationStore((state) => state.currentConversationId) || '';
|
package/src/hooks/query-keys.ts
CHANGED
|
@@ -24,6 +24,10 @@ export const conversationQueryKeys = {
|
|
|
24
24
|
conversationId,
|
|
25
25
|
],
|
|
26
26
|
runFlow: (conversationId: string) => ['runConversationFlow', conversationId],
|
|
27
|
+
closeSession: (conversationId: string) => [
|
|
28
|
+
'closeConversationSession',
|
|
29
|
+
conversationId,
|
|
30
|
+
],
|
|
27
31
|
detailByBotId: (botId: string) => ['conversationByBotId', botId],
|
|
28
32
|
detailBySourceId: (sourceId: string) => ['detailBySourceId', sourceId],
|
|
29
33
|
joinedGroup: (groupId?: string) => [...conversationQueryKeys.all, groupId],
|
|
@@ -44,3 +48,9 @@ export const stickerQueryKeys = {
|
|
|
44
48
|
[...stickerQueryKeys.all, 'category', categoryId] as const,
|
|
45
49
|
recent: () => [...stickerQueryKeys.all, 'recent'] as const,
|
|
46
50
|
};
|
|
51
|
+
|
|
52
|
+
export const resourceCenterQueryKeys = {
|
|
53
|
+
all: ['threadResources'] as const,
|
|
54
|
+
byType: (recvID: string, contentType: number) =>
|
|
55
|
+
[...resourceCenterQueryKeys.all, recvID, contentType] as const,
|
|
56
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
3
|
+
import { resourceCenterQueryKeys } from '../query-keys';
|
|
4
|
+
import { useChatContext } from '../../context';
|
|
5
|
+
import { ResourceCenterService } from '../../services/resourceCenter';
|
|
6
|
+
import type {
|
|
7
|
+
DThreadResource,
|
|
8
|
+
DThreadResourceContentType,
|
|
9
|
+
} from '../../types/chat';
|
|
10
|
+
|
|
11
|
+
const PAGE_SIZE = 15;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* U5 resolved against a real staging response: revoked (recalled) messages ARE
|
|
15
|
+
* still returned by `/messages/resources` with `isRevoked: true` — BE does NOT
|
|
16
|
+
* pre-filter them. This client-side check is load-bearing, not defense-in-depth.
|
|
17
|
+
* `deleted`/`hidden` flags weren't observed in the tested rows — if BE adds them
|
|
18
|
+
* later, extend this predicate.
|
|
19
|
+
*/
|
|
20
|
+
const isVisibleResource = (item: DThreadResource): boolean => !item.isRevoked;
|
|
21
|
+
|
|
22
|
+
export const useThreadResources = (
|
|
23
|
+
recvID: string,
|
|
24
|
+
contentType: DThreadResourceContentType,
|
|
25
|
+
chatType: 'SINGLE' | 'GROUP'
|
|
26
|
+
) => {
|
|
27
|
+
const { applicationType } = useChatContext();
|
|
28
|
+
const queryClient = useQueryClient();
|
|
29
|
+
const queryKey = resourceCenterQueryKeys.byType(recvID, contentType);
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
data,
|
|
33
|
+
isLoading,
|
|
34
|
+
isError,
|
|
35
|
+
hasNextPage,
|
|
36
|
+
fetchNextPage,
|
|
37
|
+
isFetchingNextPage,
|
|
38
|
+
refetch,
|
|
39
|
+
} = useInfiniteQuery({
|
|
40
|
+
queryKey,
|
|
41
|
+
// `initialPageParam` is a react-query v5-only option — this library's
|
|
42
|
+
// `@tanstack/react-query` peer dep is unpinned (`"*"`), so a host app on v4
|
|
43
|
+
// (confirmed: biz-mobile pins 4.40.1) silently ignores this option and the
|
|
44
|
+
// first fetch's `pageParam` comes back `undefined`, not `1`. The `?? 1`
|
|
45
|
+
// fallback below is what actually makes page 1 work under v4 — do not remove
|
|
46
|
+
// it even though it looks redundant next to `initialPageParam`.
|
|
47
|
+
initialPageParam: 1,
|
|
48
|
+
queryFn: ({ pageParam }) =>
|
|
49
|
+
ResourceCenterService.fetchThreadResources({
|
|
50
|
+
recvID,
|
|
51
|
+
contentType,
|
|
52
|
+
chatType,
|
|
53
|
+
page: pageParam ?? 1,
|
|
54
|
+
pageSize: PAGE_SIZE,
|
|
55
|
+
applicationType,
|
|
56
|
+
}),
|
|
57
|
+
getNextPageParam: (lastPage) =>
|
|
58
|
+
lastPage.hasMore ? lastPage.page + 1 : undefined,
|
|
59
|
+
enabled: !!recvID && !!chatType,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// De-dupe by `id` across pages — offset pagination (`page`/`pageSize`) can hand
|
|
63
|
+
// back the same item twice if new messages land in the thread between page
|
|
64
|
+
// fetches (classic offset-pagination drift), which LegendList's `keyExtractor`
|
|
65
|
+
// requires be unique or it throws ("overlapping key" / missing items & gaps).
|
|
66
|
+
const items = useMemo(() => {
|
|
67
|
+
const seen = new Set<string>();
|
|
68
|
+
const deduped: DThreadResource[] = [];
|
|
69
|
+
for (const page of data?.pages ?? []) {
|
|
70
|
+
for (const item of page.items) {
|
|
71
|
+
if (seen.has(item.id)) continue;
|
|
72
|
+
seen.add(item.id);
|
|
73
|
+
if (isVisibleResource(item)) deduped.push(item);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return deduped;
|
|
77
|
+
}, [data]);
|
|
78
|
+
|
|
79
|
+
// Pull-to-refresh should (a) only re-fetch page 1, not every already-loaded
|
|
80
|
+
// page, and (b) keep the currently-shown items visible while that fetch is in
|
|
81
|
+
// flight — no flash to empty/loading. Neither `refetch()` nor `resetQueries()`
|
|
82
|
+
// give us both:
|
|
83
|
+
// - `refetch()` on an infinite query re-fetches ALL loaded pages (wasteful).
|
|
84
|
+
// - `resetQueries()` clears cached data FIRST, then fetches — causes exactly
|
|
85
|
+
// the empty-state flash we don't want.
|
|
86
|
+
// - `refetch({ refetchPage })` (page-1-only) is a react-query v4-only option,
|
|
87
|
+
// removed in v5 — another cross-version footgun like `initialPageParam`.
|
|
88
|
+
// So: fetch page 1 directly via the service, then splice it into the cache
|
|
89
|
+
// with `setQueryData` — `data`/`items` never go empty in between, and exactly
|
|
90
|
+
// one request is made regardless of how many pages were already loaded.
|
|
91
|
+
const [isRefreshingFirstPage, setIsRefreshingFirstPage] = useState(false);
|
|
92
|
+
|
|
93
|
+
const refreshFirstPage = useCallback(async () => {
|
|
94
|
+
setIsRefreshingFirstPage(true);
|
|
95
|
+
try {
|
|
96
|
+
const firstPage = await ResourceCenterService.fetchThreadResources({
|
|
97
|
+
recvID,
|
|
98
|
+
contentType,
|
|
99
|
+
chatType,
|
|
100
|
+
page: 1,
|
|
101
|
+
pageSize: PAGE_SIZE,
|
|
102
|
+
applicationType,
|
|
103
|
+
});
|
|
104
|
+
queryClient.setQueryData(queryKey, {
|
|
105
|
+
pages: [firstPage],
|
|
106
|
+
pageParams: [1],
|
|
107
|
+
});
|
|
108
|
+
} finally {
|
|
109
|
+
setIsRefreshingFirstPage(false);
|
|
110
|
+
}
|
|
111
|
+
}, [queryClient, queryKey, recvID, contentType, chatType, applicationType]);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
items,
|
|
115
|
+
isLoading,
|
|
116
|
+
isError,
|
|
117
|
+
hasMore: !!hasNextPage,
|
|
118
|
+
loadMore: fetchNextPage,
|
|
119
|
+
isFetchingNextPage,
|
|
120
|
+
isRefetching: isRefreshingFirstPage,
|
|
121
|
+
refetch,
|
|
122
|
+
refreshFirstPage,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
@@ -171,8 +171,7 @@ export const useImageAttachment = (): UseImageAttachmentReturn => {
|
|
|
171
171
|
const maxSize = isVideoFromPicker(item)
|
|
172
172
|
? UPLOAD_LIMITS.video
|
|
173
173
|
: UPLOAD_LIMITS.image;
|
|
174
|
-
|
|
175
|
-
console.log('item.size', item.size);
|
|
174
|
+
|
|
176
175
|
return (item.size || 0) <= maxSize;
|
|
177
176
|
};
|
|
178
177
|
|
|
@@ -5,11 +5,20 @@ export interface MediaItem {
|
|
|
5
5
|
isVideo?: boolean;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export interface ShowMediaViewerOptions {
|
|
9
|
+
pagination?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
8
12
|
export interface UseMediaViewerReturn {
|
|
9
13
|
visible: boolean;
|
|
10
14
|
mediaList: MediaItem[];
|
|
11
15
|
initIndex: number;
|
|
12
|
-
|
|
16
|
+
pagination: boolean;
|
|
17
|
+
show: (
|
|
18
|
+
items: MediaItem[],
|
|
19
|
+
index?: number,
|
|
20
|
+
options?: ShowMediaViewerOptions
|
|
21
|
+
) => void;
|
|
13
22
|
hide: () => void;
|
|
14
23
|
}
|
|
15
24
|
|
|
@@ -17,16 +26,21 @@ export function useMediaViewer(): UseMediaViewerReturn {
|
|
|
17
26
|
const [visible, setVisible] = useState(false);
|
|
18
27
|
const [mediaList, setMediaList] = useState<MediaItem[]>([]);
|
|
19
28
|
const [initIndex, setInitIndex] = useState(0);
|
|
29
|
+
const [pagination, setPagination] = useState(true);
|
|
20
30
|
|
|
21
|
-
const show = useCallback(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
const show = useCallback(
|
|
32
|
+
(items: MediaItem[], index = 0, options?: ShowMediaViewerOptions) => {
|
|
33
|
+
setMediaList(items);
|
|
34
|
+
setInitIndex(index);
|
|
35
|
+
setPagination(options?.pagination ?? true);
|
|
36
|
+
setVisible(true);
|
|
37
|
+
},
|
|
38
|
+
[]
|
|
39
|
+
);
|
|
26
40
|
|
|
27
41
|
const hide = useCallback(() => {
|
|
28
42
|
setVisible(false);
|
|
29
43
|
}, []);
|
|
30
44
|
|
|
31
|
-
return { visible, mediaList, initIndex, show, hide };
|
|
45
|
+
return { visible, mediaList, initIndex, pagination, show, hide };
|
|
32
46
|
}
|
|
@@ -2,6 +2,7 @@ import { useCallback, useState } from 'react';
|
|
|
2
2
|
import { AttachmentOrchestratorService } from '../services/attachmentOrchestrator';
|
|
3
3
|
import type { Attachment, AttachmentSendOptions } from '../types/attachment';
|
|
4
4
|
import { getMimeType } from '../utils/imageUtils';
|
|
5
|
+
import { useChatContext } from '../context';
|
|
5
6
|
|
|
6
7
|
export interface ImageItem {
|
|
7
8
|
type: 'image';
|
|
@@ -70,6 +71,7 @@ export interface UseSendAttachmentReturn {
|
|
|
70
71
|
export function useSendAttachment(): UseSendAttachmentReturn {
|
|
71
72
|
const [isLoading, setIsLoading] = useState(false);
|
|
72
73
|
const [error, setError] = useState<string | null>(null);
|
|
74
|
+
const applicationType = useChatContext().applicationType;
|
|
73
75
|
|
|
74
76
|
const send = useCallback(
|
|
75
77
|
async (
|
|
@@ -242,6 +244,7 @@ export function useSendAttachment(): UseSendAttachmentReturn {
|
|
|
242
244
|
uploadedUrls,
|
|
243
245
|
{
|
|
244
246
|
strategy: 'individual',
|
|
247
|
+
applicationType,
|
|
245
248
|
...options,
|
|
246
249
|
}
|
|
247
250
|
);
|
|
@@ -278,7 +281,7 @@ export function useSendAttachment(): UseSendAttachmentReturn {
|
|
|
278
281
|
console.log('[useSendAttachment] Finished');
|
|
279
282
|
}
|
|
280
283
|
},
|
|
281
|
-
[]
|
|
284
|
+
[applicationType]
|
|
282
285
|
);
|
|
283
286
|
|
|
284
287
|
const clearError = useCallback(() => setError(null), []);
|
package/src/index.tsx
CHANGED
|
@@ -28,6 +28,8 @@ export { AttachmentPreview } from './components/AttachmentPreview';
|
|
|
28
28
|
export { MediaViewerModal } from './components/MediaViewerModal';
|
|
29
29
|
export { Inbox } from './screens/inbox';
|
|
30
30
|
export { default as MediaViewerScreen } from './screens/MediaView';
|
|
31
|
+
export { default as ResourceCenterScreen } from './screens/ResourceCenter';
|
|
32
|
+
export type { ResourceCenterScreenProps } from './screens/ResourceCenter/types';
|
|
31
33
|
export * from './screens/chat-detail';
|
|
32
34
|
|
|
33
35
|
export type {
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
|
2
2
|
import type { ComponentType } from 'react';
|
|
3
3
|
import {
|
|
4
|
+
ActivityIndicator,
|
|
4
5
|
BackHandler,
|
|
5
6
|
Platform,
|
|
6
7
|
StyleSheet,
|
|
7
|
-
FlatList,
|
|
8
|
-
type ListRenderItemInfo,
|
|
9
8
|
type ViewToken,
|
|
10
9
|
StatusBar,
|
|
11
10
|
} from 'react-native';
|
|
11
|
+
import {
|
|
12
|
+
LegendList,
|
|
13
|
+
type LegendListRef,
|
|
14
|
+
type LegendListRenderItemProps,
|
|
15
|
+
} from '@legendapp/list/react-native';
|
|
12
16
|
|
|
13
17
|
import LinearGradient from 'react-native-linear-gradient';
|
|
14
18
|
import Animated, {
|
|
@@ -19,6 +23,9 @@ import Animated, {
|
|
|
19
23
|
} from 'react-native-reanimated';
|
|
20
24
|
import { Pagination } from 'react-native-snap-carousel';
|
|
21
25
|
import PhotoView from 'react-native-photo-view';
|
|
26
|
+
import RNFS from 'react-native-fs';
|
|
27
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
28
|
+
import { saveDocuments } from '@react-native-documents/picker';
|
|
22
29
|
import {
|
|
23
30
|
KColors,
|
|
24
31
|
KContainer,
|
|
@@ -29,6 +36,9 @@ import {
|
|
|
29
36
|
KSpacingValue,
|
|
30
37
|
} from '@droppii/libs';
|
|
31
38
|
import VideoPlayer from './VideoPlayer';
|
|
39
|
+
import { UIUtils } from '../../utils/UIUtils';
|
|
40
|
+
import { trans } from '../../translation';
|
|
41
|
+
import { getMimeType } from '../../utils/imageUtils';
|
|
32
42
|
|
|
33
43
|
interface MediaViewerParams {
|
|
34
44
|
images: any[];
|
|
@@ -49,15 +59,62 @@ const MediaViewerScreen = (props: MediaViewerParams) => {
|
|
|
49
59
|
onDismiss,
|
|
50
60
|
} = props;
|
|
51
61
|
|
|
52
|
-
const
|
|
62
|
+
const insets = useSafeAreaInsets();
|
|
63
|
+
const listRef = useRef<LegendListRef>(null);
|
|
53
64
|
const [index, setIndex] = useState(initIndex);
|
|
54
65
|
const [isScrolling, setIsScrolling] = useState(false);
|
|
66
|
+
const [isDownloading, setIsDownloading] = useState(false);
|
|
55
67
|
|
|
56
68
|
const dismiss = useCallback(() => {
|
|
57
69
|
onDismiss?.();
|
|
58
70
|
}, [onDismiss]);
|
|
59
71
|
|
|
60
|
-
|
|
72
|
+
// Downloads the currently-viewed image/video, mirroring `useFileDownload`'s
|
|
73
|
+
// RNFS-download + Save-As-document pattern (no gallery/CameraRoll dependency
|
|
74
|
+
// exists in this repo — Save As is the only working "download" mechanism here).
|
|
75
|
+
const onDownload = useCallback(async () => {
|
|
76
|
+
const item = images[index];
|
|
77
|
+
if (!item?.url || isDownloading) return;
|
|
78
|
+
|
|
79
|
+
setIsDownloading(true);
|
|
80
|
+
try {
|
|
81
|
+
const urlPath = item.url.split('?')[0];
|
|
82
|
+
const fallbackExt = item.isVideo ? 'mp4' : 'jpg';
|
|
83
|
+
const fileName =
|
|
84
|
+
urlPath.split('/').pop() ||
|
|
85
|
+
`${item.isVideo ? 'video' : 'image'}-${index}.${fallbackExt}`;
|
|
86
|
+
const destPath = `${RNFS.CachesDirectoryPath}/${fileName}`;
|
|
87
|
+
|
|
88
|
+
const exists = await RNFS.exists(destPath);
|
|
89
|
+
if (!exists) {
|
|
90
|
+
const result = await RNFS.downloadFile({
|
|
91
|
+
fromUrl: item.url,
|
|
92
|
+
toFile: destPath,
|
|
93
|
+
}).promise;
|
|
94
|
+
if (result.statusCode < 200 || result.statusCode >= 300) {
|
|
95
|
+
throw new Error(`HTTP ${result.statusCode}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await saveDocuments({
|
|
100
|
+
sourceUris: [`file://${destPath}`],
|
|
101
|
+
mimeType: getMimeType(fileName),
|
|
102
|
+
fileName,
|
|
103
|
+
copy: true,
|
|
104
|
+
});
|
|
105
|
+
UIUtils.toast.open({
|
|
106
|
+
title: trans('chat:file_downloaded_success', { fileName }),
|
|
107
|
+
theme: 'info',
|
|
108
|
+
});
|
|
109
|
+
} catch {
|
|
110
|
+
UIUtils.toast.open({
|
|
111
|
+
title: trans('chat:file_download_failed'),
|
|
112
|
+
theme: 'warning',
|
|
113
|
+
});
|
|
114
|
+
} finally {
|
|
115
|
+
setIsDownloading(false);
|
|
116
|
+
}
|
|
117
|
+
}, [images, index, isDownloading]);
|
|
61
118
|
|
|
62
119
|
useEffect(() => {
|
|
63
120
|
StatusBar.setHidden(true);
|
|
@@ -79,22 +136,6 @@ const MediaViewerScreen = (props: MediaViewerParams) => {
|
|
|
79
136
|
const onMomentumScrollBegin = useCallback(() => setIsScrolling(true), []);
|
|
80
137
|
const onMomentumScrollEnd = useCallback(() => setIsScrolling(false), []);
|
|
81
138
|
|
|
82
|
-
const onScrollToIndexFailed = useCallback(
|
|
83
|
-
(info: any) => {
|
|
84
|
-
const len = images.length;
|
|
85
|
-
const wait = new Promise((resolve) => setTimeout(() => resolve(true), 0));
|
|
86
|
-
wait.then(() => {
|
|
87
|
-
if (info.index > -1 && info.index < len) {
|
|
88
|
-
listRef.current?.scrollToIndex({
|
|
89
|
-
index: info.index,
|
|
90
|
-
animated: false,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
},
|
|
95
|
-
[images]
|
|
96
|
-
);
|
|
97
|
-
|
|
98
139
|
const onViewableItemsChanged = useRef(
|
|
99
140
|
({ viewableItems }: { viewableItems: ViewToken[] }) => {
|
|
100
141
|
const i = viewableItems[0]?.index;
|
|
@@ -105,7 +146,7 @@ const MediaViewerScreen = (props: MediaViewerParams) => {
|
|
|
105
146
|
const keyEx = useCallback((item: any, i: number) => `${item.url}-${i}`, []);
|
|
106
147
|
|
|
107
148
|
const renderItem = useCallback(
|
|
108
|
-
({ item, index: itemIndex }:
|
|
149
|
+
({ item, index: itemIndex }: LegendListRenderItemProps<any>) => (
|
|
109
150
|
<KContainer.Touchable center activeOpacity={1}>
|
|
110
151
|
<KContainer.VisibleView visible={!!item.isVideo}>
|
|
111
152
|
<VideoPlayer
|
|
@@ -130,9 +171,21 @@ const MediaViewerScreen = (props: MediaViewerParams) => {
|
|
|
130
171
|
);
|
|
131
172
|
|
|
132
173
|
const renderFooter = useMemo(() => {
|
|
133
|
-
|
|
174
|
+
// `pagination` only toggles the dot indicator (noisy on large sets like
|
|
175
|
+
// Resource Center grids) — the current-index counter should always show.
|
|
176
|
+
if (images.length <= 1) return null;
|
|
134
177
|
return (
|
|
135
|
-
<KContainer.View
|
|
178
|
+
<KContainer.View
|
|
179
|
+
style={[
|
|
180
|
+
styles.footer,
|
|
181
|
+
{
|
|
182
|
+
bottom:
|
|
183
|
+
insets.bottom > 0
|
|
184
|
+
? insets.bottom + KSpacingValue['0.5rem']
|
|
185
|
+
: KSpacingValue['2rem'],
|
|
186
|
+
},
|
|
187
|
+
]}
|
|
188
|
+
>
|
|
136
189
|
<KLabel.Text
|
|
137
190
|
typo="TextNmNormal"
|
|
138
191
|
color={KColors.white}
|
|
@@ -140,18 +193,20 @@ const MediaViewerScreen = (props: MediaViewerParams) => {
|
|
|
140
193
|
>
|
|
141
194
|
{index + 1}/{images.length}
|
|
142
195
|
</KLabel.Text>
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
196
|
+
<KContainer.VisibleView visible={pagination !== false}>
|
|
197
|
+
<Pagination
|
|
198
|
+
dotContainerStyle={styles.dotContainerStyle}
|
|
199
|
+
dotStyle={styles.dot}
|
|
200
|
+
inactiveDotStyle={styles.inactiveDot}
|
|
201
|
+
inactiveDotScale={0.6}
|
|
202
|
+
dotsLength={images.length}
|
|
203
|
+
activeDotIndex={index}
|
|
204
|
+
inactiveDotOpacity={0.5}
|
|
205
|
+
/>
|
|
206
|
+
</KContainer.VisibleView>
|
|
152
207
|
</KContainer.View>
|
|
153
208
|
);
|
|
154
|
-
}, [pagination, images.length, index]);
|
|
209
|
+
}, [pagination, images.length, index, insets.bottom]);
|
|
155
210
|
return (
|
|
156
211
|
<Animated.View
|
|
157
212
|
style={styles.wrapper}
|
|
@@ -166,7 +221,7 @@ const MediaViewerScreen = (props: MediaViewerParams) => {
|
|
|
166
221
|
{/* Top gradient overlay */}
|
|
167
222
|
<LinearGradient
|
|
168
223
|
colors={['rgba(0,0,0,0.9)', 'rgba(0,0,0,0)']}
|
|
169
|
-
style={[styles.topOverlay]}
|
|
224
|
+
style={[styles.topOverlay, { paddingTop: insets.top }]}
|
|
170
225
|
pointerEvents="box-none"
|
|
171
226
|
>
|
|
172
227
|
{/* Header first row — matches Figma: [close] [flex-1 spacer] [action-group] */}
|
|
@@ -184,22 +239,33 @@ const MediaViewerScreen = (props: MediaViewerParams) => {
|
|
|
184
239
|
<KContainer.View flex />
|
|
185
240
|
|
|
186
241
|
{/* Download button */}
|
|
187
|
-
{download
|
|
242
|
+
<KContainer.VisibleView visible={!!download}>
|
|
188
243
|
<KContainer.Touchable
|
|
189
244
|
style={styles.headerBtn}
|
|
190
|
-
onPress={onDownload}
|
|
245
|
+
onPress={isDownloading ? undefined : onDownload}
|
|
246
|
+
disabled={isDownloading}
|
|
191
247
|
>
|
|
192
|
-
<
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
248
|
+
<KContainer.VisibleView visible={isDownloading}>
|
|
249
|
+
<ActivityIndicator size="small" color={KColors.white} />
|
|
250
|
+
</KContainer.VisibleView>
|
|
251
|
+
<KContainer.VisibleView visible={!isDownloading}>
|
|
252
|
+
<KImage.VectorIcons
|
|
253
|
+
name="download-o"
|
|
254
|
+
size={24}
|
|
255
|
+
color={KColors.white}
|
|
256
|
+
/>
|
|
257
|
+
</KContainer.VisibleView>
|
|
197
258
|
</KContainer.Touchable>
|
|
198
|
-
|
|
259
|
+
</KContainer.VisibleView>
|
|
199
260
|
</KContainer.View>
|
|
200
261
|
</LinearGradient>
|
|
201
|
-
{/* Media content
|
|
202
|
-
|
|
262
|
+
{/* Media content. `recycleItems` intentionally NOT set — `VideoPlayer`
|
|
263
|
+
wraps a native player instance keyed off mount, not just the `url`
|
|
264
|
+
prop, so reusing a recycled cell across a video/image swap risks
|
|
265
|
+
stale playback state. Dataset here is always small (one message's
|
|
266
|
+
attachments or a Resource Center snapshot), so the perf win isn't
|
|
267
|
+
worth that risk. */}
|
|
268
|
+
<LegendList
|
|
203
269
|
ref={listRef}
|
|
204
270
|
initialScrollIndex={
|
|
205
271
|
initIndex >= images?.length ? images?.length - 1 : initIndex
|
|
@@ -207,17 +273,14 @@ const MediaViewerScreen = (props: MediaViewerParams) => {
|
|
|
207
273
|
data={images}
|
|
208
274
|
renderItem={renderItem}
|
|
209
275
|
keyExtractor={keyEx}
|
|
276
|
+
estimatedItemSize={KDims.width}
|
|
210
277
|
pagingEnabled
|
|
211
278
|
horizontal
|
|
212
279
|
showsHorizontalScrollIndicator={false}
|
|
213
280
|
onViewableItemsChanged={onViewableItemsChanged}
|
|
214
281
|
viewabilityConfig={{ itemVisiblePercentThreshold: 50 }}
|
|
215
|
-
onScrollToIndexFailed={onScrollToIndexFailed}
|
|
216
282
|
onMomentumScrollBegin={onMomentumScrollBegin}
|
|
217
283
|
onMomentumScrollEnd={onMomentumScrollEnd}
|
|
218
|
-
removeClippedSubviews
|
|
219
|
-
maxToRenderPerBatch={2}
|
|
220
|
-
windowSize={3}
|
|
221
284
|
style={styles.flex}
|
|
222
285
|
/>
|
|
223
286
|
|
|
@@ -244,13 +307,14 @@ const styles = StyleSheet.create({
|
|
|
244
307
|
height: KDims.height,
|
|
245
308
|
},
|
|
246
309
|
topOverlay: {
|
|
247
|
-
//
|
|
310
|
+
// `paddingTop` is overridden inline with `useSafeAreaInsets().top` (real
|
|
311
|
+
// notch/status-bar inset, both platforms).
|
|
312
|
+
position: 'absolute',
|
|
248
313
|
top: 0,
|
|
249
314
|
left: 0,
|
|
250
315
|
right: 0,
|
|
251
316
|
zIndex: 100,
|
|
252
317
|
minHeight: 100,
|
|
253
|
-
paddingTop: 30,
|
|
254
318
|
},
|
|
255
319
|
// Figma: Header-First-Row — flex row, padding 8, alignItems center
|
|
256
320
|
headerRow: {
|
|
@@ -303,11 +367,10 @@ const styles = StyleSheet.create({
|
|
|
303
367
|
letterSpacing: 0.12,
|
|
304
368
|
},
|
|
305
369
|
footer: {
|
|
370
|
+
// `bottom` is overridden inline with `useSafeAreaInsets()` (real device
|
|
371
|
+
// inset, both platforms) — `KDims.bottomSafeHeight` only covers iPhone
|
|
372
|
+
// notch/Dynamic Island and misses Android gesture-nav insets.
|
|
306
373
|
position: 'absolute',
|
|
307
|
-
bottom:
|
|
308
|
-
KDims.bottomSafeHeight > 0
|
|
309
|
-
? KDims.bottomSafeHeight + KSpacingValue['0.5rem']
|
|
310
|
-
: KSpacingValue['2rem'],
|
|
311
374
|
alignSelf: 'center',
|
|
312
375
|
alignItems: 'center',
|
|
313
376
|
zIndex: 10,
|