six-degrees 0.4.8 → 0.4.9

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 (247) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/routes-manifest.json +6 -0
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  12. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.rsc +12 -12
  15. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +12 -12
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +11 -11
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js +2 -1
  23. package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/company-scores/route.js +2 -1
  25. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/scraper/route.js +2 -2
  39. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/settings/route.js +3 -2
  41. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js +2 -1
  43. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js +2 -1
  45. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/api/social/messages/route/app-paths-manifest.json +3 -0
  48. package/.next/standalone/.next/server/app/api/social/messages/route/build-manifest.json +12 -0
  49. package/.next/standalone/.next/server/app/api/social/messages/route/server-reference-manifest.json +4 -0
  50. package/.next/standalone/.next/server/app/api/social/messages/route.js +9 -0
  51. package/.next/standalone/.next/server/app/api/social/messages/route.js.map +5 -0
  52. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -0
  53. package/.next/standalone/.next/server/app/api/social/messages/route_client-reference-manifest.js +3 -0
  54. package/.next/standalone/.next/server/app/api/social/route.js +4 -2
  55. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  64. package/.next/standalone/.next/server/app/import.html +1 -1
  65. package/.next/standalone/.next/server/app/import.rsc +14 -14
  66. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +14 -14
  67. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
  68. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +13 -13
  69. package/.next/standalone/.next/server/app/index.html +1 -1
  70. package/.next/standalone/.next/server/app/index.rsc +14 -14
  71. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +13 -13
  72. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +14 -14
  73. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  74. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  76. package/.next/standalone/.next/server/app/launch.html +2 -2
  77. package/.next/standalone/.next/server/app/launch.rsc +14 -14
  78. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +14 -14
  79. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
  80. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +13 -13
  81. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  82. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  83. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  84. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  85. package/.next/standalone/.next/server/app/paths.html +1 -1
  86. package/.next/standalone/.next/server/app/paths.rsc +14 -14
  87. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +14 -14
  88. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
  89. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +13 -13
  90. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  91. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  92. package/.next/standalone/.next/server/app/profile.html +1 -1
  93. package/.next/standalone/.next/server/app/profile.rsc +14 -14
  94. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +14 -14
  95. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
  96. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +13 -13
  97. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  98. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  99. package/.next/standalone/.next/server/app/queue.html +1 -1
  100. package/.next/standalone/.next/server/app/queue.rsc +14 -14
  101. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +14 -14
  102. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
  103. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +13 -13
  104. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  105. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  106. package/.next/standalone/.next/server/app/scores.html +1 -1
  107. package/.next/standalone/.next/server/app/scores.rsc +14 -14
  108. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +14 -14
  109. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
  110. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +13 -13
  111. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  112. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  113. package/.next/standalone/.next/server/app/settings.html +1 -1
  114. package/.next/standalone/.next/server/app/settings.rsc +14 -14
  115. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +14 -14
  116. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  117. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +13 -13
  118. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  119. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/app/setup.html +1 -1
  121. package/.next/standalone/.next/server/app/setup.rsc +14 -14
  122. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +14 -14
  123. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
  124. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +13 -13
  125. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  126. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  127. package/.next/standalone/.next/server/app/social.html +1 -1
  128. package/.next/standalone/.next/server/app/social.rsc +14 -14
  129. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +14 -14
  130. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +1 -1
  131. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +13 -13
  132. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  133. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06mi9k2._.js +3 -0
  134. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +8 -0
  135. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0bat8nx._.js +1 -1
  136. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17hjtzl._.js +8 -0
  137. package/.next/standalone/.next/server/chunks/[root-of-the-server]__20tmem7._.js +1 -1
  138. package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +3 -0
  139. package/.next/standalone/.next/server/chunks/_1_fynia._.js +6 -6
  140. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  141. package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
  142. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_messages_route_actions_0skh09r.js +3 -0
  143. package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +8 -0
  144. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +1 -1
  145. package/.next/standalone/.next/server/chunks/lib_rpc_03i8xef.js +1 -1
  146. package/.next/standalone/.next/server/chunks/lib_rpc_08fc3rp.js +1 -1
  147. package/.next/standalone/.next/server/chunks/lib_rpc_08r2t_q.js +1 -1
  148. package/.next/standalone/.next/server/chunks/lib_rpc_0btpv1t.js +1 -1
  149. package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -5
  150. package/.next/standalone/.next/server/chunks/lib_rpc_0cm2ev3.js +1 -1
  151. package/.next/standalone/.next/server/chunks/lib_rpc_0iovswz.js +1 -1
  152. package/.next/standalone/.next/server/chunks/lib_rpc_0is8flp.js +1 -1
  153. package/.next/standalone/.next/server/chunks/lib_rpc_0v8ajtd.js +1 -1
  154. package/.next/standalone/.next/server/chunks/lib_rpc_0xgmvvt.js +1 -1
  155. package/.next/standalone/.next/server/chunks/lib_rpc_144mzbf.js +1 -1
  156. package/.next/standalone/.next/server/chunks/lib_rpc_1bggi0n.js +1 -1
  157. package/.next/standalone/.next/server/chunks/lib_rpc_1dkcu9f.js +1 -1
  158. package/.next/standalone/.next/server/chunks/lib_rpc_1o0jb6w.js +1 -1
  159. package/.next/standalone/.next/server/chunks/lib_scoring_1-e4ur0.js +3 -0
  160. package/.next/standalone/.next/server/chunks/node_modules_next_1zc5q0a._.js +1 -1
  161. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1feicqq._.js → [root-of-the-server]__0qdeuyo._.js} +1 -1
  162. package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +3 -0
  163. package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +1 -1
  164. package/.next/standalone/.next/server/chunks/ssr/{_1mj739k._.js → _0l8cxmo._.js} +2 -2
  165. package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +3 -0
  166. package/.next/standalone/.next/server/chunks/ssr/{_0wxbq78._.js → app_components_19c2d9c._.js} +2 -2
  167. package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
  168. package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
  169. package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
  170. package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +1 -1
  171. package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
  172. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  173. package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +1 -1
  174. package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +3 -0
  175. package/.next/standalone/.next/server/chunks/ssr/{node_modules_1wax83z._.js → node_modules_0h91jdk._.js} +2 -2
  176. package/.next/standalone/.next/server/chunks/ssr/{node_modules_1nkgb45._.js → node_modules_1f8t_rp._.js} +1 -1
  177. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  178. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  179. package/.next/standalone/.next/server/pages/404.html +1 -1
  180. package/.next/standalone/.next/server/pages/500.html +1 -1
  181. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  182. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  183. package/.next/{static/chunks/1z-zp93cyoxvz.js → standalone/.next/static/chunks/04bbi55urkrd6.js} +1 -1
  184. package/.next/{static/chunks/3q3pmt5vv-4pi.js → standalone/.next/static/chunks/054zdthcnyd2h.js} +1 -1
  185. package/.next/standalone/.next/static/chunks/{236f0jtp68j_h.js → 1_mohm2643nxp.js} +1 -1
  186. package/.next/{static/chunks/3zw8bv7-mc4n6.js → standalone/.next/static/chunks/1z_rt4jteewqp.js} +1 -1
  187. package/.next/standalone/.next/static/chunks/2bftn9gubwf0w.js +1 -0
  188. package/.next/{static/chunks/0g_p4p7oci18u.js → standalone/.next/static/chunks/2kfhc58df2xyo.js} +1 -1
  189. package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +1 -0
  190. package/.next/standalone/.next/static/chunks/39csdcx7yv227.js +1 -0
  191. package/.next/standalone/.next/static/chunks/3yzlf70j5t0-o.js +1 -0
  192. package/.next/standalone/.next/static/chunks/{2_wcobfqry2uj.js → 3z-swu5pet507.js} +1 -1
  193. package/.next/standalone/CHANGELOG.md +39 -0
  194. package/.next/standalone/app/api/social/messages/route.js +130 -0
  195. package/.next/standalone/app/api/social/route.js +89 -47
  196. package/.next/standalone/app/components/ScanStatusBar.js +40 -3
  197. package/.next/standalone/app/components/settings/DataSection.js +2 -1
  198. package/.next/standalone/app/paths/page.js +2 -0
  199. package/.next/standalone/app/profile/page.js +2 -0
  200. package/.next/standalone/app/queue/page.js +2 -0
  201. package/.next/standalone/app/scores/page.js +2 -0
  202. package/.next/standalone/app/settings/page.js +2 -0
  203. package/.next/standalone/app/setup/page.js +6 -3
  204. package/.next/standalone/app/social/Conversations.js +246 -0
  205. package/.next/standalone/app/social/page.js +90 -9
  206. package/.next/standalone/lib/galaxy-lab.js +6 -1
  207. package/.next/standalone/lib/linkedin-export.js +175 -5
  208. package/.next/standalone/lib/social-store.js +73 -0
  209. package/.next/standalone/package-lock.json +2 -2
  210. package/.next/standalone/package.json +1 -1
  211. package/.next/standalone/scripts/scrape.py +156 -23
  212. package/.next/{standalone/.next/static/chunks/1z-zp93cyoxvz.js → static/chunks/04bbi55urkrd6.js} +1 -1
  213. package/.next/{standalone/.next/static/chunks/3q3pmt5vv-4pi.js → static/chunks/054zdthcnyd2h.js} +1 -1
  214. package/.next/static/chunks/{236f0jtp68j_h.js → 1_mohm2643nxp.js} +1 -1
  215. package/.next/{standalone/.next/static/chunks/3zw8bv7-mc4n6.js → static/chunks/1z_rt4jteewqp.js} +1 -1
  216. package/.next/static/chunks/2bftn9gubwf0w.js +1 -0
  217. package/.next/{standalone/.next/static/chunks/0g_p4p7oci18u.js → static/chunks/2kfhc58df2xyo.js} +1 -1
  218. package/.next/static/chunks/2pw6_osz1n_te.js +1 -0
  219. package/.next/static/chunks/39csdcx7yv227.js +1 -0
  220. package/.next/static/chunks/3yzlf70j5t0-o.js +1 -0
  221. package/.next/static/chunks/{2_wcobfqry2uj.js → 3z-swu5pet507.js} +1 -1
  222. package/CHANGELOG.md +39 -0
  223. package/lib/galaxy-lab.js +6 -1
  224. package/lib/linkedin-export.js +175 -5
  225. package/lib/social-store.js +73 -0
  226. package/package.json +1 -1
  227. package/scripts/scrape.py +156 -23
  228. package/.next/standalone/.next/server/chunks/[root-of-the-server]__17kn1mk._.js +0 -8
  229. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1dmc7-t._.js +0 -8
  230. package/.next/standalone/.next/server/chunks/ssr/_005q9n1._.js +0 -3
  231. package/.next/standalone/.next/server/chunks/ssr/_217nddo._.js +0 -3
  232. package/.next/standalone/.next/server/chunks/ssr/app_0i3fw_m._.js +0 -3
  233. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_server-async-storage_0t1dato.js +0 -3
  234. package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  235. package/.next/standalone/.next/static/chunks/0y1yms5kez2f3.js +0 -1
  236. package/.next/standalone/.next/static/chunks/14m1zu-8warte.js +0 -1
  237. package/.next/standalone/.next/static/chunks/15qbli9udo0vh.js +0 -1
  238. package/.next/static/chunks/0a5iadnmt9u9k.js +0 -1
  239. package/.next/static/chunks/0y1yms5kez2f3.js +0 -1
  240. package/.next/static/chunks/14m1zu-8warte.js +0 -1
  241. package/.next/static/chunks/15qbli9udo0vh.js +0 -1
  242. /package/.next/standalone/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_buildManifest.js +0 -0
  243. /package/.next/standalone/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_clientMiddlewareManifest.js +0 -0
  244. /package/.next/standalone/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_ssgManifest.js +0 -0
  245. /package/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_buildManifest.js +0 -0
  246. /package/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_clientMiddlewareManifest.js +0 -0
  247. /package/.next/static/{T0eWMsKjgSiPnmx1E0q3G → yPlsfKhdS8MMtyXF7twSZ}/_ssgManifest.js +0 -0
