SocialGraph #691
@ -1,9 +1,8 @@
|
|||||||
import { db } from "Db";
|
import { db } from "Db";
|
||||||
import { unixNowMs } from "@snort/shared";
|
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 { RefreshFeedCache } from "./RefreshFeedCache";
|
||||||
import { LoginSession } from "Login";
|
import { LoginSession } from "Login";
|
||||||
import SocialGraph from "SocialGraph/SocialGraph";
|
|
||||||
|
|
||||||
export class FollowListCache extends RefreshFeedCache<TaggedNostrEvent> {
|
export class FollowListCache extends RefreshFeedCache<TaggedNostrEvent> {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -27,7 +26,7 @@ export class FollowListCache extends RefreshFeedCache<TaggedNostrEvent> {
|
|||||||
loaded: unixNowMs(),
|
loaded: unixNowMs(),
|
||||||
});
|
});
|
||||||
if (update !== "no_change") {
|
if (update !== "no_change") {
|
||||||
SocialGraph.handleFollowEvent(e);
|
socialGraphInstance.handleFollowEvent(e);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -43,6 +42,6 @@ export class FollowListCache extends RefreshFeedCache<TaggedNostrEvent> {
|
|||||||
|
|
||||||
override async preload() {
|
override async preload() {
|
||||||
await super.preload();
|
await super.preload();
|
||||||
this.snapshot().forEach(e => SocialGraph.handleFollowEvent(e));
|
this.snapshot().forEach(e => socialGraphInstance.handleFollowEvent(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import "./ProfileImage.css";
|
import "./ProfileImage.css";
|
||||||
|
|
||||||
import React, { ReactNode } from "react";
|
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 { useUserProfile } from "@snort/system-react";
|
||||||
import { useHover } from "@uidotdev/usehooks";
|
import { useHover } from "@uidotdev/usehooks";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
@ -12,7 +12,6 @@ import Icon from "Icons/Icon";
|
|||||||
import DisplayName from "./DisplayName";
|
import DisplayName from "./DisplayName";
|
||||||
import { ProfileLink } from "./ProfileLink";
|
import { ProfileLink } from "./ProfileLink";
|
||||||
import { ProfileCard } from "./ProfileCard";
|
import { ProfileCard } from "./ProfileCard";
|
||||||
import SocialGraph from "../../SocialGraph/SocialGraph";
|
|
||||||
|
|
||||||
export interface ProfileImageProps {
|
export interface ProfileImageProps {
|
||||||
pubkey: HexKey;
|
pubkey: HexKey;
|
||||||
@ -51,7 +50,7 @@ export default function ProfileImage({
|
|||||||
}: ProfileImageProps) {
|
}: ProfileImageProps) {
|
||||||
const user = useUserProfile(profile ? "" : pubkey) ?? profile;
|
const user = useUserProfile(profile ? "" : pubkey) ?? profile;
|
||||||
const nip05 = defaultNip ? defaultNip : user?.nip05;
|
const nip05 = defaultNip ? defaultNip : user?.nip05;
|
||||||
const followDistance = SocialGraph.getFollowDistance(pubkey);
|
const followDistance = socialGraphInstance.getFollowDistance(pubkey);
|
||||||
const [ref, hovering] = useHover<HTMLDivElement>();
|
const [ref, hovering] = useHover<HTMLDivElement>();
|
||||||
|
|
||||||
function handleClick(e: React.MouseEvent) {
|
function handleClick(e: React.MouseEvent) {
|
||||||
@ -65,7 +64,7 @@ export default function ProfileImage({
|
|||||||
let followDistanceColor = "";
|
let followDistanceColor = "";
|
||||||
if (followDistance <= 1) {
|
if (followDistance <= 1) {
|
||||||
followDistanceColor = "success";
|
followDistanceColor = "success";
|
||||||
} else if (followDistance === 2 && SocialGraph.followedByFriendsCount(pubkey) >= 10) {
|
} else if (followDistance === 2 && socialGraphInstance.followedByFriendsCount(pubkey) >= 10) {
|
||||||
followDistanceColor = "text-nostr-orange";
|
followDistanceColor = "text-nostr-orange";
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
@ -2,7 +2,7 @@ import * as secp from "@noble/curves/secp256k1";
|
|||||||
import * as utils from "@noble/curves/abstract/utils";
|
import * as utils from "@noble/curves/abstract/utils";
|
||||||
import { v4 as uuid } from "uuid";
|
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 { deepClone, sanitizeRelayUrl, unwrap, ExternalStore } from "@snort/shared";
|
||||||
|
|
||||||
import { DefaultRelays } from "Const";
|
import { DefaultRelays } from "Const";
|
||||||
@ -74,6 +74,10 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
if (!this.#activeAccount) {
|
if (!this.#activeAccount) {
|
||||||
this.#activeAccount = this.#accounts.keys().next().value;
|
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) {
|
for (const [, v] of this.#accounts) {
|
||||||
// reset readonly on load
|
// reset readonly on load
|
||||||
if (v.type === LoginSessionType.PrivateKey && v.readonly) {
|
if (v.type === LoginSessionType.PrivateKey && v.readonly) {
|
||||||
@ -116,6 +120,8 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
switchAccount(id: string) {
|
switchAccount(id: string) {
|
||||||
if (this.#accounts.has(id)) {
|
if (this.#accounts.has(id)) {
|
||||||
this.#activeAccount = id;
|
this.#activeAccount = id;
|
||||||
|
const pubKey = this.#accounts.get(id)?.publicKey || "";
|
||||||
|
socialGraphInstance.setRoot(pubKey);
|
||||||
this.#save();
|
this.#save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,6 +146,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
if (this.#accounts.has(key)) {
|
if (this.#accounts.has(key)) {
|
||||||
throw new Error("Already logged in with this pubkey");
|
throw new Error("Already logged in with this pubkey");
|
||||||
}
|
}
|
||||||
|
socialGraphInstance.setRoot(key);
|
||||||
const initRelays = this.decideInitRelays(relays);
|
const initRelays = this.decideInitRelays(relays);
|
||||||
const newSession = {
|
const newSession = {
|
||||||
...LoggedOut,
|
...LoggedOut,
|
||||||
@ -180,6 +187,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
if (this.#accounts.has(pubKey)) {
|
if (this.#accounts.has(pubKey)) {
|
||||||
throw new Error("Already logged in with this pubkey");
|
throw new Error("Already logged in with this pubkey");
|
||||||
}
|
}
|
||||||
|
socialGraphInstance.setRoot(pubKey);
|
||||||
const initRelays = this.decideInitRelays(relays);
|
const initRelays = this.decideInitRelays(relays);
|
||||||
const newSession = {
|
const newSession = {
|
||||||
...LoggedOut,
|
...LoggedOut,
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import ForceGraph3D, { NodeObject } from "react-force-graph-3d";
|
import ForceGraph3D, { NodeObject } from "react-force-graph-3d";
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { STR, UID } from "../SocialGraph/UniqueIds";
|
import { MetadataCache, socialGraphInstance, STR, UID } from "@snort/system";
|
||||||
import SocialGraph from "../SocialGraph/SocialGraph";
|
|
||||||
import { MetadataCache } from "@snort/system";
|
|
||||||
import { SnortContext } from "@snort/system-react";
|
import { SnortContext } from "@snort/system-react";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { defaultAvatar } from "../SnortUtils";
|
import { defaultAvatar } from "../SnortUtils";
|
||||||
@ -96,17 +94,17 @@ const NetworkGraph = () => {
|
|||||||
const nodesVisited = new Set<UID>();
|
const nodesVisited = new Set<UID>();
|
||||||
const userCountByDistance = Array.from(
|
const userCountByDistance = Array.from(
|
||||||
{ length: 6 },
|
{ length: 6 },
|
||||||
(_, i) => SocialGraph.usersByFollowDistance.get(i)?.size || 0,
|
(_, i) => socialGraphInstance.usersByFollowDistance.get(i)?.size || 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Go through all the nodes
|
// Go through all the nodes
|
||||||
for (let distance = 0; distance <= showDistance; ++distance) {
|
for (let distance = 0; distance <= showDistance; ++distance) {
|
||||||
const users = SocialGraph.usersByFollowDistance.get(distance);
|
const users = socialGraphInstance.usersByFollowDistance.get(distance);
|
||||||
if (!users) break;
|
if (!users) break;
|
||||||
for (const UID of users) {
|
for (const UID of users) {
|
||||||
if (renderLimit && nodes.size >= renderLimit) break; // Temporary hack
|
if (renderLimit && nodes.size >= renderLimit) break; // Temporary hack
|
||||||
const inboundCount = SocialGraph.followersByUser.get(UID)?.size || 0;
|
const inboundCount = socialGraphInstance.followersByUser.get(UID)?.size || 0;
|
||||||
const outboundCount = SocialGraph.followedByUser.get(UID)?.size || 0;
|
const outboundCount = socialGraphInstance.followedByUser.get(UID)?.size || 0;
|
||||||
const pubkey = STR(UID);
|
const pubkey = STR(UID);
|
||||||
const node = {
|
const node = {
|
||||||
id: UID,
|
id: UID,
|
||||||
@ -132,7 +130,7 @@ const NetworkGraph = () => {
|
|||||||
// Add links
|
// Add links
|
||||||
for (const node of nodes.values()) {
|
for (const node of nodes.values()) {
|
||||||
if (direction === Direction.OUTBOUND || direction === Direction.BOTH) {
|
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 (!nodes.has(followedID)) continue; // Skip links to nodes that we're not rendering
|
||||||
if (nodesVisited.has(followedID)) continue;
|
if (nodesVisited.has(followedID)) continue;
|
||||||
links.push({
|
links.push({
|
||||||
@ -144,7 +142,7 @@ const NetworkGraph = () => {
|
|||||||
}
|
}
|
||||||
// TODO: Fix filtering
|
// TODO: Fix filtering
|
||||||
/* if (direction === Direction.INBOUND || direction === Direction.BOTH) {
|
/* 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;
|
if (nodesVisited.has(followerID)) continue;
|
||||||
const follower = nodes.get(followerID);
|
const follower = nodes.get(followerID);
|
||||||
if (!follower) continue; // Skip links to nodes that we're not rendering
|
if (!follower) continue; // Skip links to nodes that we're not rendering
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
mapEventToProfile,
|
mapEventToProfile,
|
||||||
PowWorker,
|
PowWorker,
|
||||||
encodeTLVEntries,
|
encodeTLVEntries,
|
||||||
|
socialGraphInstance,
|
||||||
} from "@snort/system";
|
} from "@snort/system";
|
||||||
import { SnortContext } from "@snort/system-react";
|
import { SnortContext } from "@snort/system-react";
|
||||||
import { removeUndefined, throwIfOffline } from "@snort/shared";
|
import { removeUndefined, throwIfOffline } from "@snort/shared";
|
||||||
@ -109,6 +110,12 @@ System.on("auth", async (c, r, cb) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
System.on("event", ev => {
|
||||||
Kieran marked this conversation as resolved
|
|||||||
|
if (ev.kind === 3) {
|
||||||
|
socialGraphInstance.handleFollowEvent(ev);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function fetchProfile(key: string) {
|
async function fetchProfile(key: string) {
|
||||||
try {
|
try {
|
||||||
throwIfOffline();
|
throwIfOffline();
|
||||||
|
@ -1,27 +1,53 @@
|
|||||||
import { ID, STR, UID } from "./UniqueIds";
|
import { ID, STR, UID } from "./UniqueIds";
|
||||||
import { LoginStore } from "../Login";
|
import { HexKey, NostrEvent } from "..";
|
||||||
import { unwrap } from "../SnortUtils";
|
|
||||||
import { HexKey, MetadataCache, NostrEvent } from "@snort/system";
|
|
||||||
|
|
||||||
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 = {
|
constructor(root: HexKey) {
|
||||||
pubKey: null as HexKey | null,
|
this.root = ID(root);
|
||||||
getPubKey: () => {
|
this.followDistanceByUser.set(this.root, 0);
|
||||||
return unwrap(LoginStore.snapshot().publicKey);
|
this.usersByFollowDistance.set(0, new Set([this.root]));
|
||||||
},
|
}
|
||||||
isMine: (user: HexKey) => user === Key.getPubKey(),
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
setRoot(root: HexKey) {
|
||||||
followDistanceByUser: new Map<UID, number>(),
|
const rootId = ID(root);
|
||||||
usersByFollowDistance: new Map<number, Set<UID>>(),
|
if (rootId === this.root) {
|
||||||
profiles: new Map<UID, MetadataCache>(), // JSON.parsed event.content of profile events
|
return;
|
||||||
followedByUser: new Map<UID, Set<UID>>(),
|
}
|
||||||
followersByUser: new Map<UID, Set<UID>>(),
|
this.root = rootId;
|
||||||
latestFollowEventTimestamps: new Map<UID, number>(),
|
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 {
|
try {
|
||||||
const author = ID(event.pubkey);
|
const author = ID(event.pubkey);
|
||||||
const timestamp = event.created_at;
|
const timestamp = event.created_at;
|
||||||
@ -59,27 +85,27 @@ export default {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// might not be logged in or sth
|
// 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 followedUserId = ID(followedUser);
|
||||||
const followerId = ID(follower);
|
const followerId = ID(follower);
|
||||||
return !!this.followedByUser.get(followerId)?.has(followedUserId);
|
return !!this.followedByUser.get(followerId)?.has(followedUserId);
|
||||||
},
|
}
|
||||||
|
|
||||||
getFollowDistance: function (user: HexKey): number {
|
getFollowDistance(user: HexKey): number {
|
||||||
try {
|
try {
|
||||||
if (Key.isMine(user)) {
|
const userId = ID(user);
|
||||||
|
if (userId === this.root) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const userId = ID(user);
|
|
||||||
const distance = this.followDistanceByUser.get(userId);
|
const distance = this.followDistanceByUser.get(userId);
|
||||||
return distance === undefined ? 1000 : distance;
|
return distance === undefined ? 1000 : distance;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// might not be logged in or sth
|
// might not be logged in or sth
|
||||||
return 1000;
|
return 1000;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
addUserByFollowDistance(distance: number, user: UID) {
|
addUserByFollowDistance(distance: number, user: UID) {
|
||||||
if (!this.usersByFollowDistance.has(distance)) {
|
if (!this.usersByFollowDistance.has(distance)) {
|
||||||
@ -101,21 +127,12 @@ export default {
|
|||||||
this.usersByFollowDistance.get(d)?.delete(user);
|
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") {
|
if (typeof followedUser !== "number" || typeof follower !== "number") {
|
||||||
throw new Error("Invalid user id");
|
throw new Error("Invalid user id");
|
||||||
}
|
}
|
||||||
this.ensureRootUser();
|
|
||||||
if (!this.followersByUser.has(followedUser)) {
|
if (!this.followersByUser.has(followedUser)) {
|
||||||
this.followersByUser.set(followedUser, new Set<UID>());
|
this.followersByUser.set(followedUser, new Set<UID>());
|
||||||
}
|
}
|
||||||
@ -124,11 +141,10 @@ export default {
|
|||||||
if (!this.followedByUser.has(follower)) {
|
if (!this.followedByUser.has(follower)) {
|
||||||
this.followedByUser.set(follower, new Set<UID>());
|
this.followedByUser.set(follower, new Set<UID>());
|
||||||
}
|
}
|
||||||
const myId = ID(Key.getPubKey());
|
|
||||||
|
|
||||||
if (followedUser !== myId) {
|
if (followedUser !== this.root) {
|
||||||
let newFollowDistance;
|
let newFollowDistance;
|
||||||
if (follower === myId) {
|
if (follower === this.root) {
|
||||||
// basically same as the next "else" block, but faster
|
// basically same as the next "else" block, but faster
|
||||||
newFollowDistance = 1;
|
newFollowDistance = 1;
|
||||||
this.addUserByFollowDistance(newFollowDistance, followedUser);
|
this.addUserByFollowDistance(newFollowDistance, followedUser);
|
||||||
@ -145,19 +161,20 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.followedByUser.get(follower)?.add(followedUser);
|
this.followedByUser.get(follower)?.add(followedUser);
|
||||||
if (this.followedByUser.get(myId)?.has(follower)) {
|
if (this.followedByUser.get(this.root)?.has(follower)) {
|
||||||
/*
|
/*
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
PubSub.subscribe({ authors: [STR(followedUser)], kinds: [0, 3] }, undefined, true);
|
PubSub.subscribe({ authors: [STR(followedUser)], kinds: [0, 3] }, undefined, true);
|
||||||
}, 0);
|
}, 0);
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
removeFollower: function (unfollowedUser: UID, follower: UID) {
|
|
||||||
|
removeFollower(unfollowedUser: UID, follower: UID) {
|
||||||
this.followersByUser.get(unfollowedUser)?.delete(follower);
|
this.followersByUser.get(unfollowedUser)?.delete(follower);
|
||||||
this.followedByUser.get(follower)?.delete(unfollowedUser);
|
this.followedByUser.get(follower)?.delete(unfollowedUser);
|
||||||
|
|
||||||
if (unfollowedUser === ID(Key.getPubKey())) {
|
if (unfollowedUser === this.root) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,31 +192,27 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.followDistanceByUser.set(unfollowedUser, smallest);
|
this.followDistanceByUser.set(unfollowedUser, smallest);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
// TODO subscription methods for followersByUser and followedByUser. and maybe messagesByTime. and replies
|
// TODO subscription methods for followersByUser and followedByUser. and maybe messagesByTime. and replies
|
||||||
followerCount: function (address: HexKey) {
|
followerCount(address: HexKey) {
|
||||||
const id = ID(address);
|
const id = ID(address);
|
||||||
return this.followersByUser.get(id)?.size ?? 0;
|
return this.followersByUser.get(id)?.size ?? 0;
|
||||||
},
|
}
|
||||||
followedByFriendsCount: function (address: HexKey) {
|
|
||||||
|
followedByFriendsCount(address: HexKey) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const myId = ID(Key.getPubKey());
|
|
||||||
const id = ID(address);
|
const id = ID(address);
|
||||||
for (const follower of this.followersByUser.get(id) ?? []) {
|
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?
|
count++; // should we stop at 10?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
},
|
}
|
||||||
getFollowedByUser: function (
|
|
||||||
user: HexKey,
|
getFollowedByUser(user: HexKey, includeSelf = false): Set<HexKey> {
|
||||||
cb?: (followedUsers: Set<HexKey>) => void,
|
|
||||||
includeSelf = false,
|
|
||||||
): Unsubscribe {
|
|
||||||
const userId = ID(user);
|
const userId = ID(user);
|
||||||
const callback = () => {
|
|
||||||
if (cb) {
|
|
||||||
const set = new Set<HexKey>();
|
const set = new Set<HexKey>();
|
||||||
for (const id of this.followedByUser.get(userId) || []) {
|
for (const id of this.followedByUser.get(userId) || []) {
|
||||||
set.add(STR(id));
|
set.add(STR(id));
|
||||||
@ -207,28 +220,19 @@ export default {
|
|||||||
if (includeSelf) {
|
if (includeSelf) {
|
||||||
set.add(user);
|
set.add(user);
|
||||||
}
|
}
|
||||||
cb(set);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (this.followedByUser.has(userId) || includeSelf) {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
//return PubSub.subscribe({ kinds: [3], authors: [user] }, callback);
|
//return PubSub.subscribe({ kinds: [3], authors: [user] }, callback);
|
||||||
return () => {};
|
return set;
|
||||||
},
|
}
|
||||||
getFollowersByUser: function (address: HexKey, cb?: (followers: Set<HexKey>) => void): Unsubscribe {
|
|
||||||
|
getFollowersByUser(address: HexKey): Set<HexKey> {
|
||||||
const userId = ID(address);
|
const userId = ID(address);
|
||||||
const callback = () => {
|
|
||||||
if (cb) {
|
|
||||||
const set = new Set<HexKey>();
|
const set = new Set<HexKey>();
|
||||||
for (const id of this.followersByUser.get(userId) || []) {
|
for (const id of this.followersByUser.get(userId) || []) {
|
||||||
set.add(STR(id));
|
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 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("");
|
@ -10,6 +10,8 @@ import { base64 } from "@scure/base";
|
|||||||
|
|
||||||
export * from "./nostr-system";
|
export * from "./nostr-system";
|
||||||
export { default as EventKind } from "./event-kind";
|
export { default as EventKind } from "./event-kind";
|
||||||
|
export { default as SocialGraph, socialGraphInstance } from "./SocialGraph/SocialGraph";
|
||||||
|
export * from "./SocialGraph/UniqueIds";
|
||||||
export * from "./nostr";
|
export * from "./nostr";
|
||||||
export * from "./links";
|
export * from "./links";
|
||||||
export * from "./nips";
|
export * from "./nips";
|
||||||
|
Loading…
Reference in New Issue
Block a user
Do you want any kind3 to be added to the graph? I had it setup so that only follows kind3 will be added
Yes, I don't think it adds too much overhead.