@nadohq/mobile-client 0.65.0 → 0.67.0

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.
Files changed (211) hide show
  1. package/README.md +202 -10
  2. package/dist/{MobileClient-CfVZXTcz.cjs → MobileClient-vuI5wlB_.cjs} +303 -21
  3. package/dist/MobileClient-vuI5wlB_.cjs.map +1 -0
  4. package/dist/MobileClient.cjs +1 -1
  5. package/dist/MobileClient.d.cts +175 -20
  6. package/dist/MobileClient.d.cts.map +1 -1
  7. package/dist/MobileClient.d.ts +175 -20
  8. package/dist/MobileClient.d.ts.map +1 -1
  9. package/dist/MobileClient.js +303 -21
  10. package/dist/MobileClient.js.map +1 -1
  11. package/dist/{MobileServerFailureError-BZVwlsMg.d.ts → MobileServerFailureError-BeCCNShj.d.cts} +2 -3
  12. package/dist/MobileServerFailureError-BeCCNShj.d.cts.map +1 -0
  13. package/dist/{MobileServerFailureError-CbtK8kD4.d.cts → MobileServerFailureError-hHv28weI.d.ts} +2 -3
  14. package/dist/MobileServerFailureError-hHv28weI.d.ts.map +1 -0
  15. package/dist/MobileUnsupportedNotificationRouteError-DpiNBvvY.d.cts +15 -0
  16. package/dist/MobileUnsupportedNotificationRouteError-DpiNBvvY.d.cts.map +1 -0
  17. package/dist/MobileUnsupportedNotificationRouteError-DpiNBvvY.d.ts +15 -0
  18. package/dist/MobileUnsupportedNotificationRouteError-DpiNBvvY.d.ts.map +1 -0
  19. package/dist/{buildSignedMobileRequest-C1v0flCR.d.cts → buildSignedMobileRequest-DcEMKU_4.d.cts} +2 -2
  20. package/dist/{buildSignedMobileRequest-C1v0flCR.d.cts.map → buildSignedMobileRequest-DcEMKU_4.d.cts.map} +1 -1
  21. package/dist/{buildSignedMobileRequest-UeeWdYGU.d.ts → buildSignedMobileRequest-DlTMvoXR.d.ts} +2 -2
  22. package/dist/{buildSignedMobileRequest-UeeWdYGU.d.ts.map → buildSignedMobileRequest-DlTMvoXR.d.ts.map} +1 -1
  23. package/dist/{canonicalize-5eeZrkzQ.d.ts → canonicalize-BOZlflI3.d.ts} +2 -2
  24. package/dist/canonicalize-BOZlflI3.d.ts.map +1 -0
  25. package/dist/{canonicalize-CBaZprTo.d.cts → canonicalize-BvCozzvJ.d.cts} +2 -2
  26. package/dist/canonicalize-BvCozzvJ.d.cts.map +1 -0
  27. package/dist/{clientTypes-_mEgzmNs.d.cts → clientTypes-KpHlCDGZ.d.cts} +341 -38
  28. package/dist/clientTypes-KpHlCDGZ.d.cts.map +1 -0
  29. package/dist/{clientTypes-C9dJufxk.d.ts → clientTypes-XRI41VdC.d.ts} +341 -38
  30. package/dist/clientTypes-XRI41VdC.d.ts.map +1 -0
  31. package/dist/dataMappers.cjs +257 -20
  32. package/dist/dataMappers.cjs.map +1 -1
  33. package/dist/dataMappers.d.cts +98 -15
  34. package/dist/dataMappers.d.cts.map +1 -1
  35. package/dist/dataMappers.d.ts +98 -15
  36. package/dist/dataMappers.d.ts.map +1 -1
  37. package/dist/dataMappers.js +242 -20
  38. package/dist/dataMappers.js.map +1 -1
  39. package/dist/{eip712MethodByType-D8jLoxDu.d.cts → eip712MethodByType-DyAmzH7b.d.cts} +2 -2
  40. package/dist/eip712MethodByType-DyAmzH7b.d.cts.map +1 -0
  41. package/dist/{eip712MethodByType-DcUh8h-l.d.ts → eip712MethodByType-qSSAF6i2.d.ts} +2 -2
  42. package/dist/eip712MethodByType-qSSAF6i2.d.ts.map +1 -0
  43. package/dist/feed-BrgkFqP3.d.cts +23 -0
  44. package/dist/feed-BrgkFqP3.d.cts.map +1 -0
  45. package/dist/feed-DcbhLoo1.d.ts +23 -0
  46. package/dist/feed-DcbhLoo1.d.ts.map +1 -0
  47. package/dist/index-BVVgDSdq.d.cts +1 -0
  48. package/dist/index-BVVgDSdq.d.ts +1 -0
  49. package/dist/index.cjs +8 -2
  50. package/dist/index.d.cts +14 -16
  51. package/dist/index.d.ts +14 -16
  52. package/dist/index.js +4 -3
  53. package/dist/{mobileErrorCodes-CmjYEIYm.d.ts → mobileErrorCodes-CdwX6QTO.d.cts} +8 -3
  54. package/dist/mobileErrorCodes-CdwX6QTO.d.cts.map +1 -0
  55. package/dist/{mobileErrorCodes-CmjYEIYm.d.cts → mobileErrorCodes-CdwX6QTO.d.ts} +8 -3
  56. package/dist/mobileErrorCodes-CdwX6QTO.d.ts.map +1 -0
  57. package/dist/{payloadHash-DhHmgRqL.d.cts → payloadHash-BgGSZock.d.cts} +2 -2
  58. package/dist/payloadHash-BgGSZock.d.cts.map +1 -0
  59. package/dist/{payloadHash-E1_8fpLW.d.ts → payloadHash-Bi6upCLQ.d.ts} +2 -2
  60. package/dist/payloadHash-Bi6upCLQ.d.ts.map +1 -0
  61. package/dist/{serverExecuteTypes-BwWGspOg.d.ts → serverExecuteTypes-DuCWe_H_.d.cts} +23 -14
  62. package/dist/serverExecuteTypes-DuCWe_H_.d.cts.map +1 -0
  63. package/dist/{serverExecuteTypes-B-OJok19.d.cts → serverExecuteTypes-usJ6HMJv.d.ts} +23 -14
  64. package/dist/serverExecuteTypes-usJ6HMJv.d.ts.map +1 -0
  65. package/dist/{serverModelTypes-CiA-dy1R.d.ts → serverModelTypes-CTHMNIQS.d.cts} +170 -13
  66. package/dist/serverModelTypes-CTHMNIQS.d.cts.map +1 -0
  67. package/dist/{serverModelTypes-CiA-dy1R.d.cts → serverModelTypes-CTHMNIQS.d.ts} +170 -13
  68. package/dist/serverModelTypes-CTHMNIQS.d.ts.map +1 -0
  69. package/dist/{serverResponseGuards-CShiXpHT.d.ts → serverResponseGuards-CzmldAad.d.cts} +2 -2
  70. package/dist/{serverResponseGuards-BW69PS7O.d.cts.map → serverResponseGuards-CzmldAad.d.cts.map} +1 -1
  71. package/dist/{serverResponseGuards-BW69PS7O.d.cts → serverResponseGuards-Dvkbjp2_.d.ts} +2 -2
  72. package/dist/{serverResponseGuards-CShiXpHT.d.ts.map → serverResponseGuards-Dvkbjp2_.d.ts.map} +1 -1
  73. package/dist/signing/buildSignedMobileRequest.d.cts +1 -1
  74. package/dist/signing/buildSignedMobileRequest.d.ts +1 -1
  75. package/dist/signing/canonicalize.cjs +53 -2
  76. package/dist/signing/canonicalize.cjs.map +1 -1
  77. package/dist/signing/canonicalize.d.cts +1 -1
  78. package/dist/signing/canonicalize.d.ts +1 -1
  79. package/dist/signing/canonicalize.js +53 -2
  80. package/dist/signing/canonicalize.js.map +1 -1
  81. package/dist/signing/eip712MethodByType.cjs +7 -0
  82. package/dist/signing/eip712MethodByType.cjs.map +1 -1
  83. package/dist/signing/eip712MethodByType.d.cts +1 -1
  84. package/dist/signing/eip712MethodByType.d.ts +1 -1
  85. package/dist/signing/eip712MethodByType.js +7 -0
  86. package/dist/signing/eip712MethodByType.js.map +1 -1
  87. package/dist/signing/index.d.cts +5 -5
  88. package/dist/signing/index.d.ts +5 -5
  89. package/dist/signing/payloadHash.cjs +1 -1
  90. package/dist/signing/payloadHash.d.cts +1 -1
  91. package/dist/signing/payloadHash.d.ts +1 -1
  92. package/dist/signing/payloadHash.js +1 -1
  93. package/dist/signing/types.d.cts +1 -1
  94. package/dist/signing/types.d.ts +1 -1
  95. package/dist/types/MobileServerFailureError.d.cts +1 -1
  96. package/dist/types/MobileServerFailureError.d.ts +1 -1
  97. package/dist/types/MobileUnsupportedNotificationRouteError.cjs +19 -0
  98. package/dist/types/MobileUnsupportedNotificationRouteError.cjs.map +1 -0
  99. package/dist/types/MobileUnsupportedNotificationRouteError.d.cts +2 -0
  100. package/dist/types/MobileUnsupportedNotificationRouteError.d.ts +2 -0
  101. package/dist/types/MobileUnsupportedNotificationRouteError.js +18 -0
  102. package/dist/types/MobileUnsupportedNotificationRouteError.js.map +1 -0
  103. package/dist/types/clientTypes.d.cts +2 -2
  104. package/dist/types/clientTypes.d.ts +2 -2
  105. package/dist/types/index.cjs +4 -0
  106. package/dist/types/index.d.cts +8 -9
  107. package/dist/types/index.d.ts +8 -9
  108. package/dist/types/index.js +3 -2
  109. package/dist/types/mobileErrorCodes.cjs +11 -1
  110. package/dist/types/mobileErrorCodes.cjs.map +1 -1
  111. package/dist/types/mobileErrorCodes.d.cts +1 -1
  112. package/dist/types/mobileErrorCodes.d.ts +1 -1
  113. package/dist/types/mobileErrorCodes.js +11 -1
  114. package/dist/types/mobileErrorCodes.js.map +1 -1
  115. package/dist/types/serverBaseTypes.d.cts +1 -1
  116. package/dist/types/serverBaseTypes.d.ts +1 -1
  117. package/dist/types/serverExecuteTypes.d.cts +2 -2
  118. package/dist/types/serverExecuteTypes.d.ts +2 -2
  119. package/dist/types/serverModelTypes.cjs +15 -0
  120. package/dist/types/serverModelTypes.cjs.map +1 -1
  121. package/dist/types/serverModelTypes.d.cts +2 -2
  122. package/dist/types/serverModelTypes.d.ts +2 -2
  123. package/dist/types/serverModelTypes.js +14 -1
  124. package/dist/types/serverModelTypes.js.map +1 -1
  125. package/dist/types/serverQueryTypes.d.cts +2 -2
  126. package/dist/types/serverQueryTypes.d.ts +2 -2
  127. package/dist/types/serverRequestTypes.d.cts +2 -2
  128. package/dist/types/serverRequestTypes.d.ts +2 -2
  129. package/dist/types-CByl_NyJ.d.ts +825 -0
  130. package/dist/types-CByl_NyJ.d.ts.map +1 -0
  131. package/dist/types-CR5iaKR4.d.cts +825 -0
  132. package/dist/types-CR5iaKR4.d.cts.map +1 -0
  133. package/dist/utils/buildMobileFeedImageMultipart.cjs +31 -0
  134. package/dist/utils/buildMobileFeedImageMultipart.cjs.map +1 -0
  135. package/dist/utils/buildMobileFeedImageMultipart.d.cts +15 -0
  136. package/dist/utils/buildMobileFeedImageMultipart.d.cts.map +1 -0
  137. package/dist/utils/buildMobileFeedImageMultipart.d.ts +15 -0
  138. package/dist/utils/buildMobileFeedImageMultipart.d.ts.map +1 -0
  139. package/dist/utils/buildMobileFeedImageMultipart.js +30 -0
  140. package/dist/utils/buildMobileFeedImageMultipart.js.map +1 -0
  141. package/dist/utils/feed.cjs +12 -0
  142. package/dist/utils/feed.cjs.map +1 -1
  143. package/dist/utils/feed.d.cts +2 -2
  144. package/dist/utils/feed.d.ts +2 -2
  145. package/dist/utils/feed.js +11 -1
  146. package/dist/utils/feed.js.map +1 -1
  147. package/dist/utils/index.cjs +3 -1
  148. package/dist/utils/index.d.cts +3 -3
  149. package/dist/utils/index.d.ts +3 -3
  150. package/dist/utils/index.js +2 -2
  151. package/dist/utils/serverResponseGuards.d.cts +1 -1
  152. package/dist/utils/serverResponseGuards.d.ts +1 -1
  153. package/package.json +3 -3
  154. package/src/MobileClient.ts +444 -19
  155. package/src/dataMappers.test.ts +511 -6
  156. package/src/dataMappers.ts +325 -24
  157. package/src/feedImages.test.ts +309 -0
  158. package/src/notifications.test.ts +950 -0
  159. package/src/signing/canonicalize.ts +72 -0
  160. package/src/signing/eip712MethodByType.ts +10 -0
  161. package/src/signing/signing.test.ts +67 -1
  162. package/src/signing/types.ts +55 -9
  163. package/src/socialFeed.test.ts +63 -2
  164. package/src/types/MobileUnsupportedNotificationRouteError.ts +16 -0
  165. package/src/types/clientTypes.ts +427 -36
  166. package/src/types/index.ts +1 -0
  167. package/src/types/mobileErrorCodes.ts +10 -0
  168. package/src/types/serverExecuteTypes.ts +22 -10
  169. package/src/types/serverModelTypes.ts +216 -11
  170. package/src/types/serverQueryTypes.ts +106 -9
  171. package/src/types/serverRequestTypes.ts +114 -17
  172. package/src/utils/buildMobileFeedImageMultipart.test.ts +45 -0
  173. package/src/utils/buildMobileFeedImageMultipart.ts +36 -0
  174. package/src/utils/feed.ts +16 -0
  175. package/dist/MobileClient-CfVZXTcz.cjs.map +0 -1
  176. package/dist/MobileServerFailureError-BZVwlsMg.d.ts.map +0 -1
  177. package/dist/MobileServerFailureError-CbtK8kD4.d.cts.map +0 -1
  178. package/dist/canonicalize-5eeZrkzQ.d.ts.map +0 -1
  179. package/dist/canonicalize-CBaZprTo.d.cts.map +0 -1
  180. package/dist/clientTypes-C9dJufxk.d.ts.map +0 -1
  181. package/dist/clientTypes-_mEgzmNs.d.cts.map +0 -1
  182. package/dist/eip712MethodByType-D8jLoxDu.d.cts.map +0 -1
  183. package/dist/eip712MethodByType-DcUh8h-l.d.ts.map +0 -1
  184. package/dist/feed-DTJlsg8W.d.cts +0 -14
  185. package/dist/feed-DTJlsg8W.d.cts.map +0 -1
  186. package/dist/feed-DTJlsg8W.d.ts +0 -14
  187. package/dist/feed-DTJlsg8W.d.ts.map +0 -1
  188. package/dist/mobileErrorCodes-CmjYEIYm.d.cts.map +0 -1
  189. package/dist/mobileErrorCodes-CmjYEIYm.d.ts.map +0 -1
  190. package/dist/payloadHash-DhHmgRqL.d.cts.map +0 -1
  191. package/dist/payloadHash-E1_8fpLW.d.ts.map +0 -1
  192. package/dist/serverBaseTypes-B5J1_TyC.d.cts +0 -25
  193. package/dist/serverBaseTypes-B5J1_TyC.d.cts.map +0 -1
  194. package/dist/serverBaseTypes-DnLzsdm-.d.ts +0 -25
  195. package/dist/serverBaseTypes-DnLzsdm-.d.ts.map +0 -1
  196. package/dist/serverExecuteTypes-B-OJok19.d.cts.map +0 -1
  197. package/dist/serverExecuteTypes-BwWGspOg.d.ts.map +0 -1
  198. package/dist/serverModelTypes-CiA-dy1R.d.cts.map +0 -1
  199. package/dist/serverModelTypes-CiA-dy1R.d.ts.map +0 -1
  200. package/dist/serverQueryTypes-CJcjLB7n.d.ts +0 -283
  201. package/dist/serverQueryTypes-CJcjLB7n.d.ts.map +0 -1
  202. package/dist/serverQueryTypes-V1f9yE7L.d.cts +0 -283
  203. package/dist/serverQueryTypes-V1f9yE7L.d.cts.map +0 -1
  204. package/dist/serverRequestTypes-CRlkNSu5.d.cts +0 -270
  205. package/dist/serverRequestTypes-CRlkNSu5.d.cts.map +0 -1
  206. package/dist/serverRequestTypes-DZoOhV2o.d.ts +0 -270
  207. package/dist/serverRequestTypes-DZoOhV2o.d.ts.map +0 -1
  208. package/dist/types-BiRmH5Iv.d.cts +0 -108
  209. package/dist/types-BiRmH5Iv.d.cts.map +0 -1
  210. package/dist/types-DoJuq0Ij.d.ts +0 -108
  211. package/dist/types-DoJuq0Ij.d.ts.map +0 -1