@@ -0,0 +1,246 @@
1
+ 'use client';
2
+
3
+ // The Social tab's Conversations: every conversation with one of your
4
+ // connections, from your export and the live messages sync
5
+ // (app/api/social/messages). The list is only who, when, who wrote last,
6
+ // unread and how many. The messages themselves show only while Keep my
7
+ // messages is on, and live only in the app's data folder (lib/social-store.js).
8
+
9
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
10
+ import { sortConversations } from '../../lib/linkedin-export';
11
+ import { Body, LINE } from '../components/ui';
12
+
13
+ const TIER = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
14
+ const TIER_ORDER = ['S', 'A', 'B', 'C', 'D'];
15
+ const LIST_PAGE = 100;
16
+ const THREAD_PAGE = 100;
17
+ const EARLIER_PAGE = 200;
18
+ const day = (t) => (t ? new Date(t).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }) : '');
19
+ const when = (t) => (t ? new Date(t).toLocaleString(undefined, { day: 'numeric', month: 'short', year: 'numeric', hour: 'numeric', minute: '2-digit' }) : '');
20
+ const slug = (u) => String(u || '').replace(/\/+$/, '').split('/').pop();
21
+ // Only LinkedIn's own pages go behind "Open on LinkedIn".
22
+ const linkedin = (u) => (/^https:\/\/(www\.)?linkedin\.com\//.test(String(u || '')) ? u : null);
23
+
24
+ export default function Conversations({ byKey, connected, ready, keep, onKeep, switching, token }) {
25
+ const [data, setData] = useState(null);
26
+ const [q, setQ] = useState('');
27
+ const [hits, setHits] = useState(null);
28
+ const [open, setOpen] = useState(null);
29
+ const [shown, setShown] = useState(LIST_PAGE);
30
+
31
+ useEffect(() => {
32
+ let live = true;
33
+ fetch('/api/social/messages').then((r) => r.json()).then((d) => { if (live) setData(d); }).catch(() => { if (live) setData({ conversations: [] }); });
34
+ return () => { live = false; };
35
+ }, [token, keep]);
36
+
37
+ // Searching the words happens where they're kept, a moment after typing stops.
38
+ useEffect(() => {
39
+ const words = q.trim();
40
+ if (!keep || words.length < 2) return undefined;
41
+ let live = true;
42
+ const t = setTimeout(() => {
43
+ fetch(`/api/social/messages?q=${encodeURIComponent(words)}`).then((r) => r.json())
44
+ .then((d) => { if (live) setHits({ q: words, ids: new Set(d.ids || []) }); }).catch(() => {});
45
+ }, 250);
46
+ return () => { live = false; clearTimeout(t); };
47
+ }, [q, keep, token]);
48
+
49
+ // Only conversations with one of your connections; a group counts when at
50
+ // least one of them is in it. The rest are counted, not shown.
51
+ const { rows, leftOut } = useMemo(() => {
52
+ const all = data?.conversations || [];
53
+ const mine = all.filter((c) => c.people.some((k) => connected.has(k)));
54
+ const withPeople = mine.map((c) => {
55
+ const persons = c.people.map((k) => byKey.get(k)).filter(Boolean);
56
+ const tier = TIER_ORDER.find((t) => persons.some((p) => p.tier === t)) || null;
57
+ return { ...c, persons, tier };
58
+ });
59
+ return { rows: sortConversations(withPeople, (c) => c.tier), leftOut: all.length - mine.length };
60
+ }, [data, byKey, connected]);
61
+
62
+ const filtered = useMemo(() => {
63
+ const words = q.trim().toLowerCase();
64
+ if (!words) return rows;
65
+ // Word matches count only for what's typed now, and only while they're kept.
66
+ const said = keep && hits?.q === q.trim() ? hits.ids : null;
67
+ return rows.filter((c) => c.persons.some((p) => String(p.name || '').toLowerCase().includes(words))
68
+ || String(c.title || '').toLowerCase().includes(words)
69
+ || said?.has(c.id));
70
+ }, [rows, q, hits, keep]);
71
+
72
+ const box = { padding: '12px 14px', borderRadius: 8, border: LINE, background: 'rgba(255,255,255,0.03)', marginTop: 10 };
73
+
74
+ return (
75
+ <>
76
+ <div style={box}>
77
+ <label style={{ display: 'flex', gap: 8, alignItems: 'center', fontSize: 13.5, color: '#fff', cursor: switching ? 'default' : 'pointer' }}>
78
+ <input type="checkbox" checked={keep} disabled={switching} onChange={(e) => onKeep(e.target.checked)} />
79
+ <b>Keep my messages on this computer</b>
80
+ <span style={{ fontSize: 11.5, color: keep ? '#00ff88' : '#8b9a9a' }}>{keep ? 'on' : 'off'}</span>
81
+ </label>
82
+ <Body style={{ margin: '6px 0 0', fontSize: 12.5 }}>
83
+ Off unless you turn it on. When it&rsquo;s on, the messages themselves, including what other people wrote to
84
+ you, are saved only in the app&rsquo;s data folder on this computer. They&rsquo;re never sent anywhere, and
85
+ they aren&rsquo;t in &ldquo;Save a copy of my network&rdquo;. <i>Forget it</i>, or switching this off, deletes
86
+ them. With it off, the list below still shows who, when and who wrote last.
87
+ </Body>
88
+ </div>
89
+
90
+ <div style={{ ...box, padding: 0 }}>
91
+ <div style={{ padding: '10px 14px', borderBottom: LINE, display: 'flex', gap: 10, alignItems: 'center' }}>
92
+ <input
93
+ type="search"
94
+ value={q}
95
+ onChange={(e) => { setQ(e.target.value); setShown(LIST_PAGE); }}
96
+ placeholder={keep ? 'Search by name or by what was said' : 'Search by name'}
97
+ aria-label="Search conversations"
98
+ style={{ flex: 1, minWidth: 0, padding: '6px 10px', borderRadius: 6, border: LINE, background: 'rgba(0,0,0,0.3)', color: '#fff', fontSize: 13 }}
99
+ />
100
+ <span style={{ fontSize: 12, color: '#8b9a9a', whiteSpace: 'nowrap' }}>
101
+ {filtered.length.toLocaleString()} {filtered.length === 1 ? 'conversation' : 'conversations'}
102
+ </span>
103
+ </div>
104
+ {(!data || !ready) && <div style={{ padding: 12, fontSize: 13, color: '#8b9a9a' }}>Loading…</div>}
105
+ {data && ready && rows.length === 0 && (
106
+ <div style={{ padding: 12, fontSize: 13, color: '#8b9a9a' }}>
107
+ No conversations with your connections yet. Choose your export folder above, or sync your messages.
108
+ </div>
109
+ )}
110
+ {data && ready && rows.length > 0 && filtered.length === 0 && (
111
+ <div style={{ padding: 12, fontSize: 13, color: '#8b9a9a' }}>Nothing matches that.</div>
112
+ )}
113
+ {ready && filtered.slice(0, shown).map((c) => (
114
+ <Row key={c.id} c={c} open={open === c.id} onToggle={() => setOpen(open === c.id ? null : c.id)} keep={keep} token={token} />
115
+ ))}
116
+ {ready && filtered.length > shown && (
117
+ <button onClick={() => setShown(shown + LIST_PAGE)} style={{ width: '100%', padding: 10, background: 'none', border: 'none', color: '#3498DB', cursor: 'pointer', fontSize: 13 }}>
118
+ Show {Math.min(LIST_PAGE, filtered.length - shown).toLocaleString()} more
119
+ </button>
120
+ )}
121
+ </div>
122
+ <div style={{ fontSize: 11.5, color: '#778', marginTop: 6 }}>
123
+ Waiting on you first (they wrote last), then S and A tier, then the most recent. Groups are shown but not
124
+ counted in warmth.{leftOut > 0 ? ` ${leftOut.toLocaleString()} ${leftOut === 1 ? 'conversation is' : 'conversations are'} with people who aren’t your connections and aren’t shown.` : ''}
125
+ </div>
126
+ </>
127
+ );
128
+ }
129
+
130
+ function Row({ c, open, onToggle, keep, token }) {
131
+ const one = !c.group ? c.persons[0] : null;
132
+ const names = c.persons.map((p) => p.name).filter(Boolean);
133
+ const label = c.group
134
+ ? (c.title || (names.length ? `${names.slice(0, 3).join(', ')}${c.people.length > 3 ? ` +${c.people.length - 3}` : ''}` : `${c.people.length} people`))
135
+ : (one?.name || slug(c.people[0]));
136
+ const href = linkedin(c.threadUrl) || (!c.group ? linkedin(one?.profile_url || c.people[0]) : null);
137
+ const waiting = !c.group && c.lastFromThem === true;
138
+ return (
139
+ <div style={{ borderBottom: '1px solid rgba(255,255,255,0.04)' }}>
140
+ <div
141
+ role="button"
142
+ tabIndex={0}
143
+ aria-expanded={open}
144
+ onClick={onToggle}
145
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggle(); } }}
146
+ style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 10, padding: '8px 14px', fontSize: 13, cursor: 'pointer', background: open ? 'rgba(255,255,255,0.04)' : 'none' }}
147
+ >
148
+ <span style={{ width: 8, height: 8, borderRadius: '50%', background: TIER[c.tier] || '#556', flexShrink: 0 }} />
149
+ <span style={{ flex: '1 1 160px', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
150
+ <b>{label}</b>
151
+ {c.group && <span style={{ marginLeft: 6, fontSize: 10.5, padding: '1px 6px', borderRadius: 4, background: 'rgba(255,255,255,0.08)', color: '#b8c4c4' }}>Group</span>}
152
+ {one?.company ? <span style={{ color: '#8b9a9a' }}> · {one.company}</span> : null}
153
+ </span>
154
+ {c.unread > 0 && <span style={{ fontSize: 11, padding: '1px 7px', borderRadius: 10, background: '#3498DB', color: '#fff' }}>{c.unread} unread</span>}
155
+ {c.lastFromThem != null && (
156
+ <span style={{ color: waiting ? '#ff9f43' : '#8b9a9a', fontSize: 12, whiteSpace: 'nowrap' }}>{c.lastFromThem ? 'They wrote last' : 'You wrote last'}</span>
157
+ )}
158
+ <span style={{ color: '#b8c4c4', fontSize: 12, whiteSpace: 'nowrap', minWidth: 86, textAlign: 'right' }}>{day(c.last)}</span>
159
+ <span style={{ color: '#8b9a9a', fontSize: 12, whiteSpace: 'nowrap', minWidth: 74, textAlign: 'right' }}>
160
+ {c.count != null ? `${c.count.toLocaleString()} ${c.count === 1 ? 'message' : 'messages'}` : ''}
161
+ </span>
162
+ {href
163
+ ? <a href={href} target="_blank" rel="noreferrer" onClick={(e) => e.stopPropagation()} style={{ color: '#3498DB', textDecoration: 'none', whiteSpace: 'nowrap', fontSize: 12 }}>Open on LinkedIn ↗</a>
164
+ : <span style={{ minWidth: 104 }} />}
165
+ </div>
166
+ {open && <Thread key={`${c.id}:${token}:${keep}`} id={c.id} keep={keep} group={c.group} token={token} />}
167
+ </div>
168
+ );
169
+ }
170
+
171
+ // One conversation's messages, newest at the bottom, a page at a time: the
172
+ // newest 100 first and 200 more each time you ask, so a thread of thousands
173
+ // opens as fast as a short one.
174
+ function Thread({ id, keep, group, token }) {
175
+ const [t, setT] = useState(null);
176
+ const [err, setErr] = useState(null);
177
+ const scroller = useRef(null);
178
+ const keepAt = useRef(null);
179
+
180
+ useEffect(() => {
181
+ if (!keep) return undefined;
182
+ let live = true;
183
+ fetch(`/api/social/messages?id=${encodeURIComponent(id)}&limit=${THREAD_PAGE}`).then((r) => r.json())
184
+ .then((d) => { if (live) { if (d.error) setErr(d.error); else { keepAt.current = 'bottom'; setT(d); } } })
185
+ .catch(() => { if (live) setErr('It couldn’t be read.'); });
186
+ return () => { live = false; };
187
+ }, [id, keep, token]);
188
+
189
+ // Opened at the newest; after "Show earlier", where you were stays put.
190
+ useLayoutEffect(() => {
191
+ const el = scroller.current;
192
+ if (!el || !keepAt.current) return;
193
+ if (keepAt.current === 'bottom') el.scrollTop = el.scrollHeight;
194
+ else el.scrollTop = el.scrollHeight - keepAt.current;
195
+ keepAt.current = null;
196
+ }, [t]);
197
+
198
+ async function earlier() {
199
+ const el = scroller.current;
200
+ const d = await fetch(`/api/social/messages?id=${encodeURIComponent(id)}&before=${t.start}&limit=${EARLIER_PAGE}`).then((r) => r.json()).catch(() => null);
201
+ if (!d || d.error) return;
202
+ keepAt.current = el ? el.scrollHeight - el.scrollTop : null;
203
+ setT({ ...t, start: d.start, messages: [...d.messages, ...t.messages] });
204
+ }
205
+
206
+ const pad = { padding: '10px 14px 14px 32px', fontSize: 12.5, color: '#8b9a9a' };
207
+ if (!keep) return <div style={pad}>Turn on <b>Keep my messages</b> to read them here.</div>;
208
+ if (err) return <div style={{ ...pad, color: '#ff9b9b' }}>{err}</div>;
209
+ if (!t) return <div style={pad}>Opening…</div>;
210
+ if (!t.total) {
211
+ return (
212
+ <div style={pad}>
213
+ No messages are kept for this conversation yet. Choose your export folder again, or sync your messages, to
214
+ bring them in.
215
+ </div>
216
+ );
217
+ }
218
+ return (
219
+ <div ref={scroller} style={{ maxHeight: 440, overflowY: 'auto', padding: '8px 14px 12px', background: 'rgba(0,0,0,0.2)' }}>
220
+ {t.start > 0 && (
221
+ <div style={{ textAlign: 'center', margin: '4px 0 10px' }}>
222
+ <button onClick={earlier} style={{ background: 'rgba(255,255,255,0.06)', border: LINE, borderRadius: 6, color: '#cfe6f7', cursor: 'pointer', fontSize: 12, padding: '4px 12px' }}>
223
+ Show earlier ({t.start.toLocaleString()} more)
224
+ </button>
225
+ </div>
226
+ )}
227
+ {t.messages.map((m, i) => (
228
+ <div key={t.start + i} style={{ display: 'flex', justifyContent: m.fromMe ? 'flex-end' : 'flex-start', margin: '6px 0' }}>
229
+ <div style={{ maxWidth: '72%' }}>
230
+ <div style={{
231
+ padding: '7px 11px', borderRadius: 12, fontSize: 13, lineHeight: 1.45, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere',
232
+ background: m.fromMe ? 'rgba(52,152,219,0.35)' : 'rgba(255,255,255,0.08)', color: '#eef',
233
+ borderBottomRightRadius: m.fromMe ? 4 : 12, borderBottomLeftRadius: m.fromMe ? 12 : 4,
234
+ }}
235
+ >
236
+ {m.text || <i style={{ color: '#8b9a9a' }}>(no words: an attachment or a reaction)</i>}
237
+ </div>
238
+ <div style={{ fontSize: 10.5, color: '#778', marginTop: 2, textAlign: m.fromMe ? 'right' : 'left' }}>
239
+ {m.fromMe ? 'You' : group ? 'Someone in the group' : 'Them'} · {when(m.t)}
240
+ </div>
241
+ </div>
242
+ </div>
243
+ ))}
244
+ </div>
245
+ );
246
+ }
@@ -6,25 +6,54 @@
6
6
  // your data), read here in the browser: full history, no traffic to LinkedIn;
