@onekeyfe/react-native-native-list 3.0.157-alpha.253 → 3.0.157-alpha.255
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/android/build.gradle +7 -0
- package/android/src/main/java/com/onekey/nativelist/HybridNativeList.kt +20 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityDetailsView.kt +179 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +23 -2
- package/android/src/main/java/com/onekey/nativelist/NativeListContainerSlotAdapter.kt +53 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListEndAlignment.kt +5 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMediaPreviewSlot.kt +215 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMediaTileRenderer.kt +15 -6
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +3 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListScrollPositionTracker.kt +29 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +16 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +163 -26
- package/android/src/main/res/values/native_scroll_coordinator_ids.xml +4 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListEndAlignmentTest.kt +34 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListScrollPositionTrackerTest.kt +28 -0
- package/docs/DESIGN.md +4 -2
- package/docs/ROW_TEMPLATES.md +45 -0
- package/docs/SPEC.md +227 -1
- package/docs/STYLE_SPEC.md +8 -0
- package/ios/HybridNativeList.swift +30 -0
- package/ios/NativeListActivityDetailsView.swift +229 -0
- package/ios/NativeListActivityRenderer.swift +24 -2
- package/ios/NativeListMediaPreviewSlot.swift +199 -0
- package/ios/NativeListMediaTileRenderer.swift +10 -4
- package/ios/NativeListModels.swift +4 -1
- package/ios/NativeListScrollPositionTracker.swift +30 -0
- package/ios/NativeListSectionHeaderRenderer.swift +27 -1
- package/ios/RNCNativeListView.swift +159 -4
- package/ios/tests/NativeListActivityPresentationTests.swift +43 -0
- package/ios/tests/NativeListLifecycleTests.swift +213 -0
- package/ios/tests/NativeListMediaVisibilityTests.swift +42 -0
- package/ios/tests/NativeListScrollPositionTrackerTests.swift +25 -0
- package/lib/module/NativeList.js +56 -3
- package/lib/module/NativeList.web.js +5 -0
- package/lib/module/validation.js +64 -4
- package/lib/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
- package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +39 -3
- package/lib/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +6 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +15 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/Func_void_bool.kt +78 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +26 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListManager.kt +24 -9
- package/lib/nitrogen/generated/android/nativelistOnLoad.cpp +2 -0
- package/lib/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.cpp +8 -0
- package/lib/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.hpp +37 -0
- package/lib/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +23 -2
- package/lib/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +32 -0
- package/lib/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +3 -0
- package/lib/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +54 -0
- package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +6 -0
- package/lib/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +8 -2
- package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +6 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +11 -2
- package/lib/nitrogen/generated/shared/json/NativeListConfig.json +3 -0
- package/lib/typescript/src/NativeList.nitro.d.ts +3 -0
- package/lib/typescript/src/NativeList.types.d.ts +14 -1
- package/lib/typescript/src/models.d.ts +33 -5
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
- package/nitrogen/generated/android/c++/JHybridNativeListSpec.cpp +39 -3
- package/nitrogen/generated/android/c++/JHybridNativeListSpec.hpp +6 -0
- package/nitrogen/generated/android/c++/views/JHybridNativeListStateUpdater.cpp +15 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/Func_void_bool.kt +78 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/HybridNativeListSpec.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nativelist/views/HybridNativeListManager.kt +24 -9
- package/nitrogen/generated/android/nativelistOnLoad.cpp +2 -0
- package/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.cpp +8 -0
- package/nitrogen/generated/ios/NativeListModule-Swift-Cxx-Bridge.hpp +37 -0
- package/nitrogen/generated/ios/c++/HybridNativeListSpecSwift.hpp +23 -2
- package/nitrogen/generated/ios/c++/views/HybridNativeListComponent.mm +32 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridNativeListSpec.swift +3 -0
- package/nitrogen/generated/ios/swift/HybridNativeListSpec_cxx.swift +54 -0
- package/nitrogen/generated/shared/c++/HybridNativeListSpec.cpp +6 -0
- package/nitrogen/generated/shared/c++/HybridNativeListSpec.hpp +8 -2
- package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.cpp +6 -0
- package/nitrogen/generated/shared/c++/views/HybridNativeListComponent.hpp +11 -2
- package/nitrogen/generated/shared/json/NativeListConfig.json +3 -0
- package/package.json +6 -5
- package/react-native-native-list.podspec +6 -0
- package/scripts/enable-container-slots.mjs +58 -0
- package/scripts/enable-container-slots.test.mjs +58 -0
- package/src/NativeList.nitro.ts +3 -0
- package/src/NativeList.tsx +84 -2
- package/src/NativeList.types.ts +14 -1
- package/src/NativeList.web.tsx +5 -0
- package/src/models.ts +38 -1
- package/src/validation.ts +146 -4
package/src/NativeList.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import React, {
|
|
|
4
4
|
useImperativeHandle,
|
|
5
5
|
useMemo,
|
|
6
6
|
useRef,
|
|
7
|
+
useState,
|
|
7
8
|
} from 'react';
|
|
8
9
|
import {
|
|
9
10
|
OneKeyImageCache,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
NativeAvatarPrefetchModel,
|
|
14
15
|
NativeAvatarPrefetchQueue,
|
|
15
16
|
} from './avatarPrefetch';
|
|
17
|
+
import { View } from 'react-native';
|
|
16
18
|
import { callback, getHostComponent } from 'react-native-nitro-modules';
|
|
17
19
|
import type {
|
|
18
20
|
NativeListMethods,
|
|
@@ -96,6 +98,10 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
|
|
|
96
98
|
function NativeList(
|
|
97
99
|
{
|
|
98
100
|
snapshot,
|
|
101
|
+
listHeader,
|
|
102
|
+
listFooter,
|
|
103
|
+
listEmpty,
|
|
104
|
+
scrollPositionThresholds,
|
|
99
105
|
webVirtualizationEnabled: _webVirtualizationEnabled,
|
|
100
106
|
webSectionIndexContainerRef: _webSectionIndexContainerRef,
|
|
101
107
|
keyboardDismissMode = 'none',
|
|
@@ -106,6 +112,7 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
|
|
|
106
112
|
onReorder,
|
|
107
113
|
onEndReached,
|
|
108
114
|
onVisibleRangeChanged,
|
|
115
|
+
onScrollPositionThresholdChange,
|
|
109
116
|
onRefresh,
|
|
110
117
|
onScrollToIndexFailed,
|
|
111
118
|
initialScrollIndex,
|
|
@@ -116,6 +123,21 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
|
|
|
116
123
|
},
|
|
117
124
|
forwardedRef
|
|
118
125
|
) {
|
|
126
|
+
const [slotHeights, setSlotHeights] = useState([0, 0, 0]);
|
|
127
|
+
const containerSlotHeightsJson = JSON.stringify(slotHeights);
|
|
128
|
+
const slotInset =
|
|
129
|
+
snapshot.layout.contentPaddingHorizontal ??
|
|
130
|
+
snapshot.layout.contentPadding ??
|
|
131
|
+
0;
|
|
132
|
+
if (
|
|
133
|
+
snapshot.layout.orientation === 'horizontal' &&
|
|
134
|
+
(listHeader || listEmpty || listFooter)
|
|
135
|
+
) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
'NativeList container slots require vertical orientation'
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const slotContents = [listHeader, listEmpty, listFooter];
|
|
119
141
|
const normalizedKeyboardShouldPersistTaps =
|
|
120
142
|
keyboardShouldPersistTaps === true
|
|
121
143
|
? 'always'
|
|
@@ -130,6 +152,7 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
|
|
|
130
152
|
onReorder,
|
|
131
153
|
onEndReached,
|
|
132
154
|
onVisibleRangeChanged,
|
|
155
|
+
onScrollPositionThresholdChange,
|
|
133
156
|
onRefresh,
|
|
134
157
|
onScrollToIndexFailed,
|
|
135
158
|
});
|
|
@@ -140,9 +163,29 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
|
|
|
140
163
|
onReorder,
|
|
141
164
|
onEndReached,
|
|
142
165
|
onVisibleRangeChanged,
|
|
166
|
+
onScrollPositionThresholdChange,
|
|
143
167
|
onRefresh,
|
|
144
168
|
onScrollToIndexFailed,
|
|
145
169
|
};
|
|
170
|
+
const scrollPositionThresholdsJson = useMemo(() => {
|
|
171
|
+
if (
|
|
172
|
+
!scrollPositionThresholds ||
|
|
173
|
+
scrollPositionThresholds.enabled === false
|
|
174
|
+
)
|
|
175
|
+
return '';
|
|
176
|
+
const { start, end } = scrollPositionThresholds;
|
|
177
|
+
if (
|
|
178
|
+
!Number.isFinite(start) ||
|
|
179
|
+
!Number.isFinite(end) ||
|
|
180
|
+
start < 0 ||
|
|
181
|
+
end <= start
|
|
182
|
+
) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
'scrollPositionThresholds requires 0 <= start < end and finite values'
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return JSON.stringify({ start, end });
|
|
188
|
+
}, [scrollPositionThresholds]);
|
|
146
189
|
const snapshotJson = useMemo(() => serializeSnapshot(snapshot), [snapshot]);
|
|
147
190
|
const snapshotRef = useRef(snapshot);
|
|
148
191
|
const previousSnapshotJsonRef = useRef(snapshotJson);
|
|
@@ -391,8 +434,13 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
|
|
|
391
434
|
}),
|
|
392
435
|
onRowAction: callback((payloadJson: string) => {
|
|
393
436
|
const payload = parsePayload<RowActionEvent>(payloadJson);
|
|
394
|
-
if (
|
|
437
|
+
if (
|
|
438
|
+
payload.actionKey === 'nativeList.refresh' &&
|
|
439
|
+
payload.rowKey === undefined
|
|
440
|
+
) {
|
|
395
441
|
callbacksRef.current.onRefresh?.();
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
396
444
|
callbacksRef.current.onRowAction?.(payload);
|
|
397
445
|
}),
|
|
398
446
|
onActionAnchorInvalidated: callback((payloadJson: string) => {
|
|
@@ -427,6 +475,13 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
|
|
|
427
475
|
parsePayload<EndReachedEvent>(payloadJson)
|
|
428
476
|
);
|
|
429
477
|
}),
|
|
478
|
+
onScrollPositionThresholdChange: callback(
|
|
479
|
+
(isBeyondThreshold: boolean) => {
|
|
480
|
+
callbacksRef.current.onScrollPositionThresholdChange?.({
|
|
481
|
+
isBeyondThreshold,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
),
|
|
430
485
|
onVisibleRangeChanged: callback((payloadJson: string) => {
|
|
431
486
|
const payload = parsePayload<VisibleRangeChangedEvent>(payloadJson);
|
|
432
487
|
const previous = avatarRangeRef.current;
|
|
@@ -450,9 +505,36 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
|
|
|
450
505
|
{...viewProps}
|
|
451
506
|
{...nativeCallbacks}
|
|
452
507
|
snapshotJson={snapshotJson}
|
|
508
|
+
containerSlotHeightsJson={containerSlotHeightsJson}
|
|
509
|
+
scrollPositionThresholdsJson={scrollPositionThresholdsJson}
|
|
453
510
|
keyboardDismissMode={keyboardDismissMode}
|
|
454
511
|
keyboardShouldPersistTaps={normalizedKeyboardShouldPersistTaps}
|
|
455
|
-
|
|
512
|
+
>
|
|
513
|
+
{slotContents.map((content, index) => (
|
|
514
|
+
<View
|
|
515
|
+
key={index}
|
|
516
|
+
collapsable={false}
|
|
517
|
+
style={{
|
|
518
|
+
position: 'absolute',
|
|
519
|
+
left: slotInset,
|
|
520
|
+
right: slotInset,
|
|
521
|
+
top: 0,
|
|
522
|
+
}}
|
|
523
|
+
onLayout={(event) => {
|
|
524
|
+
const height = Math.ceil(event.nativeEvent.layout.height);
|
|
525
|
+
setSlotHeights((previous) =>
|
|
526
|
+
previous[index] === height
|
|
527
|
+
? previous
|
|
528
|
+
: previous.map((value, slot) =>
|
|
529
|
+
slot === index ? height : value
|
|
530
|
+
)
|
|
531
|
+
);
|
|
532
|
+
}}
|
|
533
|
+
>
|
|
534
|
+
{content}
|
|
535
|
+
</View>
|
|
536
|
+
))}
|
|
537
|
+
</NativeListHost>
|
|
456
538
|
);
|
|
457
539
|
}
|
|
458
540
|
);
|
package/src/NativeList.types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RefObject } from 'react';
|
|
1
|
+
import type { ReactNode, RefObject } from 'react';
|
|
2
2
|
import type { View, ViewProps } from 'react-native';
|
|
3
3
|
import type {
|
|
4
4
|
ActionAnchorInvalidatedEvent,
|
|
@@ -119,6 +119,10 @@ export type NativeListRef = Readonly<{
|
|
|
119
119
|
export type NativeListProps = Omit<ViewProps, 'children'> &
|
|
120
120
|
Readonly<{
|
|
121
121
|
snapshot: NativeListSnapshot;
|
|
122
|
+
/** Native-only, container-owned scroll content. Never rendered as a data row. */
|
|
123
|
+
listHeader?: ReactNode;
|
|
124
|
+
listFooter?: ReactNode;
|
|
125
|
+
listEmpty?: ReactNode;
|
|
122
126
|
/** Web only. Defaults to true and is ignored by the native host. */
|
|
123
127
|
webVirtualizationEnabled?: boolean;
|
|
124
128
|
/** Web only. Hosts the section index inside this container. */
|
|
@@ -136,6 +140,15 @@ export type NativeListProps = Omit<ViewProps, 'children'> &
|
|
|
136
140
|
onReorder?: (event: ReorderEvent) => void;
|
|
137
141
|
onEndReached?: (event: EndReachedEvent) => void;
|
|
138
142
|
onVisibleRangeChanged?: (event: VisibleRangeChangedEvent) => void;
|
|
143
|
+
/** Native only. Emits initial state and hysteresis transitions, not scroll frames. */
|
|
144
|
+
scrollPositionThresholds?: Readonly<{
|
|
145
|
+
start: number;
|
|
146
|
+
end: number;
|
|
147
|
+
enabled?: boolean;
|
|
148
|
+
}>;
|
|
149
|
+
onScrollPositionThresholdChange?: (
|
|
150
|
+
event: Readonly<{ isBeyondThreshold: boolean }>
|
|
151
|
+
) => void;
|
|
139
152
|
onRefresh?: () => void;
|
|
140
153
|
onScrollToIndexFailed?: (info: ScrollToIndexFailedInfo) => void;
|
|
141
154
|
}> &
|
package/src/NativeList.web.tsx
CHANGED
|
@@ -82,6 +82,11 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
|
|
|
82
82
|
function NativeList(
|
|
83
83
|
{
|
|
84
84
|
snapshot,
|
|
85
|
+
listHeader: _listHeader,
|
|
86
|
+
listEmpty: _listEmpty,
|
|
87
|
+
listFooter: _listFooter,
|
|
88
|
+
scrollPositionThresholds: _scrollPositionThresholds,
|
|
89
|
+
onScrollPositionThresholdChange: _onScrollPositionThresholdChange,
|
|
85
90
|
webVirtualizationEnabled = true,
|
|
86
91
|
webSectionIndexContainerRef,
|
|
87
92
|
keyboardDismissMode: _keyboardDismissMode,
|
package/src/models.ts
CHANGED
|
@@ -517,6 +517,16 @@ export type RailRow = RowBase &
|
|
|
517
517
|
style?: RailRowStyle;
|
|
518
518
|
}>;
|
|
519
519
|
|
|
520
|
+
export type ActivityAmount = Readonly<{
|
|
521
|
+
key: string;
|
|
522
|
+
text: string;
|
|
523
|
+
textSegments?: readonly ValueTextSegment[];
|
|
524
|
+
leading?: LeadingVisual;
|
|
525
|
+
tone?: TextTone;
|
|
526
|
+
secondaryText?: string;
|
|
527
|
+
secondaryTextSegments?: readonly ValueTextSegment[];
|
|
528
|
+
}>;
|
|
529
|
+
|
|
520
530
|
export type ActivityRow = RowBase &
|
|
521
531
|
Readonly<{
|
|
522
532
|
type: 'activity';
|
|
@@ -527,6 +537,18 @@ export type ActivityRow = RowBase &
|
|
|
527
537
|
status?: string;
|
|
528
538
|
primaryAmount?: string;
|
|
529
539
|
secondaryAmount?: string;
|
|
540
|
+
amounts?: readonly ActivityAmount[];
|
|
541
|
+
badges?: readonly BadgeModel[];
|
|
542
|
+
presentation?: 'stacked' | 'table';
|
|
543
|
+
fee?: Readonly<{
|
|
544
|
+
label?: string;
|
|
545
|
+
primary: string;
|
|
546
|
+
primaryTextSegments?: readonly ValueTextSegment[];
|
|
547
|
+
secondary?: string;
|
|
548
|
+
secondaryTextSegments?: readonly ValueTextSegment[];
|
|
549
|
+
hidden?: boolean;
|
|
550
|
+
}>;
|
|
551
|
+
descriptionActionKey?: string;
|
|
530
552
|
footerActions?: readonly FooterAction[];
|
|
531
553
|
style?: ActivityRowStyle;
|
|
532
554
|
}>;
|
|
@@ -601,6 +623,11 @@ export type MediaTileRow = RowBase &
|
|
|
601
623
|
Readonly<{
|
|
602
624
|
type: 'mediaTile';
|
|
603
625
|
variant: 'gallery' | 'browserPreview';
|
|
626
|
+
/** Native preview; video candidates display a silent first frame, never autoplay. */
|
|
627
|
+
media?: Readonly<{
|
|
628
|
+
source: ImageSource;
|
|
629
|
+
probeOrder: readonly ('image' | 'video')[];
|
|
630
|
+
}>;
|
|
604
631
|
image?: ImageSource;
|
|
605
632
|
imageState?: 'empty' | 'error';
|
|
606
633
|
networkImage?: ImageSource;
|
|
@@ -650,6 +677,7 @@ export type SectionHeaderRow = RowBase &
|
|
|
650
677
|
indexTitle?: string;
|
|
651
678
|
variant?: 'summary' | 'gallery' | 'history';
|
|
652
679
|
title: string;
|
|
680
|
+
titleLoading?: boolean;
|
|
653
681
|
subtitle?: string;
|
|
654
682
|
value?: string;
|
|
655
683
|
valueActionKey?: string;
|
|
@@ -795,7 +823,7 @@ export type NativeListSnapshot = Readonly<{
|
|
|
795
823
|
layout: Readonly<{
|
|
796
824
|
kind: NativeListLayout;
|
|
797
825
|
orientation?: NativeListOrientation;
|
|
798
|
-
gridColumns?: 2 | 3 | 4;
|
|
826
|
+
gridColumns?: 2 | 3 | 4 | 5 | 6 | 7;
|
|
799
827
|
stickyHeaders?: boolean;
|
|
800
828
|
contentPadding?: number;
|
|
801
829
|
contentPaddingHorizontal?: number;
|
|
@@ -813,6 +841,7 @@ export type NativeListSnapshot = Readonly<{
|
|
|
813
841
|
reorderable?: boolean;
|
|
814
842
|
pullToRefresh?: boolean;
|
|
815
843
|
refreshing?: boolean;
|
|
844
|
+
refreshTriggerDistance?: number;
|
|
816
845
|
loadMore?: boolean;
|
|
817
846
|
endReachedThreshold?: number;
|
|
818
847
|
sectionIndex?: SectionIndexConfig;
|
|
@@ -882,6 +911,11 @@ export type RowPatch =
|
|
|
882
911
|
| 'title'
|
|
883
912
|
| 'description'
|
|
884
913
|
| 'status'
|
|
914
|
+
| 'amounts'
|
|
915
|
+
| 'badges'
|
|
916
|
+
| 'presentation'
|
|
917
|
+
| 'fee'
|
|
918
|
+
| 'descriptionActionKey'
|
|
885
919
|
| 'primaryAmount'
|
|
886
920
|
| 'secondaryAmount'
|
|
887
921
|
| 'leading'
|
|
@@ -957,6 +991,7 @@ export type RowPatch =
|
|
|
957
991
|
| CommonPatchFields
|
|
958
992
|
| 'image'
|
|
959
993
|
| 'imageState'
|
|
994
|
+
| 'media'
|
|
960
995
|
| 'networkImage'
|
|
961
996
|
| 'title'
|
|
962
997
|
| 'subtitle'
|
|
@@ -993,6 +1028,7 @@ export type RowPatch =
|
|
|
993
1028
|
SectionHeaderRow,
|
|
994
1029
|
| CommonPatchFields
|
|
995
1030
|
| 'variant'
|
|
1031
|
+
| 'titleLoading'
|
|
996
1032
|
| 'title'
|
|
997
1033
|
| 'subtitle'
|
|
998
1034
|
| 'value'
|
|
@@ -1036,6 +1072,7 @@ export type NativeListActionSource =
|
|
|
1036
1072
|
| 'leadingAction'
|
|
1037
1073
|
| 'trailingAccessory'
|
|
1038
1074
|
| 'footerAction'
|
|
1075
|
+
| 'description'
|
|
1039
1076
|
| 'mediaClose';
|
|
1040
1077
|
|
|
1041
1078
|
export type NativeListWindowRect = Readonly<{
|
package/src/validation.ts
CHANGED
|
@@ -175,6 +175,11 @@ const TEXT_STYLE_KEYS_BY_ROW_TYPE: Readonly<
|
|
|
175
175
|
'title',
|
|
176
176
|
'description',
|
|
177
177
|
'status',
|
|
178
|
+
'amounts',
|
|
179
|
+
'badges',
|
|
180
|
+
'presentation',
|
|
181
|
+
'fee',
|
|
182
|
+
'descriptionActionKey',
|
|
178
183
|
'primaryAmount',
|
|
179
184
|
'secondaryAmount',
|
|
180
185
|
],
|
|
@@ -183,7 +188,7 @@ const TEXT_STYLE_KEYS_BY_ROW_TYPE: Readonly<
|
|
|
183
188
|
market: ['title', 'subtitle', 'price', 'change'],
|
|
184
189
|
mediaTile: ['title', 'subtitle', 'badge'],
|
|
185
190
|
metricCard: ['title', 'value', 'subtitle', 'trend'],
|
|
186
|
-
sectionHeader: ['title', 'subtitle', 'value'],
|
|
191
|
+
sectionHeader: ['titleLoading', 'title', 'subtitle', 'value'],
|
|
187
192
|
action: ['title', 'value'],
|
|
188
193
|
system: ['title', 'message', 'actionText'],
|
|
189
194
|
};
|
|
@@ -917,7 +922,84 @@ function assertVisual(row: RowModel, path: string): void {
|
|
|
917
922
|
});
|
|
918
923
|
}
|
|
919
924
|
|
|
920
|
-
function assertActivity(row: ActivityRow
|
|
925
|
+
function assertActivity(row: Partial<ActivityRow>, path: string): void {
|
|
926
|
+
assertText(row.title, `${path}.title`);
|
|
927
|
+
assertText(row.description, `${path}.description`);
|
|
928
|
+
if (
|
|
929
|
+
row.presentation !== undefined &&
|
|
930
|
+
!['stacked', 'table'].includes(row.presentation)
|
|
931
|
+
)
|
|
932
|
+
fail(`${path}.presentation`, 'must be stacked or table');
|
|
933
|
+
if (row.descriptionActionKey !== undefined)
|
|
934
|
+
assertKey(row.descriptionActionKey, `${path}.descriptionActionKey`);
|
|
935
|
+
if ((row.amounts?.length ?? 0) > 32)
|
|
936
|
+
fail(`${path}.amounts`, 'supports at most 32 amount lines');
|
|
937
|
+
const amountKeys = new Set<string>();
|
|
938
|
+
row.amounts?.forEach((amount, index) => {
|
|
939
|
+
const amountPath = `${path}.amounts[${index}]`;
|
|
940
|
+
assertKey(amount.key, `${amountPath}.key`);
|
|
941
|
+
if (amountKeys.has(amount.key)) fail(amountPath, 'duplicate amount key');
|
|
942
|
+
amountKeys.add(amount.key);
|
|
943
|
+
assertText(amount.text, `${amountPath}.text`);
|
|
944
|
+
assertLeadingVisual(amount.leading, `${amountPath}.leading`);
|
|
945
|
+
assertText(amount.secondaryText, `${amountPath}.secondaryText`);
|
|
946
|
+
if (
|
|
947
|
+
amount.tone !== undefined &&
|
|
948
|
+
!['primary', 'secondary', 'positive', 'negative'].includes(amount.tone)
|
|
949
|
+
)
|
|
950
|
+
fail(`${amountPath}.tone`, 'invalid text tone');
|
|
951
|
+
amount.textSegments?.forEach((segment, segmentIndex) => {
|
|
952
|
+
assertText(
|
|
953
|
+
segment.text,
|
|
954
|
+
`${amountPath}.textSegments[${segmentIndex}].text`
|
|
955
|
+
);
|
|
956
|
+
if (segment.style !== undefined && segment.style !== 'subscript')
|
|
957
|
+
fail(amountPath, 'invalid text segment style');
|
|
958
|
+
});
|
|
959
|
+
});
|
|
960
|
+
const assertActivitySegments = (
|
|
961
|
+
segments: readonly { text: string; style?: string }[] | undefined,
|
|
962
|
+
segmentPath: string
|
|
963
|
+
) =>
|
|
964
|
+
segments?.forEach((segment, index) => {
|
|
965
|
+
assertText(segment.text, `${segmentPath}[${index}].text`);
|
|
966
|
+
if (segment.style !== undefined && segment.style !== 'subscript')
|
|
967
|
+
fail(segmentPath, 'invalid text segment style');
|
|
968
|
+
});
|
|
969
|
+
row.amounts?.forEach((amount, index) =>
|
|
970
|
+
assertActivitySegments(
|
|
971
|
+
amount.secondaryTextSegments,
|
|
972
|
+
`${path}.amounts[${index}].secondaryTextSegments`
|
|
973
|
+
)
|
|
974
|
+
);
|
|
975
|
+
assertActivitySegments(
|
|
976
|
+
row.fee?.primaryTextSegments,
|
|
977
|
+
`${path}.fee.primaryTextSegments`
|
|
978
|
+
);
|
|
979
|
+
assertActivitySegments(
|
|
980
|
+
row.fee?.secondaryTextSegments,
|
|
981
|
+
`${path}.fee.secondaryTextSegments`
|
|
982
|
+
);
|
|
983
|
+
assertText(row.fee?.label, `${path}.fee.label`);
|
|
984
|
+
assertText(row.fee?.primary, `${path}.fee.primary`);
|
|
985
|
+
assertText(row.fee?.secondary, `${path}.fee.secondary`);
|
|
986
|
+
if ((row.badges?.length ?? 0) > 4)
|
|
987
|
+
fail(`${path}.badges`, 'supports at most 4 activity badges');
|
|
988
|
+
const badgeKeys = new Set<string>();
|
|
989
|
+
row.badges?.forEach((badge, index) => {
|
|
990
|
+
assertKey(badge.key, `${path}.badges[${index}].key`);
|
|
991
|
+
if (badgeKeys.has(badge.key)) fail(`${path}.badges`, 'duplicate badge key');
|
|
992
|
+
badgeKeys.add(badge.key);
|
|
993
|
+
assertText(badge.text, `${path}.badges[${index}].text`);
|
|
994
|
+
if (
|
|
995
|
+
badge.tone !== undefined &&
|
|
996
|
+
!['neutral', 'info', 'success', 'warning', 'danger'].includes(badge.tone)
|
|
997
|
+
)
|
|
998
|
+
fail(`${path}.badges`, 'invalid badge tone');
|
|
999
|
+
});
|
|
1000
|
+
if (row.fee?.hidden !== undefined && typeof row.fee.hidden !== 'boolean')
|
|
1001
|
+
fail(`${path}.fee.hidden`, 'must be boolean');
|
|
1002
|
+
|
|
921
1003
|
if ((row.footerActions?.length ?? 0) > MAX_FOOTER_ACTIONS) {
|
|
922
1004
|
fail(
|
|
923
1005
|
`${path}.footerActions`,
|
|
@@ -1119,6 +1201,7 @@ function assertRow(
|
|
|
1119
1201
|
assertMarketRow(row, path);
|
|
1120
1202
|
break;
|
|
1121
1203
|
case 'mediaTile':
|
|
1204
|
+
assertMediaPreview(row.media, `${path}.media`);
|
|
1122
1205
|
assertImage(row.image, `${path}.image`);
|
|
1123
1206
|
if (
|
|
1124
1207
|
row.imageState !== undefined &&
|
|
@@ -1126,10 +1209,14 @@ function assertRow(
|
|
|
1126
1209
|
) {
|
|
1127
1210
|
fail(`${path}.imageState`, 'must be empty or error when provided');
|
|
1128
1211
|
}
|
|
1129
|
-
if (
|
|
1212
|
+
if (
|
|
1213
|
+
row.imageState === undefined &&
|
|
1214
|
+
row.image === undefined &&
|
|
1215
|
+
row.media === undefined
|
|
1216
|
+
) {
|
|
1130
1217
|
fail(
|
|
1131
1218
|
`${path}.image`,
|
|
1132
|
-
'is required unless imageState is empty or error'
|
|
1219
|
+
'is required unless media is provided or imageState is empty or error'
|
|
1133
1220
|
);
|
|
1134
1221
|
}
|
|
1135
1222
|
assertImage(row.networkImage, `${path}.networkImage`);
|
|
@@ -1168,6 +1255,11 @@ function assertRow(
|
|
|
1168
1255
|
}
|
|
1169
1256
|
break;
|
|
1170
1257
|
case 'sectionHeader':
|
|
1258
|
+
if (
|
|
1259
|
+
row.titleLoading !== undefined &&
|
|
1260
|
+
typeof row.titleLoading !== 'boolean'
|
|
1261
|
+
)
|
|
1262
|
+
fail(`${path}.titleLoading`, 'must be boolean');
|
|
1171
1263
|
assertKey(row.sectionKey, `${path}.sectionKey`);
|
|
1172
1264
|
if (
|
|
1173
1265
|
row.presentation !== undefined &&
|
|
@@ -1292,14 +1384,57 @@ function assertGroups(rows: readonly RowModel[]): void {
|
|
|
1292
1384
|
}
|
|
1293
1385
|
}
|
|
1294
1386
|
|
|
1387
|
+
function assertMediaPreview(value: unknown, path: string) {
|
|
1388
|
+
if (value === undefined) return;
|
|
1389
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
1390
|
+
fail(path, 'must be a media descriptor');
|
|
1391
|
+
const media = value as {
|
|
1392
|
+
source?: import('./models').ImageSource;
|
|
1393
|
+
probeOrder?: unknown;
|
|
1394
|
+
};
|
|
1395
|
+
assertImage(media.source, `${path}.source`);
|
|
1396
|
+
if (!media.source) fail(`${path}.source`, 'is required');
|
|
1397
|
+
const order = media.probeOrder;
|
|
1398
|
+
if (
|
|
1399
|
+
!Array.isArray(order) ||
|
|
1400
|
+
order.length < 1 ||
|
|
1401
|
+
order.length > 2 ||
|
|
1402
|
+
order.some((kind) => kind !== 'image' && kind !== 'video') ||
|
|
1403
|
+
new Set(order).size !== order.length
|
|
1404
|
+
) {
|
|
1405
|
+
fail(
|
|
1406
|
+
`${path}.probeOrder`,
|
|
1407
|
+
'must contain one or two distinct image/video candidates'
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1295
1412
|
export function validateSnapshot(
|
|
1296
1413
|
snapshot: NativeListSnapshot
|
|
1297
1414
|
): NativeListSnapshot {
|
|
1415
|
+
if (
|
|
1416
|
+
snapshot.capabilities?.refreshTriggerDistance !== undefined &&
|
|
1417
|
+
(!Number.isFinite(snapshot.capabilities.refreshTriggerDistance) ||
|
|
1418
|
+
snapshot.capabilities.refreshTriggerDistance <= 0)
|
|
1419
|
+
)
|
|
1420
|
+
fail(
|
|
1421
|
+
'capabilities.refreshTriggerDistance',
|
|
1422
|
+
'must be a positive finite number'
|
|
1423
|
+
);
|
|
1424
|
+
|
|
1298
1425
|
assertPlainSerializable(snapshot, 'snapshot', new Set<object>());
|
|
1299
1426
|
if (snapshot.schemaVersion !== 1) fail('snapshot.schemaVersion', 'must be 1');
|
|
1300
1427
|
if (!Number.isSafeInteger(snapshot.generation) || snapshot.generation < 0) {
|
|
1301
1428
|
fail('snapshot.generation', 'must be a non-negative safe integer');
|
|
1302
1429
|
}
|
|
1430
|
+
if (
|
|
1431
|
+
snapshot.layout.gridColumns !== undefined &&
|
|
1432
|
+
(!Number.isInteger(snapshot.layout.gridColumns) ||
|
|
1433
|
+
snapshot.layout.gridColumns < 2 ||
|
|
1434
|
+
snapshot.layout.gridColumns > 7)
|
|
1435
|
+
) {
|
|
1436
|
+
fail('snapshot.layout.gridColumns', 'must be an integer from 2 to 7');
|
|
1437
|
+
}
|
|
1303
1438
|
if (snapshot.layout.kind === 'grid' && !snapshot.layout.gridColumns) {
|
|
1304
1439
|
fail('snapshot.layout.gridColumns', 'is required for grid layout');
|
|
1305
1440
|
}
|
|
@@ -1508,6 +1643,7 @@ function assertPatchChanges(patch: RowPatch, index: number): void {
|
|
|
1508
1643
|
assertLeadingVisual(patch.changes.visual, `${path}.visual`);
|
|
1509
1644
|
break;
|
|
1510
1645
|
case 'activity':
|
|
1646
|
+
assertActivity(patch.changes, path);
|
|
1511
1647
|
assertText(patch.changes.title, `${path}.title`);
|
|
1512
1648
|
assertText(patch.changes.description, `${path}.description`);
|
|
1513
1649
|
assertText(patch.changes.status, `${path}.status`);
|
|
@@ -1576,6 +1712,7 @@ function assertPatchChanges(patch: RowPatch, index: number): void {
|
|
|
1576
1712
|
);
|
|
1577
1713
|
break;
|
|
1578
1714
|
case 'mediaTile':
|
|
1715
|
+
assertMediaPreview(patch.changes.media, `${path}.media`);
|
|
1579
1716
|
assertImage(patch.changes.image, `${path}.image`);
|
|
1580
1717
|
if (
|
|
1581
1718
|
patch.changes.imageState !== undefined &&
|
|
@@ -1623,6 +1760,11 @@ function assertPatchChanges(patch: RowPatch, index: number): void {
|
|
|
1623
1760
|
}
|
|
1624
1761
|
break;
|
|
1625
1762
|
case 'sectionHeader':
|
|
1763
|
+
if (
|
|
1764
|
+
patch.changes.titleLoading !== undefined &&
|
|
1765
|
+
typeof patch.changes.titleLoading !== 'boolean'
|
|
1766
|
+
)
|
|
1767
|
+
fail(`${path}.titleLoading`, 'must be boolean');
|
|
1626
1768
|
assertSectionHeaderVariant(patch.changes.variant, `${path}.variant`);
|
|
1627
1769
|
assertText(patch.changes.title, `${path}.title`);
|
|
1628
1770
|
assertText(patch.changes.subtitle, `${path}.subtitle`);
|