@nadohq/mobile-client 0.66.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 (156) hide show
  1. package/README.md +110 -17
  2. package/dist/{MobileClient-M4oC4lrX.cjs → MobileClient-vuI5wlB_.cjs} +69 -29
  3. package/dist/MobileClient-vuI5wlB_.cjs.map +1 -0
  4. package/dist/MobileClient.cjs +1 -1
  5. package/dist/MobileClient.d.cts +50 -25
  6. package/dist/MobileClient.d.cts.map +1 -1
  7. package/dist/MobileClient.d.ts +50 -25
  8. package/dist/MobileClient.d.ts.map +1 -1
  9. package/dist/MobileClient.js +69 -29
  10. package/dist/MobileClient.js.map +1 -1
  11. package/dist/{MobileServerFailureError-MtS2reel.d.cts → MobileServerFailureError-BeCCNShj.d.cts} +2 -2
  12. package/dist/{MobileServerFailureError-MtS2reel.d.cts.map → MobileServerFailureError-BeCCNShj.d.cts.map} +1 -1
  13. package/dist/{MobileServerFailureError-C9iqNsxI.d.ts → MobileServerFailureError-hHv28weI.d.ts} +2 -2
  14. package/dist/{MobileServerFailureError-C9iqNsxI.d.ts.map → MobileServerFailureError-hHv28weI.d.ts.map} +1 -1
  15. package/dist/{buildSignedMobileRequest-DyKCK34Y.d.cts → buildSignedMobileRequest-DcEMKU_4.d.cts} +2 -2
  16. package/dist/{buildSignedMobileRequest-DyKCK34Y.d.cts.map → buildSignedMobileRequest-DcEMKU_4.d.cts.map} +1 -1
  17. package/dist/{buildSignedMobileRequest-BR-KJhIF.d.ts → buildSignedMobileRequest-DlTMvoXR.d.ts} +2 -2
  18. package/dist/{buildSignedMobileRequest-BR-KJhIF.d.ts.map → buildSignedMobileRequest-DlTMvoXR.d.ts.map} +1 -1
  19. package/dist/{canonicalize-Bv6UIAbR.d.ts → canonicalize-BOZlflI3.d.ts} +2 -2
  20. package/dist/canonicalize-BOZlflI3.d.ts.map +1 -0
  21. package/dist/{canonicalize-CWh3XZzj.d.cts → canonicalize-BvCozzvJ.d.cts} +2 -2
  22. package/dist/canonicalize-BvCozzvJ.d.cts.map +1 -0
  23. package/dist/{clientTypes-DOpUcu80.d.cts → clientTypes-KpHlCDGZ.d.cts} +75 -49
  24. package/dist/clientTypes-KpHlCDGZ.d.cts.map +1 -0
  25. package/dist/{clientTypes-BUn5_OzK.d.ts → clientTypes-XRI41VdC.d.ts} +75 -49
  26. package/dist/clientTypes-XRI41VdC.d.ts.map +1 -0
  27. package/dist/dataMappers.cjs +56 -17
  28. package/dist/dataMappers.cjs.map +1 -1
  29. package/dist/dataMappers.d.cts +33 -13
  30. package/dist/dataMappers.d.cts.map +1 -1
  31. package/dist/dataMappers.d.ts +33 -13
  32. package/dist/dataMappers.d.ts.map +1 -1
  33. package/dist/dataMappers.js +53 -17
  34. package/dist/dataMappers.js.map +1 -1
  35. package/dist/{eip712MethodByType-CJ6BoayT.d.cts → eip712MethodByType-DyAmzH7b.d.cts} +2 -2
  36. package/dist/eip712MethodByType-DyAmzH7b.d.cts.map +1 -0
  37. package/dist/{eip712MethodByType-Dx4mvgni.d.ts → eip712MethodByType-qSSAF6i2.d.ts} +2 -2
  38. package/dist/eip712MethodByType-qSSAF6i2.d.ts.map +1 -0
  39. package/dist/feed-BrgkFqP3.d.cts +23 -0
  40. package/dist/feed-BrgkFqP3.d.cts.map +1 -0
  41. package/dist/feed-DcbhLoo1.d.ts +23 -0
  42. package/dist/feed-DcbhLoo1.d.ts.map +1 -0
  43. package/dist/index-BVVgDSdq.d.cts +1 -0
  44. package/dist/index-BVVgDSdq.d.ts +1 -0
  45. package/dist/index.cjs +2 -1
  46. package/dist/index.d.cts +13 -13
  47. package/dist/index.d.ts +13 -13
  48. package/dist/index.js +2 -2
  49. package/dist/{mobileErrorCodes-vD6rbFuF.d.ts → mobileErrorCodes-CdwX6QTO.d.cts} +3 -3
  50. package/dist/{mobileErrorCodes-vD6rbFuF.d.cts.map → mobileErrorCodes-CdwX6QTO.d.cts.map} +1 -1
  51. package/dist/{mobileErrorCodes-vD6rbFuF.d.cts → mobileErrorCodes-CdwX6QTO.d.ts} +3 -3
  52. package/dist/{mobileErrorCodes-vD6rbFuF.d.ts.map → mobileErrorCodes-CdwX6QTO.d.ts.map} +1 -1
  53. package/dist/{payloadHash-CdkF2Ocr.d.cts → payloadHash-BgGSZock.d.cts} +2 -2
  54. package/dist/payloadHash-BgGSZock.d.cts.map +1 -0
  55. package/dist/{payloadHash-HD29K77M.d.ts → payloadHash-Bi6upCLQ.d.ts} +2 -2
  56. package/dist/payloadHash-Bi6upCLQ.d.ts.map +1 -0
  57. package/dist/{serverExecuteTypes-5JyyIXRK.d.ts → serverExecuteTypes-DuCWe_H_.d.cts} +7 -14
  58. package/dist/{serverExecuteTypes-CHrQ9Rtq.d.cts.map → serverExecuteTypes-DuCWe_H_.d.cts.map} +1 -1
  59. package/dist/{serverExecuteTypes-CHrQ9Rtq.d.cts → serverExecuteTypes-usJ6HMJv.d.ts} +7 -14
  60. package/dist/{serverExecuteTypes-5JyyIXRK.d.ts.map → serverExecuteTypes-usJ6HMJv.d.ts.map} +1 -1
  61. package/dist/{serverModelTypes-CGUZCi0y.d.ts → serverModelTypes-CTHMNIQS.d.cts} +16 -14
  62. package/dist/serverModelTypes-CTHMNIQS.d.cts.map +1 -0
  63. package/dist/{serverModelTypes-CGUZCi0y.d.cts → serverModelTypes-CTHMNIQS.d.ts} +16 -14
  64. package/dist/serverModelTypes-CTHMNIQS.d.ts.map +1 -0
  65. package/dist/{serverResponseGuards-i1njcYKN.d.cts → serverResponseGuards-CzmldAad.d.cts} +2 -2
  66. package/dist/{serverResponseGuards-i1njcYKN.d.cts.map → serverResponseGuards-CzmldAad.d.cts.map} +1 -1
  67. package/dist/{serverResponseGuards-Bvw8ltYJ.d.ts → serverResponseGuards-Dvkbjp2_.d.ts} +2 -2
  68. package/dist/{serverResponseGuards-Bvw8ltYJ.d.ts.map → serverResponseGuards-Dvkbjp2_.d.ts.map} +1 -1
  69. package/dist/signing/buildSignedMobileRequest.d.cts +1 -1
  70. package/dist/signing/buildSignedMobileRequest.d.ts +1 -1
  71. package/dist/signing/canonicalize.cjs +4 -2
  72. package/dist/signing/canonicalize.cjs.map +1 -1
  73. package/dist/signing/canonicalize.d.cts +1 -1
  74. package/dist/signing/canonicalize.d.ts +1 -1
  75. package/dist/signing/canonicalize.js +4 -2
  76. package/dist/signing/canonicalize.js.map +1 -1
  77. package/dist/signing/eip712MethodByType.d.cts +1 -1
  78. package/dist/signing/eip712MethodByType.d.ts +1 -1
  79. package/dist/signing/index.d.cts +5 -5
  80. package/dist/signing/index.d.ts +5 -5
  81. package/dist/signing/payloadHash.d.cts +1 -1
  82. package/dist/signing/payloadHash.d.ts +1 -1
  83. package/dist/signing/types.d.cts +1 -1
  84. package/dist/signing/types.d.ts +1 -1
  85. package/dist/types/MobileServerFailureError.d.cts +1 -1
  86. package/dist/types/MobileServerFailureError.d.ts +1 -1
  87. package/dist/types/clientTypes.d.cts +2 -2
  88. package/dist/types/clientTypes.d.ts +2 -2
  89. package/dist/types/index.d.cts +7 -7
  90. package/dist/types/index.d.ts +7 -7
  91. package/dist/types/mobileErrorCodes.cjs +2 -2
  92. package/dist/types/mobileErrorCodes.cjs.map +1 -1
  93. package/dist/types/mobileErrorCodes.d.cts +1 -1
  94. package/dist/types/mobileErrorCodes.d.ts +1 -1
  95. package/dist/types/mobileErrorCodes.js +2 -2
  96. package/dist/types/mobileErrorCodes.js.map +1 -1
  97. package/dist/types/serverBaseTypes.d.cts +1 -1
  98. package/dist/types/serverBaseTypes.d.ts +1 -1
  99. package/dist/types/serverExecuteTypes.d.cts +2 -2
  100. package/dist/types/serverExecuteTypes.d.ts +2 -2
  101. package/dist/types/serverModelTypes.cjs.map +1 -1
  102. package/dist/types/serverModelTypes.d.cts +2 -2
  103. package/dist/types/serverModelTypes.d.ts +2 -2
  104. package/dist/types/serverModelTypes.js.map +1 -1
  105. package/dist/types/serverQueryTypes.d.cts +2 -2
  106. package/dist/types/serverQueryTypes.d.ts +2 -2
  107. package/dist/types/serverRequestTypes.d.cts +1 -1
  108. package/dist/types/serverRequestTypes.d.ts +1 -1
  109. package/dist/{types-C9YE4EIt.d.cts → types-CByl_NyJ.d.ts} +49 -18
  110. package/dist/types-CByl_NyJ.d.ts.map +1 -0
  111. package/dist/{types-Cwy1y_9x.d.ts → types-CR5iaKR4.d.cts} +49 -18
  112. package/dist/types-CR5iaKR4.d.cts.map +1 -0
  113. package/dist/utils/feed.cjs +9 -0
  114. package/dist/utils/feed.cjs.map +1 -1
  115. package/dist/utils/feed.d.cts +2 -2
  116. package/dist/utils/feed.d.ts +2 -2
  117. package/dist/utils/feed.js +9 -1
  118. package/dist/utils/feed.js.map +1 -1
  119. package/dist/utils/index.cjs +1 -0
  120. package/dist/utils/index.d.cts +3 -3
  121. package/dist/utils/index.d.ts +3 -3
  122. package/dist/utils/index.js +2 -2
  123. package/dist/utils/serverResponseGuards.d.cts +1 -1
  124. package/dist/utils/serverResponseGuards.d.ts +1 -1
  125. package/package.json +3 -3
  126. package/src/MobileClient.ts +82 -34
  127. package/src/dataMappers.test.ts +408 -9
  128. package/src/dataMappers.ts +76 -23
  129. package/src/signing/canonicalize.ts +6 -0
  130. package/src/signing/signing.test.ts +51 -0
  131. package/src/signing/types.ts +4 -0
  132. package/src/socialFeed.test.ts +53 -28
  133. package/src/types/clientTypes.ts +82 -47
  134. package/src/types/mobileErrorCodes.ts +2 -2
  135. package/src/types/serverExecuteTypes.ts +4 -12
  136. package/src/types/serverModelTypes.ts +19 -13
  137. package/src/types/serverQueryTypes.ts +32 -4
  138. package/src/types/serverRequestTypes.ts +20 -12
  139. package/src/utils/feed.ts +13 -0
  140. package/dist/MobileClient-M4oC4lrX.cjs.map +0 -1
  141. package/dist/canonicalize-Bv6UIAbR.d.ts.map +0 -1
  142. package/dist/canonicalize-CWh3XZzj.d.cts.map +0 -1
  143. package/dist/clientTypes-BUn5_OzK.d.ts.map +0 -1
  144. package/dist/clientTypes-DOpUcu80.d.cts.map +0 -1
  145. package/dist/eip712MethodByType-CJ6BoayT.d.cts.map +0 -1
  146. package/dist/eip712MethodByType-Dx4mvgni.d.ts.map +0 -1
  147. package/dist/feed-DErOLaoY.d.cts +0 -16
  148. package/dist/feed-DErOLaoY.d.cts.map +0 -1
  149. package/dist/feed-DErOLaoY.d.ts +0 -16
  150. package/dist/feed-DErOLaoY.d.ts.map +0 -1
  151. package/dist/payloadHash-CdkF2Ocr.d.cts.map +0 -1
  152. package/dist/payloadHash-HD29K77M.d.ts.map +0 -1
  153. package/dist/serverModelTypes-CGUZCi0y.d.cts.map +0 -1
  154. package/dist/serverModelTypes-CGUZCi0y.d.ts.map +0 -1
  155. package/dist/types-C9YE4EIt.d.cts.map +0 -1
  156. package/dist/types-Cwy1y_9x.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,
