six-degrees 0.4.10 → 0.4.11

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 (213) 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 +4 -4
  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 +13 -13
  15. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  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.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
  41. package/.next/standalone/.next/server/app/api/social/crm/route/app-paths-manifest.json +3 -0
  42. package/.next/standalone/.next/server/app/api/social/crm/route/build-manifest.json +12 -0
  43. package/.next/standalone/.next/server/app/api/social/crm/route/server-reference-manifest.json +4 -0
  44. package/.next/standalone/.next/server/app/api/social/crm/route.js +10 -0
  45. package/.next/standalone/.next/server/app/api/social/crm/route.js.map +5 -0
  46. package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -0
  47. package/.next/standalone/.next/server/app/api/social/crm/route_client-reference-manifest.js +3 -0
  48. package/.next/standalone/.next/server/app/api/social/messages/route.js +4 -3
  49. package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
  50. package/.next/standalone/.next/server/app/api/social/route.js +4 -3
  51. package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
  55. package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/.next/server/app/import.html +1 -1
  60. package/.next/standalone/.next/server/app/import.rsc +15 -15
  61. package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
  62. package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
  63. package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +14 -14
  64. package/.next/standalone/.next/server/app/index.html +1 -1
  65. package/.next/standalone/.next/server/app/index.rsc +15 -15
  66. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  67. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  68. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  69. package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/app/launch.html +2 -2
  72. package/.next/standalone/.next/server/app/launch.rsc +15 -15
  73. package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
  74. package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
  75. package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +14 -14
  76. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  77. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  78. package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
  79. package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
  80. package/.next/standalone/.next/server/app/paths.html +1 -1
  81. package/.next/standalone/.next/server/app/paths.rsc +15 -15
  82. package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
  83. package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
  84. package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +14 -14
  85. package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
  86. package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
  87. package/.next/standalone/.next/server/app/profile.html +1 -1
  88. package/.next/standalone/.next/server/app/profile.rsc +15 -15
  89. package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
  90. package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
  91. package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +14 -14
  92. package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
  93. package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
  94. package/.next/standalone/.next/server/app/queue.html +1 -1
  95. package/.next/standalone/.next/server/app/queue.rsc +15 -15
  96. package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
  97. package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
  98. package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +14 -14
  99. package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
  100. package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
  101. package/.next/standalone/.next/server/app/scores.html +1 -1
  102. package/.next/standalone/.next/server/app/scores.rsc +15 -15
  103. package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
  104. package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
  105. package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +14 -14
  106. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  107. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  108. package/.next/standalone/.next/server/app/settings.html +1 -1
  109. package/.next/standalone/.next/server/app/settings.rsc +15 -15
  110. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
  111. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
  112. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +14 -14
  113. package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
  114. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  115. package/.next/standalone/.next/server/app/setup.html +1 -1
  116. package/.next/standalone/.next/server/app/setup.rsc +15 -15
  117. package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
  118. package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
  119. package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +14 -14
  120. package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
  121. package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
  122. package/.next/standalone/.next/server/app/social.html +1 -1
  123. package/.next/standalone/.next/server/app/social.rsc +15 -15
  124. package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
  125. package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
  126. package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +14 -14
  127. package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
  128. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__06mi9k2._.js → [root-of-the-server]__05_3-22._.js} +7 -2
  129. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
  130. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tir0vc._.js +8 -0
  131. package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +1 -1
  132. package/.next/standalone/.next/server/chunks/_1_fynia._.js +4 -4
  133. package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
  134. package/.next/standalone/.next/server/chunks/_1oqfuh8._.js +3 -0
  135. package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
  136. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_crm_route_actions_0d1aflv.js +3 -0
  137. package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +7 -7
  138. package/.next/standalone/.next/server/chunks/lib_linkedin-export_12imqxg.js +3 -0
  139. package/.next/standalone/.next/server/chunks/lib_rpc_05gt5ya.js +3 -0
  140. package/.next/standalone/.next/server/chunks/ssr/_0-85h6g._.js +3 -0
  141. package/.next/standalone/.next/server/chunks/ssr/_08cfj2b._.js +3 -0
  142. package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +1 -1
  143. package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
  144. package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +2 -1
  145. package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +3 -0
  146. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  147. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  148. package/.next/standalone/.next/server/pages/404.html +1 -1
  149. package/.next/standalone/.next/server/pages/500.html +1 -1
  150. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  151. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  152. package/.next/standalone/.next/static/chunks/15sjx07xhpn6p.js +1 -0
  153. package/.next/standalone/.next/static/chunks/1a74dky22dxd6.js +1 -0
  154. package/.next/standalone/.next/static/chunks/{1xptlvtcz6654.css → 1x_ysly42-teq.css} +1 -1
  155. package/.next/standalone/.next/static/chunks/297e1z34yi--z.js +1 -0
  156. package/.next/standalone/.next/static/chunks/2_ak6qjxb6oa5.js +1 -0
  157. package/.next/standalone/.next/static/chunks/3ui4d-uqkqj5d.js +2 -0
  158. package/.next/standalone/CHANGELOG.md +48 -0
  159. package/.next/standalone/app/api/data/export/route.js +2 -1
  160. package/.next/standalone/app/api/scraper/route.js +5 -1
  161. package/.next/standalone/app/api/social/crm/route.js +37 -0
  162. package/.next/standalone/app/api/social/messages/route.js +44 -8
  163. package/.next/standalone/app/api/social/route.js +80 -22
  164. package/.next/standalone/app/components/ScanStatusBar.js +1 -0
  165. package/.next/standalone/app/components/settings/DataSection.js +12 -3
  166. package/.next/standalone/app/globals.css +11 -0
  167. package/.next/standalone/app/social/Crm.js +683 -0
  168. package/.next/standalone/app/social/Thread.js +90 -0
  169. package/.next/standalone/app/social/page.js +71 -60
  170. package/.next/standalone/lib/data-export.js +10 -4
  171. package/.next/standalone/lib/data-folder.js +9 -0
  172. package/.next/standalone/lib/data-import.js +13 -3
  173. package/.next/standalone/lib/linkedin-export.js +267 -46
  174. package/.next/standalone/lib/social-crm.js +415 -0
  175. package/.next/standalone/lib/social-store.js +13 -6
  176. package/.next/standalone/package-lock.json +2 -2
  177. package/.next/standalone/package.json +1 -1
  178. package/.next/standalone/scripts/scrape.py +126 -22
  179. package/.next/static/chunks/15sjx07xhpn6p.js +1 -0
  180. package/.next/static/chunks/1a74dky22dxd6.js +1 -0
  181. package/.next/static/chunks/{1xptlvtcz6654.css → 1x_ysly42-teq.css} +1 -1
  182. package/.next/static/chunks/297e1z34yi--z.js +1 -0
  183. package/.next/static/chunks/2_ak6qjxb6oa5.js +1 -0
  184. package/.next/static/chunks/3ui4d-uqkqj5d.js +2 -0
  185. package/CHANGELOG.md +48 -0
  186. package/lib/data-export.js +10 -4
  187. package/lib/data-folder.js +9 -0
  188. package/lib/data-import.js +13 -3
  189. package/lib/linkedin-export.js +267 -46
  190. package/lib/social-crm.js +415 -0
  191. package/lib/social-store.js +13 -6
  192. package/package.json +1 -1
  193. package/scripts/scrape.py +126 -22
  194. package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +0 -8
  195. package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +0 -8
  196. package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +0 -3
  197. package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +0 -3
  198. package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +0 -3
  199. package/.next/standalone/.next/static/chunks/07dyt47mp4s9-.js +0 -1
  200. package/.next/standalone/.next/static/chunks/206rm6j42gl_a.js +0 -1
  201. package/.next/standalone/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
  202. package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +0 -1
  203. package/.next/standalone/app/social/Conversations.js +0 -246
  204. package/.next/static/chunks/07dyt47mp4s9-.js +0 -1
  205. package/.next/static/chunks/206rm6j42gl_a.js +0 -1
  206. package/.next/static/chunks/2coxzjm_k6ilr.js +0 -1
  207. package/.next/static/chunks/2pw6_osz1n_te.js +0 -1
  208. /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → jqE2D3zWqSGz5su948Azf}/_buildManifest.js +0 -0
  209. /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → jqE2D3zWqSGz5su948Azf}/_clientMiddlewareManifest.js +0 -0
  210. /package/.next/standalone/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → jqE2D3zWqSGz5su948Azf}/_ssgManifest.js +0 -0
  211. /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → jqE2D3zWqSGz5su948Azf}/_buildManifest.js +0 -0
  212. /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → jqE2D3zWqSGz5su948Azf}/_clientMiddlewareManifest.js +0 -0
  213. /package/.next/static/{7zWJnSkfSoRhRsqY6ZKtD → jqE2D3zWqSGz5su948Azf}/_ssgManifest.js +0 -0
