@droppii-org/chat-mobile 0.2.51 → 0.2.52
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/core/notification/pushToken.js +7 -7
- package/lib/module/core/notification/pushToken.js.map +1 -1
- package/lib/module/hooks/query-keys.js +4 -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/services/attachmentOrchestrator.js +19 -4
- package/lib/module/services/attachmentOrchestrator.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 +8 -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/hooks/query-keys.d.ts +4 -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/services/attachmentOrchestrator.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 +58 -0
- 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/core/notification/pushToken.ts +7 -7
- package/src/hooks/query-keys.ts +6 -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/services/attachmentOrchestrator.ts +15 -2
- package/src/services/resourceCenter.ts +137 -0
- package/src/translation/resources/i18n.ts +7 -0
- package/src/types/attachment.ts +3 -0
- package/src/types/chat.ts +73 -0
- package/src/utils/url.ts +11 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { memo, useCallback } from 'react';
|
|
2
|
+
import type { ReactElement } from 'react';
|
|
3
|
+
import { ActivityIndicator, StyleSheet } from 'react-native';
|
|
4
|
+
import { LegendList } from '@legendapp/list/react-native';
|
|
5
|
+
import { KContainer, KColors, KSpacingValue } from '@droppii/libs';
|
|
6
|
+
import { FileResourceRow } from './FileResourceRow';
|
|
7
|
+
import { LinkResourceRow } from './LinkResourceRow';
|
|
8
|
+
import { DThreadResourceContentType } from '../../types/chat';
|
|
9
|
+
import type { DThreadResource } from '../../types/chat';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
items: DThreadResource[];
|
|
13
|
+
contentType: DThreadResourceContentType;
|
|
14
|
+
onEndReached: () => void;
|
|
15
|
+
isFetchingNextPage: boolean;
|
|
16
|
+
onPressUrl?: (url: string) => void;
|
|
17
|
+
onRefresh: () => void;
|
|
18
|
+
isRefetching: boolean;
|
|
19
|
+
emptyComponent: ReactElement;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ROW_ESTIMATE = 80;
|
|
23
|
+
|
|
24
|
+
export const ListRenderer = memo(
|
|
25
|
+
({
|
|
26
|
+
items,
|
|
27
|
+
contentType,
|
|
28
|
+
onEndReached,
|
|
29
|
+
isFetchingNextPage,
|
|
30
|
+
onPressUrl,
|
|
31
|
+
onRefresh,
|
|
32
|
+
isRefetching,
|
|
33
|
+
emptyComponent,
|
|
34
|
+
}: Props) => {
|
|
35
|
+
const isLink = contentType === DThreadResourceContentType.URL_TEXT;
|
|
36
|
+
|
|
37
|
+
const renderItem = useCallback(
|
|
38
|
+
({ item }: { item: DThreadResource }) =>
|
|
39
|
+
isLink ? (
|
|
40
|
+
<LinkResourceRow item={item} onPressUrl={onPressUrl} />
|
|
41
|
+
) : (
|
|
42
|
+
<FileResourceRow item={item} />
|
|
43
|
+
),
|
|
44
|
+
[isLink, onPressUrl]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const keyExtractor = useCallback((item: DThreadResource) => item.id, []);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<LegendList
|
|
51
|
+
data={items}
|
|
52
|
+
renderItem={renderItem}
|
|
53
|
+
keyExtractor={keyExtractor}
|
|
54
|
+
estimatedItemSize={ROW_ESTIMATE}
|
|
55
|
+
contentContainerStyle={styles.content}
|
|
56
|
+
onEndReached={onEndReached}
|
|
57
|
+
onEndReachedThreshold={0.5}
|
|
58
|
+
onRefresh={onRefresh}
|
|
59
|
+
refreshing={isRefetching}
|
|
60
|
+
ListEmptyComponent={emptyComponent}
|
|
61
|
+
ListFooterComponent={
|
|
62
|
+
<KContainer.VisibleView visible={isFetchingNextPage}>
|
|
63
|
+
<ActivityIndicator
|
|
64
|
+
style={styles.footer}
|
|
65
|
+
color={KColors.palette.primary.w400}
|
|
66
|
+
/>
|
|
67
|
+
</KContainer.VisibleView>
|
|
68
|
+
}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const styles = StyleSheet.create({
|
|
75
|
+
content: {
|
|
76
|
+
padding: KSpacingValue['0.75rem'],
|
|
77
|
+
},
|
|
78
|
+
footer: {
|
|
79
|
+
paddingVertical: KSpacingValue['1rem'],
|
|
80
|
+
},
|
|
81
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
TabBar,
|
|
5
|
+
type Route,
|
|
6
|
+
type TabBarProps,
|
|
7
|
+
type TabDescriptor,
|
|
8
|
+
} from 'react-native-tab-view';
|
|
9
|
+
import { KLabel, KColors, KSpacingValue } from '@droppii/libs';
|
|
10
|
+
|
|
11
|
+
// Stable module-level reference — must NOT be defined inside the component body
|
|
12
|
+
// (or inside the `useMemo` below), or React treats it as a new type every render.
|
|
13
|
+
const renderTabLabel: NonNullable<TabDescriptor<Route>['label']> = ({
|
|
14
|
+
labelText,
|
|
15
|
+
color,
|
|
16
|
+
}) => (
|
|
17
|
+
<KLabel.Text typo="TextNmBold" color={color}>
|
|
18
|
+
{labelText}
|
|
19
|
+
</KLabel.Text>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Uses `react-native-tab-view`'s own `TabBar` (animated sliding indicator,
|
|
23
|
+
// built-in accessibility/ripple) instead of a hand-rolled tab row — only the
|
|
24
|
+
// design tokens are custom (colors/indicator/label typography), matching the
|
|
25
|
+
// Figma underline-tab spec via the library's supported customization surface
|
|
26
|
+
// (`activeColor`/`inactiveColor`/`indicatorStyle`/per-route `label`).
|
|
27
|
+
//
|
|
28
|
+
// NOT wrapped in `React.memo`: `TabView` calls `renderTabBar(props)` as a plain
|
|
29
|
+
// function (not `<Comp/>` JSX — see `TabView.js`: `renderTabBar({...})`), and a
|
|
30
|
+
// `memo()`-wrapped component is a React element object, not a callable function.
|
|
31
|
+
// Passing the memoized object here throws `renderTabBar is not a function`.
|
|
32
|
+
export function ResourceCenterTabBar<T extends Route>(props: TabBarProps<T>) {
|
|
33
|
+
const options = useMemo<Record<string, TabDescriptor<T>>>(
|
|
34
|
+
() =>
|
|
35
|
+
Object.fromEntries(
|
|
36
|
+
props.navigationState.routes.map((route) => [
|
|
37
|
+
route.key,
|
|
38
|
+
{
|
|
39
|
+
labelText: (route as Route & { title?: string }).title,
|
|
40
|
+
label: renderTabLabel,
|
|
41
|
+
} satisfies TabDescriptor<T>,
|
|
42
|
+
])
|
|
43
|
+
),
|
|
44
|
+
[props.navigationState.routes]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<TabBar
|
|
49
|
+
{...props}
|
|
50
|
+
scrollEnabled
|
|
51
|
+
options={options}
|
|
52
|
+
activeColor={KColors.palette.primary.w400}
|
|
53
|
+
inactiveColor={KColors.palette.gray.w400}
|
|
54
|
+
pressColor="transparent"
|
|
55
|
+
style={styles.bar}
|
|
56
|
+
tabStyle={styles.tab}
|
|
57
|
+
indicatorStyle={styles.indicator}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const styles = StyleSheet.create({
|
|
63
|
+
// Figma `Tab-Bar` (14208:92918): solid white background + 1px bottom hairline
|
|
64
|
+
// (`gray.w25` #EFEFEF) — no shadow/elevation.
|
|
65
|
+
bar: {
|
|
66
|
+
backgroundColor: KColors.white,
|
|
67
|
+
borderBottomWidth: 1,
|
|
68
|
+
borderBottomColor: KColors.palette.gray.w25,
|
|
69
|
+
shadowOpacity: 0,
|
|
70
|
+
elevation: 0,
|
|
71
|
+
},
|
|
72
|
+
// `width: 'auto'` is REQUIRED alongside `scrollEnabled` above — without it,
|
|
73
|
+
// `TabBar`'s internal `getComputedTabWidth` falls back to `layout.width / 5 * 2`
|
|
74
|
+
// (40% of screen width per tab) before it has measured anything, rendering
|
|
75
|
+
// hugely oversized tabs. `'auto'` makes it use each tab's real measured label
|
|
76
|
+
// width instead (`tabWidths[route.key]`) — matches Figma's hug-content tabs
|
|
77
|
+
// (widths 53/65/73/81pt), fixed to each tab's own text, not stretched/guessed.
|
|
78
|
+
tab: {
|
|
79
|
+
width: 'auto',
|
|
80
|
+
paddingHorizontal: KSpacingValue['0.75rem'],
|
|
81
|
+
minHeight: 0,
|
|
82
|
+
},
|
|
83
|
+
// Figma: 3px bar, cornerRadius 4, primary blue.
|
|
84
|
+
indicator: {
|
|
85
|
+
backgroundColor: KColors.palette.primary.w400,
|
|
86
|
+
height: 3,
|
|
87
|
+
borderRadius: 4,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { memo, useCallback } from 'react';
|
|
2
|
+
import { ActivityIndicator, StyleSheet } from 'react-native';
|
|
3
|
+
import type { RefObject } from 'react';
|
|
4
|
+
import { KContainer, KLabel, KColors } from '@droppii/libs';
|
|
5
|
+
import { trans } from '../../translation';
|
|
6
|
+
import { useThreadResources } from '../../hooks/resourceCenter/useThreadResources';
|
|
7
|
+
import { ResourceEmptyState } from './ResourceEmptyState';
|
|
8
|
+
import { GridRenderer } from './GridRenderer';
|
|
9
|
+
import { ListRenderer } from './ListRenderer';
|
|
10
|
+
import { DThreadResourceContentType } from '../../types/chat';
|
|
11
|
+
import type { ResourceCenterLayout } from './types';
|
|
12
|
+
import type { MediaViewerModalHandle } from '../../components/MediaViewerModal';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
recvID: string;
|
|
16
|
+
chatType: 'SINGLE' | 'GROUP';
|
|
17
|
+
contentType: DThreadResourceContentType;
|
|
18
|
+
layout: ResourceCenterLayout;
|
|
19
|
+
emptyKey: string;
|
|
20
|
+
onPressUrl?: (url: string) => void;
|
|
21
|
+
mediaViewerRef: RefObject<MediaViewerModalHandle | null>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// One scene per tab — owns its own `useThreadResources` call so react-query
|
|
25
|
+
// caches independently per (recvID, contentType) and `lazy` TabView scenes
|
|
26
|
+
// don't all fetch on mount.
|
|
27
|
+
//
|
|
28
|
+
// Known limitation (deliberate, not an oversight): the shared `MediaViewerModal`
|
|
29
|
+
// is opened with a static snapshot of the currently-loaded page. If the user swipes
|
|
30
|
+
// to the last loaded item while more pages exist server-side, the viewer does not
|
|
31
|
+
// auto-fetch more — they need to close it, scroll the grid to trigger `loadMore`,
|
|
32
|
+
// then reopen. Wiring live pagination into the viewer would require either coupling
|
|
33
|
+
// the shared `MediaView` (used by in-chat bubbles too) to react-query state, or
|
|
34
|
+
// forking its ~230 lines of zoom/video/download logic into a separate component —
|
|
35
|
+
// not worth it for this ticket's scope.
|
|
36
|
+
export const ResourceCenterTabScene = memo(
|
|
37
|
+
({
|
|
38
|
+
recvID,
|
|
39
|
+
chatType,
|
|
40
|
+
contentType,
|
|
41
|
+
layout,
|
|
42
|
+
emptyKey,
|
|
43
|
+
onPressUrl,
|
|
44
|
+
mediaViewerRef,
|
|
45
|
+
}: Props) => {
|
|
46
|
+
const {
|
|
47
|
+
items,
|
|
48
|
+
isLoading,
|
|
49
|
+
isError,
|
|
50
|
+
loadMore,
|
|
51
|
+
isFetchingNextPage,
|
|
52
|
+
isRefetching,
|
|
53
|
+
refetch,
|
|
54
|
+
refreshFirstPage,
|
|
55
|
+
} = useThreadResources(recvID, contentType, chatType);
|
|
56
|
+
|
|
57
|
+
const onPressGridItem = useCallback(
|
|
58
|
+
(index: number) => {
|
|
59
|
+
const mediaItems = items.map((item) => ({
|
|
60
|
+
url: item.sourceUrl ?? '',
|
|
61
|
+
isVideo: contentType === DThreadResourceContentType.VIDEO,
|
|
62
|
+
}));
|
|
63
|
+
// Resource Center grids (Ảnh/Video tabs) can hold hundreds of items —
|
|
64
|
+
// the viewer's dot/counter pagination indicator is meant for small,
|
|
65
|
+
// in-chat message groups and becomes noise at this scale. Hide it here.
|
|
66
|
+
mediaViewerRef.current?.show(mediaItems, index, { pagination: false });
|
|
67
|
+
},
|
|
68
|
+
[items, contentType, mediaViewerRef]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (isLoading) {
|
|
72
|
+
return (
|
|
73
|
+
<KContainer.View style={styles.centerFill}>
|
|
74
|
+
<ActivityIndicator color={KColors.palette.primary.w400} />
|
|
75
|
+
</KContainer.View>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isError) {
|
|
80
|
+
return (
|
|
81
|
+
<KContainer.View style={styles.centerFill}>
|
|
82
|
+
<KLabel.Text
|
|
83
|
+
typo="TextSmNormal"
|
|
84
|
+
color={KColors.palette.gray.w400}
|
|
85
|
+
style={styles.errorText}
|
|
86
|
+
>
|
|
87
|
+
{trans('chat:generic_error')}
|
|
88
|
+
</KLabel.Text>
|
|
89
|
+
<KContainer.Touchable onPress={() => refetch()}>
|
|
90
|
+
<KLabel.Text typo="TextNmBold" color={KColors.palette.primary.w400}>
|
|
91
|
+
{trans('chat:retry')}
|
|
92
|
+
</KLabel.Text>
|
|
93
|
+
</KContainer.Touchable>
|
|
94
|
+
</KContainer.View>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// `ResourceEmptyState` is passed as `ListEmptyComponent` (not an early return)
|
|
99
|
+
// so the list itself — and its `RefreshControl` — stays mounted even with 0
|
|
100
|
+
// items, letting pull-to-refresh work on the empty state too.
|
|
101
|
+
const emptyComponent = <ResourceEmptyState emptyKey={emptyKey} />;
|
|
102
|
+
|
|
103
|
+
if (layout === 'grid') {
|
|
104
|
+
return (
|
|
105
|
+
<GridRenderer
|
|
106
|
+
items={items}
|
|
107
|
+
onEndReached={loadMore}
|
|
108
|
+
isFetchingNextPage={isFetchingNextPage}
|
|
109
|
+
onPressItem={onPressGridItem}
|
|
110
|
+
onRefresh={refreshFirstPage}
|
|
111
|
+
isRefetching={isRefetching}
|
|
112
|
+
emptyComponent={emptyComponent}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<ListRenderer
|
|
119
|
+
items={items}
|
|
120
|
+
contentType={contentType}
|
|
121
|
+
onEndReached={loadMore}
|
|
122
|
+
isFetchingNextPage={isFetchingNextPage}
|
|
123
|
+
onPressUrl={onPressUrl}
|
|
124
|
+
onRefresh={refreshFirstPage}
|
|
125
|
+
isRefetching={isRefetching}
|
|
126
|
+
emptyComponent={emptyComponent}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const styles = StyleSheet.create({
|
|
133
|
+
centerFill: {
|
|
134
|
+
flex: 1,
|
|
135
|
+
alignItems: 'center',
|
|
136
|
+
justifyContent: 'center',
|
|
137
|
+
gap: 8,
|
|
138
|
+
},
|
|
139
|
+
errorText: {
|
|
140
|
+
textAlign: 'center',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { memo } from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import { KContainer, KImage, KLabel, KColors } from '@droppii/libs';
|
|
4
|
+
import { trans } from '../../translation';
|
|
5
|
+
import Images from '../../assets/images';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
emptyKey: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ResourceEmptyState = memo(({ emptyKey }: Props) => (
|
|
12
|
+
<KContainer.View style={styles.wrapper}>
|
|
13
|
+
<KImage.Base
|
|
14
|
+
uri={Images.RESOURCE_CENTER_EMPTY_BOX}
|
|
15
|
+
width={120}
|
|
16
|
+
height={120}
|
|
17
|
+
/>
|
|
18
|
+
<KLabel.Text
|
|
19
|
+
typo="TextSmNormal"
|
|
20
|
+
color={KColors.palette.gray.w400}
|
|
21
|
+
style={styles.text}
|
|
22
|
+
>
|
|
23
|
+
{trans(emptyKey)}
|
|
24
|
+
</KLabel.Text>
|
|
25
|
+
</KContainer.View>
|
|
26
|
+
));
|
|
27
|
+
|
|
28
|
+
const styles = StyleSheet.create({
|
|
29
|
+
wrapper: {
|
|
30
|
+
flex: 1,
|
|
31
|
+
alignItems: 'center',
|
|
32
|
+
justifyContent: 'center',
|
|
33
|
+
paddingTop: 60,
|
|
34
|
+
gap: 12,
|
|
35
|
+
},
|
|
36
|
+
text: {
|
|
37
|
+
textAlign: 'center',
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import { TabView } from 'react-native-tab-view';
|
|
4
|
+
import { KNavigation, KContainer, KColors } from '@droppii/libs';
|
|
5
|
+
import { SessionType } from '@droppii/openim-rn-client-sdk';
|
|
6
|
+
import { trans } from '../../translation';
|
|
7
|
+
import { useConversation } from '../../store';
|
|
8
|
+
import {
|
|
9
|
+
MediaViewerModal,
|
|
10
|
+
type MediaViewerModalHandle,
|
|
11
|
+
} from '../../components/MediaViewerModal';
|
|
12
|
+
import { ResourceCenterTabBar } from './ResourceCenterTabBar';
|
|
13
|
+
import { ResourceCenterTabScene } from './ResourceCenterTabScene';
|
|
14
|
+
import { RESOURCE_TABS } from './types';
|
|
15
|
+
import type { ResourceCenterScreenProps, ResourceCenterLayout } from './types';
|
|
16
|
+
import type { DThreadResourceContentType } from '../../types/chat';
|
|
17
|
+
|
|
18
|
+
interface TabRoute {
|
|
19
|
+
key: string;
|
|
20
|
+
title: string;
|
|
21
|
+
contentType: DThreadResourceContentType;
|
|
22
|
+
layout: ResourceCenterLayout;
|
|
23
|
+
emptyKey: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ResourceCenterScreen = memo(
|
|
27
|
+
({
|
|
28
|
+
conversationId,
|
|
29
|
+
fallbackGroupID,
|
|
30
|
+
fallbackUserID,
|
|
31
|
+
initialTab,
|
|
32
|
+
onBack,
|
|
33
|
+
onPressUrl,
|
|
34
|
+
}: ResourceCenterScreenProps) => {
|
|
35
|
+
// Same derivation every host app previously duplicated: groupID-first
|
|
36
|
+
// (group/bot threads), userID fallback (1-1 threads), with route-param
|
|
37
|
+
// fallbacks for the window before `conversationId` lands in the store.
|
|
38
|
+
const conversation = useConversation(conversationId);
|
|
39
|
+
const groupID = conversation?.groupID || fallbackGroupID;
|
|
40
|
+
const recvID = groupID || conversation?.userID || fallbackUserID || '';
|
|
41
|
+
// `chatType` must reflect the conversation's actual session type, not just
|
|
42
|
+
// "did a groupID show up somewhere" — `conversationType` (SessionType.Group
|
|
43
|
+
// === 3, covers WorkingGroup too since it shares the same numeric value) is
|
|
44
|
+
// the authoritative signal once the conversation is loaded. Only fall back
|
|
45
|
+
// to the groupID-presence heuristic for the window before it lands in the
|
|
46
|
+
// store (conversation === undefined).
|
|
47
|
+
const isGroupConversation = conversation
|
|
48
|
+
? conversation?.conversationType === SessionType.Group
|
|
49
|
+
: !!groupID;
|
|
50
|
+
const chatType: 'SINGLE' | 'GROUP' = isGroupConversation
|
|
51
|
+
? 'GROUP'
|
|
52
|
+
: 'SINGLE';
|
|
53
|
+
|
|
54
|
+
const [routes] = useState<TabRoute[]>(() =>
|
|
55
|
+
RESOURCE_TABS.map((tab) => ({
|
|
56
|
+
key: String(tab.contentType),
|
|
57
|
+
title: trans(tab.labelKey),
|
|
58
|
+
contentType: tab.contentType,
|
|
59
|
+
layout: tab.layout,
|
|
60
|
+
emptyKey: tab.emptyKey,
|
|
61
|
+
}))
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const initialIndex = useMemo(() => {
|
|
65
|
+
const idx = routes.findIndex((r) => r.contentType === initialTab);
|
|
66
|
+
return idx >= 0 ? idx : 0;
|
|
67
|
+
}, [routes, initialTab]);
|
|
68
|
+
|
|
69
|
+
const [index, setIndex] = useState(initialIndex);
|
|
70
|
+
const mediaViewerRef = useRef<MediaViewerModalHandle>(null);
|
|
71
|
+
|
|
72
|
+
// `renderScene` is a render prop consumed by `TabView` — matching `Inbox.tsx`'s
|
|
73
|
+
// convention of `useCallback`-wrapping it so it doesn't get a new identity (and
|
|
74
|
+
// force unnecessary internal re-renders) on every parent render.
|
|
75
|
+
// `renderTabBar` needs no wrapper: `ResourceCenterTabBar` already implements
|
|
76
|
+
// `TabView`'s own `renderTabBar` prop contract directly (it wraps the library's
|
|
77
|
+
// `TabBar`), so its component reference — already stable — is passed as-is.
|
|
78
|
+
const renderScene = useCallback(
|
|
79
|
+
({ route }: { route: TabRoute }) => (
|
|
80
|
+
<ResourceCenterTabScene
|
|
81
|
+
recvID={recvID}
|
|
82
|
+
chatType={chatType}
|
|
83
|
+
contentType={route.contentType}
|
|
84
|
+
layout={route.layout}
|
|
85
|
+
emptyKey={route.emptyKey}
|
|
86
|
+
onPressUrl={onPressUrl}
|
|
87
|
+
mediaViewerRef={mediaViewerRef}
|
|
88
|
+
/>
|
|
89
|
+
),
|
|
90
|
+
[recvID, chatType, onPressUrl]
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<KContainer.View style={styles.flex}>
|
|
95
|
+
<KNavigation.Header
|
|
96
|
+
title={trans('chat:resource_center_title')}
|
|
97
|
+
alignment="left"
|
|
98
|
+
size="small"
|
|
99
|
+
theme="light"
|
|
100
|
+
leftButton={{
|
|
101
|
+
id: 'back',
|
|
102
|
+
icon: { vectorName: 'arrow-left-o' },
|
|
103
|
+
onPress: onBack,
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
<TabView<TabRoute>
|
|
107
|
+
navigationState={{ index, routes }}
|
|
108
|
+
onIndexChange={setIndex}
|
|
109
|
+
lazy
|
|
110
|
+
renderTabBar={ResourceCenterTabBar}
|
|
111
|
+
renderScene={renderScene}
|
|
112
|
+
/>
|
|
113
|
+
|
|
114
|
+
<MediaViewerModal ref={mediaViewerRef} />
|
|
115
|
+
</KContainer.View>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
ResourceCenterScreen.displayName = 'Screen.ResourceCenter';
|
|
121
|
+
|
|
122
|
+
export default ResourceCenterScreen;
|
|
123
|
+
|
|
124
|
+
const styles = StyleSheet.create({
|
|
125
|
+
flex: {
|
|
126
|
+
flex: 1,
|
|
127
|
+
backgroundColor: KColors.white,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DThreadResourceContentType } from '../../types/chat';
|
|
2
|
+
|
|
3
|
+
export interface ResourceCenterScreenProps {
|
|
4
|
+
conversationId: string;
|
|
5
|
+
/**
|
|
6
|
+
* Fallback groupID/userID for the race-condition window where `conversationId`
|
|
7
|
+
* isn't in the store yet (fresh deep-link / cold nav). Mirrors the route-param
|
|
8
|
+
* fallback pattern host apps already use elsewhere (e.g. `useSendMessage`).
|
|
9
|
+
*/
|
|
10
|
+
fallbackGroupID?: string;
|
|
11
|
+
fallbackUserID?: string;
|
|
12
|
+
initialTab?: DThreadResourceContentType;
|
|
13
|
+
onBack?: () => void;
|
|
14
|
+
/**
|
|
15
|
+
* Host-provided link-open handler — mirrors `ChatDetailProps.onPressUrl`. This library does
|
|
16
|
+
* NOT implement in-app-browser/deeplink/external-app logic itself (traced: `ChatDetail.tsx`
|
|
17
|
+
* only normalizes the URL and forwards to this same prop) — the host app supplies it.
|
|
18
|
+
*/
|
|
19
|
+
onPressUrl?: (url: string) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ResourceCenterLayout = 'grid' | 'list';
|
|
23
|
+
|
|
24
|
+
export interface ResourceTabConfig {
|
|
25
|
+
contentType: DThreadResourceContentType;
|
|
26
|
+
labelKey: string;
|
|
27
|
+
emptyKey: string;
|
|
28
|
+
layout: ResourceCenterLayout;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const RESOURCE_TABS: ResourceTabConfig[] = [
|
|
32
|
+
{
|
|
33
|
+
contentType: DThreadResourceContentType.IMAGE,
|
|
34
|
+
labelKey: 'chat:resource_center_tab_image',
|
|
35
|
+
emptyKey: 'chat:resource_center_empty_generic',
|
|
36
|
+
layout: 'grid',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
contentType: DThreadResourceContentType.VIDEO,
|
|
40
|
+
labelKey: 'chat:resource_center_tab_video',
|
|
41
|
+
emptyKey: 'chat:resource_center_empty_generic',
|
|
42
|
+
layout: 'grid',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
contentType: DThreadResourceContentType.FILE,
|
|
46
|
+
labelKey: 'chat:resource_center_tab_file',
|
|
47
|
+
emptyKey: 'chat:resource_center_empty_generic',
|
|
48
|
+
layout: 'list',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
contentType: DThreadResourceContentType.URL_TEXT,
|
|
52
|
+
labelKey: 'chat:resource_center_tab_link',
|
|
53
|
+
emptyKey: 'chat:resource_center_empty_link',
|
|
54
|
+
layout: 'list',
|
|
55
|
+
},
|
|
56
|
+
];
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
AttachmentSendResult,
|
|
12
12
|
} from '../types/attachment';
|
|
13
13
|
import { getAttachmentHandler } from '../types/attachmentHandler';
|
|
14
|
+
import type { DChatApplicationType, IMessageItemEx } from '../types/chat';
|
|
14
15
|
import { useConversationStore } from '../store';
|
|
15
16
|
import { useMessageStore } from '../store/message';
|
|
16
17
|
import {
|
|
@@ -97,6 +98,7 @@ export namespace AttachmentOrchestratorService {
|
|
|
97
98
|
try {
|
|
98
99
|
const mergedMsgId = await sendMergedMessage(
|
|
99
100
|
payloads.map((p) => p.payload),
|
|
101
|
+
options.applicationType,
|
|
100
102
|
options.includeCaption
|
|
101
103
|
);
|
|
102
104
|
console.log('[AttachmentOrchestrator] Messages sent via merger:', {
|
|
@@ -122,6 +124,7 @@ export namespace AttachmentOrchestratorService {
|
|
|
122
124
|
|
|
123
125
|
async function sendMergedMessage(
|
|
124
126
|
payloads: unknown[],
|
|
127
|
+
applicationType?: DChatApplicationType,
|
|
125
128
|
_caption?: string
|
|
126
129
|
): Promise<string> {
|
|
127
130
|
const currentConversation = useConversationStore
|
|
@@ -274,8 +277,18 @@ export namespace AttachmentOrchestratorService {
|
|
|
274
277
|
}
|
|
275
278
|
);
|
|
276
279
|
|
|
277
|
-
const
|
|
278
|
-
const
|
|
280
|
+
const { currentUserId, currentGroupId } = useConversationStore.getState();
|
|
281
|
+
const recvID = currentConversation.userID || currentUserId || '';
|
|
282
|
+
const groupID = currentConversation.groupID || currentGroupId || '';
|
|
283
|
+
|
|
284
|
+
// Attachment messages otherwise skip `ex`, unlike text messages
|
|
285
|
+
// (buildSendParams), leaving applicationType context missing.
|
|
286
|
+
if (applicationType) {
|
|
287
|
+
const extendMessageInfo: IMessageItemEx = { applicationType };
|
|
288
|
+
for (let i = 0; i < allMessages.length; i++) {
|
|
289
|
+
(allMessages[i] as MessageItem).ex = JSON.stringify(extendMessageInfo);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
279
292
|
|
|
280
293
|
// Push all messages as "Sending" immediately so the user sees every
|
|
281
294
|
// bubble right away, instead of waiting for each network call in turn.
|