7
7
  // - an experimental live sync (the scanner's --messages): your messages list,
8
8
  // read once in your own Chrome, by hand or once a day.
9
- // Kept on this computer: dates, who wrote last and counts. Never message text.
9
+ // Kept on this computer: dates, who wrote last and counts, and the messages
10
+ // themselves only if you turn on Keep my messages (Conversations.js), in a file
11
+ // of their own that switching it off deletes.
10
12
 
11
- import { useEffect, useMemo, useState } from 'react';
13
+ import { useEffect, useMemo, useRef, useState } from 'react';
12
14
  import Link from 'next/link';
13
15
  import OnboardingGate from '../components/OnboardingGate';
14
16
  import { useUser } from '../components/UserProvider';
15
17
  import { loadNetwork } from '../../lib/network';
16
- import { readTable, messageStats, careerChapters, postingEffect, invitationSplit, warmthOf, repliesWaiting } from '../../lib/linkedin-export';
18
+ import {
19
+ readTable, messageStats, careerChapters, postingEffect, invitationSplit, warmthOf, repliesWaiting, buildConversations, conversationSummary,
20
+ } from '../../lib/linkedin-export';
17
21
  import { mergeSocial } from '../../lib/galaxy-lab';
18
22
  import { keyFor } from '../../lib/separation';