@@ -13,16 +16,22 @@ import {
13
16
  mapMobileSocialFeedPositionChart,
14
17
  mapMobileSocialFeedPositionHistory,
15
18
  mapMobileSocialFeedPositionPatch,
19
+ mapMobileSocialFeedPositionThoughts,
16
20
  mapMobileSocialFeedPresence,
17
21
  mapMobileSocialFeedTradeCard,
18
22
  } from './dataMappers';
23
+ import { MobileClient } from './MobileClient';
19
24
  import { MobileUnsupportedFeedItemError } from './types/MobileUnsupportedFeedItemError';
20
25
  import {
21
26
  MobileServerSocialFeedItem,
22
27
  MobileServerSocialFeedPost,
23
28
  MobileServerSocialFeedReply,
24
29
  } from './types/serverModelTypes';
25
- import { MobileServerSocialFeedPositionHistoryResponse } from './types/serverQueryTypes';
30
+ import {
31
+ MobileServerSocialFeedPositionHistoryResponse,
32
+ MobileServerSocialFeedPositionThoughtsResponse,
33
+ } from './types/serverQueryTypes';
34
+ import { isMobileSocialFeedUploadedImage } from './utils';
26
35
 
27
36
  const post: MobileServerSocialFeedPost = {
28
37
  id: '88888888-8888-4888-8888-888888888888',
@@ -182,6 +191,7 @@ const reply: MobileServerSocialFeedReply = {
182
191
  drawing: null,
183
192
  };
184
193
  const history: MobileServerSocialFeedPositionHistoryResponse = {
194
+ owner: post.author,
185
195
  position: {
186
196
  position_id: '22222222-2222-4222-8222-222222222222',
187
197
  product_id: 2,
@@ -365,9 +375,6 @@ const history: MobileServerSocialFeedPositionHistoryResponse = {
365
375
  expires_at_ms: 1791072900000,
366
376
  update_cursor: null,
367
377
  viewer_token: 'viewer_example_opaque',
368
- view_tracking: {
369
- mode: 'detail_visit',
370
- },
371
378
  },
372
379
  };
373
380
 
@@ -549,8 +556,9 @@ describe('[mobile-client]: content mapping', () => {
549
556
  ).toMatchObject({ kind: item.kind, images: [] });
550
557
  }
551
558
  const comments = mapMobileSocialFeedCommentsPage({
552
- comments: [serverReply],
559
+ comments: [{ ...serverReply, replies: [] }],
553
560
  total_count: 1,
561
+ top_level_count: 1,
554
562
  next_cursor: null,
555
563
  previous_cursor: null,
556
564
  anchor_reply_id: null,
@@ -559,7 +567,7 @@ describe('[mobile-client]: content mapping', () => {
559
567
  expect(comments.comments).toMatchObject([{ kind: 'reply', images: [] }]);
560
568
  });
561
569
 
562
- it('maps feed images and detail-visit read sessions', () => {
570
+ it('maps feed images and read sessions without obsolete view tracking', () => {
563
571
  const mapped = mapMobileSocialFeedItem({
564
572
  ...post,
565
573
  images: [
@@ -601,8 +609,14 @@ describe('[mobile-client]: content mapping', () => {
601
609
  next_cursor: null,
602
610
  watermark: 'w',
603
611
  read_session: history.read_session,
604
- }).readSession.viewTracking,
605
- ).toEqual({ mode: 'detail_visit' });
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
+ });
606
620
  });
607
621
 
608
622
  it('maps posts, drawings, reply previews, and nullable badges from the documented response', () => {
@@ -805,16 +819,18 @@ describe('[mobile-client]: content mapping', () => {
805
819
 
806
820
  it('maps comment cursors and anchor without inventing an observation timestamp', () => {
807
821
  const result = mapMobileSocialFeedCommentsPage({
808
- comments: [reply],
822
+ comments: [{ ...reply, replies: [] }],
809
823
  next_cursor: 'NextOpaque',
810
824
  previous_cursor: 'PreviousOpaque',
811
825
  total_count: 8,
826
+ top_level_count: 3,
812
827
  anchor_reply_id: reply.id,
813
828
  read_session: history.read_session,
814
829
  });
815
830
  expect(result.nextCursor).toBe('NextOpaque');
816
831
  expect(result.previousCursor).toBe('PreviousOpaque');
817
832
  expect(result.anchorReplyId).toBe(reply.id);
833
+ expect(result.topLevelCount).toBe(3);
818
834
  expect(result).not.toHaveProperty('asOfMs');
819
835
  });
820
836
 
@@ -832,6 +848,85 @@ describe('[mobile-client]: content mapping', () => {
832
848
  expect(result).not.toHaveProperty('asOfMs');
833
849
  });
834
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
+
835
930
  it('keeps mention labels and code-point offsets intact', () => {
836
931
  const result = mapMobileSocialFeedMentionToServer({
837
932
  subaccount: {
@@ -846,3 +941,307 @@ describe('[mobile-client]: content mapping', () => {
846
941
  expect(result.subaccount).toMatch(/^0x[0-9a-f]{64}$/);
847
942
  });
848
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
+ });
@@ -32,6 +32,7 @@ import {
32
32
  MobileSocialFeedActionResolutionResult,
33
33
  MobileSocialFeedAuthor,
34
34
  MobileSocialFeedCandle,
35
+ MobileSocialFeedCommentGroup,
35
36
  MobileSocialFeedCommentResult,
36
37
  MobileSocialFeedCommentsPage,
37
38
  MobileSocialFeedCompactCard,
@@ -59,6 +60,8 @@ import {
59
60
  MobileSocialFeedPositionHistory,
60
61
  MobileSocialFeedPositionPatch,
61
62
  MobileSocialFeedPositionResolutionResult,
63
+ MobileSocialFeedPositionThought,
64
+ MobileSocialFeedPositionThoughtsPage,
62
65
  MobileSocialFeedPositionsPage,
63
66
  MobileSocialFeedPost,
64
67
  MobileSocialFeedPresence,
@@ -76,7 +79,7 @@ import {
76
79
  MobileSocialFeedTradingContext,
77
80
  MobileSocialFeedTradingTarget,
78
81
  MobileSocialFeedUpdates,
79
- MobileSocialFeedViewsResult,
82
+ MobileSocialFeedUploadedImage,
80
83
  MobileSocialFeedWatchers,
81
84
  SearchMobileProfilesParams,
82
85
  } from './types/clientTypes';
@@ -90,7 +93,6 @@ import {
90
93
  MobileServerSocialFeedDrawingMutationResponse,
91
94
  MobileServerSocialFeedLikeResponse,
92
95
  MobileServerSocialFeedPresenceResponse,
93
- MobileServerSocialFeedViewsResponse,
94
96
  } from './types/serverExecuteTypes';
95
97
  import {
96
98
  MobileServerAvatarPreset,
@@ -109,6 +111,7 @@ import {
109
111
  MobileServerSocialFeedActionMarker,
110
112
  MobileServerSocialFeedAuthor,
111
113
  MobileServerSocialFeedCandle,
114
+ MobileServerSocialFeedCommentGroup,
112
115
  MobileServerSocialFeedCompactCard,
113
116
  MobileServerSocialFeedContentBase,
114
117
  MobileServerSocialFeedDrawing,
@@ -135,6 +138,7 @@ import {
135
138
  MobileServerSocialFeedTraderBadge,
136
139
  MobileServerSocialFeedTradingContext,
137
140
  MobileServerSocialFeedTradingTarget,
141
+ MobileServerSocialFeedUploadedImage,
138
142
  MobileSocialFeedMention,
139
143
  } from './types/serverModelTypes';
140
144
  import {
@@ -153,6 +157,8 @@ import {
153
157
  MobileServerSocialFeedPositionChartResponse,
154
158
  MobileServerSocialFeedPositionHistoryResponse,
155
159
  MobileServerSocialFeedPositionResolutionResponse,
160
+ MobileServerSocialFeedPositionThought,
161
+ MobileServerSocialFeedPositionThoughtsResponse,
156
162
  MobileServerSocialFeedPositionsResponse,
157
163
  MobileServerSocialFeedRankedResponse,
158
164
  MobileServerSocialFeedResponse,
@@ -799,9 +805,9 @@ export function mapMobileSocialFeedInteraction(
799
805
  * @param server - The service response.
800
806
  * @returns The mapped feed image.
801
807
  */
802
- export function mapMobileSocialFeedImage(
803
- server: MobileServerSocialFeedImage,
804
- ): MobileSocialFeedImage {
808
+ export function mapMobileSocialFeedUploadedImage(
809
+ server: MobileServerSocialFeedUploadedImage,
810
+ ): MobileSocialFeedUploadedImage {
805
811
  return {
806
812
  imageId: server.image_id,
807
813
  url: server.url,
@@ -817,6 +823,20 @@ export function mapMobileSocialFeedImage(
817
823
  };
818
824
  }
819
825
 
826
+ /**
827
+ * Maps either protected upload metadata or an external URL without inventing metadata.
828
+ * @param server - The service response.
829
+ * @returns The mapped uploaded image or external URL.
830
+ */
831
+ export function mapMobileSocialFeedImage(
832
+ server: MobileServerSocialFeedImage,
833
+ ): MobileSocialFeedImage {
834
+ if ('image_id' in server) {
835
+ return mapMobileSocialFeedUploadedImage(server);
836
+ }
837
+ return { url: server.url };
838
+ }
839
+
820
840
  /**
821
841
  * Maps common item metadata and server capabilities.
822
842
  * @param server - The service response.
@@ -976,7 +996,6 @@ export function mapMobileSocialFeedReadSession(
976
996
  sessionId: server.session_id,
977
997
  mode: server.mode,
978
998
  expiresAtMs: server.expires_at_ms,
979
- viewTracking: { mode: server.view_tracking.mode },
980
999
  updateCursor: server.update_cursor,
981
1000
  viewerToken: server.viewer_token,
982
1001
  };
@@ -1546,16 +1565,31 @@ export function mapMobileSocialFeedDrawingMutationResult(
1546
1565
  }
1547
1566
 
1548
1567
  /**
1549
- * Maps bidirectional comments and the actual returned session.
1568
+ * Maps a top-level comment and its complete flat descendants without sorting or dropping content.
1569
+ * @param server - A comment group from the service thread snapshot.
1570
+ * @returns The mapped top-level comment with each descendant's original parent relationship.
1571
+ */
1572
+ export function mapMobileSocialFeedCommentGroup(
1573
+ server: MobileServerSocialFeedCommentGroup,
1574
+ ): MobileSocialFeedCommentGroup {
1575
+ return {
1576
+ ...mapMobileSocialFeedReply(server),
1577
+ replies: server.replies.map(mapMobileSocialFeedReply),
1578
+ };
1579
+ }
1580
+
1581
+ /**
1582
+ * Maps complete comment groups in server order and the actual returned session.
1550
1583
  * @param server - Service payload with its original wire fields.
1551
- * @returns Mapped replies, total count, bidirectional cursors, nullable anchor, and the actual reading session.
1584
+ * @returns Mapped groups, whole-thread counts, bidirectional cursors, nullable anchor, and reading session.
1552
1585
  */
1553
1586
  export function mapMobileSocialFeedCommentsPage(
1554
1587
  server: MobileServerSocialFeedCommentsResponse,
1555
1588
  ): MobileSocialFeedCommentsPage {
1556
1589
  return {
1557
- comments: server.comments.map(mapMobileSocialFeedReply),
1590
+ comments: server.comments.map(mapMobileSocialFeedCommentGroup),
1558
1591
  totalCount: server.total_count,
1592
+ topLevelCount: server.top_level_count,
1559
1593
  nextCursor: server.next_cursor,
1560
1594
  previousCursor: server.previous_cursor,
1561
1595
  anchorReplyId: server.anchor_reply_id,
@@ -1593,20 +1627,6 @@ export function mapMobileSocialFeedLikeResult(
1593
1627
  };
1594
1628
  }
1595
1629
 
1596
- /**
1597
- * Maps exposure deduplication results without adding timestamps.
1598
- * @param server - Service payload with its original wire fields.
1599
- * @returns Newly accepted and duplicate item ID lists without inventing an observation timestamp.
1600
- */
1601
- export function mapMobileSocialFeedViewsResult(
1602
- server: MobileServerSocialFeedViewsResponse,
1603
- ): MobileSocialFeedViewsResult {
1604
- return {
1605
- acceptedItemIds: server.accepted_item_ids,
1606
- duplicateItemIds: server.duplicate_item_ids,
1607
- };
1608
- }
1609
-
1610
1630
  /**
1611
1631
  * Maps a published history take without treating unknown kinds as posts.
1612
1632
  * @param server - The published service item.
@@ -1668,6 +1688,7 @@ export function mapMobileSocialFeedPositionHistory(
1668
1688
  server: MobileServerSocialFeedPositionHistoryResponse,
1669
1689
  ): MobileSocialFeedPositionHistory {
1670
1690
  return {
1691
+ owner: mapMobileSocialFeedAuthor(server.owner),
1671
1692
  position: {
1672
1693
  ...mapMobileSocialFeedPositionCard(server.position),
1673
1694
  ...mapMobileSocialFeedHistoryMetrics(server.position),
@@ -1738,3 +1759,35 @@ export function mapMobileSocialFeedPresence(
1738
1759
  expiresInSeconds: server.expires_in_seconds,
1739
1760
  };
1740
1761
  }
1762
+
1763
+ /**
1764
+ * Maps a published thought, keeping its guaranteed analysis flag and drawing.
1765
+ * @param server - Service payload with its original wire fields.
1766
+ * @returns The mapped post or trade card with a non-null drawing.
1767
+ * @throws {MobileUnsupportedFeedItemError} If the service returns an unsupported take kind.
1768
+ */
1769
+ export function mapMobileSocialFeedPositionThought(
1770
+ server: MobileServerSocialFeedPositionThought,
1771
+ ): MobileSocialFeedPositionThought {
1772
+ return {
1773
+ ...mapMobileSocialFeedHistoryTake(server),
1774
+ hasAnalysis: server.has_analysis,
1775
+ drawing: mapMobileSocialFeedDrawing(server.drawing),
1776
+ };
1777
+ }
1778
+
1779
+ /**
1780
+ * Maps a thoughts page in server order, preserving empty pages with continuation cursors.
1781
+ * @param server - Service payload with its original wire fields.
1782
+ * @returns Thoughts, the next cursor, and the position reading session.
1783
+ * @throws {MobileUnsupportedFeedItemError} If an item is not a post or trade card.
1784
+ */
1785
+ export function mapMobileSocialFeedPositionThoughts(
1786
+ server: MobileServerSocialFeedPositionThoughtsResponse,
1787
+ ): MobileSocialFeedPositionThoughtsPage {
1788
+ return {
1789
+ items: server.items.map(mapMobileSocialFeedPositionThought),
1790
+ nextCursor: server.next_cursor,
1791
+ readSession: mapMobileSocialFeedReadSession(server.read_session),
1792
+ };
1793
+ }