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