19
23
  import { runScrape, watchScanner, scannerNow } from '../../lib/scraper-client';
20
24
  import { Body, LINE, FONT } from '../components/ui';
21
25
  import { IS_DEMO } from '../../lib/demo';
26
+ import Conversations from './Conversations';
22
27
 
23
28
  const TIER = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
24
29
  const WARMTH = { warm: ['Warm', '#ff9f43'], cool: ['Cool', '#3498DB'], dormant: ['Dormant', '#8899aa'], never: ['Never messaged', '#556'] };
25
30
  const FILES = { 'connections.csv': 'first name', 'messages.csv': 'conversation id', 'invitations.csv': 'direction', 'positions.csv': 'company name', 'shares.csv': 'date' };
26
31
  const day = (t) => (t ? new Date(t).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }) : '');
27
32
  const slug = (u) => String(u || '').replace(/\/+$/, '').split('/').pop();
33
+ // Messages go to the server in parts of about this many characters: a request
34
+ // here can carry 10 MB, and a character can take up to 3 bytes.
35
+ const PART_CHARS = 2_500_000;
36
+
37
+ /** An import's messages, in parts that each fit in one request. */
38
+ async function sendMessages(conversations) {
39
+ const parts = [];
40
+ let cur = {};
41
+ let size = 0;
42
+ for (const c of conversations) {
43
+ for (const m of c.messages) {
44
+ const n = JSON.stringify(m).length + 2;
45
+ if (size + n > PART_CHARS && size > 0) { parts.push(cur); cur = {}; size = 0; }
46
+ if (!cur[c.id]) { cur[c.id] = { title: c.title || '', messages: [] }; size += c.id.length + (c.title || '').length + 40; }
47
+ cur[c.id].messages.push(m);
48
+ size += n;
49
+ }
50
+ }
51
+ parts.push(cur);
52
+ for (let i = 0; i < parts.length; i++) {
53
+ const r = await fetch('/api/social/messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ part: i, parts: parts.length, threads: parts[i] }) });
54
+ if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || 'The messages couldn’t be saved.');
55
+ }
56
+ }
28
57
 
