system/SocialGraph, socialGraphInstance

This commit is contained in:
Martti Malmi 2023-11-17 10:07:16 +02:00
parent 55c938961f
commit 4fbe92baa2
7 changed files with 114 additions and 104 deletions

View File

@ -1,9 +1,8 @@
import { db } from "Db";
import { unixNowMs } from "@snort/shared";
import { EventKind, RequestBuilder, TaggedNostrEvent } from "@snort/system";
import { EventKind, RequestBuilder, socialGraphInstance, TaggedNostrEvent } from "@snort/system";
import { RefreshFeedCache } from "./RefreshFeedCache";
import { LoginSession } from "Login";
import SocialGraph from "SocialGraph/SocialGraph";
export class FollowListCache extends RefreshFeedCache<TaggedNostrEvent> {
constructor() {
@ -27,7 +26,7 @@ export class FollowListCache extends RefreshFeedCache<TaggedNostrEvent> {
loaded: unixNowMs(),
});
if (update !== "no_change") {
SocialGraph.handleFollowEvent(e);
socialGraphInstance.handleFollowEvent(e);
}
}),
);
@ -43,6 +42,6 @@ export class FollowListCache extends RefreshFeedCache<TaggedNostrEvent> {
override async preload() {
await super.preload();
this.snapshot().forEach(e => SocialGraph.handleFollowEvent(e));
this.snapshot().forEach(e => socialGraphInstance.handleFollowEvent(e));
}
}

View File

