Merge remote-tracking branch 'mmalmi/main'
This commit is contained in:
parent
83ffe746b1
commit
3611af9dce
@ -30,12 +30,14 @@
|
||||
"qr-code-styling": "^1.6.0-rc.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-force-graph-3d": "^1.24.0",
|
||||
"react-intersection-observer": "^9.4.1",
|
||||
"react-intl": "^6.4.4",
|
||||
"react-router-dom": "^6.5.0",
|
||||
"react-textarea-autosize": "^8.4.0",
|
||||
"react-twitter-embed": "^4.0.4",
|
||||
"recharts": "^2.8.0",
|
||||
"three": "^0.157.0",
|
||||
"use-long-press": "^3.2.0",
|
||||
"use-sync-external-store": "^1.2.0",
|
||||
"uuid": "^9.0.0",
|
||||
@ -85,6 +87,7 @@
|
||||
"@types/node": "^20.4.1",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/three": "^0.157.2",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"@types/webscopeio__react-textarea-autocomplete": "^4.7.2",
|
||||
"@types/webtorrent": "^0.109.3",
|
||||
|
48
packages/app/src/Cache/FollowListCache.ts
Normal file
48
packages/app/src/Cache/FollowListCache.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { db } from "Db";
|
||||
import { unixNowMs } from "@snort/shared";
|
||||
import { EventKind, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
||||
import { RefreshFeedCache } from "./RefreshFeedCache";
|
||||
import { LoginSession } from "Login";
|
||||
import SocialGraph from "SocialGraph/SocialGraph";
|
||||
|
||||
export class FollowListCache extends RefreshFeedCache<TaggedNostrEvent> {
|
||||
constructor() {
|
||||
super("FollowListCache", db.followLists);
|
||||
}
|
||||
|
||||
buildSub(session: LoginSession, rb: RequestBuilder): void {
|
||||
const since = this.newest();
|
||||
rb.withFilter()
|
||||
.kinds([EventKind.ContactList])
|
||||
.authors(session.follows.item)
|
||||
.since(since === 0 ? undefined : since);
|
||||
}
|
||||
|
||||
async onEvent(evs: readonly TaggedNostrEvent[]) {
|
||||
await Promise.all(
|
||||
evs.map(async e => {
|
||||
const update = await super.update({
|
||||
...e,
|
||||
created: e.created_at,
|
||||
loaded: unixNowMs(),
|
||||
});
|
||||
if (update !== "no_change") {
|
||||
SocialGraph.handleFollowEvent(e);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
key(of: TaggedNostrEvent): string {
|
||||
return of.pubkey;
|
||||
}
|
||||
|
||||
takeSnapshot() {
|
||||
return [...this.cache.values()];
|
||||
}
|
||||
|
||||
override async preload() {
|
||||
await super.preload();
|
||||
this.snapshot().forEach(e => SocialGraph.handleFollowEvent(e));
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import { Payments } from "./PaymentsCache";
|
||||
import { GiftWrapCache } from "./GiftWrapCache";
|
||||
import { NotificationsCache } from "./Notifications";
|
||||
import { FollowsFeedCache } from "./FollowsFeed";
|
||||
import { FollowListCache } from "./FollowListCache";
|
||||
|
||||
export const SystemDb = new SnortSystemDb();
|
||||
export const UserCache = new UserProfileCache(SystemDb.users);
|
||||
@ -19,6 +20,7 @@ export const InteractionCache = new EventInteractionCache();
|
||||
export const GiftsCache = new GiftWrapCache();
|
||||
export const Notifications = new NotificationsCache();
|
||||
export const FollowsFeed = new FollowsFeedCache();
|
||||
export const FollowLists = new FollowListCache();
|
||||
|
||||
export async function preload(follows?: Array<string>) {
|
||||
const preloads = [
|
||||
@ -30,6 +32,7 @@ export async function preload(follows?: Array<string>) {
|
||||
GiftsCache.preload(),
|
||||
Notifications.preload(),
|
||||
FollowsFeed.preload(),
|
||||
FollowLists.preload(),
|
||||
];
|
||||
await Promise.all(preloads);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import Dexie, { Table } from "dexie";
|
||||
import { HexKey, NostrEvent, TaggedNostrEvent, u256 } from "@snort/system";
|
||||
|
||||
export const NAME = "snortDB";
|
||||
export const VERSION = 15;
|
||||
export const VERSION = 16;
|
||||
|
||||
export interface SubCache {
|
||||
id: string;
|
||||
@ -46,6 +46,7 @@ const STORES = {
|
||||
gifts: "++id",
|
||||
notifications: "++id",
|
||||
followsFeed: "++id, created_at, kind",
|
||||
followLists: "++pubkey",
|
||||
};
|
||||
|
||||
export class SnortDB extends Dexie {
|
||||
@ -56,6 +57,7 @@ export class SnortDB extends Dexie {
|
||||
gifts!: Table<UnwrappedGift>;
|
||||
notifications!: Table<NostrEventForSession>;
|
||||
followsFeed!: Table<TaggedNostrEvent>;
|
||||
followLists!: Table<TaggedNostrEvent>;
|
||||
|
||||
constructor() {
|
||||
super(NAME);
|
||||
|
@ -448,7 +448,7 @@ export function NoteCreator() {
|
||||
className="note-creator-icon"
|
||||
link=""
|
||||
showUsername={false}
|
||||
showFollowingMark={false}
|
||||
showFollowDistance={false}
|
||||
/>
|
||||
{note.pollOptions === undefined && !note.replyTo && (
|
||||
<AsyncIcon
|
||||
|
@ -59,7 +59,7 @@ export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
|
||||
{sender && (
|
||||
<ProfileImage
|
||||
pubkey={anonZap ? "" : sender}
|
||||
showFollowingMark={false}
|
||||
showFollowDistance={false}
|
||||
overrideUsername={anonZap ? formatMessage({ defaultMessage: "Anonymous" }) : undefined}
|
||||
/>
|
||||
)}
|
||||
|
@ -36,7 +36,7 @@ export function TimelineRenderer(props: TimelineRendererProps) {
|
||||
<>
|
||||
<div className="card latest-notes" onClick={() => props.showLatest(false)} ref={ref}>
|
||||
{props.latest.slice(0, 3).map(p => {
|
||||
return <ProfileImage pubkey={p} showUsername={false} link={""} showFollowingMark={false} />;
|
||||
return <ProfileImage pubkey={p} showUsername={false} link={""} showFollowDistance={false} />;
|
||||
})}
|
||||
<FormattedMessage
|
||||
defaultMessage="{n} new {n, plural, =1 {note} other {notes}}"
|
||||
@ -49,7 +49,7 @@ export function TimelineRenderer(props: TimelineRendererProps) {
|
||||
className="card latest-notes latest-notes-fixed pointer fade-in"
|
||||
onClick={() => props.showLatest(true)}>
|
||||
{props.latest.slice(0, 3).map(p => {
|
||||
return <ProfileImage pubkey={p} showUsername={false} link={""} showFollowingMark={false} />;
|
||||
return <ProfileImage pubkey={p} showUsername={false} link={""} showFollowDistance={false} />;
|
||||
})}
|
||||
<FormattedMessage
|
||||
defaultMessage="{n} new {n, plural, =1 {note} other {notes}}"
|
||||
|
@ -10,6 +10,34 @@ export interface ModalProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
let scrollbarWidth: number | null = null;
|
||||
|
||||
const getScrollbarWidth = () => {
|
||||
if (scrollbarWidth !== null) {
|
||||
return scrollbarWidth;
|
||||
}
|
||||
|
||||
const outer = document.createElement("div");
|
||||
outer.style.visibility = "hidden";
|
||||
outer.style.width = "100px";
|
||||
|
||||
document.body.appendChild(outer);
|
||||
|
||||
const widthNoScroll = outer.offsetWidth;
|
||||
outer.style.overflow = "scroll";
|
||||
|
||||
const inner = document.createElement("div");
|
||||
inner.style.width = "100%";
|
||||
outer.appendChild(inner);
|
||||
|
||||
const widthWithScroll = inner.offsetWidth;
|
||||
|
||||
outer.parentNode?.removeChild(outer);
|
||||
|
||||
scrollbarWidth = widthNoScroll - widthWithScroll;
|
||||
return scrollbarWidth;
|
||||
};
|
||||
|
||||
export default function Modal(props: ModalProps) {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && props.onClose) {
|
||||
@ -19,10 +47,13 @@ export default function Modal(props: ModalProps) {
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.add("scroll-lock");
|
||||
document.body.style.paddingRight = `${getScrollbarWidth()}px`;
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.body.classList.remove("scroll-lock");
|
||||
document.body.style.paddingRight = "";
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
@ -16,6 +16,7 @@ export default function SearchBox() {
|
||||
const { formatMessage } = useIntl();
|
||||
const [search, setSearch] = useState("");
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
@ -120,13 +121,15 @@ export default function SearchBox() {
|
||||
value={search}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setTimeout(() => setIsFocused(false), 150)}
|
||||
/>
|
||||
{searching ? (
|
||||
<Spinner width={24} height={24} />
|
||||
) : (
|
||||
<Icon name="search" size={24} onClick={() => navigate("/search")} />
|
||||
)}
|
||||
{search && !searching && (
|
||||
{search && !searching && isFocused && (
|
||||
<div
|
||||
className="absolute top-full mt-2 w-full border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-black shadow-lg rounded-lg z-10 overflow-hidden"
|
||||
ref={resultListRef}>
|
||||
|
@ -166,7 +166,7 @@ export default function SendSats(props: SendSatsProps) {
|
||||
<ProfileImage
|
||||
pubkey={v.value}
|
||||
showUsername={false}
|
||||
showFollowingMark={false}
|
||||
showFollowDistance={false}
|
||||
imageOverlay={formatShort(Math.floor((amount?.amount ?? 0) * (v.weight / total)))}
|
||||
/>
|
||||
))}
|
||||
|
@ -8,6 +8,7 @@ import ProfileImage from "./ProfileImage";
|
||||
import { UserWebsiteLink } from "./UserWebsiteLink";
|
||||
import Text from "Element/Text";
|
||||
import { useEffect, useState } from "react";
|
||||
import useLogin from "../../Hooks/useLogin";
|
||||
|
||||
interface RectElement {
|
||||
getBoundingClientRect(): {
|
||||
@ -35,6 +36,7 @@ export function ProfileCard({
|
||||
}) {
|
||||
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
||||
const [t, setT] = useState<ReturnType<typeof setTimeout>>();
|
||||
const { publicKey: myPublicKey } = useLogin(s => ({ publicKey: s.publicKey }));
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
@ -60,14 +62,14 @@ export function ProfileCard({
|
||||
align="end">
|
||||
<div className="flex flex-col g8">
|
||||
<div className="flex justify-between">
|
||||
<ProfileImage pubkey={""} profile={user} showProfileCard={false} link="" />
|
||||
<ProfileImage pubkey={pubkey} profile={user} showProfileCard={false} link="" />
|
||||
<div className="flex g8">
|
||||
{/*<button type="button" onClick={() => {
|
||||
LoginStore.loginWithPubkey(pubkey, LoginSessionType.PublicKey, undefined, undefined, undefined, true);
|
||||
}}>
|
||||
<FormattedMessage defaultMessage="Stalk" />
|
||||
</button>*/}
|
||||
<FollowButton pubkey={pubkey} />
|
||||
{myPublicKey !== pubkey && <FollowButton pubkey={pubkey} />}
|
||||
</div>
|
||||
</div>
|
||||
<Text
|
||||
|
@ -8,11 +8,11 @@ import classNames from "classnames";
|
||||
|
||||
import Avatar from "Element/User/Avatar";
|
||||
import Nip05 from "Element/User/Nip05";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import Icon from "Icons/Icon";
|
||||
import DisplayName from "./DisplayName";
|
||||
import { ProfileLink } from "./ProfileLink";
|
||||
import { ProfileCard } from "./ProfileCard";
|
||||
import SocialGraph from "../../SocialGraph/SocialGraph";
|
||||
|
||||
export interface ProfileImageProps {
|
||||
pubkey: HexKey;
|
||||
@ -27,7 +27,7 @@ export interface ProfileImageProps {
|
||||
size?: number;
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
imageOverlay?: ReactNode;
|
||||
showFollowingMark?: boolean;
|
||||
showFollowDistance?: boolean;
|
||||
icons?: ReactNode;
|
||||
showProfileCard?: boolean;
|
||||
}
|
||||
@ -45,14 +45,13 @@ export default function ProfileImage({
|
||||
size,
|
||||
imageOverlay,
|
||||
onClick,
|
||||
showFollowingMark = true,
|
||||
showFollowDistance = true,
|
||||
icons,
|
||||
showProfileCard,
|
||||
}: ProfileImageProps) {
|
||||
const user = useUserProfile(profile ? "" : pubkey) ?? profile;
|
||||
const nip05 = defaultNip ? defaultNip : user?.nip05;
|
||||
const { follows } = useLogin();
|
||||
const doesFollow = follows.item.includes(pubkey);
|
||||
const followDistance = SocialGraph.getFollowDistance(pubkey);
|
||||
const [ref, hovering] = useHover<HTMLDivElement>();
|
||||
|
||||
function handleClick(e: React.MouseEvent) {
|
||||
@ -63,6 +62,12 @@ export default function ProfileImage({
|
||||
}
|
||||
|
||||
function inner() {
|
||||
let followDistanceColor = "";
|
||||
if (followDistance <= 1) {
|
||||
followDistanceColor = "success";
|
||||
} else if (followDistance === 2 && SocialGraph.followedByFriendsCount(pubkey) >= 10) {
|
||||
followDistanceColor = "text-nostr-orange";
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className="avatar-wrapper" ref={ref}>
|
||||
@ -72,12 +77,12 @@ export default function ProfileImage({
|
||||
size={size}
|
||||
imageOverlay={imageOverlay}
|
||||
icons={
|
||||
(doesFollow && showFollowingMark) || icons ? (
|
||||
(followDistance <= 2 && showFollowDistance) || icons ? (
|
||||
<>
|
||||
{icons}
|
||||
{showFollowingMark && (
|
||||
{showFollowDistance && (
|
||||
<div className="icon-circle">
|
||||
<Icon name="check" className="success" size={10} />
|
||||
<Icon name="check" className={followDistanceColor} size={10} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
import { SnortPubKey } from "Const";
|
||||
import { SubscriptionEvent } from "Subscription";
|
||||
import useRelaysFeedFollows from "./RelaysFeedFollows";
|
||||
import { FollowsFeed, GiftsCache, Notifications, UserRelays } from "Cache";
|
||||
import { FollowLists, FollowsFeed, GiftsCache, Notifications, UserRelays } from "Cache";
|
||||
import { Nip28Chats, Nip4Chats } from "chat";
|
||||
import { useRefreshFeedCache } from "Hooks/useRefreshFeedcache";
|
||||
|
||||
@ -38,6 +38,7 @@ export default function useLoginFeed() {
|
||||
useRefreshFeedCache(Notifications, true);
|
||||
useRefreshFeedCache(FollowsFeed, true);
|
||||
useRefreshFeedCache(GiftsCache, true);
|
||||
useRefreshFeedCache(FollowLists, false);
|
||||
|
||||
useEffect(() => {
|
||||
system.checkSigs = login.appData.item.preferences.checkSigs;
|
||||
|
@ -11,8 +11,14 @@ export interface ImgProxySettings {
|
||||
|
||||
export default function useImgProxy() {
|
||||
const settings = useLogin(s => s.appData.item.preferences.imgProxyConfig);
|
||||
const te = new TextEncoder();
|
||||
|
||||
return {
|
||||
proxy: (url: string, resize?: number) => proxyImg(url, settings, resize),
|
||||
};
|
||||
}
|
||||
|
||||
export function proxyImg(url: string, settings?: ImgProxySettings, resize?: number) {
|
||||
const te = new TextEncoder();
|
||||
function urlSafe(s: string) {
|
||||
return s.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
||||
}
|
||||
@ -25,17 +31,12 @@ export default function useImgProxy() {
|
||||
);
|
||||
return urlSafe(base64.encode(result));
|
||||
}
|
||||
|
||||
return {
|
||||
proxy: (url: string, resize?: number) => {
|
||||
if (!settings) return url;
|
||||
if (url.startsWith("data:") || url.startsWith("blob:")) return url;
|
||||
const opt = resize ? `rs:fit:${resize}:${resize}/dpr:${window.devicePixelRatio}` : "";
|
||||
const urlBytes = te.encode(url);
|
||||
const urlEncoded = urlSafe(base64.encode(urlBytes));
|
||||
const path = `/${opt}/${urlEncoded}`;
|
||||
const sig = signUrl(path);
|
||||
return `${new URL(settings.url).toString()}${sig}${path}`;
|
||||
},
|
||||
};
|
||||
if (!settings) return url;
|
||||
if (url.startsWith("data:") || url.startsWith("blob:")) return url;
|
||||
const opt = resize ? `rs:fit:${resize}:${resize}/dpr:${window.devicePixelRatio}` : "";
|
||||
const urlBytes = te.encode(url);
|
||||
const urlEncoded = urlSafe(base64.encode(urlBytes));
|
||||
const path = `/${opt}/${urlEncoded}`;
|
||||
const sig = signUrl(path);
|
||||
return `${new URL(settings.url).toString()}${sig}${path}`;
|
||||
}
|
||||
|
@ -15,9 +15,7 @@ export function useLinkList(id: string, fn: (rb: RequestBuilder) => void) {
|
||||
const listStore = useRequestBuilder(NoteCollection, sub);
|
||||
return useMemo(() => {
|
||||
if (listStore.data && listStore.data.length > 0) {
|
||||
return listStore.data
|
||||
.map(e => NostrLink.fromTags(e.tags))
|
||||
.flat();
|
||||
return listStore.data.map(e => NostrLink.fromTags(e.tags)).flat();
|
||||
}
|
||||
return [];
|
||||
}, [listStore.data]);
|
||||
|
@ -50,7 +50,7 @@ export interface UserPreferences {
|
||||
/**
|
||||
* Use imgproxy to optimize images
|
||||
*/
|
||||
imgProxyConfig: ImgProxySettings | null;
|
||||
imgProxyConfig?: ImgProxySettings;
|
||||
|
||||
/**
|
||||
* Default page to select on load
|
||||
|
@ -65,7 +65,7 @@ export function HashTagHeader({ tag }: { tag: string }) {
|
||||
<h2>#{tag}</h2>
|
||||
<div className="flex">
|
||||
{pubkeys.slice(0, 5).map(a => (
|
||||
<ProfileImage pubkey={a} showUsername={false} link={""} showFollowingMark={false} size={40} />
|
||||
<ProfileImage pubkey={a} showUsername={false} link={""} showFollowDistance={false} size={40} />
|
||||
))}
|
||||
{pubkeys.length > 5 && (
|
||||
<span>
|
||||
|
264
packages/app/src/Pages/NetworkGraph.tsx
Normal file
264
packages/app/src/Pages/NetworkGraph.tsx
Normal file
@ -0,0 +1,264 @@
|
||||
import ForceGraph3D, { NodeObject } from "react-force-graph-3d";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { STR, UID } from "../SocialGraph/UniqueIds";
|
||||
import SocialGraph from "../SocialGraph/SocialGraph";
|
||||
import { MetadataCache } from "@snort/system";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import * as THREE from "three";
|
||||
import { defaultAvatar } from "../SnortUtils";
|
||||
import { proxyImg } from "Hooks/useImgProxy";
|
||||
import { LoginStore } from "Login";
|
||||
|
||||
interface GraphNode {
|
||||
id: UID;
|
||||
profile?: MetadataCache;
|
||||
distance: number;
|
||||
val: number;
|
||||
inboundCount: number;
|
||||
outboundCount: number;
|
||||
color?: string;
|
||||
visible: boolean;
|
||||
// curvature?: number;
|
||||
}
|
||||
|
||||
interface GraphLink {
|
||||
source: UID;
|
||||
target: UID;
|
||||
distance: number;
|
||||
}
|
||||
|
||||
interface GraphMetadata {
|
||||
// usersByFollowDistance?: Map<number, Set<UID>>;
|
||||
userCountByDistance: number[];
|
||||
nodes?: Map<number, GraphNode>;
|
||||
}
|
||||
|
||||
interface GraphData {
|
||||
nodes: GraphNode[];
|
||||
links: GraphLink[];
|
||||
meta?: GraphMetadata;
|
||||
}
|
||||
|
||||
enum Direction {
|
||||
INBOUND,
|
||||
OUTBOUND,
|
||||
BOTH,
|
||||
}
|
||||
|
||||
const avatar = (node: NodeObject<NodeObject<GraphNode>>) => {
|
||||
const login = LoginStore.snapshot();
|
||||
return node.profile?.picture
|
||||
? proxyImg(node.profile?.picture, login.appData.item.preferences.imgProxyConfig)
|
||||
: defaultAvatar(node.address);
|
||||
};
|
||||
|
||||
const NODE_LIMIT = 500;
|
||||
|
||||
interface GraphConfig {
|
||||
direction: Direction;
|
||||
renderLimit: number | null;
|
||||
showDistance: number;
|
||||
}
|
||||
|
||||
const NetworkGraph = () => {
|
||||
const [graphData, setGraphData] = useState(null as GraphData | null);
|
||||
const [graphConfig, setGraphConfig] = useState({
|
||||
direction: Direction.OUTBOUND,
|
||||
renderLimit: NODE_LIMIT,
|
||||
showDistance: 2,
|
||||
});
|
||||
const [open, setOpen] = useState(false);
|
||||
const system = useContext(SnortContext);
|
||||
// const [showDistance, setShowDistance] = useState(2);
|
||||
// const [direction, setDirection] = useState(Direction.OUTBOUND);
|
||||
// const [renderLimit, setRenderLimit] = useState(NODE_LIMIT);
|
||||
|
||||
const updateConfig = async (changes: Partial<GraphConfig>) => {
|
||||
setGraphConfig(old => {
|
||||
const newConfig = Object.assign({}, old, changes);
|
||||
updateGraph(newConfig).then(graph => setGraphData(graph));
|
||||
return newConfig;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleConnections = () => {
|
||||
if (graphConfig.direction === Direction.OUTBOUND) {
|
||||
updateConfig({ direction: Direction.BOTH });
|
||||
} else {
|
||||
updateConfig({ direction: Direction.OUTBOUND });
|
||||
}
|
||||
};
|
||||
|
||||
const updateGraph = async (newConfig?: GraphConfig) => {
|
||||
const { direction, renderLimit, showDistance } = newConfig ?? graphConfig;
|
||||
const nodes = new Map<number, GraphNode>();
|
||||
const links: GraphLink[] = [];
|
||||
const nodesVisited = new Set<UID>();
|
||||
const userCountByDistance = Array.from(
|
||||
{ length: 6 },
|
||||
(_, i) => SocialGraph.usersByFollowDistance.get(i)?.size || 0,
|
||||
);
|
||||
|
||||
// Go through all the nodes
|
||||
for (let distance = 0; distance <= showDistance; ++distance) {
|
||||
const users = SocialGraph.usersByFollowDistance.get(distance);
|
||||
if (!users) break;
|
||||
for (const UID of users) {
|
||||
if (renderLimit && nodes.size >= renderLimit) break; // Temporary hack
|
||||
const inboundCount = SocialGraph.followersByUser.get(UID)?.size || 0;
|
||||
const outboundCount = SocialGraph.followedByUser.get(UID)?.size || 0;
|
||||
const pubkey = STR(UID);
|
||||
const node = {
|
||||
id: UID,
|
||||
address: pubkey,
|
||||
profile: system.ProfileLoader.Cache.getFromCache(pubkey),
|
||||
distance,
|
||||
inboundCount,
|
||||
outboundCount,
|
||||
visible: true, // Setting to false only hides the rendered element, does not prevent calculations
|
||||
// curvature: 0.6,
|
||||
// Node size is based on the follower count
|
||||
val: Math.log10(inboundCount) + 1, // 1 followers -> 1, 10 followers -> 2, 100 followers -> 3, etc.,
|
||||
} as GraphNode;
|
||||
// A visibility boost for the origin user:
|
||||
if (node.distance === 0) {
|
||||
node.val = 10; // they're always larger than life
|
||||
node.color = "#603285";
|
||||
}
|
||||
nodes.set(UID, node);
|
||||
}
|
||||
}
|
||||
|
||||
// Add links
|
||||
for (const node of nodes.values()) {
|
||||
if (direction === Direction.OUTBOUND || direction === Direction.BOTH) {
|
||||
for (const followedID of SocialGraph.followedByUser.get(node.id) ?? []) {
|
||||
if (!nodes.has(followedID)) continue; // Skip links to nodes that we're not rendering
|
||||
if (nodesVisited.has(followedID)) continue;
|
||||
links.push({
|
||||
source: node.id,
|
||||
target: followedID,
|
||||
distance: node.distance,
|
||||
});
|
||||
}
|
||||
}
|
||||
// TODO: Fix filtering
|
||||
/* if (direction === Direction.INBOUND || direction === Direction.BOTH) {
|
||||
for (const followerID of SocialGraph.followersByUser.get(node.id) ?? []) {
|
||||
if (nodesVisited.has(followerID)) continue;
|
||||
const follower = nodes.get(followerID);
|
||||
if (!follower) continue; // Skip links to nodes that we're not rendering
|
||||
links.push({
|
||||
source: followerID,
|
||||
target: node.id,
|
||||
distance: follower.distance,
|
||||
});
|
||||
}
|
||||
}*/
|
||||
nodesVisited.add(node.id);
|
||||
}
|
||||
|
||||
// Squash cases, where there are a lot of nodes
|
||||
|
||||
const graph: GraphData = {
|
||||
nodes: [...nodes.values()],
|
||||
links,
|
||||
meta: {
|
||||
nodes,
|
||||
userCountByDistance,
|
||||
},
|
||||
};
|
||||
|
||||
// console.log('!!', graph);
|
||||
// for (const l of links) {
|
||||
// if (!nodes.has(l.source)) {
|
||||
// console.log('source missing:', l.source);
|
||||
// }
|
||||
// if (!nodes.has(l.target)) {
|
||||
// console.log('target missing:', l.target);
|
||||
// }
|
||||
// }
|
||||
|
||||
return graph;
|
||||
};
|
||||
|
||||
const refreshData = async () => {
|
||||
updateGraph().then(setGraphData);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!open && (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
refreshData();
|
||||
}}>
|
||||
Show graph
|
||||
</button>
|
||||
)}
|
||||
{open && graphData && (
|
||||
<div className="fixed top-0 left-0 right-0 bottom-0 z-20">
|
||||
<button className="absolute top-6 right-6 z-30 btn hover:bg-gray-900" onClick={() => setOpen(false)}>
|
||||
X
|
||||
</button>
|
||||
<div className="absolute top-6 right-0 left-0 z-20 flex flex-col content-center justify-center text-center">
|
||||
<div className="text-center pb-2">Degrees of separation</div>
|
||||
<div className="flex flex-row justify-center space-x-4">
|
||||
{graphData.meta?.userCountByDistance?.map((value, i) => {
|
||||
if (i === 0 || value <= 0) return null;
|
||||
const isSelected = graphConfig.showDistance === i;
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
className={`btn bg-gray-900 py-4 h-auto flex-col ${
|
||||
isSelected ? "bg-gray-600 hover:bg-gray-600" : "hover:bg-gray-800"
|
||||
}`}
|
||||
onClick={() => isSelected || updateConfig({ showDistance: i })}>
|
||||
<div className="text-lg block leading-none">{i}</div>
|
||||
<div className="text-xs">({value})</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<ForceGraph3D
|
||||
graphData={graphData}
|
||||
nodeThreeObject={(node: NodeObject<NodeObject<GraphNode>>) => {
|
||||
const imgTexture = new THREE.TextureLoader().load(avatar(node));
|
||||
imgTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: imgTexture });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(12, 12, 1);
|
||||
|
||||
return sprite;
|
||||
}}
|
||||
nodeLabel={node => `${node.profile?.name || node.address}`}
|
||||
nodeAutoColorBy="distance"
|
||||
linkAutoColorBy="distance"
|
||||
linkDirectionalParticles={1}
|
||||
nodeVisibility="visible"
|
||||
numDimensions={3}
|
||||
linkDirectionalArrowLength={0}
|
||||
nodeOpacity={0.9}
|
||||
/>
|
||||
<div className="absolute bottom-6 right-6">
|
||||
<button className="text-lg" onClick={() => toggleConnections()}>
|
||||
Showing: {graphConfig.direction === Direction.OUTBOUND ? "Outbound" : "All"} connections
|
||||
</button>
|
||||
</div>
|
||||
<div className="absolute bottom-6 left-6">
|
||||
<span className="text-lg">Render limit: {graphConfig.renderLimit} nodes</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkGraph;
|
@ -62,13 +62,7 @@ export function RelaysTab({ id }: { id: HexKey }) {
|
||||
export function BookMarksTab({ id }: { id: HexKey }) {
|
||||
const bookmarks = useCategorizedBookmarks(id, "bookmark");
|
||||
const reactions = useReactions(`bookmark:reactions:{id}`, bookmarks.map(NostrLink.fromEvent));
|
||||
return (
|
||||
<Bookmarks
|
||||
pubkey={id}
|
||||
bookmarks={bookmarks}
|
||||
related={reactions.data ?? []}
|
||||
/>
|
||||
);
|
||||
return <Bookmarks pubkey={id} bookmarks={bookmarks} related={reactions.data ?? []} />;
|
||||
}
|
||||
|
||||
const ProfileTab = {
|
||||
|
@ -299,7 +299,7 @@ const PreferencesPage = () => {
|
||||
onChange={e =>
|
||||
updatePreferences(id, {
|
||||
...perf,
|
||||
imgProxyConfig: e.target.checked ? DefaultImgProxy : null,
|
||||
imgProxyConfig: e.target.checked ? DefaultImgProxy : undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
@ -104,6 +104,11 @@ const SettingsIndex = () => {
|
||||
<FormattedMessage defaultMessage="Cache" />
|
||||
<Icon name="arrowFront" size={16} />
|
||||
</div>
|
||||
<div className="settings-row" onClick={() => navigate("/graph")}>
|
||||
<Icon name="profile" size={24} />
|
||||
<FormattedMessage {...messages.SocialGraph} />
|
||||
<Icon name="arrowFront" size={16} />
|
||||
</div>
|
||||
<div className="settings-row" onClick={handleLogout}>
|
||||
<Icon name="logout" size={24} />
|
||||
<FormattedMessage {...messages.LogOut} />
|
||||
|
@ -60,4 +60,5 @@ export default defineMessages({
|
||||
Nip05: { defaultMessage: "NIP-05" },
|
||||
ReactionEmoji: { defaultMessage: "Reaction emoji" },
|
||||
ReactionEmojiHelp: { defaultMessage: "Emoji to send when reactiong to a note" },
|
||||
SocialGraph: { defaultMessage: "Social Graph" },
|
||||
});
|
||||
|
234
packages/app/src/SocialGraph/SocialGraph.ts
Normal file
234
packages/app/src/SocialGraph/SocialGraph.ts
Normal file
@ -0,0 +1,234 @@
|
||||
import { ID, STR, UID } from "./UniqueIds";
|
||||
import { LoginStore } from "../Login";
|
||||
import { unwrap } from "../SnortUtils";
|
||||
import { HexKey, MetadataCache, NostrEvent } from "@snort/system";
|
||||
|
||||
type Unsubscribe = () => void;
|
||||
|
||||
const Key = {
|
||||
pubKey: null as HexKey | null,
|
||||
getPubKey: () => {
|
||||
return unwrap(LoginStore.snapshot().publicKey);
|
||||
},
|
||||
isMine: (user: HexKey) => user === Key.getPubKey(),
|
||||
};
|
||||
|
||||
export default {
|
||||
followDistanceByUser: new Map<UID, number>(),
|
||||
usersByFollowDistance: new Map<number, Set<UID>>(),
|
||||
profiles: new Map<UID, MetadataCache>(), // JSON.parsed event.content of profile events
|
||||
followedByUser: new Map<UID, Set<UID>>(),
|
||||
followersByUser: new Map<UID, Set<UID>>(),
|
||||
latestFollowEventTimestamps: new Map<UID, number>(),
|
||||
|
||||
handleFollowEvent: function (event: NostrEvent) {
|
||||
try {
|
||||
const author = ID(event.pubkey);
|
||||
const timestamp = event.created_at;
|
||||
const existingTimestamp = this.latestFollowEventTimestamps.get(author);
|
||||
if (existingTimestamp && timestamp <= existingTimestamp) {
|
||||
return;
|
||||
}
|
||||
this.latestFollowEventTimestamps.set(author, timestamp);
|
||||
|
||||
// Collect all users followed in the new event.
|
||||
const followedInEvent = new Set<UID>();
|
||||
for (const tag of event.tags) {
|
||||
if (tag[0] === "p") {
|
||||
const followedUser = ID(tag[1]);
|
||||
if (followedUser !== author) {
|
||||
followedInEvent.add(followedUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the set of users currently followed by the author.
|
||||
const currentlyFollowed = this.followedByUser.get(author) || new Set<UID>();
|
||||
|
||||
// Find users that need to be removed.
|
||||
for (const user of currentlyFollowed) {
|
||||
if (!followedInEvent.has(user)) {
|
||||
this.removeFollower(user, author);
|
||||
}
|
||||
}
|
||||
|
||||
// Add or update the followers based on the new event.
|
||||
for (const user of followedInEvent) {
|
||||
this.addFollower(user, author);
|
||||
}
|
||||
} catch (e) {
|
||||
// might not be logged in or sth
|
||||
}
|
||||
},
|
||||
|
||||
isFollowing: function (follower: HexKey, followedUser: HexKey): boolean {
|
||||
const followedUserId = ID(followedUser);
|
||||
const followerId = ID(follower);
|
||||
return !!this.followedByUser.get(followerId)?.has(followedUserId);
|
||||
},
|
||||
|
||||
getFollowDistance: function (user: HexKey): number {
|
||||
try {
|
||||
if (Key.isMine(user)) {
|
||||
return 0;
|
||||
}
|
||||
const userId = ID(user);
|
||||
const distance = this.followDistanceByUser.get(userId);
|
||||
return distance === undefined ? 1000 : distance;
|
||||
} catch (e) {
|
||||
// might not be logged in or sth
|
||||
return 1000;
|
||||
}
|
||||
},
|
||||
|
||||
addUserByFollowDistance(distance: number, user: UID) {
|
||||
if (!this.usersByFollowDistance.has(distance)) {
|
||||
this.usersByFollowDistance.set(distance, new Set());
|
||||
}
|
||||
if (distance <= 2) {
|
||||
/*
|
||||
let unsub;
|
||||
// get also profile events for profile search indexing
|
||||
// eslint-disable-next-line prefer-const
|
||||
unsub = PubSub.subscribe({ authors: [STR(user)], kinds: [0] }, () => unsub?.(), true);
|
||||
// TODO subscribe once param?
|
||||
*/
|
||||
}
|
||||
this.usersByFollowDistance.get(distance)?.add(user);
|
||||
// remove from higher distances
|
||||
for (const d of this.usersByFollowDistance.keys()) {
|
||||
if (d > distance) {
|
||||
this.usersByFollowDistance.get(d)?.delete(user);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ensureRootUser: function () {
|
||||
const myId = ID(Key.getPubKey());
|
||||
if (myId && !this.followDistanceByUser.has(myId)) {
|
||||
this.followDistanceByUser.set(myId, 0);
|
||||
this.addUserByFollowDistance(0, myId);
|
||||
}
|
||||
},
|
||||
|
||||
addFollower: function (followedUser: UID, follower: UID) {
|
||||
if (typeof followedUser !== "number" || typeof follower !== "number") {
|
||||
throw new Error("Invalid user id");
|
||||
}
|
||||
this.ensureRootUser();
|
||||
if (!this.followersByUser.has(followedUser)) {
|
||||
this.followersByUser.set(followedUser, new Set<UID>());
|
||||
}
|
||||
this.followersByUser.get(followedUser)?.add(follower);
|
||||
|
||||
if (!this.followedByUser.has(follower)) {
|
||||
this.followedByUser.set(follower, new Set<UID>());
|
||||
}
|
||||
const myId = ID(Key.getPubKey());
|
||||
|
||||
if (followedUser !== myId) {
|
||||
let newFollowDistance;
|
||||
if (follower === myId) {
|
||||
// basically same as the next "else" block, but faster
|
||||
newFollowDistance = 1;
|
||||
this.addUserByFollowDistance(newFollowDistance, followedUser);
|
||||
this.followDistanceByUser.set(followedUser, newFollowDistance);
|
||||
} else {
|
||||
const existingFollowDistance = this.followDistanceByUser.get(followedUser);
|
||||
const followerDistance = this.followDistanceByUser.get(follower);
|
||||
newFollowDistance = followerDistance && followerDistance + 1;
|
||||
if (existingFollowDistance === undefined || (newFollowDistance && newFollowDistance < existingFollowDistance)) {
|
||||
this.followDistanceByUser.set(followedUser, newFollowDistance!);
|
||||
this.addUserByFollowDistance(newFollowDistance!, followedUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.followedByUser.get(follower)?.add(followedUser);
|
||||
if (this.followedByUser.get(myId)?.has(follower)) {
|
||||
/*
|
||||
setTimeout(() => {
|
||||
PubSub.subscribe({ authors: [STR(followedUser)], kinds: [0, 3] }, undefined, true);
|
||||
}, 0);
|
||||
*/
|
||||
}
|
||||
},
|
||||
removeFollower: function (unfollowedUser: UID, follower: UID) {
|
||||
this.followersByUser.get(unfollowedUser)?.delete(follower);
|
||||
this.followedByUser.get(follower)?.delete(unfollowedUser);
|
||||
|
||||
if (unfollowedUser === ID(Key.getPubKey())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// iterate over remaining followers and set the smallest follow distance
|
||||
let smallest = Infinity;
|
||||
for (const follower of this.followersByUser.get(unfollowedUser) || []) {
|
||||
const followerDistance = this.followDistanceByUser.get(follower);
|
||||
if (followerDistance !== undefined && followerDistance + 1 < smallest) {
|
||||
smallest = followerDistance + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (smallest === Infinity) {
|
||||
this.followDistanceByUser.delete(unfollowedUser);
|
||||
} else {
|
||||
this.followDistanceByUser.set(unfollowedUser, smallest);
|
||||
}
|
||||
},
|
||||
// TODO subscription methods for followersByUser and followedByUser. and maybe messagesByTime. and replies
|
||||
followerCount: function (address: HexKey) {
|
||||
const id = ID(address);
|
||||
return this.followersByUser.get(id)?.size ?? 0;
|
||||
},
|
||||
followedByFriendsCount: function (address: HexKey) {
|
||||
let count = 0;
|
||||
const myId = ID(Key.getPubKey());
|
||||
const id = ID(address);
|
||||
for (const follower of this.followersByUser.get(id) ?? []) {
|
||||
if (this.followedByUser.get(myId)?.has(follower)) {
|
||||
count++; // should we stop at 10?
|
||||
}
|
||||
}
|
||||
return count;
|
||||
},
|
||||
getFollowedByUser: function (
|
||||
user: HexKey,
|
||||
cb?: (followedUsers: Set<HexKey>) => void,
|
||||
includeSelf = false,
|
||||
): Unsubscribe {
|
||||
const userId = ID(user);
|
||||
const callback = () => {
|
||||
if (cb) {
|
||||
const set = new Set<HexKey>();
|
||||
for (const id of this.followedByUser.get(userId) || []) {
|
||||
set.add(STR(id));
|
||||
}
|
||||
if (includeSelf) {
|
||||
set.add(user);
|
||||
}
|
||||
cb(set);
|
||||
}
|
||||
};
|
||||
if (this.followedByUser.has(userId) || includeSelf) {
|
||||
callback();
|
||||
}
|
||||
//return PubSub.subscribe({ kinds: [3], authors: [user] }, callback);
|
||||
return () => {};
|
||||
},
|
||||
getFollowersByUser: function (address: HexKey, cb?: (followers: Set<HexKey>) => void): Unsubscribe {
|
||||
const userId = ID(address);
|
||||
const callback = () => {
|
||||
if (cb) {
|
||||
const set = new Set<HexKey>();
|
||||
for (const id of this.followersByUser.get(userId) || []) {
|
||||
set.add(STR(id));
|
||||
}
|
||||
cb(set);
|
||||
}
|
||||
};
|
||||
this.followersByUser.has(userId) && callback();
|
||||
//return PubSub.subscribe({ kinds: [3], '#p': [address] }, callback); // TODO this doesn't fire when a user is unfollowed
|
||||
return () => {};
|
||||
},
|
||||
};
|
38
packages/app/src/SocialGraph/UniqueIds.ts
Normal file
38
packages/app/src/SocialGraph/UniqueIds.ts
Normal file
@ -0,0 +1,38 @@
|
||||
// should this be a class instead? convert all strings to internal representation, enable comparison
|
||||
export type UID = number;
|
||||
|
||||
// save space by mapping strs to internal unique ids
|
||||
export class UniqueIds {
|
||||
static strToUniqueId = new Map<string, UID>();
|
||||
static uniqueIdToStr = new Map<UID, string>();
|
||||
static currentUniqueId = 0;
|
||||
|
||||
static id(str: string): UID {
|
||||
if (str.startsWith("npub")) {
|
||||
throw new Error("use hex instead of npub " + str);
|
||||
}
|
||||
const existing = UniqueIds.strToUniqueId.get(str);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const newId = UniqueIds.currentUniqueId++;
|
||||
UniqueIds.strToUniqueId.set(str, newId);
|
||||
UniqueIds.uniqueIdToStr.set(newId, str);
|
||||
return newId;
|
||||
}
|
||||
|
||||
static str(id: UID): string {
|
||||
const pub = UniqueIds.uniqueIdToStr.get(id);
|
||||
if (!pub) {
|
||||
throw new Error("pub: invalid id " + id);
|
||||
}
|
||||
return pub;
|
||||
}
|
||||
|
||||
static has(str: string): boolean {
|
||||
return UniqueIds.strToUniqueId.has(str);
|
||||
}
|
||||
}
|
||||
|
||||
export const STR = UniqueIds.str;
|
||||
export const ID = UniqueIds.id;
|
@ -23,6 +23,10 @@ import {
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { removeUndefined, throwIfOffline } from "@snort/shared";
|
||||
|
||||
import React, { lazy, Suspense } from "react";
|
||||
|
||||
const NetworkGraph = lazy(() => import("Pages/NetworkGraph"));
|
||||
|
||||
import * as serviceWorkerRegistration from "serviceWorkerRegistration";
|
||||
import { IntlProvider } from "IntlProvider";
|
||||
import { getCountry, unwrap } from "SnortUtils";
|
||||
@ -224,6 +228,10 @@ const mainRoutes = [
|
||||
path: "/about",
|
||||
element: <AboutPage />,
|
||||
},
|
||||
{
|
||||
path: "/graph",
|
||||
element: <NetworkGraph />,
|
||||
},
|
||||
...OnboardingRoutes,
|
||||
...WalletRoutes,
|
||||
] as Array<RouteObject>;
|
||||
@ -282,7 +290,9 @@ root.render(
|
||||
<StrictMode>
|
||||
<IntlProvider>
|
||||
<SnortContext.Provider value={System}>
|
||||
<RouterProvider router={router} />
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<RouterProvider router={router} />
|
||||
</Suspense>
|
||||
</SnortContext.Provider>
|
||||
</IntlProvider>
|
||||
</StrictMode>,
|
||||
|
@ -368,6 +368,9 @@
|
||||
"Cu/K85": {
|
||||
"defaultMessage": "Translated from {lang}"
|
||||
},
|
||||
"CzHZoc": {
|
||||
"defaultMessage": "Social Graph"
|
||||
},
|
||||
"D+KzKd": {
|
||||
"defaultMessage": "Automatically zap every note when loaded"
|
||||
},
|
||||
|
@ -121,6 +121,7 @@
|
||||
"CmZ9ls": "{n} Muted",
|
||||
"CsCUYo": "{n} sats",
|
||||
"Cu/K85": "Translated from {lang}",
|
||||
"CzHZoc": "Social Graph",
|
||||
"D+KzKd": "Automatically zap every note when loaded",
|
||||
"D3idYv": "Settings",
|
||||
"DBiVK1": "Cache",
|
||||
|
309
yarn.lock
309
yarn.lock
@ -5,6 +5,19 @@ __metadata:
|
||||
version: 6
|
||||
cacheKey: 8
|
||||
|
||||
"3d-force-graph@npm:^1.73":
|
||||
version: 1.73.0
|
||||
resolution: "3d-force-graph@npm:1.73.0"
|
||||
dependencies:
|
||||
accessor-fn: 1
|
||||
kapsule: 1
|
||||
three: ">=0.118 <1"
|
||||
three-forcegraph: 1
|
||||
three-render-objects: ^1.29
|
||||
checksum: be9057ced895131b8788f2f20f6fc587d027e697a353bb69df2ddd6a64e91670202b13e9ea44b794491a562f20551c779d18f85c35b6f8604dbfc27a9a3f77bc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@aashutoshrathi/word-wrap@npm:^1.2.3":
|
||||
version: 1.2.6
|
||||
resolution: "@aashutoshrathi/word-wrap@npm:1.2.6"
|
||||
@ -1401,6 +1414,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.17.8":
|
||||
version: 7.23.2
|
||||
resolution: "@babel/runtime@npm:7.23.2"
|
||||
dependencies:
|
||||
regenerator-runtime: ^0.14.0
|
||||
checksum: 6c4df4839ec75ca10175f636d6362f91df8a3137f86b38f6cd3a4c90668a0fe8e9281d320958f4fbd43b394988958585a17c3aab2a4ea6bf7316b22916a371fb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/template@npm:^7.22.5, @babel/template@npm:^7.3.3":
|
||||
version: 7.22.5
|
||||
resolution: "@babel/template@npm:7.22.5"
|
||||
@ -3104,6 +3126,7 @@ __metadata:
|
||||
"@types/node": ^20.4.1
|
||||
"@types/react": ^18.0.26
|
||||
"@types/react-dom": ^18.0.10
|
||||
"@types/three": ^0.157.2
|
||||
"@types/use-sync-external-store": ^0.0.4
|
||||
"@types/uuid": ^9.0.2
|
||||
"@types/webscopeio__react-textarea-autocomplete": ^4.7.2
|
||||
@ -3144,6 +3167,7 @@ __metadata:
|
||||
qr-code-styling: ^1.6.0-rc.1
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
react-force-graph-3d: ^1.24.0
|
||||
react-intersection-observer: ^9.4.1
|
||||
react-intl: ^6.4.4
|
||||
react-router-dom: ^6.5.0
|
||||
@ -3153,6 +3177,7 @@ __metadata:
|
||||
source-map-loader: ^4.0.1
|
||||
tailwindcss: ^3.3.3
|
||||
terser-webpack-plugin: ^5.3.9
|
||||
three: ^0.157.0
|
||||
tinybench: ^2.5.1
|
||||
ts-jest: ^29.1.1
|
||||
ts-loader: ^9.4.4
|
||||
@ -3501,6 +3526,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tweenjs/tween.js@npm:18 - 21":
|
||||
version: 21.0.0
|
||||
resolution: "@tweenjs/tween.js@npm:21.0.0"
|
||||
checksum: 3c2498e9916a599a54d9f8e2bd2dfe0ceb249e3453d29c7b4287310f0fbe12f37d82e986263b411a672d50544dafdda7827b1707a7250023a6440351a741dddc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/babel__core@npm:^7.1.14":
|
||||
version: 7.20.1
|
||||
resolution: "@types/babel__core@npm:7.20.1"
|
||||
@ -4052,6 +4084,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/stats.js@npm:*":
|
||||
version: 0.17.2
|
||||
resolution: "@types/stats.js@npm:0.17.2"
|
||||
checksum: cbe300f1a548051c353f5a17a9bde406f468cb171fad1b6ec3d50b8af539389ecbd4a99765bd67c9f7acd7608ed8c5a275b19a60c1545341c3cac58abf0f487e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/three@npm:^0.157.2":
|
||||
version: 0.157.2
|
||||
resolution: "@types/three@npm:0.157.2"
|
||||
dependencies:
|
||||
"@types/stats.js": "*"
|
||||
"@types/webxr": "*"
|
||||
fflate: ~0.6.10
|
||||
meshoptimizer: ~0.18.1
|
||||
checksum: c839492a2c09f73877fc53ffc5d1860762921bee608fdb742a5be59077d4300685346ac9ea7ac3070e41900a0a9c96dc74a452c4ad7f1eac1dcdc935abb09444
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/tough-cookie@npm:*":
|
||||
version: 4.0.2
|
||||
resolution: "@types/tough-cookie@npm:4.0.2"
|
||||
@ -4111,6 +4162,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/webxr@npm:*":
|
||||
version: 0.5.6
|
||||
resolution: "@types/webxr@npm:0.5.6"
|
||||
checksum: fbf79f471382a8649f04d9109a6d67a4b2c691bed3d4a9940f850cf288ab335230a67919425c412594eca80b8a69f4b40916375586c1e0a4947455398d57dd9d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ws@npm:^8.5.5":
|
||||
version: 8.5.5
|
||||
resolution: "@types/ws@npm:8.5.5"
|
||||
@ -4546,6 +4604,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"accessor-fn@npm:1":
|
||||
version: 1.5.0
|
||||
resolution: "accessor-fn@npm:1.5.0"
|
||||
checksum: b24398a79c1f2f6808cc7116fa2033c52bd5c5d49ac552dbb7f5a41618c515d7b04e6ebf3a06342d54df101c175b8e5583b1b900e768f93492ca9ed2a87b386e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"acorn-globals@npm:^7.0.0":
|
||||
version: 7.0.1
|
||||
resolution: "acorn-globals@npm:7.0.1"
|
||||
@ -6320,7 +6385,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:^3.1.6":
|
||||
"d3-array@npm:1 - 3, d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:^3.1.6":
|
||||
version: 3.2.4
|
||||
resolution: "d3-array@npm:3.2.4"
|
||||
dependencies:
|
||||
@ -6329,6 +6394,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-binarytree@npm:1":
|
||||
version: 1.0.2
|
||||
resolution: "d3-binarytree@npm:1.0.2"
|
||||
checksum: 6768077e82844373d78fcf5202a098217f8e534ec132231dd6aefbe49f5f04ffb1a6acb36c032e03426495a5018d291d61e0a6edf987dfb628ac447d88cd72c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-color@npm:1 - 3":
|
||||
version: 3.1.0
|
||||
resolution: "d3-color@npm:3.1.0"
|
||||
@ -6336,6 +6408,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-dispatch@npm:1 - 3":
|
||||
version: 3.0.1
|
||||
resolution: "d3-dispatch@npm:3.0.1"
|
||||
checksum: fdfd4a230f46463e28e5b22a45dd76d03be9345b605e1b5dc7d18bd7ebf504e6c00ae123fd6d03e23d9e2711e01f0e14ea89cd0632545b9f0c00b924ba4be223
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-ease@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "d3-ease@npm:3.0.1"
|
||||
@ -6343,6 +6422,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-force-3d@npm:2 - 3":
|
||||
version: 3.0.5
|
||||
resolution: "d3-force-3d@npm:3.0.5"
|
||||
dependencies:
|
||||
d3-binarytree: 1
|
||||
d3-dispatch: 1 - 3
|
||||
d3-octree: 1
|
||||
d3-quadtree: 1 - 3
|
||||
d3-timer: 1 - 3
|
||||
checksum: 36426a752187236c1f8b901f2244f49e19955c6f281936f2d1508779081ef856b867510a1e0ecf9a62ba5f6e17caed21a20ed4fedbbdc1887e745261723569a4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-format@npm:1 - 3":
|
||||
version: 3.1.0
|
||||
resolution: "d3-format@npm:3.1.0"
|
||||
@ -6350,7 +6442,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-interpolate@npm:1.2.0 - 3, d3-interpolate@npm:^3.0.1":
|
||||
"d3-interpolate@npm:1 - 3, d3-interpolate@npm:1.2.0 - 3, d3-interpolate@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "d3-interpolate@npm:3.0.1"
|
||||
dependencies:
|
||||
@ -6359,6 +6451,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-octree@npm:1":
|
||||
version: 1.0.2
|
||||
resolution: "d3-octree@npm:1.0.2"
|
||||
checksum: 73161dda8f15c58db58ea1abc0a5fbffe5628f29bb3358933c696c378cee3de70226108fe0923963bbbc6a02dfee79be1e8772e86a9e15bc4e0dbc9f739b31bb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-path@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "d3-path@npm:3.1.0"
|
||||
@ -6366,7 +6465,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-scale@npm:^4.0.2":
|
||||
"d3-quadtree@npm:1 - 3":
|
||||
version: 3.0.1
|
||||
resolution: "d3-quadtree@npm:3.0.1"
|
||||
checksum: 5469d462763811475f34a7294d984f3eb100515b0585ca5b249656f6b1a6e99b20056a2d2e463cc9944b888896d2b1d07859c50f9c0cf23438df9cd2e3146066
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-scale-chromatic@npm:1 - 3":
|
||||
version: 3.0.0
|
||||
resolution: "d3-scale-chromatic@npm:3.0.0"
|
||||
dependencies:
|
||||
d3-color: 1 - 3
|
||||
d3-interpolate: 1 - 3
|
||||
checksum: a8ce4cb0267a17b28ebbb929f5e3071d985908a9c13b6fcaa2a198e1e018f275804d691c5794b970df0049725b7944f32297b31603d235af6414004f0c7f82c0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-scale@npm:1 - 4, d3-scale@npm:^4.0.2":
|
||||
version: 4.0.2
|
||||
resolution: "d3-scale@npm:4.0.2"
|
||||
dependencies:
|
||||
@ -6406,7 +6522,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d3-timer@npm:^3.0.1":
|
||||
"d3-timer@npm:1 - 3, d3-timer@npm:^3.0.1":
|
||||
version: 3.0.1
|
||||
resolution: "d3-timer@npm:3.0.1"
|
||||
checksum: 1cfddf86d7bca22f73f2c427f52dfa35c49f50d64e187eb788dcad6e927625c636aa18ae4edd44d084eb9d1f81d8ca4ec305dae7f733c15846a824575b789d73
|
||||
@ -6420,6 +6536,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"data-joint@npm:1":
|
||||
version: 1.3.1
|
||||
resolution: "data-joint@npm:1.3.1"
|
||||
dependencies:
|
||||
index-array-by: ^1.4.0
|
||||
checksum: 9becfaccd59ca02a4caffe6b8b9b52d4e9df8ed93dc01cd3109ef67dc406e8fc678b539f8fd62662ce9b7a1061ec3b4865ca3b4d0edf69656c0cf97f9a2c1b1a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"data-urls@npm:^3.0.2":
|
||||
version: 3.0.2
|
||||
resolution: "data-urls@npm:3.0.2"
|
||||
@ -7455,6 +7580,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fflate@npm:~0.6.10":
|
||||
version: 0.6.10
|
||||
resolution: "fflate@npm:0.6.10"
|
||||
checksum: 96384bc4090987fe565c0de8204e3830f538144ec950576fea50aee1b42adbe9fc3ed5e7905dfa7979faaa20979def330dbebce548f3dcafc3e118cc9838526d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"figures@npm:^3.0.0":
|
||||
version: 3.2.0
|
||||
resolution: "figures@npm:3.2.0"
|
||||
@ -7644,6 +7776,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fromentries@npm:^1.3.2":
|
||||
version: 1.3.2
|
||||
resolution: "fromentries@npm:1.3.2"
|
||||
checksum: 33729c529ce19f5494f846f0dd4945078f4e37f4e8955f4ae8cc7385c218f600e9d93a7d225d17636c20d1889106fd87061f911550861b7072f53bf891e6b341
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fs-extra@npm:^9.0.1":
|
||||
version: 9.1.0
|
||||
resolution: "fs-extra@npm:9.1.0"
|
||||
@ -8434,6 +8573,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"index-array-by@npm:^1.4.0":
|
||||
version: 1.4.1
|
||||
resolution: "index-array-by@npm:1.4.1"
|
||||
checksum: 030ca9ddbb50b6b525ef6c4da026968df75e728915ed574a28c4437dd4b8154217b21f519136abc8c198796d9ee609ee7279c02e6f6db72d248422e460656c09
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"infer-owner@npm:^1.0.4":
|
||||
version: 1.0.4
|
||||
resolution: "infer-owner@npm:1.0.4"
|
||||
@ -9011,6 +9157,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jerrypick@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "jerrypick@npm:1.1.1"
|
||||
checksum: 8c247a4a7b5a6ece961cbc1ec0a0c76ca8009f5873a0fe26c4c367e4f3cf1cd7aa994c50442338b6d5d4394b1ba725f0acbe88293e0f9e11659dea86260139bf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jest-changed-files@npm:^29.6.3":
|
||||
version: 29.6.3
|
||||
resolution: "jest-changed-files@npm:29.6.3"
|
||||
@ -9712,6 +9865,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"kapsule@npm:1":
|
||||
version: 1.14.5
|
||||
resolution: "kapsule@npm:1.14.5"
|
||||
dependencies:
|
||||
lodash-es: 4
|
||||
checksum: 1208d7a172aa1b46ac35bb993be808b3cc8f5bc52967a6593ac34582a08ef83cedd669ca95a1ce2d1e29053234b20b4d98ba547288866244bdd9a128df7384e8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"keyv@npm:^4.5.3":
|
||||
version: 4.5.3
|
||||
resolution: "keyv@npm:4.5.3"
|
||||
@ -9849,6 +10011,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash-es@npm:4":
|
||||
version: 4.17.21
|
||||
resolution: "lodash-es@npm:4.17.21"
|
||||
checksum: 05cbffad6e2adbb331a4e16fbd826e7faee403a1a04873b82b42c0f22090f280839f85b95393f487c1303c8a3d2a010048bf06151a6cbe03eee4d388fb0a12d2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.debounce@npm:^4.0.8":
|
||||
version: 4.0.8
|
||||
resolution: "lodash.debounce@npm:4.0.8"
|
||||
@ -10181,6 +10350,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"meshoptimizer@npm:~0.18.1":
|
||||
version: 0.18.1
|
||||
resolution: "meshoptimizer@npm:0.18.1"
|
||||
checksum: 101dbed8abd4cf167cdb7a0bc13db90dd0743332c689e43b18cc5254d238f0766750752432401fa63dc7e9e32399ef68daacf48f0d89db1484042c1761c7362d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"methods@npm:~1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "methods@npm:1.1.2"
|
||||
@ -10536,6 +10712,47 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ngraph.events@npm:^1.0.0, ngraph.events@npm:^1.2.1":
|
||||
version: 1.2.2
|
||||
resolution: "ngraph.events@npm:1.2.2"
|
||||
checksum: 358d312ebdf8eeddfac9741938f38cb7fe488b402641900613f632fe81fe63da92543db050c659e5a7dc7c28750b1109ce4d78cc9d68e66d732fba7128efbbc2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ngraph.forcelayout@npm:3":
|
||||
version: 3.3.1
|
||||
resolution: "ngraph.forcelayout@npm:3.3.1"
|
||||
dependencies:
|
||||
ngraph.events: ^1.0.0
|
||||
ngraph.merge: ^1.0.0
|
||||
ngraph.random: ^1.0.0
|
||||
checksum: 580ab50debd7cf03bae7752337496f45158a8808a6494df71080a28c5f047fa146d53daf5635ed22bb06cc95b06b217e3c104fe2071d8e5beb036c7697871b7d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ngraph.graph@npm:20":
|
||||
version: 20.0.1
|
||||
resolution: "ngraph.graph@npm:20.0.1"
|
||||
dependencies:
|
||||
ngraph.events: ^1.2.1
|
||||
checksum: 16e42c4c4fdabf8243b42291c8c5bf5baa0614773d62edf911b86ef2aa9f60a38e107a9d1bb97ab6feac2ac514c8e04a0aa24d1992502b21420fdd5f563c96cb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ngraph.merge@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "ngraph.merge@npm:1.0.0"
|
||||
checksum: e4ad9e55acec7704229f56e7d6157b17a9d87736efdbd2cee6c3fab92cffb99e94b3d8e2e5e75f863f4fe972192a2bff1381e58334115d7c9c08b55b1673801f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ngraph.random@npm:^1.0.0":
|
||||
version: 1.1.0
|
||||
resolution: "ngraph.random@npm:1.1.0"
|
||||
checksum: 926ed86450d2fc983aea92e22f595bf064c45d90e5efbf4bd85f14e825253c9e01417ef1357da0875691ccb61ec411443a7c8e1ec10885f2eb841366b3bd7960
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"no-case@npm:^3.0.4":
|
||||
version: 3.0.4
|
||||
resolution: "no-case@npm:3.0.4"
|
||||
@ -11390,6 +11607,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"polished@npm:4":
|
||||
version: 4.2.2
|
||||
resolution: "polished@npm:4.2.2"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.17.8
|
||||
checksum: 97fb927dc55cd34aeb11b31ae2a3332463f114351c86e8aa6580d7755864a0120164fdc3770e6160c8b1775052f0eda14db9a6e34402cd4b08ab2d658a593725
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-attribute-case-insensitive@npm:^6.0.2":
|
||||
version: 6.0.2
|
||||
resolution: "postcss-attribute-case-insensitive@npm:6.0.2"
|
||||
@ -12342,7 +12568,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
|
||||
"prop-types@npm:15, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
|
||||
version: 15.8.1
|
||||
resolution: "prop-types@npm:15.8.1"
|
||||
dependencies:
|
||||
@ -12486,6 +12712,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-force-graph-3d@npm:^1.24.0":
|
||||
version: 1.24.0
|
||||
resolution: "react-force-graph-3d@npm:1.24.0"
|
||||
dependencies:
|
||||
3d-force-graph: ^1.73
|
||||
prop-types: 15
|
||||
react-kapsule: 2
|
||||
peerDependencies:
|
||||
react: "*"
|
||||
checksum: 5084491aac5629a38176c4fef52513e423cddb4d98a41f445851bedb1895f1800e3ab04e908b5ad39544bb13610a214518348465c0baf7d6d7c709fa0024e179
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-intersection-observer@npm:^9.4.1":
|
||||
version: 9.5.2
|
||||
resolution: "react-intersection-observer@npm:9.5.2"
|
||||
@ -12533,6 +12772,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-kapsule@npm:2":
|
||||
version: 2.4.0
|
||||
resolution: "react-kapsule@npm:2.4.0"
|
||||
dependencies:
|
||||
fromentries: ^1.3.2
|
||||
jerrypick: ^1.1.1
|
||||
peerDependencies:
|
||||
react: ">=16.13.1"
|
||||
checksum: 8d9af1c8e3329991a90cb5f2a6e64ee0085016e00de7783a58c6a95089ff710a07c0acaf6b2b6b4d393802e9448e7263f3324486f1358926706e8075e7d5cd3a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-lifecycles-compat@npm:^3.0.4":
|
||||
version: 3.0.4
|
||||
resolution: "react-lifecycles-compat@npm:3.0.4"
|
||||
@ -14217,6 +14468,47 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"three-forcegraph@npm:1":
|
||||
version: 1.41.10
|
||||
resolution: "three-forcegraph@npm:1.41.10"
|
||||
dependencies:
|
||||
accessor-fn: 1
|
||||
d3-array: 1 - 3
|
||||
d3-force-3d: 2 - 3
|
||||
d3-scale: 1 - 4
|
||||
d3-scale-chromatic: 1 - 3
|
||||
data-joint: 1
|
||||
kapsule: 1
|
||||
ngraph.forcelayout: 3
|
||||
ngraph.graph: 20
|
||||
tinycolor2: 1
|
||||
peerDependencies:
|
||||
three: ">=0.118.3"
|
||||
checksum: 921f3f7abc60816738aefde97946b51b00d06dba4e0f04b5fd9678d3dfa375579048008fe6f2994208dc09e359f0c17431f65cf5f7569b6ff7907919a7c61d41
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"three-render-objects@npm:^1.29":
|
||||
version: 1.29.0
|
||||
resolution: "three-render-objects@npm:1.29.0"
|
||||
dependencies:
|
||||
"@tweenjs/tween.js": 18 - 21
|
||||
accessor-fn: 1
|
||||
kapsule: 1
|
||||
polished: 4
|
||||
peerDependencies:
|
||||
three: "*"
|
||||
checksum: 90824e427aa4605d9b52844a6a021ac323cd5890a02e5f9ed1651c23304366e57c1670d3cd607ac18a867a852be977127b4303729aabd586791e73c8b39b089c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"three@npm:>=0.118 <1, three@npm:^0.157.0":
|
||||
version: 0.157.0
|
||||
resolution: "three@npm:0.157.0"
|
||||
checksum: 444797461c9db09d8a4cad886e494c2e6dd5754f09ac7ac4af75a3bf1143ae79641388db31999edcc76c5dba9677639f09da2df8d0128396a1dd59e41226c85c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"through@npm:^2.3.6":
|
||||
version: 2.3.8
|
||||
resolution: "through@npm:2.3.8"
|
||||
@ -14238,6 +14530,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinycolor2@npm:1":
|
||||
version: 1.6.0
|
||||
resolution: "tinycolor2@npm:1.6.0"
|
||||
checksum: 6df4d07fceeedc0a878d7bac47e2cd47c1ceeb1078340a9eb8a295bc0651e17c750f73d47b3028d829f30b85c15e0572c0fd4142083e4c21a30a597e47f47230
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tmp@npm:^0.0.33":
|
||||
version: 0.0.33
|
||||
resolution: "tmp@npm:0.0.33"
|
||||
|
Loading…
x
Reference in New Issue
Block a user