29
58
  export default function SocialPage() {
30
59
  return <OnboardingGate><SocialInner /></OnboardingGate>;
@@ -38,8 +67,16 @@ function SocialInner() {
38
67
  const [note, setNote] = useState(null);
39
68
  const [busy, setBusy] = useState(false);
40
69
  const [job, setJob] = useState(() => scannerNow());
70
+ const [switching, setSwitching] = useState(false);
71
+ const [listToken, setListToken] = useState(0);
72
+ // The last export's messages.csv, in this window only, so turning Keep my
73
+ // messages on right after an import needn't ask for the folder again.
74
+ const lastMessages = useRef(null);
75
+ const keep = social?.keepMessages === true;
41
76
 
42
- const reload = () => fetch('/api/social').then((r) => r.json()).then((d) => setSocial(d.social || null)).catch(() => setSocial(null));
77
+ // The Conversations list reads again whenever this does (listToken).
78
+ const reload = () => fetch('/api/social').then((r) => r.json()).then((d) => setSocial(d.social || null)).catch(() => setSocial(null))
79
+ .finally(() => setListToken((n) => n + 1));
43
80
  useEffect(() => { reload(); }, []);
44
81
  useEffect(() => watchScanner(() => setJob(scannerNow())), []);
45
82
  useEffect(() => {
@@ -55,6 +92,7 @@ function SocialInner() {
55
92
  for (const r of [...(net?.degree1 || []), ...(net?.degree2 || [])]) if (!m.has(keyFor(r))) m.set(keyFor(r), r);
56
93
  return m;
57
94
  }, [net]);
95
+ const connected = useMemo(() => new Set((net?.degree1 || []).map(keyFor)), [net]);
58
96
 
59
97
  async function importFolder(list) {
60
98
  setBusy(true);
@@ -68,6 +106,9 @@ function SocialInner() {
68
106
  if (!Object.keys(found).length) throw new Error('That folder doesn’t have LinkedIn’s export files in it (Connections.csv, messages.csv and so on).');
69
107
  const conns = found['connections.csv'] || [];
70
108
  const stats = found['messages.csv'] ? messageStats(found['messages.csv']) : { asOf: null, people: new Map() };
109
+ // The words are read out of messages.csv only when Keep my messages is on.
110
+ const built = found['messages.csv'] ? buildConversations(found['messages.csv'], { keepText: keep }) : null;
111
+ lastMessages.current = found['messages.csv'] || null;
71
112
  const people = {};
72
113
  for (const [url, v] of stats.people) people[url] = { last: v.last, lastFromThem: v.lastFromThem, total: v.total, recent: v.recent };
73
114
  if (keepEmails) {
@@ -82,6 +123,7 @@ function SocialInner() {
82
123
  const body = {
83
124
  asOf,
84
125
  people,
126
+ conversations: built ? built.conversations.map(conversationSummary) : [],
85
127
  chapters: careerChapters(found['positions.csv'] || [], conns, asOf),
86
128
  posts: found['shares.csv'] ? postingEffect(found['shares.csv'], conns) : null,
87
129
  invites: found['invitations.csv'] ? invitationSplit(found['invitations.csv']) : null,
@@ -89,7 +131,12 @@ function SocialInner() {
89
131
  };
90
132
  const r = await fetch('/api/social', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
91
133
  if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || 'It couldn’t be saved.');
92
- setNote({ good: true, text: `Read ${Object.keys(found).length} of LinkedIn’s files: ${stats.people.size.toLocaleString()} people you’ve messaged one to one.` });
134
+ if (built && keep) await sendMessages(built.conversations);
135
+ setNote({
136
+ good: true,
137
+ text: `Read ${Object.keys(found).length} of LinkedIn’s files: ${stats.people.size.toLocaleString()} people you’ve messaged one to one`
138
+ + (built ? `, ${built.conversations.length.toLocaleString()} conversations${keep ? ', their messages kept on this computer' : ''}.` : '.'),
139
+ });
93
140
  await reload();
94
141
  } catch (e) {
95
142
  setNote({ good: false, text: e.message });
@@ -114,9 +161,37 @@ function SocialInner() {
114
161
  reload();
115
162
  }
116
163
 
164
+ // Keep my messages. On: from now on the export's and the sync's words are
165
+ // kept, and if an export was read in this window, its messages are kept now.
166
+ // Off, once you've said so: the kept messages are deleted.
167
+ async function setKeep(on) {
168
+ if (!on && !window.confirm('Delete the messages kept on this computer? The list of conversations stays; the messages themselves are deleted.')) return;
169
+ setSwitching(true);
170
+ setNote(null);
171
+ try {
172
+ if (!on) await fetch('/api/social/messages', { method: 'DELETE' });
173
+ const r = await fetch('/api/social', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keepMessages: on }) });
174
+ if (!r.ok) throw new Error('The switch couldn’t be saved.');
175
+ if (on && lastMessages.current) {
176
+ await sendMessages(buildConversations(lastMessages.current, { keepText: true }).conversations);
177
+ setNote({ good: true, text: 'Your messages are kept on this computer now.' });
178
+ } else if (on) {
179
+ setNote({ good: true, text: 'On. Choose your export folder again, or sync your messages, to bring the messages in.' });
180
+ } else {
181
+ setNote({ good: true, text: 'Off. The kept messages are deleted.' });
182
+ }
183
+ } catch (e) {
184
+ setNote({ good: false, text: e.message });
185
+ } finally {
186
+ setSwitching(false);
187
+ reload();
188
+ }
189
+ }
190
+
117
191
  async function forget() {
118
192
  await fetch('/api/social', { method: 'DELETE' });
119
- setNote({ good: true, text: 'Forgotten. Nothing from your export or messages is kept here now.' });
193
+ lastMessages.current = null;
194
+ setNote({ good: true, text: 'Forgotten. Nothing from your export or messages is kept here now, the messages themselves included.' });
120
195
  reload();
121
196
  }
122
197
 
@@ -151,11 +226,13 @@ function SocialInner() {
151
226
  <Link href="/" style={{ color: '#888', textDecoration: 'none', fontSize: 13, fontWeight: 600, padding: '6px 14px', borderRadius: 6, background: 'rgba(255,255,255,0.06)', border: LINE }}>← Back to Map</Link>
152
227
  <h1 style={{ fontSize: 22, fontWeight: 700, margin: 0, color: '#FFD700' }}>Social <span style={{ fontSize: 12, color: '#8b9a9a', fontWeight: 600 }}>experimental</span></h1>
153
228
  </header>
229
+ {/* The scan status bar sits here, under the header's buttons (ScanStatusBar.js). */}
230
+ <div id="scan-bar-slot" />
154
231
  <main style={{ maxWidth: 980, margin: '0 auto', padding: '8px 24px 64px' }}>
155
232
  <Body style={{ marginTop: 16 }}>
156
233
  Who you&rsquo;re warm with, who&rsquo;s waiting on a reply, and how each chapter of your career built your network,
157
- from your own LinkedIn data. Only dates, who wrote last and counts are kept on this computer, never what a
158
- message says.
234
+ from your own LinkedIn data. Only dates, who wrote last and counts are kept on this computer, and what
235
+ messages say only if you turn on <i>Keep my messages</i> under Conversations.
159
236
  </Body>
160
237
 
161
238
  <h2 style={h2}>Your LinkedIn export</h2>
@@ -182,7 +259,8 @@ function SocialInner() {
182
259
  <div style={box}>
183
260
  <Body style={{ margin: 0 }}>
184
261
  Reads your messages list once, in your own Chrome: who each one-to-one conversation is with, when it was last
185
- active and whether it&rsquo;s unread. It uses no search budget and never opens or keeps a message&rsquo;s text.
262
+ active, whether it&rsquo;s unread and who wrote last. It uses no search budget and opens no conversation. The
263
+ newest message&rsquo;s words are kept only if <i>Keep my messages</i> is on.
186
264
  </Body>
187
265
  <div style={{ marginTop: 10, display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
188
266
  <button onClick={syncNow} disabled={running} style={{ padding: '7px 14px', borderRadius: 8, border: 'none', background: running ? '#333' : 'rgba(0,255,136,0.15)', color: running ? '#777' : '#00ff88', cursor: running ? 'default' : 'pointer', fontSize: 13 }}>
@@ -196,6 +274,9 @@ function SocialInner() {
196
274
  </div>
197
275
  {note && <div style={{ marginTop: 10, fontSize: 13, color: note.good ? '#00ff88' : '#ff9b9b' }}>{note.text}</div>}
198
276
 
277
+ <h2 style={h2}>Conversations</h2>
278
+ <Conversations byKey={byKey} connected={connected} ready={!!net} keep={keep} onKeep={setKeep} switching={switching || busy} token={listToken} />
279
+
199
280
  {social && (
200
281
  <>
201
282
  <h2 style={h2}>Warmth</h2>
@@ -215,7 +215,12 @@ export function mergeSocial(social) {
215
215
  for (const [url, v] of Object.entries(social?.live || {})) {
216
216
  const k = keyFor({ profile_url: url });
217
217
  const cur = merged.get(k) || { total: 0, recent: 0 };
218
- if (v.last && (!cur.last || v.last > cur.last)) cur.last = v.last;
218
+ // The live sync's word on who wrote last counts when it's the newer news:
219
+ // no export yet, or the conversation moved on since the export was made.
220
+ if (v.last && (!cur.last || v.last > cur.last)) {
221
+ cur.last = v.last;
222
+ if (typeof v.lastFromThem === 'boolean') cur.lastFromThem = v.lastFromThem;
223
+ }
219
224
  cur.unread = v.unread;
220
225
  cur.total = Math.max(cur.total || 0, 1);
221
226
  merged.set(k, cur);