@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.
@@ -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.404",
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.404",
90
- "@onekeyfe/react-native-native-logger": "6.7.0-alpha.404",
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"
@@ -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 nextSnapshotJson = JSON.stringify(accepted);
592
- nativeRef.current?.applySnapshot(nextSnapshotJson);
592
+ const native = nativeRef.current;
593
+ if (native) native.applySnapshot(JSON.stringify(accepted));
593
594
  snapshotRef.current = accepted;
594
- if (nativeRef.current && avatarLifecycle.current !== 'unmounted') {
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
- const dispatch = native
618
- ? () => {
619
- native.applyPatches(accepted.json);
620
- snapshotRef.current = nextSnapshot;
621
- }
622
- : undefined;
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 snapshotRef.current = next;
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(value) ||
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 = sanitizeDecoration(value, field);
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 && optional) ||
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())