@onekeyfe/react-native-native-list 6.7.0-alpha.404 → 6.7.0-alpha.406
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/src/main/java/com/onekey/nativelist/NativeListActivityDetailsView.kt +7 -3
- package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +17 -1
- package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +15 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMarketRenderer.kt +92 -28
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +2 -0
- package/docs/ROW_TEMPLATES.md +6 -2
- package/docs/SPEC.md +97 -3
- package/ios/NativeListActivityDetailsView.swift +13 -4
- package/ios/NativeListMarketRenderer.swift +113 -18
- package/ios/RNCNativeListView.swift +15 -2
- package/ios/tests/NativeListActivityPresentationTests.swift +97 -0
- package/ios/tests/NativeListMarketMetricsTests.swift +117 -0
- package/lib/module/NativeList.js +25 -6
- package/lib/module/validation.js +74 -6
- package/lib/typescript/src/models.d.ts +3 -1
- package/package.json +3 -3
- package/src/NativeList.tsx +30 -10
- package/src/models.ts +14 -0
- package/src/validation.ts +143 -3
package/lib/module/validation.js
CHANGED
|
@@ -12,6 +12,7 @@ const MAX_HEADER_LENGTH = 4096;
|
|
|
12
12
|
const MAX_SPACER_HEIGHT = 512;
|
|
13
13
|
const MAX_SECTION_INDEX_TITLE_LENGTH = 8;
|
|
14
14
|
const MAX_MARKET_BADGES = 3;
|
|
15
|
+
const MARKET_METRIC_FIELDS = ['key', 'text', 'textSegments', 'secondaryText', 'secondaryTextSegments', 'weight', 'alignment', 'tone', 'secondaryTone'];
|
|
15
16
|
function isDevelopment() {
|
|
16
17
|
return typeof __DEV__ !== 'undefined' && __DEV__;
|
|
17
18
|
}
|
|
@@ -524,8 +525,56 @@ function assertWebMarketTable(table, path) {
|
|
|
524
525
|
}
|
|
525
526
|
});
|
|
526
527
|
}
|
|
528
|
+
function assertMarketMetrics(metrics, path) {
|
|
529
|
+
if (metrics === undefined) return;
|
|
530
|
+
if (!Array.isArray(metrics) || ![2, 3].includes(metrics.length)) fail(path, 'must contain 2 or 3 columns');
|
|
531
|
+
const keys = new Set();
|
|
532
|
+
metrics.forEach((column, index) => {
|
|
533
|
+
const columnPath = `${path}[${index}]`;
|
|
534
|
+
const value = protocolObject(column, columnPath);
|
|
535
|
+
protocolKey(value.key, `${columnPath}.key`);
|
|
536
|
+
if (keys.has(column.key)) fail(`${columnPath}.key`, 'must be unique');
|
|
537
|
+
keys.add(column.key);
|
|
538
|
+
if (typeof value.text !== 'string') fail(`${columnPath}.text`, 'must be a string');
|
|
539
|
+
for (const field of ['textSegments', 'secondaryTextSegments']) {
|
|
540
|
+
const segments = column[field];
|
|
541
|
+
if (segments !== undefined && !Array.isArray(segments)) fail(`${columnPath}.${field}`, 'must be an array');
|
|
542
|
+
segments?.forEach((segment, segmentIndex) => {
|
|
543
|
+
const segmentPath = `${columnPath}.${field}[${segmentIndex}]`;
|
|
544
|
+
protocolObject(segment, segmentPath);
|
|
545
|
+
if (typeof segment.text !== 'string') fail(`${segmentPath}.text`, 'must be a string');
|
|
546
|
+
if (segment.style !== undefined && segment.style !== 'subscript') fail(`${segmentPath}.style`, 'must be subscript');
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
if (value.weight !== undefined && ![1, 2, 3].includes(value.weight)) fail(`${columnPath}.weight`, 'must be 1, 2, or 3');
|
|
550
|
+
if (value.alignment !== undefined && !['start', 'center', 'end'].includes(value.alignment)) fail(`${columnPath}.alignment`, 'must be start, center, or end');
|
|
551
|
+
if (value.secondaryText !== undefined && typeof value.secondaryText !== 'string') fail(`${columnPath}.secondaryText`, 'must be a string');
|
|
552
|
+
assertTextTone(value.tone, `${columnPath}.tone`);
|
|
553
|
+
assertTextTone(value.secondaryTone, `${columnPath}.secondaryTone`);
|
|
554
|
+
});
|
|
555
|
+
}
|
|
527
556
|
function assertMarketRow(row, path) {
|
|
528
557
|
assertWebMarketTable(row.webTable, `${path}.webTable`);
|
|
558
|
+
assertMarketMetrics(row.metrics, `${path}.metrics`);
|
|
559
|
+
if (row.metrics !== undefined) {
|
|
560
|
+
row.metrics.forEach((column, index) => {
|
|
561
|
+
const columnPath = `${path}.metrics[${index}]`;
|
|
562
|
+
const value = protocolObject(column, columnPath);
|
|
563
|
+
assertUnknownKeys(value, MARKET_METRIC_FIELDS, columnPath, 'Market metric key');
|
|
564
|
+
assertKey(column.key, `${columnPath}.key`);
|
|
565
|
+
assertText(column.text, `${columnPath}.text`);
|
|
566
|
+
assertText(column.secondaryText, `${columnPath}.secondaryText`);
|
|
567
|
+
assertTextTone(column.tone, `${columnPath}.tone`);
|
|
568
|
+
assertTextTone(column.secondaryTone, `${columnPath}.secondaryTone`);
|
|
569
|
+
for (const field of ['textSegments', 'secondaryTextSegments']) {
|
|
570
|
+
column[field]?.forEach((segment, segmentIndex) => {
|
|
571
|
+
const segmentPath = `${columnPath}.${field}[${segmentIndex}]`;
|
|
572
|
+
assertText(segment.text, `${segmentPath}.text`);
|
|
573
|
+
if (segment.style !== undefined && segment.style !== 'subscript') fail(`${segmentPath}.style`, 'must be subscript');
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
}
|
|
529
578
|
if (!['token', 'stock', 'perp'].includes(row.variant)) {
|
|
530
579
|
fail(`${path}.variant`, 'must be token, stock, or perp');
|
|
531
580
|
}
|
|
@@ -1183,7 +1232,7 @@ const ROW_FIELDS = {
|
|
|
1183
1232
|
activity: [...COMMON_ROW_FIELDS, 'leading', 'hideLeading', 'secondaryLeading', 'title', 'description', 'status', 'primaryAmount', 'secondaryAmount', 'amounts', 'badges', 'presentation', 'fee', 'descriptionActionKey', 'footerActions'],
|
|
1184
1233
|
message: [...COMMON_ROW_FIELDS, 'presentation', 'unread', 'leading', 'title', 'body', 'bodyLines', 'time', 'thumbnail'],
|
|
1185
1234
|
dataRow: [...COMMON_ROW_FIELDS, 'leading', 'columns', 'trailing', 'checkbox', 'index', 'badge', 'badges', 'favorite', 'favoriteActive'],
|
|
1186
|
-
market: [...COMMON_ROW_FIELDS, 'variant', 'webTable', 'leadingAction', 'leading', 'title', 'subtitle', 'subtitlePrefix', 'subtitleSegments', 'price', 'priceSegments', 'change', 'badges', 'pressActionKey', 'pressInActionKey', 'longPressActionKey', 'diagnostics'],
|
|
1235
|
+
market: [...COMMON_ROW_FIELDS, 'variant', 'webTable', 'metrics', 'leadingAction', 'leading', 'title', 'subtitle', 'subtitlePrefix', 'subtitleSegments', 'price', 'priceSegments', 'change', 'badges', 'pressActionKey', 'pressInActionKey', 'longPressActionKey', 'diagnostics'],
|
|
1187
1236
|
mediaTile: [...COMMON_ROW_FIELDS, 'variant', 'media', 'image', 'imageState', 'networkImage', 'title', 'subtitle', 'badge', 'closeActionKey'],
|
|
1188
1237
|
metricCard: [...COMMON_ROW_FIELDS, 'variant', 'title', 'value', 'subtitle', 'trend', 'trendTone', 'visual', 'badge', 'metrics', 'progress'],
|
|
1189
1238
|
sectionHeader: [...COMMON_ROW_FIELDS, 'sticky', 'valueActionTestID', 'valueSegments', 'titleActionKey', 'titleActionOnHover', 'presentation', 'indexTitle', 'variant', 'title', 'leadingBadge', 'titleLoading', 'subtitle', 'value', 'valueHelp', 'valueActionKey', 'titleIcon', 'valueIcon', 'checkbox'],
|
|
@@ -1198,7 +1247,7 @@ const PATCH_FIELDS = {
|
|
|
1198
1247
|
activity: [...COMMON_PATCH_FIELDS, 'presentation', 'leading', 'title', 'badges', 'status', 'hideLeading', 'secondaryLeading', 'description', 'primaryAmount', 'secondaryAmount', 'amounts', 'fee', 'descriptionActionKey', 'footerActions'],
|
|
1199
1248
|
message: [...COMMON_PATCH_FIELDS, 'presentation', 'title', 'unread', 'body', 'bodyLines', 'time', 'thumbnail'],
|
|
1200
1249
|
dataRow: [...COMMON_PATCH_FIELDS, 'leading', 'badges', 'trailing', 'badge', 'columns', 'checkbox', 'index', 'favorite', 'favoriteActive'],
|
|
1201
|
-
market: [...COMMON_PATCH_FIELDS, 'longPressActionKey', 'subtitleSegments', 'leading', 'leadingAction', 'title', 'subtitle', 'badges', 'webTable', 'price', 'priceSegments', 'change', 'pressActionKey', 'pressInActionKey', 'diagnostics'],
|
|
1250
|
+
market: [...COMMON_PATCH_FIELDS, 'longPressActionKey', 'subtitleSegments', 'leading', 'leadingAction', 'title', 'subtitle', 'badges', 'webTable', 'metrics', 'price', 'priceSegments', 'change', 'pressActionKey', 'pressInActionKey', 'diagnostics'],
|
|
1202
1251
|
mediaTile: [...COMMON_PATCH_FIELDS, 'title', 'subtitle', 'badge', 'media', 'image', 'imageState', 'networkImage', 'closeActionKey'],
|
|
1203
1252
|
metricCard: [...COMMON_PATCH_FIELDS, 'title', 'subtitle', 'visual', 'badge', 'variant', 'value', 'trend', 'trendTone', 'metrics', 'progress'],
|
|
1204
1253
|
sectionHeader: [...COMMON_PATCH_FIELDS, 'titleActionKey', 'titleActionOnHover', 'title', 'titleIcon', 'subtitle', 'checkbox', 'variant', 'value', 'leadingBadge', 'titleLoading', 'valueHelp', 'valueActionKey', 'valueIcon'],
|
|
@@ -1326,6 +1375,7 @@ function assertRowProtocol(row, path) {
|
|
|
1326
1375
|
}
|
|
1327
1376
|
if (row.type === 'market') {
|
|
1328
1377
|
protocolObject(row.change, `${path}.change`);
|
|
1378
|
+
assertMarketMetrics(row.metrics, `${path}.metrics`);
|
|
1329
1379
|
if (row.webTable) {
|
|
1330
1380
|
const {
|
|
1331
1381
|
columns,
|
|
@@ -1392,7 +1442,7 @@ function isRecord(value) {
|
|
|
1392
1442
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1393
1443
|
}
|
|
1394
1444
|
function isAllowedImageUri(value) {
|
|
1395
|
-
return typeof value === 'string' && value.trim().length > 0 && (!/^[a-z][a-z\d+.-]*:/i.test(value) || /^(https?|file|content|ph|assets-library|asset|res|blob):/i.test(value) || /^data:image\//i.test(value));
|
|
1445
|
+
return typeof value === 'string' && value.trim().length > 0 && (!/^[a-z][a-z\d+.-]*:/i.test(value) || /^(https?|file|content|ph|assets-library|asset|res|blob|onekey-avatar):/i.test(value) || /^data:image\//i.test(value));
|
|
1396
1446
|
}
|
|
1397
1447
|
const IMAGE_FIELDS = ['uri', 'width', 'height', 'headers', 'contentFit', 'cachePolicy', 'autoplay', 'optimizeTos', 'overscan', 'loadingStrategy', 'fallbackUri', 'fallbackUris', 'retryTimes'];
|
|
1398
1448
|
const ICON_FIELDS = ['name', 'tintColor', 'backgroundColor', 'size', 'accessibilityLabel', 'hoverActionKey'];
|
|
@@ -1633,6 +1683,16 @@ function sanitizeDecoration(value, field) {
|
|
|
1633
1683
|
}
|
|
1634
1684
|
return next;
|
|
1635
1685
|
}
|
|
1686
|
+
function sanitizeMarketMetrics(value) {
|
|
1687
|
+
if (!Array.isArray(value)) return value;
|
|
1688
|
+
return value.map(metric => {
|
|
1689
|
+
const next = pickFields(metric, MARKET_METRIC_FIELDS);
|
|
1690
|
+
for (const field of ['textSegments', 'secondaryTextSegments']) {
|
|
1691
|
+
if (next[field] !== undefined) next[field] = sanitizeDecoration(next[field], field);
|
|
1692
|
+
}
|
|
1693
|
+
return next;
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1636
1696
|
function sanitizeRowForProduction(row) {
|
|
1637
1697
|
const object = protocolObject(row, 'row');
|
|
1638
1698
|
const fields = ROW_FIELDS[row.type];
|
|
@@ -1644,7 +1704,7 @@ function sanitizeRowForProduction(row) {
|
|
|
1644
1704
|
continue;
|
|
1645
1705
|
}
|
|
1646
1706
|
if (field === 'parent' && isRecord(value)) next[field] = sanitizeRowForProduction(value);else if (field === 'children' && Array.isArray(value)) next[field] = value.map(child => sanitizeRowForProduction(child));else {
|
|
1647
|
-
const normalized = sanitizeDecoration(value, field);
|
|
1707
|
+
const normalized = row.type === 'market' && field === 'metrics' ? sanitizeMarketMetrics(value) : sanitizeDecoration(value, field);
|
|
1648
1708
|
if (normalized === undefined) delete next[field];else next[field] = normalized;
|
|
1649
1709
|
}
|
|
1650
1710
|
}
|
|
@@ -1665,7 +1725,7 @@ function normalizeEmptyImages(value, field = '') {
|
|
|
1665
1725
|
const next = {};
|
|
1666
1726
|
for (const [key, item] of Object.entries(object)) {
|
|
1667
1727
|
const optional = object.type && !['key', 'type', ...(REQUIRED_ROW_FIELDS[object.type] ?? [])].includes(key);
|
|
1668
|
-
if (item === undefined || item === null && optional || ['priceSegments', 'subtitleSegments'].includes(key) && Array.isArray(item) && item.length === 0) {
|
|
1728
|
+
if (item === undefined || item === null && optional && !(object.type === 'market' && key === 'metrics') || ['priceSegments', 'subtitleSegments'].includes(key) && Array.isArray(item) && item.length === 0) {
|
|
1669
1729
|
changed = true;
|
|
1670
1730
|
continue;
|
|
1671
1731
|
}
|
|
@@ -1718,6 +1778,11 @@ export function validateSnapshot(snapshot) {
|
|
|
1718
1778
|
assertPlainSerializable(snapshot, 'snapshot', new Set(), false);
|
|
1719
1779
|
protocolObject(snapshot, 'snapshot');
|
|
1720
1780
|
const normalized = normalizeEmptyImages(snapshot);
|
|
1781
|
+
if (Array.isArray(normalized.rows)) {
|
|
1782
|
+
normalized.rows.forEach((row, index) => {
|
|
1783
|
+
if (row.type === 'market') assertMarketMetrics(row.metrics, `snapshot.rows[${index}].metrics`);
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1721
1786
|
const canonical = isDevelopment() ? normalized : normalizeSnapshotForProduction(normalized);
|
|
1722
1787
|
assertPlainSerializable(canonical, 'snapshot', new Set());
|
|
1723
1788
|
if (isDevelopment()) assertSnapshotContract(canonical);
|
|
@@ -2009,6 +2074,7 @@ function normalizePatch(patch, index) {
|
|
|
2009
2074
|
continue;
|
|
2010
2075
|
}
|
|
2011
2076
|
if (value === null) {
|
|
2077
|
+
if (patch.type === 'market' && field === 'metrics') fail(`${path}.changes.metrics`, 'use a full snapshot to remove metrics');
|
|
2012
2078
|
if (REQUIRED_ROW_FIELDS[patch.type].includes(field) || patch.type === 'system' && field === 'message') {
|
|
2013
2079
|
// The concrete system variant is checked after merging with its baseline.
|
|
2014
2080
|
if (patch.type !== 'system') fail(`${path}.changes.${field}`, 'required field cannot be cleared');
|
|
@@ -2016,6 +2082,7 @@ function normalizePatch(patch, index) {
|
|
|
2016
2082
|
changes[field] = null;
|
|
2017
2083
|
continue;
|
|
2018
2084
|
}
|
|
2085
|
+
if (patch.type === 'market' && field === 'metrics') assertMarketMetrics(value, `${path}.changes.metrics`);
|
|
2019
2086
|
if (['priceSegments', 'subtitleSegments'].includes(field) && Array.isArray(value) && value.length === 0) {
|
|
2020
2087
|
changes[field] = null;
|
|
2021
2088
|
continue;
|
|
@@ -2025,7 +2092,7 @@ function normalizePatch(patch, index) {
|
|
|
2025
2092
|
if (!REQUIRED_ROW_FIELDS[patch.type].includes(field)) changes[field] = null;
|
|
2026
2093
|
continue;
|
|
2027
2094
|
}
|
|
2028
|
-
const canonical = isDevelopment() ? normalized : sanitizeDecoration(normalized, field);
|
|
2095
|
+
const canonical = isDevelopment() ? normalized : patch.type === 'market' && field === 'metrics' ? sanitizeMarketMetrics(normalized) : sanitizeDecoration(normalized, field);
|
|
2029
2096
|
if (canonical !== undefined) changes[field] = canonical;else if (field === 'titleImage') changes[field] = null;
|
|
2030
2097
|
}
|
|
2031
2098
|
if (patch.type === 'market' && Array.isArray(changes.priceSegments) && changes.priceSegments.length > 0 && (typeof changes.price !== 'string' || !changes.price.trim())) fail(`${path}.changes.price`, 'is required when priceSegments is updated');
|
|
@@ -2040,6 +2107,7 @@ function normalizePatch(patch, index) {
|
|
|
2040
2107
|
}
|
|
2041
2108
|
}
|
|
2042
2109
|
const values = Object.fromEntries(Object.entries(changes).filter(([, value]) => value !== null));
|
|
2110
|
+
if (patch.type === 'market' && 'metrics' in changes) assertMarketMetrics(changes.metrics, `${path}.changes.metrics`);
|
|
2043
2111
|
assertPlainSerializable(values, `${path}.changes`, new Set());
|
|
2044
2112
|
assertProtocolFields(values, `${path}.changes`);
|
|
2045
2113
|
if (isDevelopment()) assertPatchChanges({
|
|
@@ -602,6 +602,8 @@ export type MarketRow = RowBase & Readonly<{
|
|
|
602
602
|
variant: 'token' | 'stock' | 'perp';
|
|
603
603
|
/** Web-only bounded table presentation; native reads the ordinary Market fields. */
|
|
604
604
|
webTable?: WebMarketTablePresentation;
|
|
605
|
+
/** Two or three ordered native text metrics; omitting them keeps the compact quote. */
|
|
606
|
+
metrics?: readonly Pick<DataColumn, 'key' | 'text' | 'textSegments' | 'secondaryText' | 'secondaryTextSegments' | 'weight' | 'alignment' | 'tone' | 'secondaryTone'>[];
|
|
605
607
|
leadingAction?: Extract<TrailingAccessory, {
|
|
606
608
|
kind: 'icon';
|
|
607
609
|
}>;
|
|
@@ -898,7 +900,7 @@ export type RowPatch = Readonly<{
|
|
|
898
900
|
}> | Readonly<{
|
|
899
901
|
type: 'market';
|
|
900
902
|
key: string;
|
|
901
|
-
changes: PatchClearableChanges<MarketRow, CommonPatchFields | 'leadingAction' | 'leading' | 'title' | 'subtitle' | 'subtitleSegments' | 'price' | 'priceSegments' | 'change' | 'badges' | 'pressActionKey' | 'pressInActionKey' | 'longPressActionKey' | 'diagnostics' | 'webTable' | 'style'>;
|
|
903
|
+
changes: PatchClearableChanges<MarketRow, CommonPatchFields | 'leadingAction' | 'leading' | 'title' | 'subtitle' | 'subtitleSegments' | 'price' | 'priceSegments' | 'change' | 'badges' | 'pressActionKey' | 'pressInActionKey' | 'longPressActionKey' | 'diagnostics' | 'webTable' | 'metrics' | 'style'>;
|
|
902
904
|
}> | Readonly<{
|
|
903
905
|
type: 'mediaTile';
|
|
904
906
|
key: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-native-list",
|
|
3
|
-
"version": "6.7.0-alpha.
|
|
3
|
+
"version": "6.7.0-alpha.406",
|
|
4
4
|
"description": "Template-driven native RecyclerView and UICollectionView for React Native",
|
|
5
5
|
"source": "./src/index.ts",
|
|
6
6
|
"main": "./lib/module/index.js",
|
|
@@ -86,8 +86,8 @@
|
|
|
86
86
|
"typescript": "^5.9.2"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
|
-
"@onekeyfe/react-native-image": "6.7.0-alpha.
|
|
90
|
-
"@onekeyfe/react-native-native-logger": "6.7.0-alpha.
|
|
89
|
+
"@onekeyfe/react-native-image": "6.7.0-alpha.406",
|
|
90
|
+
"@onekeyfe/react-native-native-logger": "6.7.0-alpha.406",
|
|
91
91
|
"react": "*",
|
|
92
92
|
"react-native": "*",
|
|
93
93
|
"react-native-nitro-modules": "0.37.0"
|
package/src/NativeList.tsx
CHANGED
|
@@ -149,6 +149,7 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
|
|
|
149
149
|
forwardedRef
|
|
150
150
|
) {
|
|
151
151
|
const snapshotRef = useRef<NativeListSnapshot>(emptyNativeListSnapshot());
|
|
152
|
+
const needsNativeSnapshotRef = useRef(false);
|
|
152
153
|
const renderedSnapshotRef = useRef<NativeListSnapshot | undefined>(
|
|
153
154
|
undefined
|
|
154
155
|
);
|
|
@@ -588,10 +589,11 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
|
|
|
588
589
|
return;
|
|
589
590
|
}
|
|
590
591
|
pendingDisclosureSnapshotRef.current = undefined;
|
|
591
|
-
const
|
|
592
|
-
|
|
592
|
+
const native = nativeRef.current;
|
|
593
|
+
if (native) native.applySnapshot(JSON.stringify(accepted));
|
|
593
594
|
snapshotRef.current = accepted;
|
|
594
|
-
|
|
595
|
+
needsNativeSnapshotRef.current = !native;
|
|
596
|
+
if (native && avatarLifecycle.current !== 'unmounted') {
|
|
595
597
|
avatarModelRef.current?.replaceSnapshot(accepted.rows);
|
|
596
598
|
updateAvatarPrefetchRef.current();
|
|
597
599
|
}
|
|
@@ -614,12 +616,15 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
|
|
|
614
616
|
return;
|
|
615
617
|
}
|
|
616
618
|
const native = nativeRef.current;
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
619
|
+
if (!native) {
|
|
620
|
+
snapshotRef.current = nextSnapshot;
|
|
621
|
+
needsNativeSnapshotRef.current = true;
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const dispatch = () => {
|
|
625
|
+
native.applyPatches(accepted.json);
|
|
626
|
+
snapshotRef.current = nextSnapshot;
|
|
627
|
+
};
|
|
623
628
|
if (avatarLifecycle.current === 'unmounted') {
|
|
624
629
|
dispatch?.();
|
|
625
630
|
return;
|
|
@@ -646,7 +651,10 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
|
|
|
646
651
|
};
|
|
647
652
|
if (pending)
|
|
648
653
|
pendingDisclosureSnapshotRef.current = { ...pending, owned: next };
|
|
649
|
-
else
|
|
654
|
+
else {
|
|
655
|
+
snapshotRef.current = next;
|
|
656
|
+
if (!nativeRef.current) needsNativeSnapshotRef.current = true;
|
|
657
|
+
}
|
|
650
658
|
}
|
|
651
659
|
},
|
|
652
660
|
scrollToKey(paramsOrKey, animated, alignment) {
|
|
@@ -712,7 +720,19 @@ export const NativeList = forwardRef<NativeListRef, NativeListProps>(
|
|
|
712
720
|
const nativeCallbacks = useMemo(
|
|
713
721
|
() => ({
|
|
714
722
|
hybridRef: callback((ref: NativeListMethods) => {
|
|
723
|
+
const previous = nativeRef.current;
|
|
715
724
|
nativeRef.current = ref;
|
|
725
|
+
if (
|
|
726
|
+
needsNativeSnapshotRef.current ||
|
|
727
|
+
(previous && previous !== ref)
|
|
728
|
+
) {
|
|
729
|
+
// The parent can publish values before Fabric delivers the host.
|
|
730
|
+
const latest = snapshotRef.current;
|
|
731
|
+
ref.applySnapshot(JSON.stringify(latest));
|
|
732
|
+
needsNativeSnapshotRef.current = false;
|
|
733
|
+
avatarModelRef.current?.replaceSnapshot(latest.rows);
|
|
734
|
+
updateAvatarPrefetchRef.current();
|
|
735
|
+
}
|
|
716
736
|
applyInitialScrollCallbackRef.current();
|
|
717
737
|
}),
|
|
718
738
|
onRowAction: callback((payloadJson: string) => {
|
package/src/models.ts
CHANGED
|
@@ -755,6 +755,19 @@ export type MarketRow = RowBase &
|
|
|
755
755
|
variant: 'token' | 'stock' | 'perp';
|
|
756
756
|
/** Web-only bounded table presentation; native reads the ordinary Market fields. */
|
|
757
757
|
webTable?: WebMarketTablePresentation;
|
|
758
|
+
/** Two or three ordered native text metrics; omitting them keeps the compact quote. */
|
|
759
|
+
metrics?: readonly Pick<
|
|
760
|
+
DataColumn,
|
|
761
|
+
| 'key'
|
|
762
|
+
| 'text'
|
|
763
|
+
| 'textSegments'
|
|
764
|
+
| 'secondaryText'
|
|
765
|
+
| 'secondaryTextSegments'
|
|
766
|
+
| 'weight'
|
|
767
|
+
| 'alignment'
|
|
768
|
+
| 'tone'
|
|
769
|
+
| 'secondaryTone'
|
|
770
|
+
>[];
|
|
758
771
|
leadingAction?: Extract<TrailingAccessory, { kind: 'icon' }>;
|
|
759
772
|
leading: LeadingVisual;
|
|
760
773
|
title: string;
|
|
@@ -1178,6 +1191,7 @@ export type RowPatch =
|
|
|
1178
1191
|
| 'longPressActionKey'
|
|
1179
1192
|
| 'diagnostics'
|
|
1180
1193
|
| 'webTable'
|
|
1194
|
+
| 'metrics'
|
|
1181
1195
|
| 'style'
|
|
1182
1196
|
>;
|
|
1183
1197
|
}>
|
package/src/validation.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
WebMarketTableCell,
|
|
20
20
|
WebMarketTablePresentation,
|
|
21
21
|
WebMarketTableText,
|
|
22
|
+
ValueTextSegment,
|
|
22
23
|
} from './models';
|
|
23
24
|
|
|
24
25
|
const MAX_KEY_LENGTH = 256;
|
|
@@ -32,6 +33,17 @@ const MAX_HEADER_LENGTH = 4096;
|
|
|
32
33
|
const MAX_SPACER_HEIGHT = 512;
|
|
33
34
|
const MAX_SECTION_INDEX_TITLE_LENGTH = 8;
|
|
34
35
|
const MAX_MARKET_BADGES = 3;
|
|
36
|
+
const MARKET_METRIC_FIELDS = [
|
|
37
|
+
'key',
|
|
38
|
+
'text',
|
|
39
|
+
'textSegments',
|
|
40
|
+
'secondaryText',
|
|
41
|
+
'secondaryTextSegments',
|
|
42
|
+
'weight',
|
|
43
|
+
'alignment',
|
|
44
|
+
'tone',
|
|
45
|
+
'secondaryTone',
|
|
46
|
+
] as const;
|
|
35
47
|
|
|
36
48
|
function isDevelopment(): boolean {
|
|
37
49
|
return typeof __DEV__ !== 'undefined' && __DEV__;
|
|
@@ -982,8 +994,88 @@ function assertWebMarketTable(
|
|
|
982
994
|
});
|
|
983
995
|
}
|
|
984
996
|
|
|
997
|
+
function assertMarketMetrics(
|
|
998
|
+
metrics: MarketRow['metrics'],
|
|
999
|
+
path: string
|
|
1000
|
+
): void {
|
|
1001
|
+
if (metrics === undefined) return;
|
|
1002
|
+
if (!Array.isArray(metrics) || ![2, 3].includes(metrics.length))
|
|
1003
|
+
fail(path, 'must contain 2 or 3 columns');
|
|
1004
|
+
const keys = new Set<string>();
|
|
1005
|
+
metrics.forEach(
|
|
1006
|
+
(column: NonNullable<MarketRow['metrics']>[number], index: number) => {
|
|
1007
|
+
const columnPath = `${path}[${index}]`;
|
|
1008
|
+
const value = protocolObject(column, columnPath);
|
|
1009
|
+
protocolKey(value.key, `${columnPath}.key`);
|
|
1010
|
+
if (keys.has(column.key)) fail(`${columnPath}.key`, 'must be unique');
|
|
1011
|
+
keys.add(column.key);
|
|
1012
|
+
if (typeof value.text !== 'string')
|
|
1013
|
+
fail(`${columnPath}.text`, 'must be a string');
|
|
1014
|
+
for (const field of ['textSegments', 'secondaryTextSegments'] as const) {
|
|
1015
|
+
const segments = column[field];
|
|
1016
|
+
if (segments !== undefined && !Array.isArray(segments))
|
|
1017
|
+
fail(`${columnPath}.${field}`, 'must be an array');
|
|
1018
|
+
segments?.forEach((segment: ValueTextSegment, segmentIndex: number) => {
|
|
1019
|
+
const segmentPath = `${columnPath}.${field}[${segmentIndex}]`;
|
|
1020
|
+
protocolObject(segment, segmentPath);
|
|
1021
|
+
if (typeof segment.text !== 'string')
|
|
1022
|
+
fail(`${segmentPath}.text`, 'must be a string');
|
|
1023
|
+
if (segment.style !== undefined && segment.style !== 'subscript')
|
|
1024
|
+
fail(`${segmentPath}.style`, 'must be subscript');
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
if (
|
|
1028
|
+
value.weight !== undefined &&
|
|
1029
|
+
![1, 2, 3].includes(value.weight as number)
|
|
1030
|
+
)
|
|
1031
|
+
fail(`${columnPath}.weight`, 'must be 1, 2, or 3');
|
|
1032
|
+
if (
|
|
1033
|
+
value.alignment !== undefined &&
|
|
1034
|
+
!['start', 'center', 'end'].includes(value.alignment as string)
|
|
1035
|
+
)
|
|
1036
|
+
fail(`${columnPath}.alignment`, 'must be start, center, or end');
|
|
1037
|
+
if (
|
|
1038
|
+
value.secondaryText !== undefined &&
|
|
1039
|
+
typeof value.secondaryText !== 'string'
|
|
1040
|
+
)
|
|
1041
|
+
fail(`${columnPath}.secondaryText`, 'must be a string');
|
|
1042
|
+
assertTextTone(value.tone as string | undefined, `${columnPath}.tone`);
|
|
1043
|
+
assertTextTone(
|
|
1044
|
+
value.secondaryTone as string | undefined,
|
|
1045
|
+
`${columnPath}.secondaryTone`
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
985
1051
|
function assertMarketRow(row: MarketRow, path: string): void {
|
|
986
1052
|
assertWebMarketTable(row.webTable, `${path}.webTable`);
|
|
1053
|
+
assertMarketMetrics(row.metrics, `${path}.metrics`);
|
|
1054
|
+
if (row.metrics !== undefined) {
|
|
1055
|
+
row.metrics.forEach((column, index) => {
|
|
1056
|
+
const columnPath = `${path}.metrics[${index}]`;
|
|
1057
|
+
const value = protocolObject(column, columnPath);
|
|
1058
|
+
assertUnknownKeys(
|
|
1059
|
+
value,
|
|
1060
|
+
MARKET_METRIC_FIELDS,
|
|
1061
|
+
columnPath,
|
|
1062
|
+
'Market metric key'
|
|
1063
|
+
);
|
|
1064
|
+
assertKey(column.key, `${columnPath}.key`);
|
|
1065
|
+
assertText(column.text, `${columnPath}.text`);
|
|
1066
|
+
assertText(column.secondaryText, `${columnPath}.secondaryText`);
|
|
1067
|
+
assertTextTone(column.tone, `${columnPath}.tone`);
|
|
1068
|
+
assertTextTone(column.secondaryTone, `${columnPath}.secondaryTone`);
|
|
1069
|
+
for (const field of ['textSegments', 'secondaryTextSegments'] as const) {
|
|
1070
|
+
column[field]?.forEach((segment, segmentIndex) => {
|
|
1071
|
+
const segmentPath = `${columnPath}.${field}[${segmentIndex}]`;
|
|
1072
|
+
assertText(segment.text, `${segmentPath}.text`);
|
|
1073
|
+
if (segment.style !== undefined && segment.style !== 'subscript')
|
|
1074
|
+
fail(`${segmentPath}.style`, 'must be subscript');
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
987
1079
|
if (!['token', 'stock', 'perp'].includes(row.variant)) {
|
|
988
1080
|
fail(`${path}.variant`, 'must be token, stock, or perp');
|
|
989
1081
|
}
|
|
@@ -2386,6 +2478,7 @@ const ROW_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
|
2386
2478
|
...COMMON_ROW_FIELDS,
|
|
2387
2479
|
'variant',
|
|
2388
2480
|
'webTable',
|
|
2481
|
+
'metrics',
|
|
2389
2482
|
'leadingAction',
|
|
2390
2483
|
'leading',
|
|
2391
2484
|
'title',
|
|
@@ -2557,6 +2650,7 @@ const PATCH_FIELDS: Readonly<Record<string, readonly string[]>> = {
|
|
|
2557
2650
|
'subtitle',
|
|
2558
2651
|
'badges',
|
|
2559
2652
|
'webTable',
|
|
2653
|
+
'metrics',
|
|
2560
2654
|
'price',
|
|
2561
2655
|
'priceSegments',
|
|
2562
2656
|
'change',
|
|
@@ -2942,6 +3036,7 @@ function assertRowProtocol(row: RowModel, path: string): void {
|
|
|
2942
3036
|
}
|
|
2943
3037
|
if (row.type === 'market') {
|
|
2944
3038
|
protocolObject(row.change, `${path}.change`);
|
|
3039
|
+
assertMarketMetrics(row.metrics, `${path}.metrics`);
|
|
2945
3040
|
if (row.webTable) {
|
|
2946
3041
|
const { columns, cells } = row.webTable;
|
|
2947
3042
|
if (
|
|
@@ -3083,7 +3178,9 @@ function isAllowedImageUri(value: unknown): value is string {
|
|
|
3083
3178
|
typeof value === 'string' &&
|
|
3084
3179
|
value.trim().length > 0 &&
|
|
3085
3180
|
(!/^[a-z][a-z\d+.-]*:/i.test(value) ||
|
|
3086
|
-
/^(https?|file|content|ph|assets-library|asset|res|blob):/i.test(
|
|
3181
|
+
/^(https?|file|content|ph|assets-library|asset|res|blob|onekey-avatar):/i.test(
|
|
3182
|
+
value
|
|
3183
|
+
) ||
|
|
3087
3184
|
/^data:image\//i.test(value))
|
|
3088
3185
|
);
|
|
3089
3186
|
}
|
|
@@ -3803,6 +3900,21 @@ function sanitizeDecoration(value: unknown, field: string): unknown {
|
|
|
3803
3900
|
return next;
|
|
3804
3901
|
}
|
|
3805
3902
|
|
|
3903
|
+
function sanitizeMarketMetrics(value: unknown): unknown {
|
|
3904
|
+
if (!Array.isArray(value)) return value;
|
|
3905
|
+
return value.map((metric) => {
|
|
3906
|
+
const next = pickFields(
|
|
3907
|
+
metric as Record<string, unknown>,
|
|
3908
|
+
MARKET_METRIC_FIELDS
|
|
3909
|
+
);
|
|
3910
|
+
for (const field of ['textSegments', 'secondaryTextSegments'] as const) {
|
|
3911
|
+
if (next[field] !== undefined)
|
|
3912
|
+
next[field] = sanitizeDecoration(next[field], field);
|
|
3913
|
+
}
|
|
3914
|
+
return next;
|
|
3915
|
+
});
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3806
3918
|
function sanitizeRowForProduction(row: RowModel): RowModel {
|
|
3807
3919
|
const object = protocolObject(row, 'row');
|
|
3808
3920
|
const fields = ROW_FIELDS[row.type];
|
|
@@ -3824,7 +3936,10 @@ function sanitizeRowForProduction(row: RowModel): RowModel {
|
|
|
3824
3936
|
sanitizeRowForProduction(child as RowModel)
|
|
3825
3937
|
);
|
|
3826
3938
|
else {
|
|
3827
|
-
const normalized =
|
|
3939
|
+
const normalized =
|
|
3940
|
+
row.type === 'market' && field === 'metrics'
|
|
3941
|
+
? sanitizeMarketMetrics(value)
|
|
3942
|
+
: sanitizeDecoration(value, field);
|
|
3828
3943
|
if (normalized === undefined) delete next[field];
|
|
3829
3944
|
else next[field] = normalized;
|
|
3830
3945
|
}
|
|
@@ -3869,7 +3984,9 @@ function normalizeEmptyImages(value: unknown, field = ''): unknown {
|
|
|
3869
3984
|
].includes(key);
|
|
3870
3985
|
if (
|
|
3871
3986
|
item === undefined ||
|
|
3872
|
-
(item === null &&
|
|
3987
|
+
(item === null &&
|
|
3988
|
+
optional &&
|
|
3989
|
+
!(object.type === 'market' && key === 'metrics')) ||
|
|
3873
3990
|
(['priceSegments', 'subtitleSegments'].includes(key) &&
|
|
3874
3991
|
Array.isArray(item) &&
|
|
3875
3992
|
item.length === 0)
|
|
@@ -3991,6 +4108,12 @@ export function validateSnapshot(
|
|
|
3991
4108
|
assertPlainSerializable(snapshot, 'snapshot', new Set<object>(), false);
|
|
3992
4109
|
protocolObject(snapshot, 'snapshot');
|
|
3993
4110
|
const normalized = normalizeEmptyImages(snapshot) as NativeListSnapshot;
|
|
4111
|
+
if (Array.isArray(normalized.rows)) {
|
|
4112
|
+
normalized.rows.forEach((row, index) => {
|
|
4113
|
+
if (row.type === 'market')
|
|
4114
|
+
assertMarketMetrics(row.metrics, `snapshot.rows[${index}].metrics`);
|
|
4115
|
+
});
|
|
4116
|
+
}
|
|
3994
4117
|
const canonical = isDevelopment()
|
|
3995
4118
|
? normalized
|
|
3996
4119
|
: normalizeSnapshotForProduction(normalized);
|
|
@@ -4421,6 +4544,11 @@ function normalizePatch(patch: RowPatch, index: number): RowPatch | undefined {
|
|
|
4421
4544
|
continue;
|
|
4422
4545
|
}
|
|
4423
4546
|
if (value === null) {
|
|
4547
|
+
if (patch.type === 'market' && field === 'metrics')
|
|
4548
|
+
fail(
|
|
4549
|
+
`${path}.changes.metrics`,
|
|
4550
|
+
'use a full snapshot to remove metrics'
|
|
4551
|
+
);
|
|
4424
4552
|
if (
|
|
4425
4553
|
REQUIRED_ROW_FIELDS[patch.type].includes(field) ||
|
|
4426
4554
|
(patch.type === 'system' && field === 'message')
|
|
@@ -4432,6 +4560,11 @@ function normalizePatch(patch: RowPatch, index: number): RowPatch | undefined {
|
|
|
4432
4560
|
changes[field] = null;
|
|
4433
4561
|
continue;
|
|
4434
4562
|
}
|
|
4563
|
+
if (patch.type === 'market' && field === 'metrics')
|
|
4564
|
+
assertMarketMetrics(
|
|
4565
|
+
value as MarketRow['metrics'],
|
|
4566
|
+
`${path}.changes.metrics`
|
|
4567
|
+
);
|
|
4435
4568
|
if (
|
|
4436
4569
|
['priceSegments', 'subtitleSegments'].includes(field) &&
|
|
4437
4570
|
Array.isArray(value) &&
|
|
@@ -4448,6 +4581,8 @@ function normalizePatch(patch: RowPatch, index: number): RowPatch | undefined {
|
|
|
4448
4581
|
}
|
|
4449
4582
|
const canonical = isDevelopment()
|
|
4450
4583
|
? normalized
|
|
4584
|
+
: patch.type === 'market' && field === 'metrics'
|
|
4585
|
+
? sanitizeMarketMetrics(normalized)
|
|
4451
4586
|
: sanitizeDecoration(normalized, field);
|
|
4452
4587
|
if (canonical !== undefined) changes[field] = canonical;
|
|
4453
4588
|
else if (field === 'titleImage') changes[field] = null;
|
|
@@ -4473,6 +4608,11 @@ function normalizePatch(patch: RowPatch, index: number): RowPatch | undefined {
|
|
|
4473
4608
|
const values = Object.fromEntries(
|
|
4474
4609
|
Object.entries(changes).filter(([, value]) => value !== null)
|
|
4475
4610
|
);
|
|
4611
|
+
if (patch.type === 'market' && 'metrics' in changes)
|
|
4612
|
+
assertMarketMetrics(
|
|
4613
|
+
changes.metrics as MarketRow['metrics'],
|
|
4614
|
+
`${path}.changes.metrics`
|
|
4615
|
+
);
|
|
4476
4616
|
assertPlainSerializable(values, `${path}.changes`, new Set<object>());
|
|
4477
4617
|
assertProtocolFields(values, `${path}.changes`);
|
|
4478
4618
|
if (isDevelopment())
|