@@ -0,0 +1,2 @@
1
+ (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,60451,e=>{"use strict";var t=e.i(43476),n=e.i(71645),o=e.i(22016),s=e.i(2776),r=e.i(15812),a=e.i(19821),i=e.i(84278),l=e.i(39160),c=e.i(7214),d=e.i(40996),p=e.i(8819),u=e.i(64745);let h=Object.freeze([{id:"new",label:"New"},{id:"contacted",label:"Contacted"},{id:"replied",label:"Replied"},{id:"meeting",label:"Meeting"},{id:"won",label:"Won/Partner"},{id:"not-now",label:"Not now"}]);h.map(e=>e.id);let g=e=>h.find(t=>t.id===e)?.label||"",f=e=>"string"==typeof e&&e.length>0&&e.length<300;function m(){return{version:1,settings:{awaitDays:7},people:{}}}function y(e=new Date){let t=e=>String(e).padStart(2,"0");return`${e.getFullYear()}-${t(e.getMonth()+1)}-${t(e.getDate())}`}let x=/^https:\/\/(www\.)?linkedin\.com\/in\/[^/?#\s]+$/,w={S:0,A:1,B:2,C:3,D:4},b=(e,t)=>(e.degree??9)<(t.degree??9)||(e.degree??9)===(t.degree??9)&&(w[e.tier]??9)<(w[t.tier]??9),v=e=>"sponsored"===e.kind;function j(e,t,n=7){let o=[];for(let s of e){if(v(s)||!1!==s.lastFromThem||!s.last)continue;let e=Math.floor((t-s.last)/864e5);e>=n&&o.push({contact:s,waited:e})}return o.sort((e,t)=>e.waited-t.waited)}function k(e,t,n=null){let o=String(t||"").trim().toLowerCase();return!o||[e.name,e.row?.company,e.row?.role,e.row?.headline,...e.crm?.tags||[],e.crm?.notes].filter(Boolean).join("\n").toLowerCase().includes(o)||!!n&&e.conversations.some(e=>n.has(e.id))}function S(e){let t=null==e?"":String(e);return/^[=+\-@\t\r]/.test(t)&&(t=`'${t}`),/[",\r\n]/.test(t)?`"${t.replace(/"/g,'""')}"`:t}let T=e=>e?new Date(e).toISOString().slice(0,10):"",C=["Name","Company","Profile","Connection","Stage","Tags","Last contact","Who wrote last","Next follow-up","Notes"];function z({id:e,keep:o,group:s,token:r}){let[a,i]=(0,n.useState)(null),[l,c]=(0,n.useState)(null),d=(0,n.useRef)(null),u=(0,n.useRef)(null);async function h(){let t=d.current,n=await fetch(`/api/social/messages?id=${encodeURIComponent(e)}&before=${a.start}&limit=200`).then(e=>e.json()).catch(()=>null);n&&!n.error&&(u.current=t?t.scrollHeight-t.scrollTop:null,i({...a,start:n.start,messages:[...n.messages,...a.messages]}))}(0,n.useEffect)(()=>{if(!o)return;let t=!0;return fetch(`/api/social/messages?id=${encodeURIComponent(e)}&limit=100`).then(e=>e.json()).then(e=>{t&&(e.error?c(e.error):(u.current="bottom",i(e)))}).catch(()=>{t&&c("It couldn’t be read.")}),()=>{t=!1}},[e,o,r]),(0,n.useLayoutEffect)(()=>{let e=d.current;e&&u.current&&("bottom"===u.current?e.scrollTop=e.scrollHeight:e.scrollTop=e.scrollHeight-u.current,u.current=null)},[a]);let g={padding:"10px 14px 14px",fontSize:12.5,color:"#8b9a9a"};return o?l?(0,t.jsx)("div",{style:{...g,color:"#ff9b9b"},children:l}):a?a.total?(0,t.jsxs)("div",{ref:d,style:{maxHeight:440,overflowY:"auto",padding:"8px 14px 12px",background:"rgba(0,0,0,0.2)"},children:[a.start>0&&(0,t.jsx)("div",{style:{textAlign:"center",margin:"4px 0 10px"},children:(0,t.jsxs)("button",{onClick:h,style:{background:"rgba(255,255,255,0.06)",border:p.LINE,borderRadius:6,color:"#cfe6f7",cursor:"pointer",fontSize:12,padding:"4px 12px"},children:["Show earlier (",a.start.toLocaleString()," more)"]})}),a.messages.map((e,n)=>{let o;return(0,t.jsx)("div",{style:{display:"flex",justifyContent:e.fromMe?"flex-end":"flex-start",margin:"6px 0"},children:(0,t.jsxs)("div",{style:{maxWidth:"72%"},children:[(0,t.jsx)("div",{style:{padding:"7px 11px",borderRadius:12,fontSize:13,lineHeight:1.45,whiteSpace:"pre-wrap",overflowWrap:"anywhere",background:e.fromMe?"rgba(52,152,219,0.35)":"rgba(255,255,255,0.08)",color:"#eef",borderBottomRightRadius:e.fromMe?4:12,borderBottomLeftRadius:e.fromMe?12:4},children:e.text||(0,t.jsx)("i",{style:{color:"#8b9a9a"},children:"(no words: an attachment or a reaction)"})}),(0,t.jsxs)("div",{style:{fontSize:10.5,color:"#778",marginTop:2,textAlign:e.fromMe?"right":"left"},children:[e.fromMe?"You":s?"Someone in the group":"Them"," · ",(o=e.t)?new Date(o).toLocaleString(void 0,{day:"numeric",month:"short",year:"numeric",hour:"numeric",minute:"2-digit"}):""]})]})},a.start+n)})]}):(0,t.jsx)("div",{style:g,children:"No messages are kept for this conversation yet. Choose your export folder again, or sync your messages, to bring them in."}):(0,t.jsx)("div",{style:g,children:"Opening…"}):(0,t.jsxs)("div",{style:g,children:["Turn on ",(0,t.jsx)("b",{children:"Keep my messages"})," to read them here."]})}let I={S:"#FFD700",A:"#9B59B6",B:"#3498DB",C:"#95A5A6",D:"#BDC3C7"},D={new:"#8b9a9a",contacted:"#3498DB",replied:"#00d4aa",meeting:"#ff9f43",won:"#FFD700","not-now":"#667"},$=e=>e?new Date(e).toLocaleDateString(void 0,{day:"numeric",month:"short",year:"numeric"}):"",F=e=>e?new Date(`${e}T12:00:00`).toLocaleDateString(void 0,{day:"numeric",month:"short",year:"numeric"}):"",L=e=>/^https:\/\/(www\.)?linkedin\.com\//.test(String(e||""))?e:null,N=(e,t,n=`${t}s`)=>`${e.toLocaleString()} ${1===e?t:n}`,O=[["inbox","Inbox"],["awaiting","Awaiting reply"],["due","Follow-ups due"],["pipeline","Pipeline"],["sent","Sent"],["received","Received"],["all","All"]],R=(e,t="#dfe8e8")=>({fontSize:10.5,padding:"1px 6px",borderRadius:4,background:e,color:t,whiteSpace:"nowrap"}),E={background:"rgba(255,255,255,0.06)",border:p.LINE,borderRadius:6,color:"#cfe6f7",cursor:"pointer",fontSize:12,padding:"4px 10px"},M={padding:"6px 10px",borderRadius:6,border:p.LINE,background:"rgba(0,0,0,0.3)",color:"#fff",fontSize:13};function B({net:e,asOf:o,keep:s,onKeep:r,switching:a,token:l,crmToken:d}){let[u,w]=(0,n.useState)(null),[z,I]=(0,n.useState)(null),[D,L]=(0,n.useState)("inbox"),[R,U]=(0,n.useState)(""),[A,Y]=(0,n.useState)(null),[_,K]=(0,n.useState)(null),[G,V]=(0,n.useState)(100),[Q,X]=(0,n.useState)(null),Z=(0,n.useRef)(null);(0,n.useEffect)(()=>{let e=!0;return fetch("/api/social/messages").then(e=>e.json()).then(t=>{e&&w(t)}).catch(()=>{e&&w({conversations:[],invitations:[],names:{},inviteNotes:{}})}),()=>{e=!1}},[l,s]),(0,n.useEffect)(()=>{let e=!0;return fetch("/api/social/crm").then(e=>e.json()).then(t=>{e&&I(t.crm||m())}).catch(()=>{e&&I(m())}),()=>{e=!1}},[d]),(0,n.useEffect)(()=>{let e=R.trim();if(!s||e.length<2)return;let t=!0,n=setTimeout(()=>{fetch(`/api/social/messages?q=${encodeURIComponent(e)}`).then(e=>e.json()).then(n=>{t&&Y({q:e,ids:new Set(n.ids||[])})}).catch(()=>{})},250);return()=>{t=!1,clearTimeout(n)}},[R,s,l]);let ee=_?.type==="person"?_.key:null,{contacts:et,groups:en}=(0,n.useMemo)(()=>(function({conversations:e=[],invitations:t=[],network:n={},crm:o=m(),names:s={},also:r=[]}={}){let a=n.degree1||[],l=n.degree2||[],d=[...a,...l,...n.degree3||[]],p=new Map,u=new Map;for(let e of d){let t=(0,c.keyFor)(e),n=p.get(t);(!n||b(e,n))&&p.set(t,e)}for(let[e,t]of p){let n=(0,i.nameKey)(t.name);n&&u.set(n,u.has(n)&&u.get(n)!==e?null:e)}let h=new Set(a.map(c.keyFor)),g=(0,c.routeIndex)(l),y=new Map(a.map(e=>[e.id,e])),w=e=>e.startsWith("name:")&&u.get(e.slice(5))||e,v=new Map,j=e=>{var t,n,r;let a=w(e),i=v.get(a);if(i)return i;let l=p.get(a)||null;return i={key:a,name:(t=a,n=l,r=s,n?.name?n.name:r[t]?r[t]:t.startsWith("name:")?t.slice(5).replace(/\b\w/g,e=>e.toUpperCase()):String(t||"").replace(/\/+$/,"").split("/").pop()),row:l,connection:h.has(a),degree:l?.degree??null,profileUrl:x.test(a)?a:l?.profile_url&&x.test((0,c.keyFor)(l))?(0,c.keyFor)(l):null,introducers:l&&2===l.degree?[...new Set((0,c.routesFor)(l,g,y).map(e=>e.bridge?.name).filter(Boolean))]:[],conversations:[],groups:[],last:null,lastFromThem:null,unread:null,threadUrl:null,count:0,theyWrote:!1,kind:null,invitesIn:[],invitesOut:[],request:null,crm:o?.people?.[a]||null,lastContact:null},v.set(a,i),i},k=[];for(let t of e){if(!t||!Array.isArray(t.people))continue;if(t.group){k.push({...t,people:t.people.map(w)});continue}if(!t.people.length){k.push({...t,unknown:!0});continue}let e=j(t.people[0]);e.conversations.push(t),e.count+=t.count||0,(!0===t.lastFromThem||null!=t.count&&null!=t.mine&&t.count>t.mine)&&(e.theyWrote=!0),t.last&&(!e.last||t.last>=e.last)?(e.last=t.last,e.lastFromThem=t.lastFromThem,e.unread=t.unread,e.threadUrl=t.threadUrl||e.threadUrl):e.last||t.last||(e.threadUrl=t.threadUrl||e.threadUrl)}for(let e of v.values()){e.conversations.sort((e,t)=>(t.last||0)-(e.last||0));let t=new Set(e.conversations.map(e=>e.kind||null));e.kind=1===t.size?[...t][0]:null}for(let e of k)for(let t of e.people)v.get(t)?.groups.push(e.id);for(let e of t){if(!e?.key)continue;let t=j(e.key);!e.name||t.row||s[t.key]||(t.name=e.name),("in"===e.dir?t.invitesIn:t.invitesOut).push(e)}for(let e of d){if("sent"!==e.outreach_status&&"accepted"!==e.outreach_status&&"pending"!==e.unlock_status)continue;let t=j((0,c.keyFor)(e)),n=t.connection||"accepted"===e.outreach_status?"accepted":"pending";t.request&&"accepted"!==n||(t.request={status:n})}for(let e of Object.keys(o?.people||{}))j(e);for(let e of r)f(e)&&j(e);for(let e of v.values()){let t=[e.last,...e.invitesIn.map(e=>e.t),...e.invitesOut.map(e=>e.t)].filter(Boolean);e.lastContact=t.length?Math.max(...t):null}return{contacts:v,groups:k}})({conversations:u?.conversations||[],invitations:u?.invitations||[],network:e||{},crm:z||m(),names:u?.names||{},also:ee?[ee]:[]}),[u,e,z,ee]),eo=(0,n.useMemo)(()=>{let t=new Map;for(let n of[...e?.degree1||[],...e?.degree2||[]])t.has((0,c.keyFor)(n))||t.set((0,c.keyFor)(n),n);let n=u?.names||{};return e=>et.get(e)?.name||t.get(e)?.name||n[e]||null},[et,e,u]),es=z?.settings?.awaitDays||7,[er]=(0,n.useState)(()=>Date.now()),ea=o||er,ei=y(new Date(er)),el=(0,n.useMemo)(()=>[...et.values()],[et]),ec=(0,n.useMemo)(()=>{let e=(e,t)=>({type:"person",key:e.key,c:e,sub:t,t:e.lastContact||0}),t=e=>null==e.lastFromThem?"":e.lastFromThem?`They wrote ${$(e.last)}`:`You wrote ${$(e.last)}`,n=el.filter(e=>e.lastContact||e.crm||e.request).map(n=>e(n,t(n)||(n.invitesOut[0]?`You asked to connect ${$(n.invitesOut[0].t)}`:"")||(n.invitesIn[0]?`Asked to connect ${$(n.invitesIn[0].t)}`:"")||(n.request?`Tracked request \xb7 ${n.request.status}`:"")||(n.crm?.stage?g(n.crm.stage):"")));for(let e of en)n.push({type:"group",id:e.id,g:e,sub:e.last?`${e.lastFromThem?"Someone wrote":"You wrote"} ${$(e.last)}`:"",t:e.last||0});return n.sort((e,t)=>t.t-e.t),{inbox:[...el].filter(e=>!v(e)&&(!0===e.lastFromThem||e.unread>0)).sort((e,t)=>(t.unread>0)-(e.unread>0)||(t.last||0)-(e.last||0)).map(n=>e(n,n.unread>0?`${n.unread} unread \xb7 ${t(n)}`:t(n))),awaiting:j(el,ea,es).map(t=>e(t.contact,`You wrote ${$(t.contact.last)}, no reply for ${N(t.waited,"day")}`)),due:(function(e,t=y()){return[...e].filter(e=>e.crm?.followUp&&e.crm.followUp<=t).sort((e,t)=>e.crm.followUp.localeCompare(t.crm.followUp))})(el,ei).map(t=>e(t,t.crm.followUp===ei?"Follow up today":`Follow up was due ${F(t.crm.followUp)}`)),pipeline:function(e){let t=Object.fromEntries(h.map(e=>[e.id,[]]));for(let n of e)n.crm?.stage&&t[n.crm.stage].push(n);for(let e of Object.values(t))e.sort((e,t)=>(t.lastContact||0)-(e.lastContact||0));return t}(el),sent:(function(e,t,n=7){let o=[...e],s=new Map(j(o,t,n).map(e=>[e.contact.key,e.waited])),r=[];for(let e of o){let t=[];for(let n of e.invitesOut)t.push({type:"invite",id:n.id,t:n.t,status:e.connection?"accepted":"pending"});e.request&&t.push({type:"request",status:e.request.status}),s.has(e.key)&&t.push({type:"awaiting",t:e.last,waited:s.get(e.key)}),t.length&&r.push({contact:e,items:t,t:Math.max(0,...t.map(e=>e.t||0))})}return r.sort((e,t)=>t.t-e.t)})(el,ea,es).map(t=>e(t.contact,t.items.map(e=>"invite"===e.type?`Request ${$(e.t)} \xb7 ${e.status}`:"request"===e.type?`Tracked request \xb7 ${e.status}`:`No reply for ${N(e.waited,"day")}`).join(" · "))),received:(function(e){let t=[];for(let n of e)!n.connection&&n.invitesIn.length&&t.push({contact:n,t:Math.max(0,...n.invitesIn.map(e=>e.t||0))});return t.sort((e,t)=>t.t-e.t)})(el).map(t=>e(t.contact,`Asked to connect ${$(t.t)}`)),all:n}},[el,en,ea,es,ei]),ed=s&&A?.q===R.trim()?A.ids:null,ep=R.trim().toLowerCase(),eu=(0,n.useMemo)(()=>{let t="pipeline"===D?[]:ec[D];if(!ep)return t;let n=t.filter(e=>"person"===e.type?k(e.c,ep,ed):String(e.g.title||"").toLowerCase().includes(ep)||e.g.people.some(e=>String(eo(e)||"").toLowerCase().includes(ep))||ed?.has(e.g.id));if("all"===D&&ep.length>=2&&e){let t=new Set(n.map(e=>e.key)),o=new Set;for(let s of[...e.degree1||[],...e.degree2||[]]){let e=(0,c.keyFor)(s);if(!(t.has(e)||o.has(e))&&[s.name,s.company,s.role,s.headline].some(e=>String(e||"").toLowerCase().includes(ep))&&(o.add(e),n.push({type:"person",key:e,c:null,row:s,sub:"Nothing on file yet",t:0}),o.size>=50))break}}return n},[ec,D,ep,ed,e,eo]),eh=(0,n.useMemo)(()=>{if("pipeline"!==D)return null;let e={};for(let[t,n]of Object.entries(ec.pipeline))e[t]=ep?n.filter(e=>k(e,ep,ed)):n;return e},[ec,D,ep,ed]),eg={inbox:ec.inbox.length,awaiting:ec.awaiting.length,due:ec.due.length,pipeline:Object.values(ec.pipeline).reduce((e,t)=>e+t.length,0),sent:ec.sent.length,received:ec.received.length,all:ec.all.length};function ef(e){K(e),window.matchMedia?.("(max-width: 760px)").matches&&setTimeout(()=>Z.current?.scrollIntoView({behavior:"smooth",block:"start"}),50)}async function em(e,t){let n=await fetch("/api/social/crm",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:e,set:t})}),o=await n.json().catch(()=>({}));if(!n.ok)throw Error(o.error||"That couldn’t be saved.");I(t=>{let n={...t||m(),people:{...t?.people||{}}};return o.entry?n.people[e]=o.entry:delete n.people[e],n})}async function ey(e){let t=await fetch("/api/social/crm",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({settings:{awaitDays:e}})}),n=await t.json().catch(()=>({}));t.ok&&I(e=>({...e||m(),settings:n.settings}))}async function ex(){window.confirm("Delete your CRM: every stage, tag, note and follow-up date you added? This can’t be undone.")&&(await fetch("/api/social/crm",{method:"DELETE"}),I(m()),X("Your CRM notes are deleted."))}let ew=!u||!z||!e,eb={padding:"12px 14px",borderRadius:8,border:p.LINE,background:"rgba(255,255,255,0.03)",marginTop:10},ev=_?.type==="group"?en.find(e=>e.id===_.id):null,ej=ee?et.get(ee):null;return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)("div",{style:eb,children:[(0,t.jsxs)("label",{style:{display:"flex",gap:8,alignItems:"center",fontSize:13.5,color:"#fff",cursor:a?"default":"pointer"},children:[(0,t.jsx)("input",{type:"checkbox",checked:s,disabled:a,onChange:e=>r(e.target.checked)}),(0,t.jsx)("b",{children:"Keep my messages on this computer"}),(0,t.jsx)("span",{style:{fontSize:11.5,color:s?"#00ff88":"#8b9a9a"},children:s?"on":"off"})]}),(0,t.jsxs)(p.Body,{style:{margin:"6px 0 0",fontSize:12.5},children:["Off unless you turn it on. When it’s on, the messages themselves, including what other people wrote to you, and the notes sent with connection requests, are saved only in the app’s data folder on this computer. They’re never sent anywhere, and they aren’t in “Save a copy of my network”. ",(0,t.jsx)("i",{children:"Forget it"}),", or switching this off, deletes them. With it off, the CRM still shows who, when and who wrote last. Your own stages, tags, notes and follow-ups are always kept, on this computer only."]})]}),(0,t.jsxs)("div",{className:"crm-grid",style:{marginTop:10},children:[(0,t.jsxs)("div",{className:"pipeline"===D?"crm-wide":void 0,style:{...eb,marginTop:0,padding:0,minWidth:0},children:[(0,t.jsx)("div",{role:"tablist","aria-label":"CRM views",style:{display:"flex",flexWrap:"wrap",gap:4,padding:"10px 10px 6px",borderBottom:p.LINE},children:O.map(([e,n])=>(0,t.jsxs)("button",{role:"tab","aria-selected":D===e,onClick:()=>{L(e),V(100)},style:{...E,padding:"4px 9px",fontSize:12,background:D===e?"rgba(52,152,219,0.28)":"rgba(255,255,255,0.04)",color:D===e?"#fff":"#b8c4c4"},children:[n," ",(0,t.jsx)("span",{style:{color:D===e?"#cfe6f7":"#778"},children:ew?"":eg[e].toLocaleString()})]},e))}),(0,t.jsxs)("div",{style:{padding:"8px 10px",borderBottom:p.LINE,display:"flex",gap:8,alignItems:"center",flexWrap:"wrap"},children:[(0,t.jsx)("input",{type:"search",value:R,onChange:e=>{U(e.target.value),V(100)},placeholder:s?"Search names, companies, tags, notes, what was said":"Search names, companies, tags, notes","aria-label":"Search the CRM",style:{...M,flex:"1 1 180px",minWidth:0}}),(0,t.jsx)("button",{onClick:function(){var e;let t,n,o=el.filter(e=>"sponsored"!==e.kind).sort((e,t)=>(t.lastContact||0)-(e.lastContact||0)),s=`Six Degrees CRM ${y()}.csv`;e=function(e){let t=[C.map(S).join(",")];for(let n of e)t.push([n.name,n.row?.company||"",n.profileUrl||"",n.connection?"Yes":"No",g(n.crm?.stage),(n.crm?.tags||[]).join("; "),T(n.lastContact),null==n.lastFromThem?"":n.lastFromThem?"Them":"You",n.crm?.followUp||"",n.crm?.notes||""].map(S).join(","));return`\uFEFF${t.join("\r\n")}\r
2
+ `}(o),t=URL.createObjectURL(new Blob([e],{type:"text/csv;charset=utf-8"})),(n=document.createElement("a")).href=t,n.download=s,n.style.position="fixed",n.style.left="-9999px",document.body.appendChild(n),n.click(),n.remove(),setTimeout(()=>URL.revokeObjectURL(t),6e4),X(`Made “${s}” (${N(o.length,"person","people")}). Look for it in your Downloads folder, or wherever you chose to save it. It holds your notes: keep it private.`)},disabled:ew,style:E,title:"A spreadsheet of everyone here, with your stages, tags, follow-ups and notes",children:"Export CSV"})]}),"awaiting"===D&&(0,t.jsxs)("div",{style:{padding:"8px 12px",borderBottom:p.LINE,fontSize:12.5,color:"#b8c4c4",display:"flex",gap:6,alignItems:"center",flexWrap:"wrap"},children:["You wrote last and nothing came back for",(0,t.jsx)("input",{type:"number",min:1,max:365,value:es,"aria-label":"Days with no reply",onChange:e=>{let t=Number(e.target.value);t>=1&&t<=365&&ey(t)},style:{...M,width:64,padding:"3px 6px"}}),"days or more",o?`, as of ${$(o)}`:"","."]}),Q&&(0,t.jsx)("div",{style:{padding:"8px 12px",fontSize:12.5,color:"#00ff88",borderBottom:p.LINE},children:Q}),ew&&(0,t.jsx)("div",{style:{padding:12,fontSize:13,color:"#8b9a9a"},children:"Loading…"}),!ew&&"pipeline"===D&&(0,t.jsx)(P,{pipeline:eh,picked:ee,onPick:e=>ef({type:"person",key:e})}),!ew&&"pipeline"!==D&&0===eu.length&&(0,t.jsx)("div",{style:{padding:12,fontSize:13,color:"#8b9a9a"},children:ep?"Nothing matches that.":W[D]}),!ew&&"pipeline"!==D&&(0,t.jsxs)("div",{children:[eu.slice(0,G).map(e=>(0,t.jsx)(q,{r:e,nameOf:eo,active:"person"===e.type?ee===e.key:_?.id===e.id,onPick:()=>ef("person"===e.type?{type:"person",key:e.key}:{type:"group",id:e.id})},"person"===e.type?`p:${e.key}`:`g:${e.id}`)),eu.length>G&&(0,t.jsxs)("button",{onClick:()=>V(G+100),style:{width:"100%",padding:10,background:"none",border:"none",color:"#3498DB",cursor:"pointer",fontSize:13},children:["Show ",Math.min(100,eu.length-G).toLocaleString()," more of ",eu.length.toLocaleString()]})]})]}),(0,t.jsxs)("div",{ref:Z,className:"pipeline"===D?"crm-wide":void 0,style:{...eb,marginTop:0,minWidth:0,scrollMarginTop:80},children:[!_&&(0,t.jsx)("div",{style:{fontSize:13,color:"#8b9a9a"},children:"Pick someone to see everything with them, and to add a stage, tags, notes or a follow-up."}),ej&&(0,t.jsx)(H,{c:ej,nameOf:eo,groups:en,keep:s,token:l,inviteNotes:u?.inviteNotes||{},onChange:em,onPickGroup:e=>ef({type:"group",id:e})},ej.key),ev&&(0,t.jsx)(J,{g:ev,contacts:et,nameOf:eo,keep:s,token:l,onPick:e=>ef({type:"person",key:e})})]})]}),(0,t.jsxs)("div",{style:{fontSize:11.5,color:"#778",marginTop:6},children:["Everyone from your export and the live sync, connections or not; groups are listed and marked. Sponsored messages are marked and kept out of the Inbox. Awaiting reply and warmth are judged as of the newest thing your data knows.",Object.keys(z?.people||{}).length>0&&(0,t.jsxs)(t.Fragment,{children:[" ","Your notes are in the app’s data folder, not in “Save a copy of my network”."," ",(0,t.jsx)("button",{onClick:ex,style:{background:"none",border:"none",color:"#ff9b9b",cursor:"pointer",fontSize:11.5,padding:0},children:"Delete my CRM notes"})]})]})]})}let W={inbox:"No one is waiting on a reply.",awaiting:"Nobody you wrote to is overdue a reply.",due:"No follow-ups are due. Set one on anyone, and it shows here on the day.",sent:"Nothing sent yet: choose your export folder above (its Invitations.csv), or mark requests sent in the app.",received:"No requests waiting on you, as far as your export shows.",all:"Nobody here yet. Choose your export folder above, or sync your messages."};function U({tier:e}){return(0,t.jsx)("span",{style:{width:8,height:8,borderRadius:"50%",background:I[e]||"#556",flexShrink:0,display:"inline-block"}})}function A({c:e}){return(0,t.jsxs)(t.Fragment,{children:[e&&!e.connection&&(0,t.jsx)("span",{style:R("rgba(255,159,67,0.18)","#ffc58a"),children:2===e.degree?"2nd degree":"Not a connection"}),e?.kind==="sponsored"&&(0,t.jsx)("span",{style:R("rgba(255,255,255,0.08)"),children:"Sponsored"}),e?.kind==="inmail"&&(0,t.jsx)("span",{style:R("rgba(155,89,182,0.25)"),children:"InMail"}),e?.crm?.stage&&(0,t.jsx)("span",{style:R("rgba(255,255,255,0.06)",D[e.crm.stage]),children:g(e.crm.stage)})]})}function q({r:e,nameOf:n,active:o,onPick:s}){let r,a=null,i=null;if("person"===e.type){let n=e.c;r=n?.name||e.row?.name||e.key,a=n?.row?.tier||e.row?.tier,i=(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(A,{c:n||{connection:e.row?.degree===1,degree:e.row?.degree}}),n?.unread>0&&(0,t.jsxs)("span",{style:R("#3498DB","#fff"),children:[n.unread," unread"]})]})}else r=Y(e.g,n),i=(0,t.jsx)("span",{style:R("rgba(255,255,255,0.08)"),children:e.g.unknown?"To someone unnamed":"Group"});let l="person"===e.type?e.c?.row?.company||e.row?.company:null;return(0,t.jsxs)("div",{role:"button",tabIndex:0,"aria-pressed":o,onClick:s,onKeyDown:e=>{("Enter"===e.key||" "===e.key)&&(e.preventDefault(),s())},style:{padding:"8px 12px",borderBottom:"1px solid rgba(255,255,255,0.04)",cursor:"pointer",background:o?"rgba(52,152,219,0.14)":"none"},children:[(0,t.jsxs)("div",{style:{display:"flex",alignItems:"center",gap:6,fontSize:13,minWidth:0,flexWrap:"wrap"},children:[(0,t.jsx)(U,{tier:a}),(0,t.jsx)("b",{style:{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",maxWidth:"100%"},children:r}),i]}),(0,t.jsx)("div",{style:{fontSize:11.5,color:"#8b9a9a",marginTop:2,paddingLeft:14,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:[l,e.sub].filter(Boolean).join(" · ")})]})}function Y(e,t){if(e.title)return e.title;if(e.unknown)return"A conversation only you wrote in";let n=e.people.map(t).filter(Boolean);return n.length?`${n.slice(0,3).join(", ")}${e.people.length>3?` +${e.people.length-3}`:""}`:`${e.people.length} people`}function P({pipeline:e,picked:n,onPick:o}){return(0,t.jsx)("div",{className:"crm-board",children:h.map(s=>(0,t.jsxs)("div",{style:{minWidth:0},children:[(0,t.jsxs)("div",{style:{fontSize:12,fontWeight:650,color:D[s.id],margin:"0 0 6px",display:"flex",justifyContent:"space-between"},children:[(0,t.jsx)("span",{children:s.label}),(0,t.jsx)("span",{style:{color:"#778"},children:e[s.id].length.toLocaleString()})]}),0===e[s.id].length&&(0,t.jsx)("div",{style:{fontSize:11.5,color:"#667",padding:"6px 0"},children:"No one"}),e[s.id].slice(0,200).map(e=>(0,t.jsxs)("div",{role:"button",tabIndex:0,onClick:()=>o(e.key),onKeyDown:t=>{("Enter"===t.key||" "===t.key)&&(t.preventDefault(),o(e.key))},style:{padding:"7px 9px",marginBottom:6,borderRadius:6,border:p.LINE,cursor:"pointer",fontSize:12.5,background:n===e.key?"rgba(52,152,219,0.18)":"rgba(255,255,255,0.03)"},children:[(0,t.jsxs)("div",{style:{display:"flex",gap:6,alignItems:"center"},children:[(0,t.jsx)(U,{tier:e.row?.tier}),(0,t.jsx)("b",{style:{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:e.name})]}),(0,t.jsx)("div",{style:{color:"#8b9a9a",fontSize:11,marginTop:2},children:[e.row?.company,e.crm.followUp?`Follow up ${F(e.crm.followUp)}`:null].filter(Boolean).join(" · ")})]},e.key)),e[s.id].length>200&&(0,t.jsxs)("div",{style:{fontSize:11,color:"#778"},children:["and ",(e[s.id].length-200).toLocaleString()," more (search to find them)"]})]},s.id))})}let _={fontSize:12,fontWeight:650,color:"#b8c4c4",margin:"16px 0 6px",textTransform:"uppercase",letterSpacing:.4};function H({c:e,nameOf:o,groups:s,keep:r,token:a,inviteNotes:i,onChange:l,onPickGroup:c}){let d=e.row,[u,h]=(0,n.useState)(e.conversations[0]?.id||null),g=L(e.profileUrl),f=L(e.threadUrl),m=e.connection?`A connection${d?.connected_date?` since ${$(Date.parse(d.connected_date))}`:""}.`:2===e.degree?"2nd degree: not a connection yet.":"Not a connection.";return(0,t.jsxs)("div",{style:{fontSize:13},children:[(0,t.jsxs)("div",{style:{display:"flex",gap:8,alignItems:"center",flexWrap:"wrap"},children:[(0,t.jsx)(U,{tier:d?.tier}),(0,t.jsx)("h3",{style:{margin:0,fontSize:17},children:e.name}),d?.tier&&(0,t.jsxs)("span",{style:R("rgba(255,255,255,0.06)",I[d.tier]),children:[d.tier," tier"]}),(0,t.jsx)(A,{c:e})]}),(d?.headline||d?.company)&&(0,t.jsx)("div",{style:{color:"#b8c4c4",marginTop:4},children:d.headline||[d.role,d.company].filter(Boolean).join(" at ")}),(0,t.jsxs)("div",{style:{color:"#8b9a9a",marginTop:4,fontSize:12.5},children:[m,e.introducers.length>0&&(0,t.jsxs)(t.Fragment,{children:[" Who can introduce you: ",(0,t.jsx)("b",{style:{color:"#dfe8e8"},children:e.introducers.slice(0,6).join(", ")}),e.introducers.length>6?` and ${e.introducers.length-6} more`:"","."]}),d?.unlocked_from_name&&e.connection&&(0,t.jsxs)(t.Fragment,{children:[" You met through ",(0,t.jsx)("b",{style:{color:"#dfe8e8"},children:d.unlocked_from_name}),"."]})]}),(0,t.jsxs)("div",{style:{display:"flex",gap:12,marginTop:6,flexWrap:"wrap",fontSize:12.5},children:[g&&(0,t.jsx)("a",{href:g,target:"_blank",rel:"noreferrer",style:{color:"#3498DB",textDecoration:"none"},children:"Profile on LinkedIn ↗"}),f&&(0,t.jsx)("a",{href:f,target:"_blank",rel:"noreferrer",style:{color:"#3498DB",textDecoration:"none"},children:"Conversation on LinkedIn ↗"})]}),(0,t.jsx)(K,{c:e,onChange:l}),(0,t.jsx)("div",{style:_,children:"Conversations"}),0===e.conversations.length&&(0,t.jsx)("div",{style:{color:"#8b9a9a",fontSize:12.5},children:"None one to one."}),e.conversations.map(e=>(0,t.jsxs)("div",{style:{border:p.LINE,borderRadius:6,marginBottom:6,overflow:"hidden"},children:[(0,t.jsxs)("div",{role:"button",tabIndex:0,"aria-expanded":u===e.id,onClick:()=>h(u===e.id?null:e.id),onKeyDown:t=>{("Enter"===t.key||" "===t.key)&&(t.preventDefault(),h(u===e.id?null:e.id))},style:{display:"flex",gap:8,alignItems:"center",flexWrap:"wrap",padding:"7px 10px",cursor:"pointer",fontSize:12.5,background:u===e.id?"rgba(255,255,255,0.04)":"none"},children:[(0,t.jsx)("span",{style:{color:"#dfe8e8"},children:$(e.last)||"No date"}),null!=e.lastFromThem&&(0,t.jsx)("span",{style:{color:e.lastFromThem?"#ff9f43":"#8b9a9a"},children:e.lastFromThem?"They wrote last":"You wrote last"}),null!=e.count&&(0,t.jsx)("span",{style:{color:"#8b9a9a"},children:N(e.count,"message")}),null!=e.mine&&null!=e.count&&e.mine===e.count&&(0,t.jsx)("span",{style:R("rgba(255,255,255,0.06)"),children:"Only you wrote"}),e.folder&&"inbox"!==e.folder&&(0,t.jsx)("span",{style:R("rgba(255,255,255,0.06)"),children:e.folder}),"inmail"===e.kind&&(0,t.jsx)("span",{style:R("rgba(155,89,182,0.25)"),children:"InMail"}),"sponsored"===e.kind&&(0,t.jsx)("span",{style:R("rgba(255,255,255,0.08)"),children:"Sponsored"}),e.unread>0&&(0,t.jsxs)("span",{style:R("#3498DB","#fff"),children:[e.unread," unread"]})]}),u===e.id&&(0,t.jsx)(z,{id:e.id,keep:r,group:!1,token:a},`${e.id}:${a}:${r}`)]},e.id)),e.groups.length>0&&(0,t.jsxs)("div",{style:{fontSize:12.5,color:"#8b9a9a",marginTop:4},children:["Also in ",N(e.groups.length,"group"),":"," ",e.groups.slice(0,8).map((e,n)=>{let r=s.find(t=>t.id===e);return(0,t.jsxs)("span",{children:[n>0?", ":"",(0,t.jsx)("button",{onClick:()=>c(e),style:{background:"none",border:"none",color:"#3498DB",cursor:"pointer",padding:0,fontSize:12.5},children:r?Y(r,o):"a group"})]},e)})]}),(0,t.jsx)("div",{style:_,children:"Requests"}),e.invitesOut.length+e.invitesIn.length===0&&!e.request&&(0,t.jsx)("div",{style:{color:"#8b9a9a",fontSize:12.5},children:"None on file."}),[...e.invitesOut,...e.invitesIn].sort((e,t)=>(t.t||0)-(e.t||0)).map(n=>(0,t.jsxs)("div",{style:{fontSize:12.5,marginBottom:6},children:[(0,t.jsx)("span",{style:{color:"#dfe8e8"},children:"out"===n.dir?"You asked to connect":"They asked to connect"}),(0,t.jsxs)("span",{style:{color:"#8b9a9a"},children:[" · ",$(n.t)||"no date"," · ",e.connection?"accepted":"out"===n.dir?"pending":"not accepted yet"]}),r&&i[n.id]&&(0,t.jsx)("div",{style:{marginTop:3,padding:"6px 9px",borderRadius:8,background:"rgba(255,255,255,0.06)",whiteSpace:"pre-wrap",overflowWrap:"anywhere"},children:i[n.id]})]},n.id)),e.request&&(0,t.jsxs)("div",{style:{fontSize:12.5},children:[(0,t.jsx)("span",{style:{color:"#dfe8e8"},children:"Request tracked in the app"}),(0,t.jsxs)("span",{style:{color:"#8b9a9a"},children:[" · ",e.request.status]})]})]})}function K({c:e,onChange:o}){let s=e.crm||{},[r,a]=(0,n.useState)(s.notes||""),[i,l]=(0,n.useState)(""),[c,d]=(0,n.useState)(null),p=(0,n.useRef)(null),u=e.theyWrote?"replied":e.conversations.length||e.invitesOut.length||e.request?"contacted":"new";async function f(t){d("Saving…");try{await o(e.key,t),d("Saved")}catch(e){d(e.message)}}(0,n.useEffect)(()=>()=>clearTimeout(p.current),[]);let m=e=>y(new Date(Date.now()+864e5*e)),x=s.tags||[];function w(){let e=i.trim();e&&(l(""),f({tags:[...x,...e.split(",")]}))}return(0,t.jsxs)("div",{style:{marginTop:12,padding:"10px 12px",borderRadius:8,background:"rgba(52,152,219,0.06)",border:"1px solid rgba(52,152,219,0.18)"},children:[(0,t.jsxs)("div",{style:{display:"flex",gap:12,flexWrap:"wrap",alignItems:"flex-end"},children:[(0,t.jsxs)("label",{style:{fontSize:11.5,color:"#8b9a9a",display:"flex",flexDirection:"column",gap:3},children:["Stage",(0,t.jsxs)("select",{value:s.stage||"",onChange:e=>f({stage:e.target.value||null}),style:{...M,padding:"5px 8px"},children:[(0,t.jsxs)("option",{value:"",children:["Not set (looks like ",g(u),")"]}),h.map(e=>(0,t.jsx)("option",{value:e.id,children:e.label},e.id))]})]}),(0,t.jsxs)("label",{style:{fontSize:11.5,color:"#8b9a9a",display:"flex",flexDirection:"column",gap:3},children:["Next follow-up",(0,t.jsx)("input",{type:"date",value:s.followUp||"",onChange:e=>f({followUp:e.target.value||null}),style:{...M,padding:"4px 8px",colorScheme:"dark"}})]}),(0,t.jsxs)("div",{style:{display:"flex",gap:4,flexWrap:"wrap"},children:[(0,t.jsx)("button",{onClick:()=>f({followUp:m(3)}),style:E,children:"In 3 days"}),(0,t.jsx)("button",{onClick:()=>f({followUp:m(7)}),style:E,children:"In a week"}),s.followUp&&(0,t.jsx)("button",{onClick:()=>f({followUp:null}),style:E,children:"Clear"})]})]}),null==s.stage&&"new"!==u&&(0,t.jsxs)("button",{onClick:()=>f({stage:u}),style:{...E,marginTop:8},children:["Set to ",g(u)]}),(0,t.jsxs)("div",{style:{marginTop:10,display:"flex",gap:6,flexWrap:"wrap",alignItems:"center"},children:[x.map(e=>(0,t.jsxs)("span",{style:{...R("rgba(255,255,255,0.08)"),fontSize:11.5,display:"inline-flex",gap:4,alignItems:"center"},children:[e,(0,t.jsx)("button",{"aria-label":`Remove ${e}`,onClick:()=>f({tags:x.filter(t=>t!==e)}),style:{background:"none",border:"none",color:"#8b9a9a",cursor:"pointer",padding:0,fontSize:12},children:"×"})]},e)),(0,t.jsx)("input",{value:i,onChange:e=>l(e.target.value),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),w())},onBlur:w,placeholder:"Add a tag","aria-label":"Add a tag",style:{...M,padding:"3px 8px",fontSize:12,width:120}})]}),(0,t.jsx)("textarea",{value:r,onChange:e=>{var t;a(t=e.target.value),clearTimeout(p.current),p.current=setTimeout(()=>f({notes:t}),800)},onBlur:function(){clearTimeout(p.current),r!==(s.notes||"")&&f({notes:r})},placeholder:"Your notes on them: kept on this computer only","aria-label":"Notes",rows:4,style:{...M,width:"100%",marginTop:10,resize:"vertical",fontFamily:"inherit",lineHeight:1.45,boxSizing:"border-box"}}),(0,t.jsx)("div",{style:{fontSize:11,color:c&&"Saved"!==c&&"Saving…"!==c?"#ff9b9b":"#778",marginTop:4,minHeight:14},children:c||""})]})}function J({g:e,contacts:n,nameOf:o,keep:s,token:r,onPick:a}){let i=L(e.threadUrl);return(0,t.jsxs)("div",{style:{fontSize:13},children:[(0,t.jsxs)("div",{style:{display:"flex",gap:8,alignItems:"center",flexWrap:"wrap"},children:[(0,t.jsx)("h3",{style:{margin:0,fontSize:17},children:Y(e,o)}),(0,t.jsx)("span",{style:R("rgba(255,255,255,0.08)"),children:e.unknown?"To someone unnamed":"Group"}),e.unread>0&&(0,t.jsxs)("span",{style:R("#3498DB","#fff"),children:[e.unread," unread"]})]}),(0,t.jsx)("div",{style:{color:"#8b9a9a",marginTop:4,fontSize:12.5},children:[e.last?`Last active ${$(e.last)}`:null,null!=e.count?N(e.count,"message"):null].filter(Boolean).join(" · ")}),i&&(0,t.jsx)("a",{href:i,target:"_blank",rel:"noreferrer",style:{color:"#3498DB",textDecoration:"none",fontSize:12.5,display:"inline-block",marginTop:6},children:"Conversation on LinkedIn ↗"}),e.people.length>0&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("div",{style:_,children:"Who’s in it"}),(0,t.jsx)("div",{style:{display:"flex",flexWrap:"wrap",gap:6},children:e.people.map(e=>{let s=n.get(e);return(0,t.jsxs)("button",{onClick:()=>a(e),style:{...E,display:"inline-flex",gap:6,alignItems:"center"},children:[(0,t.jsx)(U,{tier:s?.row?.tier}),o(e)||e.split("/").pop(),s&&!s.connection&&(0,t.jsx)("span",{style:{color:"#ffc58a",fontSize:10.5},children:"not a connection"})]},e)})})]}),(0,t.jsx)("div",{style:_,children:"Messages"}),(0,t.jsx)("div",{style:{border:p.LINE,borderRadius:6,overflow:"hidden"},children:(0,t.jsx)(z,{id:e.id,keep:s,group:!0,token:r},`${e.id}:${r}:${s}`)})]})}let G={warm:["Warm","#ff9f43"],cool:["Cool","#3498DB"],dormant:["Dormant","#8899aa"],never:["Never messaged","#556"]},V={"connections.csv":"first name","messages.csv":"conversation id","invitations.csv":"direction","positions.csv":"company name","shares.csv":"date"},Q=e=>e?new Date(e).toLocaleDateString(void 0,{day:"numeric",month:"short",year:"numeric"}):"";async function X(e){let t=[],n={},o=0;for(let s of e)for(let e of s.messages){let r=JSON.stringify(e).length+2;o+r>25e5&&o>0&&(t.push(n),n={},o=0),n[s.id]||(n[s.id]={title:s.title||"",messages:[]},o+=s.id.length+(s.title||"").length+40),n[s.id].messages.push(e),o+=r}t.push(n);for(let e=0;e<t.length;e++){let n=await fetch("/api/social/messages",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({part:e,parts:t.length,threads:t[e]})});if(!n.ok)throw Error((await n.json().catch(()=>({}))).error||"The messages couldn’t be saved.")}}async function Z(e){let t={};for(let n of e)n.note&&(t[n.id]=n.note);let n=await fetch("/api/social/messages",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({invites:t})});if(!n.ok)throw Error((await n.json().catch(()=>({}))).error||"The requests’ notes couldn’t be saved.")}function ee(){let{userId:e}=(0,r.useUser)(),[s,h]=(0,n.useState)(void 0),[g,f]=(0,n.useState)(null),[m,y]=(0,n.useState)(!1),[x,w]=(0,n.useState)(null),[b,v]=(0,n.useState)(!1),[j,k]=(0,n.useState)(()=>(0,d.scannerNow)()),[S,T]=(0,n.useState)(!1),[C,z]=(0,n.useState)(0),[I,D]=(0,n.useState)(0),$=(0,n.useRef)(null),F=(0,n.useRef)(null),L=s?.keepMessages===!0,N=()=>fetch("/api/social").then(e=>e.json()).then(e=>h(e.social||null)).catch(()=>h(null)).finally(()=>z(e=>e+1));async function O(e){v(!0),w(null);try{let t={};for(let n of Array.from(e||[])){let e=n.name.toLowerCase();V[e]&&!t[e]&&(t[e]=(0,i.readTable)(await n.text(),V[e]))}if(!Object.keys(t).length)throw Error("That folder doesn’t have LinkedIn’s export files in it (Connections.csv, messages.csv and so on).");let n=t["connections.csv"]||[],o=t["messages.csv"]?(0,i.messageStats)(t["messages.csv"]):{asOf:null,people:new Map},s=t["messages.csv"]?(0,i.buildConversations)(t["messages.csv"],{keepText:L}):null,r=t["invitations.csv"]?(0,i.buildInvitations)(t["invitations.csv"],{keepText:L}):null;$.current=t["messages.csv"]||null,F.current=t["invitations.csv"]||null;let a={};for(let[e,t]of o.people)a[e]={last:t.last,lastFromThem:t.lastFromThem,total:t.total,recent:t.recent};if(m)for(let e of n){let t=e["email address"];if(!t)continue;let n=(0,c.keyFor)({profile_url:e.url});a[n]={...a[n]||{},email:t}}let l=o.asOf||Date.now(),d={asOf:l,people:a,conversations:s?s.conversations.map(i.conversationSummary):[],chapters:(0,i.careerChapters)(t["positions.csv"]||[],n,l),posts:t["shares.csv"]?(0,i.postingEffect)(t["shares.csv"],n):null,invites:t["invitations.csv"]?(0,i.invitationSplit)(t["invitations.csv"]):null,invitations:(r||[]).map(({note:e,...t})=>t),names:s?s.names:{},emails:m},p=await fetch("/api/social",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(d)});if(!p.ok)throw Error((await p.json().catch(()=>({}))).error||"It couldn’t be saved.");s&&L&&await X(s.conversations),r&&L&&await Z(r);let u=s?[s.drafts?`${s.drafts.toLocaleString()} unsent ${1===s.drafts?"draft":"drafts"} left out`:"",s.skipped?`${s.skipped.toLocaleString()} ${1===s.skipped?"row":"rows"} with no conversation or date left out`:""].filter(Boolean):[];w({good:!0,text:`Read ${Object.keys(t).length} of LinkedIn’s files: ${o.people.size.toLocaleString()} people you’ve messaged one to one`+(s?`, ${s.conversations.length.toLocaleString()} conversations${L?", their messages kept on this computer":""}`:"")+(r?`, ${r.length.toLocaleString()} connection requests`:"")+`.${u.length?` (${u.join("; ")}.)`:""}`}),await N()}catch(e){w({good:!1,text:e.message})}finally{v(!1)}}async function R(e=!1){w(null);try{let t=await (0,d.runScrape)(e?"messages-full":"messages");w(t?.exitCode===0?{good:!0,text:e?"Your whole messages history is read.":"Your messages list is read."}:{good:!1,text:"The sync stopped before it finished. The Scan page has the log."}),await N()}catch(e){w({good:!1,text:e.message})}}async function E(e){await fetch("/api/social",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({autoSync:e})}),N()}async function M(e){if(e||window.confirm("Delete the messages kept on this computer? The list of conversations stays; the messages themselves are deleted.")){T(!0),w(null);try{if(e||await fetch("/api/social/messages",{method:"DELETE"}),!(await fetch("/api/social",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({keepMessages:e})})).ok)throw Error("The switch couldn’t be saved.");e&&($.current||F.current)?($.current&&await X((0,i.buildConversations)($.current,{keepText:!0}).conversations),F.current&&await Z((0,i.buildInvitations)(F.current,{keepText:!0})),w({good:!0,text:"Your messages are kept on this computer now."})):e?w({good:!0,text:"On. Choose your export folder again, or sync your messages, to bring the messages in."}):w({good:!0,text:"Off. The kept messages are deleted."})}catch(e){w({good:!1,text:e.message})}finally{T(!1),N()}}}async function W(){await fetch("/api/social",{method:"DELETE"}),$.current=null,F.current=null;let e="Forgotten. Nothing from your export or messages is kept here now, the messages themselves included.";window.confirm("Also delete your CRM: the stages, tags, notes and follow-up dates you added? This can’t be undone. Cancel keeps them.")?(await fetch("/api/social/crm",{method:"DELETE"}),D(e=>e+1),e+=" Your CRM notes are deleted too."):e+=" Your CRM notes are kept.",w({good:!0,text:e}),N()}(0,n.useEffect)(()=>{N()},[]),(0,n.useEffect)(()=>(0,d.watchScanner)(()=>k((0,d.scannerNow)())),[]),(0,n.useEffect)(()=>{if(!e)return;let t=!0;return(0,a.loadNetwork)(e).then(e=>{t&&f(e)}).catch(()=>{}),()=>{t=!1}},[e]);let{merged:U,asOf:A}=(0,n.useMemo)(()=>(0,l.mergeSocial)(s),[s]),q=(0,n.useMemo)(()=>{let e={warm:0,cool:0,dormant:0,never:0};for(let t of g?.degree1||[])e[(0,i.warmthOf)(U.get((0,c.keyFor)(t)),A)]+=1;return e},[g,U,A]);if(u.IS_DEMO)return(0,t.jsx)("div",{style:{minHeight:"100vh",background:"#0a0a1a",color:"#aaa",padding:40,fontFamily:p.FONT},children:"Not part of the demo."});let Y=j?.running,P={padding:"12px 14px",borderRadius:8,border:p.LINE,background:"rgba(255,255,255,0.03)",marginTop:10},_={fontSize:16,margin:"24px 0 6px"};return(0,t.jsxs)("div",{style:{minHeight:"100vh",background:"#0a0a1a",color:"#fff",fontFamily:p.FONT},children:[(0,t.jsxs)("header",{style:{padding:"16px 24px",borderBottom:p.LINE,display:"flex",alignItems:"center",gap:16},children:[(0,t.jsx)(o.default,{href:"/",style:{color:"#888",textDecoration:"none",fontSize:13,fontWeight:600,padding:"6px 14px",borderRadius:6,background:"rgba(255,255,255,0.06)",border:p.LINE},children:"← Back to Map"}),(0,t.jsxs)("h1",{style:{fontSize:22,fontWeight:700,margin:0,color:"#FFD700"},children:["Social ",(0,t.jsx)("span",{style:{fontSize:12,color:"#8b9a9a",fontWeight:600},children:"experimental"})]})]}),(0,t.jsxs)("main",{style:{maxWidth:1180,margin:"0 auto",padding:"8px 16px 64px"},children:[(0,t.jsxs)(p.Body,{style:{marginTop:16},children:["Everyone you’ve been in touch with on LinkedIn, connections or not: who’s waiting on you, who hasn’t answered, what you’ve sent and received, and your own notes and follow-ups on each, from your own LinkedIn data. Only dates, who wrote last, counts and names are kept on this computer, and what messages say only if you turn on ",(0,t.jsx)("i",{children:"Keep my messages"})," below."]}),(0,t.jsx)("h2",{style:_,children:"Your LinkedIn export"}),(0,t.jsxs)("div",{style:P,children:[(0,t.jsxs)(p.Body,{style:{margin:0},children:["On LinkedIn: ",(0,t.jsx)("i",{children:"Settings → Data privacy → Get a copy of your data"}),", pick the larger archive, and unzip the file it emails you. Then choose that folder here. It’s read in this window; nothing goes to LinkedIn."]}),(0,t.jsxs)("label",{style:{display:"flex",gap:8,alignItems:"flex-start",fontSize:12.5,color:"#b8c4c4",marginTop:10},children:[(0,t.jsx)("input",{type:"checkbox",checked:m,onChange:e=>y(e.target.checked),style:{marginTop:3}}),(0,t.jsx)("span",{children:"Also keep email addresses from it, on this computer only, for one-click email."})]}),(0,t.jsxs)("div",{style:{marginTop:10,display:"flex",gap:10,alignItems:"center",flexWrap:"wrap"},children:[(0,t.jsxs)("label",{style:{padding:"7px 14px",borderRadius:8,background:"rgba(52,152,219,0.18)",color:"#cfe6f7",cursor:b?"default":"pointer",fontSize:13},children:[b?"Reading…":s?"Choose a newer export":"Choose the export folder",(0,t.jsx)("input",{type:"file",webkitdirectory:"",directory:"",multiple:!0,disabled:b,style:{display:"none"},onChange:e=>O(e.target.files)})]}),s&&(0,t.jsxs)("span",{style:{fontSize:12,color:"#8b9a9a"},children:["From an export up to ",Q(s.asOf),s.liveAt?` \xb7 messages synced ${Q(Date.parse(s.liveAt))}`:""]}),s&&(0,t.jsx)("button",{onClick:W,style:{background:"none",border:"none",color:"#ff9b9b",cursor:"pointer",fontSize:12},children:"Forget it"})]})]}),(0,t.jsxs)("h2",{style:_,children:["Live messages sync ",(0,t.jsx)("span",{style:{fontSize:11,color:"#FFD700"},children:"experimental"})]}),(0,t.jsxs)("div",{style:P,children:[(0,t.jsxs)(p.Body,{style:{margin:0},children:["Reads your messages list once, in your own Chrome: who each conversation is with, connections or not, when it was last active, whether it’s unread and who wrote last. It uses no search budget and opens no conversation. The newest message’s words are kept only if ",(0,t.jsx)("i",{children:"Keep my messages"})," is on. The daily sync reads the top of the list; ",(0,t.jsx)("i",{children:"Read my whole history"})," keeps scrolling until no more conversations load (at most 60 scrolls, about 1,000 conversations), at the same slow, fixed pace, so it takes a few minutes."]}),(0,t.jsxs)("div",{style:{marginTop:10,display:"flex",gap:12,alignItems:"center",flexWrap:"wrap"},children:[(0,t.jsx)("button",{onClick:()=>R(!1),disabled:Y,style:{padding:"7px 14px",borderRadius:8,border:"none",background:Y?"#333":"rgba(0,255,136,0.15)",color:Y?"#777":"#00ff88",cursor:Y?"default":"pointer",fontSize:13},children:Y?"The scanner is busy":"Sync messages now"}),(0,t.jsx)("button",{onClick:()=>R(!0),disabled:Y,style:{padding:"7px 14px",borderRadius:8,border:p.LINE,background:"none",color:Y?"#777":"#cfe6f7",cursor:Y?"default":"pointer",fontSize:13},children:"Read my whole history"}),(0,t.jsxs)("label",{style:{display:"flex",gap:6,alignItems:"center",fontSize:12.5,color:"#b8c4c4"},children:[(0,t.jsx)("input",{type:"checkbox",checked:s?.autoSync===!0,onChange:e=>E(e.target.checked)}),"Once a day while the app is open (daytime only)"]})]})]}),x&&(0,t.jsx)("div",{style:{marginTop:10,fontSize:13,color:x.good?"#00ff88":"#ff9b9b"},children:x.text}),(0,t.jsx)("h2",{style:_,children:"People"}),(0,t.jsx)(B,{net:g,asOf:s?A:null,keep:L,onKeep:M,switching:S||b,token:C,crmToken:I}),s&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("h2",{style:_,children:"Warmth"}),(0,t.jsx)("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fit, minmax(160px, 1fr))",gap:10},children:Object.entries(G).map(([e,[n,o]])=>(0,t.jsxs)("div",{style:{...P,marginTop:0},children:[(0,t.jsx)("div",{style:{fontSize:22,fontWeight:700,color:o},children:(q[e]||0).toLocaleString()}),(0,t.jsx)("div",{style:{fontSize:12.5,color:"#b8c4c4"},children:n})]},e))}),(0,t.jsx)("div",{style:{fontSize:11.5,color:"#778",marginTop:6},children:"Of your connections. Warm: messaged in the last 30 days, or 3+ messages in 90. Cool: within a year. Dormant: longer ago."}),s.chapters?.length>0&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("h2",{style:_,children:"Career chapters"}),(0,t.jsx)("div",{style:{...P,padding:0},children:s.chapters.map(e=>{let n=(e.people||[]).filter(e=>["warm","cool"].includes((0,i.warmthOf)(U.get((0,c.keyFor)({profile_url:e})),A))).length;return(0,t.jsxs)("div",{style:{display:"flex",gap:10,padding:"8px 14px",borderBottom:"1px solid rgba(255,255,255,0.04)",fontSize:13,alignItems:"baseline"},children:[(0,t.jsxs)("span",{style:{flex:1},children:[(0,t.jsx)("b",{children:e.company})," ",(0,t.jsxs)("span",{style:{color:"#8b9a9a"},children:[e.title," · ",new Date(e.from).getFullYear(),"–",e.to?new Date(e.to).getFullYear():"now"]})]}),(0,t.jsxs)("span",{style:{color:"#b8c4c4"},children:[e.made.toLocaleString()," connections made"]}),(0,t.jsx)("span",{style:{color:"#ff9f43",minWidth:90,textAlign:"right"},children:e.made?`${Math.round(100*n/e.made)}% still in touch`:""})]},`${e.company}-${e.from}`)})})]}),s.posts?.posts?.length>0&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("h2",{style:_,children:"Does posting work?"}),(0,t.jsxs)("div",{style:P,children:[(0,t.jsxs)("div",{style:{fontSize:13},children:["In the week after a post you made ",(0,t.jsx)("b",{children:s.posts.afterPosts.toFixed(1)})," new connections on average; a usual week, ",(0,t.jsx)("b",{children:s.posts.usualWeek.toFixed(1)}),"."]}),(0,t.jsx)("div",{style:{fontSize:11.5,color:"#778",marginTop:4},children:"What came after a post, not proof it came from it."}),[...s.posts.posts].sort((e,t)=>t.week-e.week).slice(0,3).map(e=>(0,t.jsxs)("div",{style:{fontSize:12.5,color:"#b8c4c4",marginTop:6},children:[Q(e.t),": ",e.week," new connections that week ",e.link?(0,t.jsx)("a",{href:e.link,target:"_blank",rel:"noreferrer",style:{color:"#3498DB",textDecoration:"none"},children:"Post ↗"}):null]},e.t))]})]}),s.invites?.total>0&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)("h2",{style:_,children:"Who reaches out"}),(0,t.jsx)("div",{style:P,children:(0,t.jsxs)("span",{style:{fontSize:13},children:[s.invites.incoming.toLocaleString()," requests came to you and you sent ",s.invites.outgoing.toLocaleString()," ","(",Math.round(100*s.invites.incoming/s.invites.total),"% came to you), as far as your export shows."]})})]})]})]})]})}e.s(["default",0,function(){return(0,t.jsx)(s.default,{children:(0,t.jsx)(ee,{})})}],60451)}]);
@@ -6,6 +6,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.11] - 2026-09-29
10
+
11
+ ### Added
12
+ - **The Social tab is a personal CRM.** Everyone you've been in touch with on LinkedIn is one contact,
13
+ connection or not: a list on the left with views and a search box, the person on the right (stacked
14
+ on a phone). The views:
15
+ - **Inbox:** they wrote last, or there's something unread. Unread first.
16
+ - **Awaiting reply:** you wrote last and nothing came back for 7 days or more (you can change the 7).
17
+ - **Follow-ups due:** a follow-up date of today or before.
18
+ - **Pipeline:** a board of the people you've given a stage.
19
+ - **Sent:** connection requests you sent (from your export's Invitations.csv, with the date, and the
20
+ note you wrote if Keep my messages is on), requests the app tracked (*Mark sent*), each accepted
21
+ once they're a connection and pending until then, and messages awaiting a reply.
22
+ - **Received:** requests to you from people who aren't connections yet.
23
+ - **All:** everyone and every group conversation, newest first.
24
+
25
+ Each person's panel shows their tier, company and how you're connected (and who can introduce you,
26
+ for 2nd degree), their conversations (the messages themselves if you keep them), requests both ways,
27
+ and **your own stage** (New, Contacted, Replied, Meeting, Won/Partner, Not now; it suggests one),
28
+ **tags**, **notes** and **next follow-up date**. Those are yours, so they're always kept, in a file of
29
+ their own per profile in the app's data folder, and never sent anywhere. Search covers names,
30
+ companies, tags and notes, and what was said while the messages are kept; in All it finds anyone in
31
+ your network, so you can add a note before you've written to them. **Export CSV** saves the table
32
+ (name, company, profile, connection, stage, tags, last contact, who wrote last, next follow-up,
33
+ notes) as a spreadsheet file. The list shows 100 at a time, so thousands of conversations stay quick.
34
+ - **Every conversation in your export, not just your connections'.** People who aren't connections
35
+ are listed by the name the export gives them, marked *Not a connection*, with their profile link when
36
+ it has one. Past (archived), current and ones only you wrote in are all there, from every folder;
37
+ group conversations are listed and marked; Sponsored Messages and InMails are marked, and adverts are
38
+ kept out of the Inbox. Unsent drafts are left out and said so.
39
+ - **Read my whole history**, beside *Sync messages now*: the live messages sync keeps scrolling your
40
+ messages list until no new conversations load (at most 60 scrolls, about 1,000 conversations), at the
41
+ same slow, fixed pace, with no random waits. The daily sync still reads just the top. The sync now
42
+ also brings in people who aren't your connections (with their name) and group conversations (without
43
+ any words), and each sync adds to what earlier ones found instead of replacing it.
44
+ - **Delete my CRM notes**, and *Forget it* asks, separately, whether to delete them too, so notes
45
+ aren't lost by accident.
46
+
47
+ ### Changed
48
+ - **"Save a copy of my network" carries the Social tab too.** A new box, ticked by default,
49
+ adds its findings, your CRM stages, tags, notes and follow-ups, and any messages you keep,
50
+ so moving to a new computer brings everything. Importing the copy puts them in place and
51
+ keeps this computer's beside the old network. Untick it to leave them behind.
52
+ - **Waiting on you has become the CRM's Inbox**, and Conversations is part of the CRM.
53
+ - **The Social tab keeps the names of people who aren't your connections**, from your export and the
54
+ sync, since nothing else in the app says who they are. A connection's name still isn't copied: the
55
+ app has it already.
56
+
9
57
  ## [0.4.10] - 2026-09-29
10
58
 
11
59
  ### Changed
@@ -20,6 +20,7 @@ export async function POST(request) {
20
20
  let body = {};
21
21
  try { body = await request.json(); } catch { /* no options: the defaults */ }
22
22
  const includePhotos = body?.photos !== false;
23
+ const includeSocial = body?.social !== false;
23
24
 
24
25
  const dir = dataDir();
25
26
  sweepLeftovers(dir);
@@ -27,7 +28,7 @@ export async function POST(request) {
27
28
  try {
28
29
  const file = path.join(work, 'export.sixdegrees');
29
30
  const made = buildExport(getDb(), {
30
- dir, outFile: file, includePhotos, appVersion: APP_VERSION, schemaSql: SCHEMA_SQL,
31
+ dir, outFile: file, includePhotos, includeSocial, appVersion: APP_VERSION, schemaSql: SCHEMA_SQL,
31
32
  });
32
33
  const fd = openSync(file, 'r');
33
34
  rmSync(work, { recursive: true, force: true });
@@ -422,8 +422,12 @@ const ACTIONS = {
422
422
  company: { flag: '--company', needsName: true, label: 'Scanning' },
423
423
  // Photos an older version kept as links to LinkedIn: saved here, once each.
424
424
  // No browser, no search; every scan does the same at its end.
425
- // Experimental, for the Social tab: your messages list, once (who, when, unread; never text).
425
+ // Experimental, for the Social tab: your messages list, once (who, when, unread; the
426
+ // newest message's words only while Keep my messages is on).
426
427
  messages: { flag: '--messages', label: 'Reading your messages list' },
428
+ // The same, scrolling until the whole list has loaded (60 scrolls or about
429
+ // 1,000 conversations at most), for the Social tab's "Read my whole history".
430
+ 'messages-full': { flag: '--messages --full-history', label: 'Reading your whole messages history' },
427
431
  photos: { flag: '--save-photos', label: 'Saving profile photos to this computer' },
428
432
  };
429
433
 
@@ -0,0 +1,37 @@
1
+ import { rmSync } from 'node:fs';
2
+ import { cleanCrm, patchCrm, emptyCrm } from '../../../../lib/social-crm';
3
+ import { socialFiles, readJson, writeJsonAtomic } from '../../../../lib/social-store';
4
+
5
+ // The Social tab's CRM: your own stage, tags, notes and next follow-up for each
6
+ // person, in crm-<profile>.json in the data folder (lib/social-crm.js has the
7
+ // shape). Yours, so always kept, whatever Keep my messages says; never sent
8
+ // anywhere, and not in "Save a copy of my network".
9
+ //
10
+ // GET the whole store
11
+ // PATCH { key, set: { stage?, tags?, notes?, followUp? } } one person's
12
+ // fields (null or '' clears one), or { settings: { awaitDays } }
13
+ // DELETE the file gone: only when you've said so, on its own confirm
14
+
15
+ export async function GET() {
16
+ const files = socialFiles();
17
+ if (!files) return Response.json({ crm: emptyCrm() });
18
+ return Response.json({ crm: cleanCrm(readJson(files.crm, null)) });
19
+ }
20
+
21
+ export async function PATCH(request) {
22
+ const files = socialFiles();
23
+ if (!files) return Response.json({ error: 'Pick a profile first.' }, { status: 400 });
24
+ let body;
25
+ try { body = await request.json(); } catch { return Response.json({ error: 'That isn’t readable.' }, { status: 400 }); }
26
+ // Read, change one person, write whole: two quick edits in a row can't
27
+ // leave half a file, and the second sees the first (Node runs one at a time).
28
+ const next = patchCrm(readJson(files.crm, null), { key: body?.key, set: body?.set, settings: body?.settings });
29
+ writeJsonAtomic(files.crm, next);
30
+ return Response.json({ ok: true, entry: body?.key ? next.people[body.key] || null : null, settings: next.settings });
31
+ }
32
+
33
+ export async function DELETE() {
34
+ const files = socialFiles();
35
+ if (files) rmSync(files.crm, { force: true });
36
+ return Response.json({ ok: true });
37
+ }
@@ -7,11 +7,14 @@ import { socialFiles, readJson, writeJsonAtomic, readMessages, deleteMessages }
7
7
  // two files and why they're apart).
8
8
  //
9
9
  // GET the list, no words: who, when, who wrote last,
10
- // unread, how many, the thread's link
10
+ // unread, how many, the thread's link; with the
11
+ // requests both ways and people's names, and the
12
+ // requests' notes when the messages are kept
11
13
  // GET ?id=<id> | ?with=<key> one thread, a page at a time (before, limit):
12
14
  // its messages only if they're kept
13
15
  // GET ?q=<words> the conversations whose kept messages say that
14
16
  // POST an import's messages, in parts
17
+ // PUT an import's request notes
15
18
  // DELETE the messages gone, the list kept
16
19
  //
17
20
  // Nothing here goes anywhere but this computer's browser: no route of the app
@@ -25,14 +28,20 @@ function state() {
25
28
  if (!files) return null;
26
29
  const social = readJson(files.social, {}) || {};
27
30
  const keep = social.keepMessages === true;
28
- const kept = keep ? readMessages(files.messages) : { threads: {}, previews: {} };
29
- return { files, social, keep, kept, list: conversationList(social.conversations, social.live) };
31
+ const kept = keep ? readMessages(files.messages) : { threads: {}, previews: {}, invites: {} };
32
+ const names = { ...(social.names || {}) };
33
+ for (const [link, v] of Object.entries(social.liveOthers || {})) if (v?.name && !names[link]) names[link] = v.name;
34
+ for (const g of social.liveGroups || []) for (const [link, n] of Object.entries(g.names || {})) if (!names[link]) names[link] = n;
35
+ const list = conversationList(social.conversations, social.live, { others: social.liveOthers, groups: social.liveGroups, names: social.names });
36
+ return { files, social, keep, kept, names, list };
30
37
  }
31
38
 
32
39
  /** A conversation's kept messages, oldest first, with the live sync's newest one after them when it's newer. */
33
40
  function messagesOf(c, kept) {
34
41
  const msgs = kept.threads[c.id]?.messages || [];
35
- const preview = !c.group && c.people.length === 1 ? kept.previews[c.people[0]] : null;
42
+ // The sync's newest message is kept by the link it gave: for someone who
43
+ // isn't a connection that can differ from the export's (liveKey).
44
+ const preview = !c.group && c.people.length === 1 ? kept.previews[c.people[0]] || (c.liveKey && kept.previews[c.liveKey]) : null;
36
45
  if (preview?.text && (!msgs.length || (preview.t || 0) > (msgs[msgs.length - 1].t || 0))) return [...msgs, preview];
37
46
  return msgs;
38
47
  }
@@ -70,7 +79,13 @@ export async function GET(request) {
70
79
  title: s.kept.threads[c.id]?.title || '',
71
80
  hasText: s.keep && messagesOf(c, s.kept).length > 0,
72
81
  }));
73
- return Response.json({ keepMessages: s.keep, conversations });
82
+ return Response.json({
83
+ keepMessages: s.keep,
84
+ conversations,
85
+ invitations: Array.isArray(s.social.invitations) ? s.social.invitations : [],
86
+ names: s.names,
87
+ inviteNotes: s.keep ? s.kept.invites || {} : {},
88
+ });
74
89
  }
75
90
 
76
91
  /** One message as it's kept, or null when it isn't one. */
@@ -97,8 +112,9 @@ export async function POST(request) {
97
112
  }
98
113
  let into;
99
114
  if (part === 0) {
100
- // A new import replaces the export's messages; the live sync's previews stay.
101
- into = { threads: {}, previews: s.kept.previews || {} };
115
+ // A new import replaces the export's messages; the live sync's previews
116
+ // and the requests' notes (PUT) stay.
117
+ into = { threads: {}, previews: s.kept.previews || {}, invites: s.kept.invites || {} };
102
118
  } else {
103
119
  into = readJson(s.files.incoming, null);
104
120
  if (!into?.threads) return Response.json({ error: 'The start of this import is missing. Choose the export folder again.' }, { status: 409 });
@@ -114,7 +130,8 @@ export async function POST(request) {
114
130
  }
115
131
  }
116
132
  if (part === parts - 1) {
117
- writeJsonAtomic(s.files.messages, into);
133
+ // The notes may have changed while the parts came in: the file's win.
134
+ writeJsonAtomic(s.files.messages, { ...into, invites: readMessages(s.files.messages).invites || into.invites || {} });
118
135
  rmSync(s.files.incoming, { force: true });
119
136
  } else {
120
137
  writeJsonAtomic(s.files.incoming, into);
@@ -122,6 +139,25 @@ export async function POST(request) {
122
139
  return Response.json({ ok: true, part, parts, done: part === parts - 1 });
123
140
  }
124
141
 
142
+ // An import's request notes: { invites: { id: note } }, the words sent with a
143
+ // connection request (Invitations.csv's Message). Written by a person like a
144
+ // message, so kept only while Keep my messages is on, in the same file, and
145
+ // gone with it. They replace the notes kept before: an export has all of them.
146
+ export async function PUT(request) {
147
+ const s = state();
148
+ if (!s) return Response.json({ error: 'Pick a profile first.' }, { status: 400 });
149
+ if (!s.keep) return Response.json({ error: 'Keep my messages is off, so nothing was saved.' }, { status: 409 });
150
+ let body;
151
+ try { body = await request.json(); } catch { return Response.json({ error: 'That isn’t readable.' }, { status: 400 }); }
152
+ const invites = {};
153
+ for (const [id, note] of Object.entries(body?.invites && typeof body.invites === 'object' ? body.invites : {}).slice(0, 50000)) {
154
+ if (id && id.length <= 400 && typeof note === 'string' && note.trim()) invites[id] = note.slice(0, 2000);
155
+ }
156
+ const kept = readJson(s.files.messages, {}) || {};
157
+ writeJsonAtomic(s.files.messages, { threads: kept.threads || {}, previews: kept.previews || {}, invites });
158
+ return Response.json({ ok: true, notes: Object.keys(invites).length });
159
+ }
160
+
125
161
  // The words gone, the list and the numbers kept: what switching Keep my
126
162
  // messages off does (the PATCH that turns it off deletes them as well).
127
163
  export async function DELETE() {
@@ -1,47 +1,81 @@
1
1
  import { rmSync } from 'node:fs';
2
2
  import { db } from '../../../lib/db';
3
3
  import { resolveProfile } from '../../../lib/profile';
4
- import { matchLive } from '../../../lib/linkedin-export';
4
+ import { matchLive, matchLiveGroups, mergeLive } from '../../../lib/linkedin-export';
5
5
  import { socialFiles, readJson, writeJsonAtomic, readMessages, deleteMessages } from '../../../lib/social-store';
6
6
 
7
7
  // The Social tab's saved findings (lib/linkedin-export.js): numbers and dates
8
8
  // from your own LinkedIn export and the live messages sync, one file per
9
- // profile in the data folder. No message text is kept in this file: the words,
10
- // when Keep my messages is on, go to their own file (lib/social-store.js,
11
- // app/api/social/messages). GET reads it, POST replaces it, PUT takes a live
12
- // sync, PATCH flips a switch, DELETE forgets it all.
9
+ // profile in the data folder, and the names of people who aren't your
10
+ // connections (nothing else in the app says who they are). No message text is
11
+ // kept in this file: the words, when Keep my messages is on, go to their own
12
+ // file (lib/social-store.js, app/api/social/messages). GET reads it, POST
13
+ // replaces it, PUT takes a live sync, PATCH flips a switch, DELETE forgets it
14
+ // all. Your CRM notes are a file of their own (app/api/social/crm), which
15
+ // Forget it leaves alone: the page asks about them separately.
13
16
 
14
17
  // A person's key: their profile link, as lib/separation.js keyFor makes it.
15
18
  const KEY_OK = (k) => typeof k === 'string' && k.length > 0 && k.length < 300;
19
+ const num = (n) => (Number.isFinite(Number(n)) && n !== null ? Number(n) : null);
20
+ const name = (s) => (typeof s === 'string' ? s.replace(/[\u0000-\u001f]/g, ' ').trim().slice(0, 120) : '');
16
21
 
17
- /** An import's list of conversations, with nothing but the fields the list shows. */
22
+ /**
23
+ * An import's list of conversations, with nothing but the fields the list
24
+ * shows. A conversation with no one but you in it (the export didn't say to
25
+ * whom) stays, with no people: nothing is dropped.
26
+ */
18
27
  function cleanConversations(list) {
19
28
  if (!Array.isArray(list)) return [];
20
29
  const out = [];
21
30
  for (const c of list) {
22
31
  if (!c || typeof c.id !== 'string' || !c.id || c.id.length > 200 || !Array.isArray(c.people)) continue;
23
32
  const people = c.people.filter(KEY_OK).slice(0, 200);
24
- if (!people.length) continue;
25
33
  out.push({
26
34
  id: c.id,
27
35
  people,
28
36
  group: c.group === true,
37
+ kind: ['sponsored', 'inmail'].includes(c.kind) ? c.kind : null,
38
+ folder: typeof c.folder === 'string' && /^[\w -]{0,30}$/.test(c.folder) ? c.folder.toLowerCase() : '',
29
39
  last: Number(c.last) || null,
30
40
  lastFromThem: typeof c.lastFromThem === 'boolean' ? c.lastFromThem : null,
31
- count: Number.isFinite(Number(c.count)) ? Number(c.count) : null,
41
+ count: num(c.count),
42
+ mine: num(c.mine),
32
43
  });
33
44
  }
34
45
  return out;
35
46
  }
36
47
 
48
+ /** An import's requests (buildInvitations), without their notes: those go with the messages, and only while they're kept. */
49
+ function cleanInvitations(list) {
50
+ if (!Array.isArray(list)) return [];
51
+ const out = [];
52
+ for (const i of list.slice(0, 50000)) {
53
+ if (!i || !['in', 'out'].includes(i.dir) || !KEY_OK(i.key) || typeof i.id !== 'string' || i.id.length > 400) continue;
54
+ out.push({ id: i.id, dir: i.dir, key: i.key, name: name(i.name), t: Number(i.t) || null });
55
+ }
56
+ return out;
57
+ }
58
+
59
+ /** Names by key, as the export spells them. */
60
+ function cleanNames(names) {
61
+ const out = {};
62
+ if (!names || typeof names !== 'object') return out;
63
+ for (const [k, v] of Object.entries(names).slice(0, 200000)) {
64
+ const n = name(v);
65
+ if (KEY_OK(k) && n) out[k] = n;
66
+ }
67
+ return out;
68
+ }
69
+
37
70
  export async function GET() {
38
71
  const files = socialFiles();
39
72
  if (!files) return Response.json({ social: null });
40
73
  const social = readJson(files.social, null);
41
74
  if (!social) return Response.json({ social: null });
42
- // The list of conversations can run to thousands; the Galaxy and the tab's
43
- // counts don't need it, and GET /api/social/messages serves it.
44
- const { conversations, ...rest } = social;
75
+ // The lists (conversations, requests, names, what the sync found beyond your
76
+ // connections) can run to thousands; the Galaxy and the tab's counts don't
77
+ // need them, and GET /api/social/messages serves them.
78
+ const { conversations, invitations, names, liveOthers, liveGroups, ...rest } = social;
45
79
  return Response.json({ social: { ...rest, conversationCount: Array.isArray(conversations) ? conversations.length : 0 } });
46
80
  }
47
81
 
@@ -55,6 +89,8 @@ export async function POST(request) {
55
89
  const clean = {
56
90
  live: before.live || {},
57
91
  liveAt: before.liveAt || null,
92
+ liveOthers: before.liveOthers || {},
93
+ liveGroups: before.liveGroups || [],
58
94
  autoSync: before.autoSync === true,
59
95
  keepMessages: before.keepMessages === true,
60
96
  importedAt: new Date().toISOString(),
@@ -64,6 +100,8 @@ export async function POST(request) {
64
100
  chapters: Array.isArray(body?.chapters) ? body.chapters : [],
65
101
  posts: body?.posts && typeof body.posts === 'object' ? body.posts : null,
66
102
  invites: body?.invites && typeof body.invites === 'object' ? body.invites : null,
103
+ invitations: cleanInvitations(body?.invitations),
104
+ names: cleanNames(body?.names),
67
105
  emails: body?.emails === true,
68
106
  };
69
107
  for (const p of Object.values(clean.people)) {
@@ -73,14 +111,18 @@ export async function POST(request) {
73
111
  }
74
112
  }
75
113
  writeJsonAtomic(files.social, clean);
76
- return Response.json({ ok: true, people: Object.keys(clean.people).length, conversations: clean.conversations.length });
114
+ return Response.json({ ok: true, people: Object.keys(clean.people).length, conversations: clean.conversations.length, invitations: clean.invitations.length });
77
115
  }
78
116
 
79
117
  // From the scanner's experimental messages sync (scripts/scrape.py sync_messages):
80
118
  // per person, when your 1:1 conversation was last active, unread count, the
81
- // thread's link, who wrote last and the newest message. Merged in beside what
82
- // the export gave, kept under `live`. The newest message's words go to the
83
- // messages file, and only while Keep my messages is on.
119
+ // thread's link, who wrote last and the newest message; and the group
120
+ // conversations, without any words. Kept beside what the export gave: your
121
+ // connections under `live`, everyone else (with their name) under
122
+ // `liveOthers`, groups under `liveGroups`, each merged with what earlier syncs
123
+ // found, so a quick sync doesn't forget what a full-history one read further
124
+ // down. The newest message's words go to the messages file, and only while
125
+ // Keep my messages is on.
84
126
  export async function PUT(request) {
85
127
  const files = socialFiles();
86
128
  if (!files) return Response.json({ error: 'Pick a profile first.' }, { status: 400 });
@@ -95,19 +137,34 @@ export async function PUT(request) {
95
137
  if (me?.id) q.eq('user_id', me.id);
96
138
  conns = (await q).data || [];
97
139
  } catch { /* no connections yet */ }
98
- const { live: matched, unmatched } = matchLive(body?.live || {}, conns);
140
+ const { live: matched, others: found, unmatched } = matchLive(body?.live || {}, conns);
99
141
  const live = {};
142
+ const others = {};
100
143
  const previews = {};
101
- for (const [key, { preview, ...rest }] of Object.entries(matched)) {
102
- live[key] = rest;
103
- if (saved.keepMessages === true && preview?.text) previews[key] = preview;
144
+ for (const [into, from] of [[live, matched], [others, found]]) {
145
+ for (const [key, { preview, ...rest }] of Object.entries(from)) {
146
+ into[key] = rest;
147
+ if (saved.keepMessages === true && preview?.text) previews[key] = preview;
148
+ }
149
+ }
150
+ const groups = {};
151
+ for (const g of saved.liveGroups || []) groups[g.threadUrl || g.people.join(',')] = g;
152
+ for (const g of matchLiveGroups(body?.groups || [], conns)) {
153
+ const k = g.threadUrl || g.people.join(',');
154
+ if (!groups[k] || (g.last || 0) >= (groups[k].last || 0)) groups[k] = g;
104
155
  }
105
- writeJsonAtomic(files.social, { ...saved, live, liveAt: new Date().toISOString() });
156
+ writeJsonAtomic(files.social, {
157
+ ...saved,
158
+ live: mergeLive(saved.live, live),
159
+ liveOthers: mergeLive(saved.liveOthers, others),
160
+ liveGroups: Object.values(groups),
161
+ liveAt: new Date().toISOString(),
162
+ });
106
163
  if (Object.keys(previews).length) {
107
164
  const kept = readJson(files.messages, {}) || {};
108
165
  writeJsonAtomic(files.messages, { ...kept, previews: { ...(kept.previews || {}), ...previews } });
109
166
  }
110
- return Response.json({ ok: true, people: Object.keys(live).length, unmatched, previews: Object.keys(previews).length });
167
+ return Response.json({ ok: true, people: Object.keys(live).length, unmatched, groups: Object.keys(groups).length, previews: Object.keys(previews).length });
111
168
  }
112
169
 
113
170
  // The Social tab's switches: "once a day" for the live messages sync, and
@@ -127,7 +184,8 @@ export async function PATCH(request) {
127
184
  return Response.json({ ok: true, autoSync: next.autoSync === true, keepMessages: next.keepMessages === true });
128
185
  }
129
186
 
130
- // Forget it: both files, the numbers and the words.
187
+ // Forget it: both files, the numbers and the words. Not your CRM notes: the
188
+ // page asks about those on their own (DELETE /api/social/crm).
131
189
  export async function DELETE() {
132
190
  const files = socialFiles();
133
191
  if (files) {
@@ -31,6 +31,7 @@ const WHAT = {
31
31
  company: 'Scanning a company',
32
32
  photos: 'Saving photos',
33
33
  messages: 'Reading your messages list',
34
+ 'messages-full': 'Reading your whole messages history',
34
35
  };
35
36
 
36
37
  // Green while there's plenty left, amber past 60%, red past 90%.
@@ -79,6 +79,7 @@ export default function DataSection() {
79
79
  const [exportNote, setExportNote] = useState(null);
80
80
  const [importNote, setImportNote] = useState(null);
81
81
  const [photos, setPhotos] = useState(true);
82
+ const [social, setSocial] = useState(true);
82
83
  const [file, setFile] = useState(null);
83
84
  const [confirmed, setConfirmed] = useState(false);
84
85
  const [restarting, setRestarting] = useState(false);
@@ -137,7 +138,7 @@ export default function DataSection() {
137
138
  const res = await fetch('/api/data/export', {
138
139
  method: 'POST',
139
140
  headers: { 'Content-Type': 'application/json' },
140
- body: JSON.stringify({ photos }),
141
+ body: JSON.stringify({ photos, social }),
141
142
  });
142
143
  if (!res.ok) await answer(res, 'The copy could not be made.');
143
144
  const blob = await res.blob();
@@ -324,6 +325,13 @@ export default function DataSection() {
324
325
  They can’t be downloaded again without a new scan.
325
326
  </span>
326
327
  </label>
328
+ <label style={check}>
329
+ <input type="checkbox" checked={social} onChange={(e) => setSocial(e.target.checked)} style={{ marginTop: 3 }} />
330
+ <span>
331
+ Include the Social tab: its findings, your CRM notes, stages and follow-ups, and any messages you keep
332
+ there.
333
+ </span>
334
+ </label>
327
335
  <div style={row}>
328
336
  <Btn primary onClick={exportNow} disabled={!!busy || restarting || info.people === 0}>
329
337
  {busy === 'export' ? 'Saving…' : 'Save a copy of my network'}
@@ -334,8 +342,9 @@ export default function DataSection() {
334
342
  <Body style={small}>
335
343
  The copy holds the names, headlines and photos of the people in your network. Keep it private: don’t
336
344
  post it or share it, and delete it once it has been imported. A network opened from a LinkedIn CSV lives
337
- only in its browser tab, so it isn’t in the copy; import the CSV again on the new computer. The Social
338
- tab’s findings, and any messages you keep there, stay on this computer and aren’t in the copy either.
345
+ only in its browser tab, so it isn’t in the copy; import the CSV again on the new computer. With the
346
+ Social tab included, the copy also holds your CRM notes and any messages you keep, so it&rsquo;s as private
347
+ as your inbox.
339
348
  </Body>
340
349
 
341
350
  <h3 style={subhead}>Bring in a network from another computer</h3>
@@ -46,3 +46,14 @@ html:has([data-field-question]) {
46
46
  /* The notch's running dot (app/components/ScanStatusBar.js). */
47
47
  @keyframes notch-pulse { 50% { opacity: 0.35; } }
48
48
  @media (prefers-reduced-motion: no-preference) { .notch-pulse { animation: notch-pulse 1.6s ease-in-out infinite; } }
49
+
50
+ /* The Social tab's CRM (app/social/Crm.js): the list and the person side by
51
+ side, stacked on a phone. In Pipeline the board takes the whole width and
52
+ the person sits under it. */
53
+ .crm-grid { display: grid; grid-template-columns: minmax(280px, 400px) minmax(0, 1fr); gap: 10px; align-items: start; }
54
+ .crm-grid > .crm-wide { grid-column: 1 / -1; }
55
+ .crm-board { display: grid; grid-template-columns: repeat(6, minmax(150px, 1fr)); gap: 10px; padding: 10px; overflow-x: auto; }
56
+ @media (max-width: 760px) {
57
+ .crm-grid { grid-template-columns: minmax(0, 1fr); }
58
+ .crm-board { grid-template-columns: minmax(0, 1fr); }
59
+ }