@ -1,7 +1,7 @@
import "./ProfileImage.css";
import React, { ReactNode } from "react";
import { HexKey, UserMetadata } from "@snort/system";
import { HexKey, socialGraphInstance, UserMetadata } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { useHover } from "@uidotdev/usehooks";
import classNames from "classnames";
@ -12,7 +12,6 @@ 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;
@ -51,7 +50,7 @@ export default function ProfileImage({
}: ProfileImageProps) {
const user = useUserProfile(profile ? "" : pubkey) ?? profile;
const nip05 = defaultNip ? defaultNip : user?.nip05;
const followDistance = SocialGraph.getFollowDistance(pubkey);
const followDistance = socialGraphInstance.getFollowDistance(pubkey);
const [ref, hovering] = useHover<HTMLDivElement>();
function handleClick(e: React.MouseEvent) {
@ -65,7 +64,7 @@ export default function ProfileImage({
let followDistanceColor = "";
if (followDistance <= 1) {
followDistanceColor = "success";
} else if (followDistance === 2 && SocialGraph.followedByFriendsCount(pubkey) >= 10) {
} else if (followDistance === 2 && socialGraphInstance.followedByFriendsCount(pubkey) >= 10) {
followDistanceColor = "text-nostr-orange";
}
return (

View File

@ -2,7 +2,7 @@ import * as secp from "@noble/curves/secp256k1";
import * as utils from "@noble/curves/abstract/utils";
import { v4 as uuid } from "uuid";
import { HexKey, RelaySettings, EventPublisher, KeyStorage, NotEncrypted } from "@snort/system";
import { HexKey, RelaySettings, EventPublisher, KeyStorage, NotEncrypted, socialGraphInstance } from "@snort/system";
import { deepClone, sanitizeRelayUrl, unwrap, ExternalStore } from "@snort/shared";
import { DefaultRelays } from "Const";
@ -74,6 +74,10 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
if (!this.#activeAccount) {
this.#activeAccount = this.#accounts.keys().next().value;
}
if (this.#activeAccount) {
const pubKey = this.#accounts.get(this.#activeAccount)?.publicKey;
socialGraphInstance.setRoot(pubKey || "");
}
for (const [, v] of this.#accounts) {
// reset readonly on load
if (v.type === LoginSessionType.PrivateKey && v.readonly) {
@ -116,6 +120,8 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
switchAccount(id: string) {
if (this.#accounts.has(id)) {
this.#activeAccount = id;
const pubKey = this.#accounts.get(id)?.publicKey || "";
socialGraphInstance.setRoot(pubKey);
this.#save();
}
}
@ -140,6 +146,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
if (this.#accounts.has(key)) {
throw new Error("Already logged in with this pubkey");
}
socialGraphInstance.setRoot(key);
const initRelays = this.decideInitRelays(relays);
const newSession = {
...LoggedOut,
@ -180,6 +187,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
if (this.#accounts.has(pubKey)) {
throw new Error("Already logged in with this pubkey");
}
socialGraphInstance.setRoot(pubKey);
const initRelays = this.decideInitRelays(relays);
const newSession = {
...LoggedOut,

View File

@ -1,8 +1,6 @@
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 { MetadataCache, socialGraphInstance, STR, UID } from "@snort/system";
import { SnortContext } from "@snort/system-react";
import * as THREE from "three";
import { defaultAvatar } from "../SnortUtils";
@ -96,17 +94,17 @@ const NetworkGraph = () => {
const nodesVisited = new Set<UID>();
const userCountByDistance = Array.from(
{ length: 6 },
(_, i) => SocialGraph.usersByFollowDistance.get(i)?.size || 0,
(_, i) => socialGraphInstance.usersByFollowDistance.get(i)?.size || 0,
);
// Go through all the nodes
for (let distance = 0; distance <= showDistance; ++distance) {
const users = SocialGraph.usersByFollowDistance.get(distance);
const users = socialGraphInstance.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 inboundCount = socialGraphInstance.followersByUser.get(UID)?.size || 0;
const outboundCount = socialGraphInstance.followedByUser.get(UID)?.size || 0;
const pubkey = STR(UID);
const node = {
id: UID,
@ -132,7 +130,7 @@ const NetworkGraph = () => {
// Add links
for (const node of nodes.values()) {
if (direction === Direction.OUTBOUND || direction === Direction.BOTH) {
for (const followedID of SocialGraph.followedByUser.get(node.id) ?? []) {
for (const followedID of socialGraphInstance.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({
@ -144,7 +142,7 @@ const NetworkGraph = () => {
}
// TODO: Fix filtering
/* if (direction === Direction.INBOUND || direction === Direction.BOTH) {
for (const followerID of SocialGraph.followersByUser.get(node.id) ?? []) {
for (const followerID of socialGraphInstance.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

View File

@ -1,27 +1,53 @@
import { ID, STR, UID } from "./UniqueIds";
import { LoginStore } from "../Login";
import { unwrap } from "../SnortUtils";
import { HexKey, MetadataCache, NostrEvent } from "@snort/system";
import { HexKey, NostrEvent } from "..";
type Unsubscribe = () => void;
export default class SocialGraph {
root: UID;
followDistanceByUser = new Map<UID, number>();
usersByFollowDistance = new Map<number, Set<UID>>();
followedByUser = new Map<UID, Set<UID>>();
followersByUser = new Map<UID, Set<UID>>();
latestFollowEventTimestamps = new Map<UID, number>();
const Key = {
pubKey: null as HexKey | null,
getPubKey: () => {
return unwrap(LoginStore.snapshot().publicKey);
},
isMine: (user: HexKey) => user === Key.getPubKey(),
};
constructor(root: HexKey) {
this.root = ID(root);
this.followDistanceByUser.set(this.root, 0);
this.usersByFollowDistance.set(0, new Set([this.root]));
}
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>(),
setRoot(root: HexKey) {
const rootId = ID(root);
if (rootId === this.root) {
return;
}
this.root = rootId;
this.followDistanceByUser.clear();
this.usersByFollowDistance.clear();
this.followDistanceByUser.set(this.root, 0);
this.usersByFollowDistance.set(0, new Set([this.root]));
handleFollowEvent: function (event: NostrEvent) {
const queue = [this.root];
while (queue.length > 0) {
const user = queue.shift()!;
const distance = this.followDistanceByUser.get(user)!;
const followers = this.followersByUser.get(user) || new Set<UID>();
for (const follower of followers) {
if (!this.followDistanceByUser.has(follower)) {
const newFollowDistance = distance + 1;
this.followDistanceByUser.set(follower, newFollowDistance);
if (!this.usersByFollowDistance.has(newFollowDistance)) {
this.usersByFollowDistance.set(newFollowDistance, new Set());
}
this.usersByFollowDistance.get(newFollowDistance)!.add(follower);
queue.push(follower);
}
}
}
}
handleFollowEvent(event: NostrEvent) {
try {
const author = ID(event.pubkey);
const timestamp = event.created_at;
@ -59,27 +85,27 @@ export default {
} catch (e) {
// might not be logged in or sth
}
},
}
isFollowing: function (follower: HexKey, followedUser: HexKey): boolean {
isFollowing(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 {
getFollowDistance(user: HexKey): number {
try {
if (Key.isMine(user)) {
const userId = ID(user);
if (userId === this.root) {
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)) {
@ -101,21 +127,12 @@ export default {
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) {
addFollower(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>());
}
@ -124,11 +141,10 @@ export default {
if (!this.followedByUser.has(follower)) {
this.followedByUser.set(follower, new Set<UID>());
}
const myId = ID(Key.getPubKey());
if (followedUser !== myId) {
if (followedUser !== this.root) {
let newFollowDistance;
if (follower === myId) {
if (follower === this.root) {
// basically same as the next "else" block, but faster
newFollowDistance = 1;
this.addUserByFollowDistance(newFollowDistance, followedUser);
@ -145,19 +161,20 @@ export default {
}
this.followedByUser.get(follower)?.add(followedUser);
if (this.followedByUser.get(myId)?.has(follower)) {
if (this.followedByUser.get(this.root)?.has(follower)) {
/*
setTimeout(() => {
PubSub.subscribe({ authors: [STR(followedUser)], kinds: [0, 3] }, undefined, true);
}, 0);
*/
}
},
removeFollower: function (unfollowedUser: UID, follower: UID) {
}
removeFollower(unfollowedUser: UID, follower: UID) {
this.followersByUser.get(unfollowedUser)?.delete(follower);
this.followedByUser.get(follower)?.delete(unfollowedUser);
if (unfollowedUser === ID(Key.getPubKey())) {
if (unfollowedUser === this.root) {
return;
}
@ -175,60 +192,47 @@ export default {
} else {
this.followDistanceByUser.set(unfollowedUser, smallest);
}
},
}
// TODO subscription methods for followersByUser and followedByUser. and maybe messagesByTime. and replies
followerCount: function (address: HexKey) {
followerCount(address: HexKey) {
const id = ID(address);
return this.followersByUser.get(id)?.size ?? 0;
},
followedByFriendsCount: function (address: HexKey) {
}
followedByFriendsCount(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)) {
if (this.followedByUser.get(this.root)?.has(follower)) {
count++; // should we stop at 10?
}
}
return count;
},
getFollowedByUser: function (
user: HexKey,
cb?: (followedUsers: Set<HexKey>) => void,
includeSelf = false,
): Unsubscribe {
}
getFollowedByUser(user: HexKey, includeSelf = false): Set<HexKey> {
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();
const set = new Set<HexKey>();
for (const id of this.followedByUser.get(userId) || []) {
set.add(STR(id));
}
if (includeSelf) {
set.add(user);
}
//return PubSub.subscribe({ kinds: [3], authors: [user] }, callback);
return () => {};
},
getFollowersByUser: function (address: HexKey, cb?: (followers: Set<HexKey>) => void): Unsubscribe {
return set;
}
getFollowersByUser(address: HexKey): Set<HexKey> {
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();
const set = new Set<HexKey>();
for (const id of this.followersByUser.get(userId) || []) {
set.add(STR(id));
}
//return PubSub.subscribe({ kinds: [3], '#p': [address] }, callback); // TODO this doesn't fire when a user is unfollowed
return () => {};
},
};
return set;
}
}
export const socialGraphInstance = new SocialGraph("");

View File

@ -10,6 +10,8 @@ import { base64 } from "@scure/base";
export * from "./nostr-system";
export { default as EventKind } from "./event-kind";
export { default as SocialGraph, socialGraphInstance } from "./SocialGraph/SocialGraph";
export * from "./SocialGraph/UniqueIds";
export * from "./nostr";
export * from "./links";
export * from "./nips";