@@ -1,4 +1,7 @@
1
1
  import { describe, expect, it } from '@jest/globals';
2
+ import { createWalletClient, http } from 'viem';
3
+ import { privateKeyToAccount } from 'viem/accounts';
4
+ import { ink } from 'viem/chains';
2
5
  import {
3
6
  mapMobileSocialFeedActionCandidate,
4
7
  mapMobileSocialFeedCommentsPage,
@@ -6,22 +9,29 @@ import {
6
9
  mapMobileSocialFeedDrawingResult,
7
10
  mapMobileSocialFeedItem,
8
11
  mapMobileSocialFeedItemPatch,
12
+ mapMobileSocialFeedItemResponse,
9
13
  mapMobileSocialFeedMentionToServer,
10
14
  mapMobileSocialFeedPage,
11
15
  mapMobileSocialFeedPositionAction,
12
16
  mapMobileSocialFeedPositionChart,
13
17
  mapMobileSocialFeedPositionHistory,
14
18
  mapMobileSocialFeedPositionPatch,
19
+ mapMobileSocialFeedPositionThoughts,
15
20
  mapMobileSocialFeedPresence,
16
21
  mapMobileSocialFeedTradeCard,
17
22
  } from './dataMappers';
23
+ import { MobileClient } from './MobileClient';
18
24
  import { MobileUnsupportedFeedItemError } from './types/MobileUnsupportedFeedItemError';
19
25
  import {
20
26
  MobileServerSocialFeedItem,
21
27
  MobileServerSocialFeedPost,
22
28
  MobileServerSocialFeedReply,
23
29
  } from './types/serverModelTypes';
24
- import { MobileServerSocialFeedPositionHistoryResponse } from './types/serverQueryTypes';
30
+ import {
31
+ MobileServerSocialFeedPositionHistoryResponse,
32
+ MobileServerSocialFeedPositionThoughtsResponse,
33
+ } from './types/serverQueryTypes';
34
+ import { isMobileSocialFeedUploadedImage } from './utils';
25
35
 
26
36
  const post: MobileServerSocialFeedPost = {
27
37
  id: '88888888-8888-4888-8888-888888888888',
@@ -52,6 +62,7 @@ const post: MobileServerSocialFeedPost = {
52
62
  can_comment: true,
53
63
  can_record_view: true,
54
64
  },
65
+ images: [],
55
66
  text: 'BTC 回踩后继续持有多仓。',
56
67
  mentions: [],
57
68
  position: {
@@ -136,6 +147,7 @@ const reply: MobileServerSocialFeedReply = {
136
147
  can_comment: true,
137
148
  can_record_view: false,
138
149
  },
150
+ images: [],
139
151
  text: '我会继续观察支撑位。',
140
152
  mentions: [],
141
153
  root_item_id: '11111111-1111-4111-8111-111111111111',
@@ -179,6 +191,7 @@ const reply: MobileServerSocialFeedReply = {
179
191
  drawing: null,
180
192
  };
181
193
  const history: MobileServerSocialFeedPositionHistoryResponse = {
194
+ owner: post.author,
182
195
  position: {
183
196
  position_id: '22222222-2222-4222-8222-222222222222',
184
197
  product_id: 2,
@@ -280,6 +293,7 @@ const history: MobileServerSocialFeedPositionHistoryResponse = {
280
293
  can_comment: true,
281
294
  can_record_view: true,
282
295
  },
296
+ images: [],
283
297
  text: '持有这一仓位,关注后续价格变化。',
284
298
  mentions: [],
285
299
  position: {
@@ -361,10 +375,6 @@ const history: MobileServerSocialFeedPositionHistoryResponse = {
361
375
  expires_at_ms: 1791072900000,
362
376
  update_cursor: null,
363
377
  viewer_token: 'viewer_example_opaque',
364
- view_tracking: {
365
- min_visible_ratio: 0.5,
366
- min_duration_ms: 1000,
367
- },
368
378
  },
369
379
  };
370
380
 
@@ -499,6 +509,116 @@ describe('[mobile-client]: content mapping', () => {
499
509
  });
500
510
  });
501
511
 
512
+ it('maps empty image arrays in feeds, item details, and comments', () => {
513
+ const serverReply = { ...reply };
514
+ const serverPost = {
515
+ ...post,
516
+ comment_preview: {
517
+ ...post.comment_preview,
518
+ items: [serverReply],
519
+ },
520
+ };
521
+ const serverCard = {
522
+ ...serverPost,
523
+ kind: 'trade_card',
524
+ action: 'opened',
525
+ } as const;
526
+ const items: MobileServerSocialFeedItem[] = [
527
+ serverPost,
528
+ serverCard,
529
+ serverReply,
530
+ ];
531
+ const page = mapMobileSocialFeedPage({
532
+ items,
533
+ next_cursor: null,
534
+ watermark: 'Watermark',
535
+ read_session: history.read_session,
536
+ });
537
+ expect(page.items).toMatchObject([
538
+ {
539
+ kind: 'post',
540
+ images: [],
541
+ commentPreview: { items: [{ images: [] }] },
542
+ },
543
+ {
544
+ kind: 'trade_card',
545
+ images: [],
546
+ commentPreview: { items: [{ images: [] }] },
547
+ },
548
+ { kind: 'reply', images: [] },
549
+ ]);
550
+ for (const item of items) {
551
+ expect(
552
+ mapMobileSocialFeedItemResponse({
553
+ item,
554
+ read_session: history.read_session,
555
+ }).item,
556
+ ).toMatchObject({ kind: item.kind, images: [] });
557
+ }
558
+ const comments = mapMobileSocialFeedCommentsPage({
559
+ comments: [{ ...serverReply, replies: [] }],
560
+ total_count: 1,
561
+ top_level_count: 1,
562
+ next_cursor: null,
563
+ previous_cursor: null,
564
+ anchor_reply_id: null,
565
+ read_session: history.read_session,
566
+ });
567
+ expect(comments.comments).toMatchObject([{ kind: 'reply', images: [] }]);
568
+ });
569
+
570
+ it('maps feed images and read sessions without obsolete view tracking', () => {
571
+ const mapped = mapMobileSocialFeedItem({
572
+ ...post,
573
+ images: [
574
+ {
575
+ image_id: '88888888-8888-4888-8888-888888888888',
576
+ url: '/v1/mobile/feed/images/88888888-8888-4888-8888-888888888888/standard',
577
+ width: 1024,
578
+ height: 256,
579
+ content_type: 'image/png',
580
+ byte_size: 123456,
581
+ thumbnail: {
582
+ url: '/v1/mobile/feed/images/88888888-8888-4888-8888-888888888888/thumbnail',
583
+ width: 512,
584
+ height: 128,
585
+ },
586
+ },
587
+ ],
588
+ });
589
+ expect(mapped.kind).toBe('post');
590
+ if (mapped.kind !== 'post') throw new Error('Expected post');
591
+ expect(mapped.images).toEqual([
592
+ {
593
+ imageId: '88888888-8888-4888-8888-888888888888',
594
+ url: '/v1/mobile/feed/images/88888888-8888-4888-8888-888888888888/standard',
595
+ width: 1024,
596
+ height: 256,
597
+ contentType: 'image/png',
598
+ byteSize: 123456,
599
+ thumbnail: {
600
+ url: '/v1/mobile/feed/images/88888888-8888-4888-8888-888888888888/thumbnail',
601
+ width: 512,
602
+ height: 128,
603
+ },
604
+ },
605
+ ]);
606
+ expect(
607
+ mapMobileSocialFeedPage({
608
+ items: [],
609
+ next_cursor: null,
610
+ watermark: 'w',
611
+ read_session: history.read_session,
612
+ }).readSession,
613
+ ).toEqual({
614
+ sessionId: history.read_session.session_id,
615
+ mode: history.read_session.mode,
616
+ expiresAtMs: history.read_session.expires_at_ms,
617
+ updateCursor: history.read_session.update_cursor,
618
+ viewerToken: history.read_session.viewer_token,
619
+ });
620
+ });
621
+
502
622
  it('maps posts, drawings, reply previews, and nullable badges from the documented response', () => {
503
623
  const mapped = mapMobileSocialFeedItem({
504
624
  ...post,
@@ -699,16 +819,18 @@ describe('[mobile-client]: content mapping', () => {
699
819
 
700
820
  it('maps comment cursors and anchor without inventing an observation timestamp', () => {
701
821
  const result = mapMobileSocialFeedCommentsPage({
702
- comments: [reply],
822
+ comments: [{ ...reply, replies: [] }],
703
823
  next_cursor: 'NextOpaque',
704
824
  previous_cursor: 'PreviousOpaque',
705
825
  total_count: 8,
826
+ top_level_count: 3,
706
827
  anchor_reply_id: reply.id,
707
828
  read_session: history.read_session,
708
829
  });
709
830
  expect(result.nextCursor).toBe('NextOpaque');
710
831
  expect(result.previousCursor).toBe('PreviousOpaque');
711
832
  expect(result.anchorReplyId).toBe(reply.id);
833
+ expect(result.topLevelCount).toBe(3);
712
834
  expect(result).not.toHaveProperty('asOfMs');
713
835
  });
714
836
 
@@ -726,6 +848,85 @@ describe('[mobile-client]: content mapping', () => {
726
848
  expect(result).not.toHaveProperty('asOfMs');
727
849
  });
728
850
 
851
+ it('preserves complete flat comment groups, descendant parents, counts, and an anchor at any depth', () => {
852
+ const child = {
853
+ ...reply,
854
+ id: '44444444-4444-4444-8444-444444444444',
855
+ reply_to_item_id: reply.id,
856
+ reply_to_author: reply.author,
857
+ };
858
+ const grandchild = {
859
+ ...child,
860
+ id: '55555555-5555-4555-8555-555555555555',
861
+ reply_to_item_id: child.id,
862
+ images: [{ url: 'https://example.com/reply.png' }],
863
+ };
864
+ const mapped = mapMobileSocialFeedCommentsPage({
865
+ comments: [
866
+ {
867
+ ...reply,
868
+ reply_to_item_id: undefined,
869
+ reply_to_author: null,
870
+ replies: [child, grandchild],
871
+ },
872
+ { ...reply, id: '66666666-6666-4666-8666-666666666666', replies: [] },
873
+ ],
874
+ total_count: 12,
875
+ top_level_count: 5,
876
+ next_cursor: 'NextGroup',
877
+ previous_cursor: 'PreviousGroup',
878
+ anchor_reply_id: grandchild.id,
879
+ read_session: history.read_session,
880
+ });
881
+ expect(mapped.comments.map((group) => group.id)).toEqual([
882
+ reply.id,
883
+ '66666666-6666-4666-8666-666666666666',
884
+ ]);
885
+ expect(mapped.comments[0]).not.toHaveProperty('replyToItemId');
886
+ expect(mapped.comments[0].replyToAuthor).toBeNull();
887
+ expect(mapped.comments[0].replies.map((item) => item.id)).toEqual([
888
+ child.id,
889
+ grandchild.id,
890
+ ]);
891
+ expect(
892
+ mapped.comments[0].replies.map((item) => item.replyToItemId),
893
+ ).toEqual([reply.id, child.id]);
894
+ expect(mapped.comments[0].replies[1].images).toEqual(grandchild.images);
895
+ expect(mapped.comments[0].replies[1].replyToAuthor?.displayName).toBe(
896
+ reply.author.display_name,
897
+ );
898
+ expect(mapped.comments[0].replies[1].interaction.viewCount).toBe(
899
+ reply.interaction.view_count,
900
+ );
901
+ expect(mapped.comments[0].replies[0]).not.toHaveProperty('replies');
902
+ expect(mapped.comments[1].replies).toEqual([]);
903
+ expect(mapped).toMatchObject({
904
+ totalCount: 12,
905
+ topLevelCount: 5,
906
+ nextCursor: 'NextGroup',
907
+ previousCursor: 'PreviousGroup',
908
+ anchorReplyId: grandchild.id,
909
+ });
910
+ });
911
+
912
+ it('preserves an empty comment window with continuation and whole-thread counts', () => {
913
+ const mapped = mapMobileSocialFeedCommentsPage({
914
+ comments: [],
915
+ total_count: 8,
916
+ top_level_count: 3,
917
+ next_cursor: 'NextGroup',
918
+ previous_cursor: null,
919
+ anchor_reply_id: null,
920
+ read_session: history.read_session,
921
+ });
922
+ expect(mapped.comments).toEqual([]);
923
+ expect(mapped).toMatchObject({
924
+ totalCount: 8,
925
+ topLevelCount: 3,
926
+ nextCursor: 'NextGroup',
927
+ });
928
+ });
929
+
729
930
  it('keeps mention labels and code-point offsets intact', () => {
730
931
  const result = mapMobileSocialFeedMentionToServer({
731
932
  subaccount: {
@@ -740,3 +941,307 @@ describe('[mobile-client]: content mapping', () => {
740
941
  expect(result.subaccount).toMatch(/^0x[0-9a-f]{64}$/);
741
942
  });
742
943
  });
944
+
945
+ describe('[mobile-client]: position feed changes', () => {
946
+ it('keeps an unnamed owner even when no timeline content is available', () => {
947
+ const mapped = mapMobileSocialFeedPositionHistory({
948
+ ...history,
949
+ owner: {
950
+ ...post.author,
951
+ username: null,
952
+ display_name: null,
953
+ avatar_url: null,
954
+ is_following: null,
955
+ },
956
+ timeline: [],
957
+ summary: { action_count: 0, take_count: 0 },
958
+ });
959
+ expect(mapped.owner).toEqual({
960
+ subaccount: post.author.subaccount,
961
+ username: null,
962
+ displayName: null,
963
+ avatarUrl: null,
964
+ isFollowing: null,
965
+ traderBadge: null,
966
+ traderDataStatus: 'unavailable',
967
+ });
968
+ expect(mapped.timeline).toEqual([]);
969
+ expect(mapped.summary).toEqual({ actionCount: 0, takeCount: 0 });
970
+ });
971
+
972
+ it('maps recent actions without manufacturing related takes or complete-history counts', () => {
973
+ const entry = history.timeline[0];
974
+ if (entry.kind !== 'action') throw new Error('Expected action fixture');
975
+ const mapped = mapMobileSocialFeedPositionHistory({
976
+ ...history,
977
+ timeline: [{ ...entry, related_take: null }],
978
+ next_cursor: null,
979
+ summary: { action_count: 1, take_count: 0 },
980
+ });
981
+ expect(mapped.timeline[0].kind).toBe('action');
982
+ if (mapped.timeline[0].kind !== 'action') {
983
+ throw new Error('Expected mapped action');
984
+ }
985
+ expect(mapped.timeline[0].relatedTake).toBeNull();
986
+ expect(mapped.timeline[0].relatedItemId).toBe(entry.related_item_id);
987
+ expect(mapped.timeline[0].action.executedQuantity.toFixed()).toBe(
988
+ entry.action.executed_quantity_x18,
989
+ );
990
+ expect(mapped.owner.subaccount).toBe(post.author.subaccount);
991
+ expect(mapped.summary).toEqual({ actionCount: 1, takeCount: 0 });
992
+ expect(mapped.nextCursor).toBeNull();
993
+ });
994
+
995
+ function thoughtsPage(): MobileServerSocialFeedPositionThoughtsResponse {
996
+ return {
997
+ items: [
998
+ {
999
+ ...post,
1000
+ has_analysis: true,
1001
+ drawing: { data: false, created_at_ms: 20, updated_at_ms: 40 },
1002
+ },
1003
+ {
1004
+ ...post,
1005
+ id: '77777777-7777-4777-8777-777777777777',
1006
+ kind: 'trade_card',
1007
+ action: 'opened',
1008
+ display_at_ms: post.display_at_ms + 100,
1009
+ has_analysis: true,
1010
+ drawing: {
1011
+ data: { lines: [] },
1012
+ created_at_ms: 10,
1013
+ updated_at_ms: 50,
1014
+ },
1015
+ },
1016
+ ],
1017
+ next_cursor: 'Continue',
1018
+ read_session: { ...history.read_session, mode: 'position' },
1019
+ };
1020
+ }
1021
+
1022
+ it('preserves thoughts order and complete parsed drawings independently of display/update time', () => {
1023
+ const server = thoughtsPage();
1024
+ const mapped = mapMobileSocialFeedPositionThoughts(server);
1025
+ expect(mapped.items.map((item) => item.id)).toEqual(
1026
+ server.items.map((item) => item.id),
1027
+ );
1028
+ expect(mapped.items[0].drawing).toEqual({
1029
+ data: false,
1030
+ createdAtMs: 20,
1031
+ updatedAtMs: 40,
1032
+ });
1033
+ expect(mapped.items[1].drawing.data).toEqual({ lines: [] });
1034
+ expect(mapped.items[1].kind).toBe('trade_card');
1035
+ expect(mapped.items[1].position.quantity.toFixed()).toBe(
1036
+ post.position.quantity_x18,
1037
+ );
1038
+ expect(mapped.nextCursor).toBe('Continue');
1039
+ expect(mapped.readSession.mode).toBe('position');
1040
+ });
1041
+
1042
+ it.each([null, 'Continue'])(
1043
+ 'preserves empty thought pages with cursor %s',
1044
+ (cursor) => {
1045
+ const mapped = mapMobileSocialFeedPositionThoughts({
1046
+ ...thoughtsPage(),
1047
+ items: [],
1048
+ next_cursor: cursor,
1049
+ });
1050
+ expect(mapped.items).toEqual([]);
1051
+ expect(mapped.nextCursor).toBe(cursor);
1052
+ },
1053
+ );
1054
+
1055
+ it('maps mixed uploaded/external images in posts and replies without inventing metadata', () => {
1056
+ const images = [
1057
+ {
1058
+ image_id: '66666666-6666-4666-8666-666666666666',
1059
+ url: '/v1/mobile/feed/images/test/standard',
1060
+ width: 1024,
1061
+ height: 512,
1062
+ content_type: 'image/png' as const,
1063
+ byte_size: 100,
1064
+ thumbnail: {
1065
+ url: '/v1/mobile/feed/images/test/thumbnail',
1066
+ width: 512,
1067
+ height: 256,
1068
+ },
1069
+ },
1070
+ { url: 'https://example.com/first.png' },
1071
+ { url: 'https://example.com/second.png' },
1072
+ ];
1073
+ for (const item of [post, reply]) {
1074
+ const mapped = mapMobileSocialFeedItem({ ...item, images });
1075
+ if (mapped.kind === 'trade_row')
1076
+ throw new Error('Expected authored content');
1077
+ expect(mapped.images[0]).toMatchObject({
1078
+ imageId: images[0].image_id,
1079
+ thumbnail: { width: 512 },
1080
+ });
1081
+ expect(mapped.images.slice(1)).toEqual(images.slice(1));
1082
+ expect(
1083
+ mapped.images
1084
+ .filter(isMobileSocialFeedUploadedImage)
1085
+ .map((image) => image.imageId),
1086
+ ).toEqual([images[0].image_id]);
1087
+ }
1088
+ });
1089
+ });
1090
+
1091
+ describe('[mobile-client]: position and image transport', () => {
1092
+ function clientWithResponse(data: Record<string, unknown>) {
1093
+ const client = new MobileClient({
1094
+ url: 'https://mobile.example/v1/mobile',
1095
+ });
1096
+ const requests: Record<string, unknown>[] = [];
1097
+ client.axiosInstance.defaults.adapter = (config) => {
1098
+ requests.push(JSON.parse(String(config.data)) as Record<string, unknown>);
1099
+ expect(config.url).toBe(
1100
+ `https://mobile.example/v1/mobile/${client.opts.walletClient ? 'execute' : 'public_query'}`,
1101
+ );
1102
+ return Promise.resolve({
1103
+ data: { status: 'success', ...data },
1104
+ status: 200,
1105
+ statusText: 'OK',
1106
+ headers: {},
1107
+ config,
1108
+ });
1109
+ };
1110
+ return { client, requests };
1111
+ }
1112
+
1113
+ it('routes thoughts publicly, preserving continuation credentials after an empty page', async () => {
1114
+ const { client, requests } = clientWithResponse({
1115
+ items: [],
1116
+ next_cursor: 'Next',
1117
+ read_session: history.read_session,
1118
+ });
1119
+ const first = await client.getPositionThoughts({
1120
+ positionId: post.position.position_id,
1121
+ limit: 2,
1122
+ });
1123
+ await client.getPositionThoughts({
1124
+ positionId: post.position.position_id,
1125
+ limit: 2,
1126
+ sessionId: first.readSession.sessionId,
1127
+ viewerToken: first.readSession.viewerToken,
1128
+ cursor: first.nextCursor ?? undefined,
1129
+ });
1130
+ expect(requests).toEqual([
1131
+ {
1132
+ type: 'position_thoughts',
1133
+ position_id: post.position.position_id,
1134
+ limit: 2,
1135
+ },
1136
+ {
1137
+ type: 'position_thoughts',
1138
+ position_id: post.position.position_id,
1139
+ limit: 2,
1140
+ session_id: history.read_session.session_id,
1141
+ viewer_token: history.read_session.viewer_token,
1142
+ cursor: 'Next',
1143
+ },
1144
+ ]);
1145
+ });
1146
+
1147
+ it('reads grouped comments through the public API and preserves paging credentials', async () => {
1148
+ const { client, requests } = clientWithResponse({
1149
+ comments: [{ ...reply, replies: [] }],
1150
+ total_count: 12,
1151
+ top_level_count: 5,
1152
+ next_cursor: 'NextGroup',
1153
+ previous_cursor: null,
1154
+ anchor_reply_id: null,
1155
+ read_session: history.read_session,
1156
+ });
1157
+ const page = await client.getFeedComments({
1158
+ rootItemId: post.id,
1159
+ limit: 1,
1160
+ });
1161
+ expect(page.comments[0].replies).toEqual([]);
1162
+ expect(page).toMatchObject({ totalCount: 12, topLevelCount: 5 });
1163
+ await client.getFeedComments({
1164
+ rootItemId: post.id,
1165
+ limit: 1,
1166
+ cursor: page.nextCursor ?? undefined,
1167
+ sessionId: page.readSession.sessionId,
1168
+ viewerToken: page.readSession.viewerToken,
1169
+ });
1170
+ expect(requests).toEqual([
1171
+ { type: 'feed_comments', root_item_id: post.id, limit: 1 },
1172
+ {
1173
+ type: 'feed_comments',
1174
+ root_item_id: post.id,
1175
+ limit: 1,
1176
+ cursor: 'NextGroup',
1177
+ session_id: history.read_session.session_id,
1178
+ viewer_token: history.read_session.viewer_token,
1179
+ },
1180
+ ]);
1181
+ });
1182
+
1183
+ it('routes recent actions publicly without pagination fields and maps the owner', async () => {
1184
+ const { client, requests } = clientWithResponse({
1185
+ ...history,
1186
+ timeline: [],
1187
+ summary: { action_count: 0, take_count: 0 },
1188
+ next_cursor: null,
1189
+ });
1190
+ const result = await client.getPositionHistoryAll({
1191
+ positionId: post.position.position_id,
1192
+ viewerToken: 'ExistingViewer',
1193
+ });
1194
+ expect(requests).toEqual([
1195
+ {
1196
+ type: 'position_history_all',
1197
+ position_id: post.position.position_id,
1198
+ viewer_token: 'ExistingViewer',
1199
+ },
1200
+ ]);
1201
+ expect(result.owner.subaccount).toBe(post.author.subaccount);
1202
+ expect(result.timeline).toEqual([]);
1203
+ });
1204
+
1205
+ it.each(['post', 'comment'] as const)(
1206
+ 'passes image URL arrays through signed %s publication',
1207
+ async (kind) => {
1208
+ const { client, requests } = clientWithResponse(
1209
+ kind === 'post'
1210
+ ? { item: post }
1211
+ : { comment: reply, root_comment_count: 1 },
1212
+ );
1213
+ const account = privateKeyToAccount(
1214
+ '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b786907',
1215
+ );
1216
+ client.opts.walletClient = createWalletClient({
1217
+ account,
1218
+ chain: ink,
1219
+ transport: http(),
1220
+ });
1221
+ const params = {
1222
+ subaccountOwner: account.address,
1223
+ subaccountName: 'default',
1224
+ chainId: ink.id,
1225
+ verifyingAddr: '0x0000000000000000000000000000000000000001' as const,
1226
+ clientRequestId: '11111111-1111-4111-8111-111111111111',
1227
+ text: 'external images',
1228
+ mentions: [],
1229
+ imageIds: [],
1230
+ imageUrls: ['https://example.com/A.png', 'https://example.com/B.png'],
1231
+ };
1232
+ if (kind === 'post')
1233
+ await client.createFeedPost({
1234
+ ...params,
1235
+ positionId: post.position.position_id,
1236
+ });
1237
+ else await client.createFeedComment({ ...params, rootItemId: post.id });
1238
+ expect(requests[0]).toMatchObject({
1239
+ type: `create_feed_${kind}`,
1240
+ image_ids: [],
1241
+ image_urls: params.imageUrls,
1242
+ signature: expect.stringMatching(/^0x[0-9a-f]+$/),
1243
+ });
1244
+ expect(requests[0]).not.toHaveProperty('image_url');
1245
+ },
1246
+ );
1247
+ });