six-degrees 0.4.9 → 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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/routes-manifest.json +6 -0
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +12 -12
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/crm/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/social/crm/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js +10 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/social/crm/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/social/messages/route.js +4 -3
- package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/route.js +4 -3
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +15 -15
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/scores.html +1 -1
- package/.next/standalone/.next/server/app/scores.rsc +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/social.html +1 -1
- package/.next/standalone/.next/server/app/social.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__06mi9k2._.js → [root-of-the-server]__05_3-22._.js} +7 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tir0vc._.js +8 -0
- package/.next/standalone/.next/server/chunks/_0ea9qwy._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +4 -4
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1oqfuh8._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1xh07bp._.js +1 -1
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_social_crm_route_actions_0d1aflv.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +7 -7
- package/.next/standalone/.next/server/chunks/lib_linkedin-export_12imqxg.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_05gt5ya.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0-85h6g._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08cfj2b._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0c_y09g._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_scores_page_09wepvv.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_social_page_0h7zxda.js +2 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_galaxy-lab_108_lu-.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/0a5iadnmt9u9k.js +1 -0
- package/.next/standalone/.next/static/chunks/15sjx07xhpn6p.js +1 -0
- package/.next/standalone/.next/static/chunks/{054zdthcnyd2h.js → 160wjb73ddwqd.js} +1 -1
- package/.next/standalone/.next/static/chunks/1a74dky22dxd6.js +1 -0
- package/.next/standalone/.next/static/chunks/{04bbi55urkrd6.js → 1c_n0973t07l0.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0bo1dxn5gwzpk.css → 1x_ysly42-teq.css} +1 -1
- package/.next/standalone/.next/static/chunks/{1_mohm2643nxp.js → 236f0jtp68j_h.js} +1 -1
- package/.next/standalone/.next/static/chunks/297e1z34yi--z.js +1 -0
- package/.next/standalone/.next/static/chunks/2_ak6qjxb6oa5.js +1 -0
- package/.next/standalone/.next/static/chunks/{38wwtni5e1ec1.js → 39hr8d-b2ibtw.js} +1 -1
- package/.next/standalone/.next/static/chunks/3ui4d-uqkqj5d.js +2 -0
- package/.next/{static/chunks/1z_rt4jteewqp.js → standalone/.next/static/chunks/3zw8bv7-mc4n6.js} +1 -1
- package/.next/standalone/CHANGELOG.md +57 -0
- package/.next/standalone/app/api/data/export/route.js +2 -1
- package/.next/standalone/app/api/scraper/route.js +5 -1
- package/.next/standalone/app/api/social/crm/route.js +37 -0
- package/.next/standalone/app/api/social/messages/route.js +44 -8
- package/.next/standalone/app/api/social/route.js +80 -22
- package/.next/standalone/app/components/GalaxyLab.js +4 -1
- package/.next/standalone/app/components/ScanStatusBar.js +97 -71
- package/.next/standalone/app/components/settings/DataSection.js +12 -3
- package/.next/standalone/app/globals.css +15 -0
- package/.next/standalone/app/paths/page.js +0 -2
- package/.next/standalone/app/profile/page.js +0 -2
- package/.next/standalone/app/queue/page.js +0 -2
- package/.next/standalone/app/scores/page.js +0 -2
- package/.next/standalone/app/settings/page.js +0 -2
- package/.next/standalone/app/setup/page.js +0 -2
- package/.next/standalone/app/social/Crm.js +683 -0
- package/.next/standalone/app/social/Thread.js +90 -0
- package/.next/standalone/app/social/page.js +71 -62
- package/.next/standalone/lib/data-export.js +10 -4
- package/.next/standalone/lib/data-folder.js +9 -0
- package/.next/standalone/lib/data-import.js +13 -3
- package/.next/standalone/lib/island.js +36 -0
- package/.next/standalone/lib/linkedin-export.js +267 -46
- package/.next/standalone/lib/social-crm.js +415 -0
- package/.next/standalone/lib/social-store.js +13 -6
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +126 -22
- package/.next/static/chunks/0a5iadnmt9u9k.js +1 -0
- package/.next/static/chunks/15sjx07xhpn6p.js +1 -0
- package/.next/static/chunks/{054zdthcnyd2h.js → 160wjb73ddwqd.js} +1 -1
- package/.next/static/chunks/1a74dky22dxd6.js +1 -0
- package/.next/static/chunks/{04bbi55urkrd6.js → 1c_n0973t07l0.js} +1 -1
- package/.next/static/chunks/{0bo1dxn5gwzpk.css → 1x_ysly42-teq.css} +1 -1
- package/.next/static/chunks/{1_mohm2643nxp.js → 236f0jtp68j_h.js} +1 -1
- package/.next/static/chunks/297e1z34yi--z.js +1 -0
- package/.next/static/chunks/2_ak6qjxb6oa5.js +1 -0
- package/.next/static/chunks/{38wwtni5e1ec1.js → 39hr8d-b2ibtw.js} +1 -1
- package/.next/static/chunks/3ui4d-uqkqj5d.js +2 -0
- package/.next/{standalone/.next/static/chunks/1z_rt4jteewqp.js → static/chunks/3zw8bv7-mc4n6.js} +1 -1
- package/CHANGELOG.md +57 -0
- package/lib/data-export.js +10 -4
- package/lib/data-folder.js +9 -0
- package/lib/data-import.js +13 -3
- package/lib/island.js +36 -0
- package/lib/linkedin-export.js +267 -46
- package/lib/social-crm.js +415 -0
- package/lib/social-store.js +13 -6
- package/package.json +1 -1
- package/scripts/scrape.py +126 -22
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08f5awg._.js +0 -8
- package/.next/standalone/.next/server/chunks/lib_18cr1gi._.js +0 -8
- package/.next/standalone/.next/server/chunks/ssr/_0ezj4e0._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1errbuf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +0 -3
- package/.next/standalone/.next/static/chunks/2bftn9gubwf0w.js +0 -1
- package/.next/standalone/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/standalone/.next/static/chunks/39csdcx7yv227.js +0 -1
- package/.next/standalone/.next/static/chunks/3yzlf70j5t0-o.js +0 -1
- package/.next/standalone/.next/static/chunks/3z-swu5pet507.js +0 -1
- package/.next/standalone/app/social/Conversations.js +0 -246
- package/.next/static/chunks/2bftn9gubwf0w.js +0 -1
- package/.next/static/chunks/2pw6_osz1n_te.js +0 -1
- package/.next/static/chunks/39csdcx7yv227.js +0 -1
- package/.next/static/chunks/3yzlf70j5t0-o.js +0 -1
- package/.next/static/chunks/3z-swu5pet507.js +0 -1
- /package/.next/standalone/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_ssgManifest.js +0 -0
- /package/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_buildManifest.js +0 -0
- /package/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{yPlsfKhdS8MMtyXF7twSZ → jqE2D3zWqSGz5su948Azf}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// One conversation's messages in the Social tab's CRM (Crm.js), newest at the
|
|
4
|
+
// bottom. Only while Keep my messages is on: the words live only in the app's
|
|
5
|
+
// data folder (lib/social-store.js), and GET /api/social/messages serves them
|
|
6
|
+
// a page at a time.
|
|
7
|
+
|
|
8
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
9
|
+
import { LINE } from '../components/ui';
|
|
10
|
+
|
|
11
|
+
const THREAD_PAGE = 100;
|
|
12
|
+
const EARLIER_PAGE = 200;
|
|
13
|
+
const when = (t) => (t ? new Date(t).toLocaleString(undefined, { day: 'numeric', month: 'short', year: 'numeric', hour: 'numeric', minute: '2-digit' }) : '');
|
|
14
|
+
|
|
15
|
+
// One conversation's messages, newest at the bottom, a page at a time: the
|
|
16
|
+
// newest 100 first and 200 more each time you ask, so a thread of thousands
|
|
17
|
+
// opens as fast as a short one.
|
|
18
|
+
export default function Thread({ id, keep, group, token }) {
|
|
19
|
+
const [t, setT] = useState(null);
|
|
20
|
+
const [err, setErr] = useState(null);
|
|
21
|
+
const scroller = useRef(null);
|
|
22
|
+
const keepAt = useRef(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!keep) return undefined;
|
|
26
|
+
let live = true;
|
|
27
|
+
fetch(`/api/social/messages?id=${encodeURIComponent(id)}&limit=${THREAD_PAGE}`).then((r) => r.json())
|
|
28
|
+
.then((d) => { if (live) { if (d.error) setErr(d.error); else { keepAt.current = 'bottom'; setT(d); } } })
|
|
29
|
+
.catch(() => { if (live) setErr('It couldn’t be read.'); });
|
|
30
|
+
return () => { live = false; };
|
|
31
|
+
}, [id, keep, token]);
|
|
32
|
+
|
|
33
|
+
// Opened at the newest; after "Show earlier", where you were stays put.
|
|
34
|
+
useLayoutEffect(() => {
|
|
35
|
+
const el = scroller.current;
|
|
36
|
+
if (!el || !keepAt.current) return;
|
|
37
|
+
if (keepAt.current === 'bottom') el.scrollTop = el.scrollHeight;
|
|
38
|
+
else el.scrollTop = el.scrollHeight - keepAt.current;
|
|
39
|
+
keepAt.current = null;
|
|
40
|
+
}, [t]);
|
|
41
|
+
|
|
42
|
+
async function earlier() {
|
|
43
|
+
const el = scroller.current;
|
|
44
|
+
const d = await fetch(`/api/social/messages?id=${encodeURIComponent(id)}&before=${t.start}&limit=${EARLIER_PAGE}`).then((r) => r.json()).catch(() => null);
|
|
45
|
+
if (!d || d.error) return;
|
|
46
|
+
keepAt.current = el ? el.scrollHeight - el.scrollTop : null;
|
|
47
|
+
setT({ ...t, start: d.start, messages: [...d.messages, ...t.messages] });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pad = { padding: '10px 14px 14px', fontSize: 12.5, color: '#8b9a9a' };
|
|
51
|
+
if (!keep) return <div style={pad}>Turn on <b>Keep my messages</b> to read them here.</div>;
|
|
52
|
+
if (err) return <div style={{ ...pad, color: '#ff9b9b' }}>{err}</div>;
|
|
53
|
+
if (!t) return <div style={pad}>Opening…</div>;
|
|
54
|
+
if (!t.total) {
|
|
55
|
+
return (
|
|
56
|
+
<div style={pad}>
|
|
57
|
+
No messages are kept for this conversation yet. Choose your export folder again, or sync your messages, to
|
|
58
|
+
bring them in.
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return (
|
|
63
|
+
<div ref={scroller} style={{ maxHeight: 440, overflowY: 'auto', padding: '8px 14px 12px', background: 'rgba(0,0,0,0.2)' }}>
|
|
64
|
+
{t.start > 0 && (
|
|
65
|
+
<div style={{ textAlign: 'center', margin: '4px 0 10px' }}>
|
|
66
|
+
<button onClick={earlier} style={{ background: 'rgba(255,255,255,0.06)', border: LINE, borderRadius: 6, color: '#cfe6f7', cursor: 'pointer', fontSize: 12, padding: '4px 12px' }}>
|
|
67
|
+
Show earlier ({t.start.toLocaleString()} more)
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
{t.messages.map((m, i) => (
|
|
72
|
+
<div key={t.start + i} style={{ display: 'flex', justifyContent: m.fromMe ? 'flex-end' : 'flex-start', margin: '6px 0' }}>
|
|
73
|
+
<div style={{ maxWidth: '72%' }}>
|
|
74
|
+
<div style={{
|
|
75
|
+
padding: '7px 11px', borderRadius: 12, fontSize: 13, lineHeight: 1.45, whiteSpace: 'pre-wrap', overflowWrap: 'anywhere',
|
|
76
|
+
background: m.fromMe ? 'rgba(52,152,219,0.35)' : 'rgba(255,255,255,0.08)', color: '#eef',
|
|
77
|
+
borderBottomRightRadius: m.fromMe ? 4 : 12, borderBottomLeftRadius: m.fromMe ? 12 : 4,
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
{m.text || <i style={{ color: '#8b9a9a' }}>(no words: an attachment or a reaction)</i>}
|
|
81
|
+
</div>
|
|
82
|
+
<div style={{ fontSize: 10.5, color: '#778', marginTop: 2, textAlign: m.fromMe ? 'right' : 'left' }}>
|
|
83
|
+
{m.fromMe ? 'You' : group ? 'Someone in the group' : 'Them'} · {when(m.t)}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
// your data), read here in the browser: full history, no traffic to LinkedIn;
|
|
7
7
|
// - an experimental live sync (the scanner's --messages): your messages list,
|
|
8
8
|
// read once in your own Chrome, by hand or once a day.
|
|
9
|
-
// Kept on this computer: dates, who wrote last and counts,
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// Kept on this computer: dates, who wrote last and counts, the names of people
|
|
10
|
+
// who aren't your connections, and the messages themselves (and the notes sent
|
|
11
|
+
// with requests) only if you turn on Keep my messages, in a file of their own
|
|
12
|
+
// that switching it off deletes. The CRM (Crm.js) is the main section: everyone
|
|
13
|
+
// you've been in touch with, and your own stage, tags, notes and follow-ups.
|
|
12
14
|
|
|
13
15
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
14
16
|
import Link from 'next/link';
|
|
@@ -16,20 +18,18 @@ import OnboardingGate from '../components/OnboardingGate';
|
|
|
16
18
|
import { useUser } from '../components/UserProvider';
|
|
17
19
|
import { loadNetwork } from '../../lib/network';
|
|
18
20
|
import {
|
|
19
|
-
readTable, messageStats, careerChapters, postingEffect, invitationSplit, warmthOf,
|
|
21
|
+
readTable, messageStats, careerChapters, postingEffect, invitationSplit, warmthOf, buildConversations, conversationSummary, buildInvitations,
|
|
20
22
|
} from '../../lib/linkedin-export';
|
|
21
23
|
import { mergeSocial } from '../../lib/galaxy-lab';
|
|
22
24
|
import { keyFor } from '../../lib/separation';
|
|
23
25
|
import { runScrape, watchScanner, scannerNow } from '../../lib/scraper-client';
|
|
24
26
|
import { Body, LINE, FONT } from '../components/ui';
|
|
25
27
|
import { IS_DEMO } from '../../lib/demo';
|
|
26
|
-
import
|
|
28
|
+
import Crm from './Crm';
|
|
27
29
|
|
|
28
|
-
const TIER = { S: '#FFD700', A: '#9B59B6', B: '#3498DB', C: '#95A5A6', D: '#BDC3C7' };
|
|
29
30
|
const WARMTH = { warm: ['Warm', '#ff9f43'], cool: ['Cool', '#3498DB'], dormant: ['Dormant', '#8899aa'], never: ['Never messaged', '#556'] };
|
|
30
31
|
const FILES = { 'connections.csv': 'first name', 'messages.csv': 'conversation id', 'invitations.csv': 'direction', 'positions.csv': 'company name', 'shares.csv': 'date' };
|
|
31
32
|
const day = (t) => (t ? new Date(t).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' }) : '');
|
|
32
|
-
const slug = (u) => String(u || '').replace(/\/+$/, '').split('/').pop();
|
|
33
33
|
// Messages go to the server in parts of about this many characters: a request
|
|
34
34
|
// here can carry 10 MB, and a character can take up to 3 bytes.
|
|
35
35
|
const PART_CHARS = 2_500_000;
|
|
@@ -55,6 +55,14 @@ async function sendMessages(conversations) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/** The notes sent with requests, when the messages are kept: they ride in the same file and go with it. */
|
|
59
|
+
async function sendInviteNotes(invitations) {
|
|
60
|
+
const invites = {};
|
|
61
|
+
for (const i of invitations) if (i.note) invites[i.id] = i.note;
|
|
62
|
+
const r = await fetch('/api/social/messages', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ invites }) });
|
|
63
|
+
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || 'The requests’ notes couldn’t be saved.');
|
|
64
|
+
}
|
|
65
|
+
|
|
58
66
|
export default function SocialPage() {
|
|
59
67
|
return <OnboardingGate><SocialInner /></OnboardingGate>;
|
|
60
68
|
}
|
|
@@ -69,9 +77,11 @@ function SocialInner() {
|
|
|
69
77
|
const [job, setJob] = useState(() => scannerNow());
|
|
70
78
|
const [switching, setSwitching] = useState(false);
|
|
71
79
|
const [listToken, setListToken] = useState(0);
|
|
72
|
-
|
|
73
|
-
//
|
|
80
|
+
const [crmToken, setCrmToken] = useState(0);
|
|
81
|
+
// The last export's messages.csv and Invitations.csv, in this window only, so
|
|
82
|
+
// turning Keep my messages on right after an import needn't ask for the folder again.
|
|
74
83
|
const lastMessages = useRef(null);
|
|
84
|
+
const lastInvites = useRef(null);
|
|
75
85
|
const keep = social?.keepMessages === true;
|
|
76
86
|
|
|
77
87
|
// The Conversations list reads again whenever this does (listToken).
|
|
@@ -86,14 +96,6 @@ function SocialInner() {
|
|
|
86
96
|
return () => { live = false; };
|
|
87
97
|
}, [userId]);
|
|
88
98
|
|
|
89
|
-
// Everyone you know, by profile: name and tier for the lists below.
|
|
90
|
-
const byKey = useMemo(() => {
|
|
91
|
-
const m = new Map();
|
|
92
|
-
for (const r of [...(net?.degree1 || []), ...(net?.degree2 || [])]) if (!m.has(keyFor(r))) m.set(keyFor(r), r);
|
|
93
|
-
return m;
|
|
94
|
-
}, [net]);
|
|
95
|
-
const connected = useMemo(() => new Set((net?.degree1 || []).map(keyFor)), [net]);
|
|
96
|
-
|
|
97
99
|
async function importFolder(list) {
|
|
98
100
|
setBusy(true);
|
|
99
101
|
setNote(null);
|
|
@@ -108,7 +110,9 @@ function SocialInner() {
|
|
|
108
110
|
const stats = found['messages.csv'] ? messageStats(found['messages.csv']) : { asOf: null, people: new Map() };
|
|
109
111
|
// The words are read out of messages.csv only when Keep my messages is on.
|
|
110
112
|
const built = found['messages.csv'] ? buildConversations(found['messages.csv'], { keepText: keep }) : null;
|
|
113
|
+
const invitations = found['invitations.csv'] ? buildInvitations(found['invitations.csv'], { keepText: keep }) : null;
|
|
111
114
|
lastMessages.current = found['messages.csv'] || null;
|
|
115
|
+
lastInvites.current = found['invitations.csv'] || null;
|
|
112
116
|
const people = {};
|
|
113
117
|
for (const [url, v] of stats.people) people[url] = { last: v.last, lastFromThem: v.lastFromThem, total: v.total, recent: v.recent };
|
|
114
118
|
if (keepEmails) {
|
|
@@ -127,15 +131,26 @@ function SocialInner() {
|
|
|
127
131
|
chapters: careerChapters(found['positions.csv'] || [], conns, asOf),
|
|
128
132
|
posts: found['shares.csv'] ? postingEffect(found['shares.csv'], conns) : null,
|
|
129
133
|
invites: found['invitations.csv'] ? invitationSplit(found['invitations.csv']) : null,
|
|
134
|
+
// One record per request, without the notes: those go with the messages, below.
|
|
135
|
+
invitations: (invitations || []).map(({ note, ...rest }) => rest),
|
|
136
|
+
// Names as the export spells them, so people who aren't connections can be listed.
|
|
137
|
+
names: built ? built.names : {},
|
|
130
138
|
emails: keepEmails,
|
|
131
139
|
};
|
|
132
140
|
const r = await fetch('/api/social', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
133
141
|
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || 'It couldn’t be saved.');
|
|
134
142
|
if (built && keep) await sendMessages(built.conversations);
|
|
143
|
+
if (invitations && keep) await sendInviteNotes(invitations);
|
|
144
|
+
const odd = built ? [
|
|
145
|
+
built.drafts ? `${built.drafts.toLocaleString()} unsent ${built.drafts === 1 ? 'draft' : 'drafts'} left out` : '',
|
|
146
|
+
built.skipped ? `${built.skipped.toLocaleString()} ${built.skipped === 1 ? 'row' : 'rows'} with no conversation or date left out` : '',
|
|
147
|
+
].filter(Boolean) : [];
|
|
135
148
|
setNote({
|
|
136
149
|
good: true,
|
|
137
150
|
text: `Read ${Object.keys(found).length} of LinkedIn’s files: ${stats.people.size.toLocaleString()} people you’ve messaged one to one`
|
|
138
|
-
+ (built ? `, ${built.conversations.length.toLocaleString()} conversations${keep ? ', their messages kept on this computer' : ''}
|
|
151
|
+
+ (built ? `, ${built.conversations.length.toLocaleString()} conversations${keep ? ', their messages kept on this computer' : ''}` : '')
|
|
152
|
+
+ (invitations ? `, ${invitations.length.toLocaleString()} connection requests` : '')
|
|
153
|
+
+ `.${odd.length ? ` (${odd.join('; ')}.)` : ''}`,
|
|
139
154
|
});
|
|
140
155
|
await reload();
|
|
141
156
|
} catch (e) {
|
|
@@ -145,11 +160,15 @@ function SocialInner() {
|
|
|
145
160
|
}
|
|
146
161
|
}
|
|
147
162
|
|
|
148
|
-
|
|
163
|
+
// The daily sync reads the top of the list; `full` scrolls until the whole
|
|
164
|
+
// history has loaded (scripts/scrape.py --full-history), at the same pace.
|
|
165
|
+
async function syncNow(full = false) {
|
|
149
166
|
setNote(null);
|
|
150
167
|
try {
|
|
151
|
-
const end = await runScrape('messages');
|
|
152
|
-
setNote(end?.exitCode === 0
|
|
168
|
+
const end = await runScrape(full ? 'messages-full' : 'messages');
|
|
169
|
+
setNote(end?.exitCode === 0
|
|
170
|
+
? { good: true, text: full ? 'Your whole messages history is read.' : 'Your messages list is read.' }
|
|
171
|
+
: { good: false, text: 'The sync stopped before it finished. The Scan page has the log.' });
|
|
153
172
|
await reload();
|
|
154
173
|
} catch (e) {
|
|
155
174
|
setNote({ good: false, text: e.message });
|
|
@@ -172,8 +191,9 @@ function SocialInner() {
|
|
|
172
191
|
if (!on) await fetch('/api/social/messages', { method: 'DELETE' });
|
|
173
192
|
const r = await fetch('/api/social', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ keepMessages: on }) });
|
|
174
193
|
if (!r.ok) throw new Error('The switch couldn’t be saved.');
|
|
175
|
-
if (on && lastMessages.current) {
|
|
176
|
-
await sendMessages(buildConversations(lastMessages.current, { keepText: true }).conversations);
|
|
194
|
+
if (on && (lastMessages.current || lastInvites.current)) {
|
|
195
|
+
if (lastMessages.current) await sendMessages(buildConversations(lastMessages.current, { keepText: true }).conversations);
|
|
196
|
+
if (lastInvites.current) await sendInviteNotes(buildInvitations(lastInvites.current, { keepText: true }));
|
|
177
197
|
setNote({ good: true, text: 'Your messages are kept on this computer now.' });
|
|
178
198
|
} else if (on) {
|
|
179
199
|
setNote({ good: true, text: 'On. Choose your export folder again, or sync your messages, to bring the messages in.' });
|
|
@@ -188,10 +208,21 @@ function SocialInner() {
|
|
|
188
208
|
}
|
|
189
209
|
}
|
|
190
210
|
|
|
211
|
+
// Forget it: the export's and the sync's findings, and the messages. Your CRM
|
|
212
|
+
// notes are your own work, so they're a question of their own, asked after.
|
|
191
213
|
async function forget() {
|
|
192
214
|
await fetch('/api/social', { method: 'DELETE' });
|
|
193
215
|
lastMessages.current = null;
|
|
194
|
-
|
|
216
|
+
lastInvites.current = null;
|
|
217
|
+
let text = 'Forgotten. Nothing from your export or messages is kept here now, the messages themselves included.';
|
|
218
|
+
if (window.confirm('Also delete your CRM: the stages, tags, notes and follow-up dates you added? This can’t be undone. Cancel keeps them.')) {
|
|
219
|
+
await fetch('/api/social/crm', { method: 'DELETE' });
|
|
220
|
+
setCrmToken((n) => n + 1);
|
|
221
|
+
text += ' Your CRM notes are deleted too.';
|
|
222
|
+
} else {
|
|
223
|
+
text += ' Your CRM notes are kept.';
|
|
224
|
+
}
|
|
225
|
+
setNote({ good: true, text });
|
|
195
226
|
reload();
|
|
196
227
|
}
|
|
197
228
|
|
|
@@ -205,16 +236,6 @@ function SocialInner() {
|
|
|
205
236
|
return counts;
|
|
206
237
|
}, [net, merged, asOf]);
|
|
207
238
|
|
|
208
|
-
const waiting = useMemo(() => {
|
|
209
|
-
const stats = { asOf, people: new Map([...merged].filter(([, v]) => v.lastFromThem != null)) };
|
|
210
|
-
const list = repliesWaiting(stats).map((w) => ({ ...w, person: byKey.get(w.url) }));
|
|
211
|
-
for (const [k, v] of merged) {
|
|
212
|
-
if (v.unread > 0 && !list.some((w) => w.url === k)) list.push({ url: k, unread: v.unread, person: byKey.get(k) });
|
|
213
|
-
}
|
|
214
|
-
const rank = (w) => ({ S: 0, A: 1 }[w.person?.tier] ?? 2);
|
|
215
|
-
return list.sort((a, b) => rank(a) - rank(b) || (a.days ?? 0) - (b.days ?? 0)).slice(0, 15);
|
|
216
|
-
}, [merged, asOf, byKey]);
|
|
217
|
-
|
|
218
239
|
if (IS_DEMO) return <div style={{ minHeight: '100vh', background: '#0a0a1a', color: '#aaa', padding: 40, fontFamily: FONT }}>Not part of the demo.</div>;
|
|
219
240
|
const running = job?.running;
|
|
220
241
|
const box = { padding: '12px 14px', borderRadius: 8, border: LINE, background: 'rgba(255,255,255,0.03)', marginTop: 10 };
|
|
@@ -226,13 +247,12 @@ function SocialInner() {
|
|
|
226
247
|
<Link href="/" style={{ color: '#888', textDecoration: 'none', fontSize: 13, fontWeight: 600, padding: '6px 14px', borderRadius: 6, background: 'rgba(255,255,255,0.06)', border: LINE }}>← Back to Map</Link>
|
|
227
248
|
<h1 style={{ fontSize: 22, fontWeight: 700, margin: 0, color: '#FFD700' }}>Social <span style={{ fontSize: 12, color: '#8b9a9a', fontWeight: 600 }}>experimental</span></h1>
|
|
228
249
|
</header>
|
|
229
|
-
{
|
|
230
|
-
<div id="scan-bar-slot" />
|
|
231
|
-
<main style={{ maxWidth: 980, margin: '0 auto', padding: '8px 24px 64px' }}>
|
|
250
|
+
<main style={{ maxWidth: 1180, margin: '0 auto', padding: '8px 16px 64px' }}>
|
|
232
251
|
<Body style={{ marginTop: 16 }}>
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
252
|
+
Everyone you’ve been in touch with on LinkedIn, connections or not: who’s waiting on you, who
|
|
253
|
+
hasn’t answered, what you’ve sent and received, and your own notes and follow-ups on each, from
|
|
254
|
+
your own LinkedIn data. Only dates, who wrote last, counts and names are kept on this computer, and what
|
|
255
|
+
messages say only if you turn on <i>Keep my messages</i> below.
|
|
236
256
|
</Body>
|
|
237
257
|
|
|
238
258
|
<h2 style={h2}>Your LinkedIn export</h2>
|
|
@@ -258,14 +278,19 @@ function SocialInner() {
|
|
|
258
278
|
<h2 style={h2}>Live messages sync <span style={{ fontSize: 11, color: '#FFD700' }}>experimental</span></h2>
|
|
259
279
|
<div style={box}>
|
|
260
280
|
<Body style={{ margin: 0 }}>
|
|
261
|
-
Reads your messages list once, in your own Chrome: who each
|
|
262
|
-
active, whether it’s unread and who wrote last. It uses no search budget and opens no
|
|
263
|
-
newest message’s words are kept only if <i>Keep my messages</i> is on.
|
|
281
|
+
Reads your messages list once, in your own Chrome: who each conversation is with, connections or not, when it
|
|
282
|
+
was last active, whether it’s unread and who wrote last. It uses no search budget and opens no
|
|
283
|
+
conversation. The newest message’s words are kept only if <i>Keep my messages</i> is on. The daily sync
|
|
284
|
+
reads the top of the list; <i>Read my whole history</i> keeps scrolling until no more conversations load
|
|
285
|
+
(at most 60 scrolls, about 1,000 conversations), at the same slow, fixed pace, so it takes a few minutes.
|
|
264
286
|
</Body>
|
|
265
287
|
<div style={{ marginTop: 10, display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
266
|
-
<button onClick={syncNow} disabled={running} style={{ padding: '7px 14px', borderRadius: 8, border: 'none', background: running ? '#333' : 'rgba(0,255,136,0.15)', color: running ? '#777' : '#00ff88', cursor: running ? 'default' : 'pointer', fontSize: 13 }}>
|
|
288
|
+
<button onClick={() => syncNow(false)} disabled={running} style={{ padding: '7px 14px', borderRadius: 8, border: 'none', background: running ? '#333' : 'rgba(0,255,136,0.15)', color: running ? '#777' : '#00ff88', cursor: running ? 'default' : 'pointer', fontSize: 13 }}>
|
|
267
289
|
{running ? 'The scanner is busy' : 'Sync messages now'}
|
|
268
290
|
</button>
|
|
291
|
+
<button onClick={() => syncNow(true)} disabled={running} style={{ padding: '7px 14px', borderRadius: 8, border: LINE, background: 'none', color: running ? '#777' : '#cfe6f7', cursor: running ? 'default' : 'pointer', fontSize: 13 }}>
|
|
292
|
+
Read my whole history
|
|
293
|
+
</button>
|
|
269
294
|
<label style={{ display: 'flex', gap: 6, alignItems: 'center', fontSize: 12.5, color: '#b8c4c4' }}>
|
|
270
295
|
<input type="checkbox" checked={social?.autoSync === true} onChange={(e) => setAuto(e.target.checked)} />
|
|
271
296
|
Once a day while the app is open (daytime only)
|
|
@@ -274,8 +299,8 @@ function SocialInner() {
|
|
|
274
299
|
</div>
|
|
275
300
|
{note && <div style={{ marginTop: 10, fontSize: 13, color: note.good ? '#00ff88' : '#ff9b9b' }}>{note.text}</div>}
|
|
276
301
|
|
|
277
|
-
<h2 style={h2}>
|
|
278
|
-
<
|
|
302
|
+
<h2 style={h2}>People</h2>
|
|
303
|
+
<Crm net={net} asOf={social ? asOf : null} keep={keep} onKeep={setKeep} switching={switching || busy} token={listToken} crmToken={crmToken} />
|
|
279
304
|
|
|
280
305
|
{social && (
|
|
281
306
|
<>
|
|
@@ -292,22 +317,6 @@ function SocialInner() {
|
|
|
292
317
|
Of your connections. Warm: messaged in the last 30 days, or 3+ messages in 90. Cool: within a year. Dormant: longer ago.
|
|
293
318
|
</div>
|
|
294
319
|
|
|
295
|
-
<h2 style={h2}>Waiting on you</h2>
|
|
296
|
-
<div style={{ ...box, padding: 0 }}>
|
|
297
|
-
{waiting.length === 0 && <div style={{ padding: 12, fontSize: 13, color: '#8b9a9a' }}>No one is waiting on a reply.</div>}
|
|
298
|
-
{waiting.map((w) => (
|
|
299
|
-
<div key={w.url} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 14px', borderBottom: '1px solid rgba(255,255,255,0.04)', fontSize: 13 }}>
|
|
300
|
-
<span style={{ width: 8, height: 8, borderRadius: '50%', background: TIER[w.person?.tier] || '#556', flexShrink: 0 }} />
|
|
301
|
-
<span style={{ flex: 1, minWidth: 0 }}>
|
|
302
|
-
<b>{w.person?.name || slug(w.url)}</b>
|
|
303
|
-
{w.person?.company ? <span style={{ color: '#8b9a9a' }}> · {w.person.company}</span> : null}
|
|
304
|
-
</span>
|
|
305
|
-
<span style={{ color: '#b8c4c4' }}>{w.unread ? `${w.unread} unread` : `wrote ${w.days} days before your export`}</span>
|
|
306
|
-
<a href={w.url} target="_blank" rel="noreferrer" style={{ color: '#3498DB', textDecoration: 'none' }}>Open ↗</a>
|
|
307
|
-
</div>
|
|
308
|
-
))}
|
|
309
|
-
</div>
|
|
310
|
-
|
|
311
320
|
{social.chapters?.length > 0 && (
|
|
312
321
|
<>
|
|
313
322
|
<h2 style={h2}>Career chapters</h2>
|
|
@@ -14,7 +14,7 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
14
14
|
import { readFileSync, lstatSync, statSync } from 'node:fs';
|
|
15
15
|
import { createHash } from 'node:crypto';
|
|
16
16
|
import path from 'node:path';
|
|
17
|
-
import { AVATAR_FILE, TRAVELLING_FILES, namesInFolder } from './data-folder.js';
|
|
17
|
+
import { AVATAR_FILE, TRAVELLING_FILES, SOCIAL_FILE, namesInFolder } from './data-folder.js';
|
|
18
18
|
|
|
19
19
|
/** Bump when the file's layout changes in a way an older import can't read. */
|
|
20
20
|
export const EXPORT_FORMAT = 1;
|
|
@@ -72,7 +72,7 @@ export function referencedPhotos(db) {
|
|
|
72
72
|
* plain files are taken, never a link, and avatars/ is read only if it is a
|
|
73
73
|
* real folder, so nothing outside the data folder can be pulled in through one.
|
|
74
74
|
*/
|
|
75
|
-
export function travellingFiles(dir, { includePhotos = true, photos = null } = {}) {
|
|
75
|
+
export function travellingFiles(dir, { includePhotos = true, photos = null, includeSocial = true } = {}) {
|
|
76
76
|
const plain = (p) => {
|
|
77
77
|
try { return lstatSync(p).isFile(); } catch { return false; }
|
|
78
78
|
};
|
|
@@ -81,6 +81,12 @@ export function travellingFiles(dir, { includePhotos = true, photos = null } = {
|
|
|
81
81
|
const abs = path.join(dir, name);
|
|
82
82
|
if (plain(abs)) out.push({ rel: name, abs });
|
|
83
83
|
}
|
|
84
|
+
if (includeSocial) {
|
|
85
|
+
for (const name of namesInFolder(dir).sort()) {
|
|
86
|
+
const abs = path.join(dir, name);
|
|
87
|
+
if (SOCIAL_FILE.test(name) && plain(abs)) out.push({ rel: name, abs });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
84
90
|
if (includePhotos) {
|
|
85
91
|
for (const name of namesInFolder(path.join(dir, 'avatars')).sort()) {
|
|
86
92
|
if (!AVATAR_FILE.test(name) || (photos && !photos.has(name))) continue;
|
|
@@ -96,7 +102,7 @@ export function travellingFiles(dir, { includePhotos = true, photos = null } = {
|
|
|
96
102
|
* `outFile`, which must not exist yet.
|
|
97
103
|
* @returns {{ bytes, people, photos, files, counts }}
|
|
98
104
|
*/
|
|
99
|
-
export function buildExport(db, { dir, outFile, includePhotos = true, appVersion, schemaSql, now = new Date() }) {
|
|
105
|
+
export function buildExport(db, { dir, outFile, includePhotos = true, includeSocial = true, appVersion, schemaSql, now = new Date() }) {
|
|
100
106
|
db.exec(`VACUUM INTO ${sqlString(outFile)}`);
|
|
101
107
|
|
|
102
108
|
const out = new DatabaseSync(outFile);
|
|
@@ -127,7 +133,7 @@ export function buildExport(db, { dir, outFile, includePhotos = true, appVersion
|
|
|
127
133
|
// photo of someone whose circle was deleted since; the copy has no row
|
|
128
134
|
// for them, so their face has no reason to travel.
|
|
129
135
|
const inNetwork = includePhotos ? referencedPhotos(out) : null;
|
|
130
|
-
for (const f of travellingFiles(dir, { includePhotos, photos: inNetwork })) {
|
|
136
|
+
for (const f of travellingFiles(dir, { includePhotos, photos: inNetwork, includeSocial })) {
|
|
131
137
|
let bytes;
|
|
132
138
|
try { bytes = readFileSync(f.abs); } catch { continue; } // replaced since the listing
|
|
133
139
|
put.run(f.rel, bytes, sha256(bytes));
|
|
@@ -44,6 +44,15 @@ export const BUDGET_FILES = [
|
|
|
44
44
|
'linkedin-cooldown.json',
|
|
45
45
|
];
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* The Social tab's files, one set per profile: its findings, the messages kept
|
|
49
|
+
* with "Keep my messages on this computer", and the CRM's stages, tags and
|
|
50
|
+
* notes (lib/social-store.js). A copy carries them when asked (Blake,
|
|
51
|
+
* 2026-09-29: everything goes with the network), and an import puts them back
|
|
52
|
+
* in place of this computer's, which are kept beside the old network.
|
|
53
|
+
*/
|
|
54
|
+
export const SOCIAL_FILE = /^(?:social|social-messages|crm)-[\w-]+\.json$/;
|
|
55
|
+
|
|
47
56
|
/** Everything of the scanner's that an export carries. */
|
|
48
57
|
export const TRAVELLING_FILES = [...NETWORK_FILES, ...BUDGET_FILES];
|
|
49
58
|
|
|
@@ -31,7 +31,7 @@ import { runningNow } from './scan-state.js';
|
|
|
31
31
|
import { durable } from './durable.js';
|
|
32
32
|
import { budgetFileProblem, mergeBudgetFiles } from './linkedin-limits.js';
|
|
33
33
|
import {
|
|
34
|
-
AVATAR_FILE, TRAVELLING_FILES, NETWORK_FILES, BUDGET_FILES, PENDING_DIR, IMPORT_BACKUP_PREFIX,
|
|
34
|
+
AVATAR_FILE, TRAVELLING_FILES, NETWORK_FILES, BUDGET_FILES, SOCIAL_FILE, PENDING_DIR, IMPORT_BACKUP_PREFIX,
|
|
35
35
|
sweepLeftovers, namesInFolder,
|
|
36
36
|
} from './data-folder.js';
|
|
37
37
|
import {
|
|
@@ -158,10 +158,11 @@ export async function receiveUpload(body, file, { declared }) {
|
|
|
158
158
|
return received;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
/** 'photo', 'file' (one of the scanner's travelling files), or null for anything an export never carries. */
|
|
161
|
+
/** 'photo', 'file' (one of the scanner's travelling files), 'social', or null for anything an export never carries. */
|
|
162
162
|
export function exportedFileKind(rel) {
|
|
163
163
|
if (typeof rel !== 'string') return null;
|
|
164
164
|
if (TRAVELLING_FILES.includes(rel)) return 'file';
|
|
165
|
+
if (SOCIAL_FILE.test(rel)) return 'social';
|
|
165
166
|
// The same pattern the /avatars route serves: no folders, no dots but the
|
|
166
167
|
// extension, so no path can climb out of avatars/.
|
|
167
168
|
if (rel.startsWith('avatars/') && AVATAR_FILE.test(rel.slice('avatars/'.length))) return 'photo';
|
|
@@ -421,7 +422,7 @@ export function stageImport(upload, {
|
|
|
421
422
|
throw new ImportError(`“${shortName(rel)}” inside this file is damaged, so nothing was imported.`);
|
|
422
423
|
}
|
|
423
424
|
writeFileSync(path.join(filesDir, rel), row.bytes, { flag: 'wx' });
|
|
424
|
-
if (kind === 'file') scannerFiles.push(path.join(filesDir, rel));
|
|
425
|
+
if (kind === 'file' || kind === 'social') scannerFiles.push(path.join(filesDir, rel));
|
|
425
426
|
}
|
|
426
427
|
} finally {
|
|
427
428
|
db.close();
|
|
@@ -731,6 +732,14 @@ export function applyPendingImport({ dir, dbFile, now = new Date(), copyDatabase
|
|
|
731
732
|
keptFolder();
|
|
732
733
|
move(from, path.join(keptFiles, name));
|
|
733
734
|
}
|
|
735
|
+
// This computer's Social files step aside only when the copy brings its
|
|
736
|
+
// own; a copy made without them leaves them where they are.
|
|
737
|
+
if (namesInFolder(path.join(pending, 'files')).some((n) => SOCIAL_FILE.test(n))) {
|
|
738
|
+
for (const name of namesInFolder(dir).filter((n) => SOCIAL_FILE.test(n))) {
|
|
739
|
+
keptFolder();
|
|
740
|
+
move(path.join(dir, name), path.join(keptFiles, name));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
734
743
|
for (const name of BUDGET_FILES) {
|
|
735
744
|
const from = path.join(dir, name);
|
|
736
745
|
if (!exists(from) || !lstatSync(from).isFile()) continue;
|
|
@@ -786,6 +795,7 @@ export function applyPendingImport({ dir, dbFile, now = new Date(), copyDatabase
|
|
|
786
795
|
const from = path.join(files, name);
|
|
787
796
|
if (exists(from)) move(from, path.join(dir, name));
|
|
788
797
|
}
|
|
798
|
+
for (const name of namesInFolder(files).filter((n) => SOCIAL_FILE.test(n))) move(path.join(files, name), path.join(dir, name));
|
|
789
799
|
mergeBudgetFiles({ dir, from: files });
|
|
790
800
|
durable.syncFolder(dir);
|
|
791
801
|
step('done');
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The notch (app/components/ScanStatusBar.js): one small pill hanging under the
|
|
2
|
+
// header that shows what the app is doing. The scanner shows there on its own;
|
|
3
|
+
// anything else that runs for a while (a replay being recorded, an import) can
|
|
4
|
+
// show there too, through this list.
|
|
5
|
+
//
|
|
6
|
+
// const done = showActivity({ id: 'replay', label: 'Recording the replay', detail: '12 s' });
|
|
7
|
+
// … later: done();
|
|
8
|
+
//
|
|
9
|
+
// An activity is { id, label, detail?, progress? (0–1), tone? ('run' | 'idle' | 'warn') }.
|
|
10
|
+
// Showing the same id again replaces it. The list only changes when something
|
|
11
|
+
// in it does, as useSyncExternalStore needs.
|
|
12
|
+
|
|
13
|
+
const listeners = new Set();
|
|
14
|
+
let activities = [];
|
|
15
|
+
|
|
16
|
+
export const activitiesNow = () => activities;
|
|
17
|
+
const EMPTY = [];
|
|
18
|
+
export const noActivities = () => EMPTY;
|
|
19
|
+
|
|
20
|
+
export function watchActivities(f) {
|
|
21
|
+
listeners.add(f);
|
|
22
|
+
return () => listeners.delete(f);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function showActivity(a) {
|
|
26
|
+
if (!a?.id) return () => {};
|
|
27
|
+
activities = [...activities.filter((x) => x.id !== a.id), a];
|
|
28
|
+
listeners.forEach((f) => f());
|
|
29
|
+
return () => clearActivity(a.id);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function clearActivity(id) {
|
|
33
|
+
if (!activities.some((x) => x.id === id)) return;
|
|
34
|
+
activities = activities.filter((x) => x.id !== id);
|
|
35
|
+
listeners.forEach((f) => f());
|
|
36
|
+
}
|