map pubkeys to internal user ids

This commit is contained in:
Martti Malmi 2023-07-23 10:23:55 +03:00
parent 6b386d413f
commit 296160bab3
15 changed files with 227 additions and 99 deletions

View File

@ -3,6 +3,7 @@ import { Helmet } from 'react-helmet';
import { Router, RouterOnChangeArgs } from 'preact-router';
import Footer from './components/Footer';
import Show from './components/helpers/Show';
import MediaPlayer from './components/MediaPlayer';
import Menu from './components/Menu';
import Modal from './components/modal/Modal';
@ -107,7 +108,9 @@ class Main extends Component<Props, ReactState> {
return (
<div className="flex justify-center">
<section className="flex w-full max-w-screen-xl justify-between relative">
{s.loggedIn ? <Menu /> : null}
<Show when={s.loggedIn}>
<Menu />
</Show>
<Helmet titleTemplate={titleTemplate} defaultTitle={defaultTitle}>
<title>{title}</title>
<meta name="description" content="Connecting People" />
@ -153,7 +156,7 @@ class Main extends Component<Props, ReactState> {
<Footer />
</section>
{this.state.showLoginModal && (
<Show when={s.showLoginModal}>
<Modal
centerVertically={true}
showContainer={true}
@ -161,7 +164,7 @@ class Main extends Component<Props, ReactState> {
>
<Login />
</Modal>
)}
</Show>
</div>
);
}

View File

@ -97,7 +97,7 @@ class MyAvatar extends Component<Props, State> {
this.state.picture &&
!this.state.hasError &&
!this.props.hidePicture &&
!SocialNetwork.blockedUsers.has(this.props.str as string);
!SocialNetwork.isBlocked(this.props.str as string);
const hasPictureStyle = hasPicture ? 'has-picture' : '';
const showTooltip = this.props.showTooltip ? 'tooltip' : '';

View File

@ -18,7 +18,7 @@ export default function Badge(props): JSX.Element | null {
if (!hexAddress) {
return null;
}
const following = SocialNetwork.followedByUser.get(myPub)?.has(hexAddress);
const following = SocialNetwork.isFollowing(myPub, hexAddress);
if (following) {
return (
<span class="mx-2 text-iris-blue tooltip" data-tip={t('following')}>

View File

@ -3,6 +3,7 @@ import { memo, useEffect, useState } from 'react';
import AnimalName from '../AnimalName';
import Key from '../nostr/Key';
import SocialNetwork from '../nostr/SocialNetwork';
import { ID } from '../nostr/UserIds';
import Badge from './Badge';
@ -26,7 +27,7 @@ const Name = (props: Props) => {
let displayName;
let isNameGenerated = false;
const profile = SocialNetwork.profiles.get(nostrAddr);
const profile = SocialNetwork.profiles.get(ID(nostrAddr));
// should we change SocialNetwork.getProfile() and use it here?
if (profile) {
name = profile.name?.trim().slice(0, 100) || '';

View File

@ -142,7 +142,7 @@ const EventComponent = (props: EventComponentProps) => {
);
}
if (SocialNetwork.blockedUsers.has(state.event.pubkey)) {
if (SocialNetwork.isBlocked(state.event.pubkey)) {
if (props.standalone || props.isQuote) {
return (
<div className="msg">

View File

@ -8,6 +8,7 @@ import Events from '../../nostr/Events';
import Key from '../../nostr/Key';
import PubSub, { Unsubscribe } from '../../nostr/PubSub';
import SocialNetwork from '../../nostr/SocialNetwork';
import { ID } from '../../nostr/UserIds';
import { translate as t } from '../../translations/Translation.mjs';
import Show from '../helpers/Show';
@ -188,7 +189,7 @@ class Feed extends BaseComponent<FeedProps, FeedState> {
if (results.has(event.id)) {
return;
}
if (SocialNetwork.blockedUsers.has(event.pubkey)) {
if (SocialNetwork.isBlocked(event.pubkey)) {
return;
}
const repliedMsg = Events.getEventReplyingTo(event);
@ -197,7 +198,7 @@ class Feed extends BaseComponent<FeedProps, FeedState> {
return;
}
const author = Events.db.by('id', repliedMsg)?.pubkey;
if (author && SocialNetwork.blockedUsers.has(author)) {
if (author && SocialNetwork.isBlocked(author)) {
return;
}
}
@ -218,10 +219,11 @@ class Feed extends BaseComponent<FeedProps, FeedState> {
});
} else {
this.unsub = this.getEvents(callback);
const followCount = SocialNetwork.followedByUser.get(Key.getPubKey())?.size;
const myId = ID(Key.getPubKey());
const followCount = SocialNetwork.followedByUser.get(myId)?.size;
const unsub = PubSub.subscribe({ authors: [Key.getPubKey()], kinds: [3] }, () => {
// is this needed?
if (followCount !== SocialNetwork.followedByUser.get(Key.getPubKey())?.size) {
if (followCount !== SocialNetwork.followedByUser.get(myId)?.size) {
unsub();
this.subscribe();
}

View File

@ -2,6 +2,7 @@ import Events from '../../nostr/Events';
import Key from '../../nostr/Key';
import PubSub from '../../nostr/PubSub';
import SocialNetwork from '../../nostr/SocialNetwork';
import { ID, PUB } from '../../nostr/UserIds';
import BaseFeed from './BaseFeed';
@ -65,7 +66,9 @@ class Feed extends BaseFeed {
subscribeToFollows(since, callback) {
const myPub = Key.getPubKey();
const followedUsers = Array.from(SocialNetwork.followedByUser.get(myPub) || []);
const followedUsers = Array.from(SocialNetwork.followedByUser.get(ID(myPub)) || []).map(
(user) => PUB(user),
);
followedUsers.push(myPub);
const filter = {
kinds: [1, 6],
@ -79,7 +82,7 @@ class Feed extends BaseFeed {
return PubSub.subscribe(
filter,
(e) => {
if (e.pubkey === myPub || SocialNetwork.followedByUser.get(myPub)?.has(e.pubkey)) {
if (e.pubkey === myPub || SocialNetwork.isFollowing(myPub, e.pubkey)) {
callback(e);
}
},

View File

@ -15,6 +15,7 @@ import Relays from './Relays';
import Session from './Session';
import SocialNetwork from './SocialNetwork';
import SortedLimitedEventSet from './SortedLimitedEventSet';
import { ID, PUB, UserId, UserIds } from './UserIds';
const startTime = Date.now() / 1000;
@ -27,6 +28,13 @@ const events = db.addCollection('events', {
unique: ['id'],
});
// print event db size
/*
setInterval(() => {
console.log('event db size', events.count());
}, 1000);
*/
// add collections for Tags and Users?
let mutedNotes;
@ -52,7 +60,7 @@ localState.get('dev').on((d) => {
const Events = {
DEFAULT_GLOBAL_FILTER,
getEventHash,
db: events,
db: events, // TODO gb2 own indexing with Maps. easier to evict & understand what it actually does
eventsMetaDb: new EventMetaStore(),
seen: new Set<string>(),
deletedEvents: new Set<string>(),
@ -102,7 +110,7 @@ const Events = {
if (
event.created_at > startTime ||
event.pubkey === myPub ||
SocialNetwork.followedByUser.get(myPub)?.has(event.pubkey)
SocialNetwork.isFollowing(myPub, event.pubkey)
) {
//Events.getEventById(id); // generates lots of subscriptions
}
@ -180,7 +188,7 @@ const Events = {
this.insert(event);
const myPub = Key.getPubKey();
if (event.pubkey === myPub || SocialNetwork.followedByUser.get(myPub)?.has(event.pubkey)) {
if (event.pubkey === myPub || SocialNetwork.isFollowing(myPub, event.pubkey)) {
LocalForage.loaded && LocalForage.saveProfilesAndFollows();
}
@ -190,22 +198,25 @@ const Events = {
const pub = tag[1];
// ensure pub is hex
if (pub.length === 64 && /^[0-9a-f]+$/.test(pub)) {
SocialNetwork.addFollower(tag[1], event.pubkey);
SocialNetwork.addFollower(ID(tag[1]), ID(event.pubkey));
} else {
// console.error('non-hex follow tag', tag, 'by', event.pubkey);
}
}
}
}
if (SocialNetwork.followedByUser.has(event.pubkey)) {
for (const previouslyFollowed of SocialNetwork.followedByUser.get(event.pubkey) || []) {
if (!event.tags || !event.tags?.find((t) => t[0] === 'p' && t[1] === previouslyFollowed)) {
SocialNetwork.removeFollower(previouslyFollowed, event.pubkey);
if (SocialNetwork.followedByUser.has(ID(event.pubkey))) {
for (const previouslyFollowed of SocialNetwork.followedByUser.get(ID(event.pubkey)) || []) {
if (
!event.tags ||
!event.tags?.find((t) => t[0] === 'p' && t[1] === PUB(previouslyFollowed))
) {
SocialNetwork.removeFollower(previouslyFollowed, ID(event.pubkey));
}
}
}
if (event.pubkey === myPub && event.tags?.length) {
if ((SocialNetwork.followedByUser.get(myPub)?.size || 0) > 10) {
if ((SocialNetwork.followedByUser.get(ID(myPub))?.size || 0) > 10) {
localState.get('showFollowSuggestions').put(false);
}
}
@ -241,7 +252,11 @@ const Events = {
try {
content = await Key.decrypt(event.content);
const blockList = JSON.parse(content);
SocialNetwork.blockedUsers = new Set(blockList);
const set = new Set<UserId>();
for (const id of blockList) {
set.add(ID(id));
}
SocialNetwork.blockedUsers = set;
} catch (e) {
console.log('failed to parse your block list', content, event);
}
@ -275,7 +290,7 @@ const Events = {
return;
}
try {
const existing = SocialNetwork.profiles.get(event.pubkey);
const existing = SocialNetwork.profiles.get(ID(event.pubkey));
if (existing?.created_at >= event.created_at) {
return false;
}
@ -291,13 +306,13 @@ const Events = {
}
profile.created_at = event.created_at;
delete profile['nip05valid']; // not robust
SocialNetwork.profiles.set(event.pubkey, profile);
SocialNetwork.profiles.set(ID(event.pubkey), profile);
const key = Key.toNostrBech32Address(event.pubkey, 'npub');
FuzzySearch.add({
key,
name: profile.name,
display_name: profile.display_name,
followers: SocialNetwork.followersByUser.get(event.pubkey) ?? new Set(),
followers: SocialNetwork.followersByUser.get(ID(event.pubkey)) ?? new Set(),
});
//}
} catch (e) {
@ -372,8 +387,12 @@ const Events = {
if (globalFilter.maxFollowDistance && !!Key.getPubKey()) {
if (!PubSub.subscribedAuthors.has(event.pubkey) && !PubSub.subscribedEventIds.has(event.id)) {
// unless we specifically subscribed to the user or post, ignore long follow distance users
if (SocialNetwork.followDistanceByUser.has(event.pubkey)) {
const distance = SocialNetwork.followDistanceByUser.get(event.pubkey);
if (!event.pubkey) {
console.log('what', event);
return false;
}
if (UserIds.has(event.pubkey) && SocialNetwork.getFollowDistance(event.pubkey)) {
const distance = SocialNetwork.followDistanceByUser.get(ID(event.pubkey));
if (distance && distance > globalFilter.maxFollowDistance) {
// follow distance too high, reject
return false;
@ -381,7 +400,7 @@ const Events = {
if (distance === globalFilter.maxFollowDistance) {
// require at least 5 followers
// TODO followers should be follow distance 2
const followerCount = SocialNetwork.followersByUser.get(event.pubkey)?.size || 0;
const followerCount = SocialNetwork.followersByUser.get(ID(event.pubkey))?.size || 0;
if (
followerCount <
(globalFilter.minFollowersAtMaxDistance ||
@ -449,7 +468,7 @@ const Events = {
}
// Accepting metadata so we still get their name. But should we instead save the name on our own list?
// They might spam with 1 MB events and keep changing their name or something.
if (SocialNetwork.blockedUsers.has(event.pubkey) && event.kind !== 0) {
if (SocialNetwork.isBlocked(event.pubkey) && event.kind !== 0) {
return false;
}
if (this.deletedEvents.has(event.id)) {
@ -535,7 +554,7 @@ const Events = {
// TODO since we're only querying relays since lastSeen, we need to store all beforeseen events and correctly query them on demand
// otherwise feed will be incomplete
if (saveToIdb) {
const followDistance = SocialNetwork.followDistanceByUser.get(event.pubkey) || Infinity;
const followDistance = SocialNetwork.followDistanceByUser.get(ID(event.pubkey)) || Infinity;
if (followDistance <= 1) {
// save all our own events and events from people we follow
if (dev.indexedDbSave !== false) {
@ -616,8 +635,8 @@ const Events = {
if (event.pubkey !== myPub && event.tags?.some((tag) => tag[0] === 'p' && tag[1] === myPub)) {
if (event.kind === 3) {
// only notify if we know that they previously weren't following us
const existingFollows = SocialNetwork.followedByUser.get(event.pubkey);
if (!existingFollows || existingFollows.has(myPub)) {
const existingFollows = SocialNetwork.followedByUser.get(ID(event.pubkey));
if (!existingFollows || existingFollows.has(ID(myPub))) {
return;
}
}

View File

@ -78,11 +78,11 @@ export default {
return false;
}
},
getPubKey() {
return this.key?.rpub;
getPubKey(): string {
return this.key?.rpub || '';
},
getPrivKey() {
return this.key?.priv;
getPrivKey(): string {
return this.key?.priv || '';
},
encrypt: async function (data: string, pub?: string): Promise<string> {
const k = this.key;

View File

@ -17,7 +17,7 @@ const getLatestByFollows = () => {
latestByFollows.applyFind({ kind: 1 });
latestByFollows.applySimpleSort('created_at', { desc: true });
latestByFollows.applyWhere((event: Event) => {
const distance = SocialNetwork.followDistanceByUser.get(event.pubkey) || Infinity;
const distance = SocialNetwork.getFollowDistance(event.pubkey);
return distance <= 1;
});
return latestByFollows;
@ -73,7 +73,7 @@ export default {
...Events.db.find({ kind: 0 }),
];
const followEvents = Events.db.find({ kind: 3 }).filter((e: Event) => {
return e.pubkey === myPub || SocialNetwork.followedByUser.get(myPub)?.has(e.pubkey);
return e.pubkey === myPub || SocialNetwork.isFollowing(myPub, e.pubkey);
});
const followEvents2 = [Events.db.findOne({ kind: 3, pubkey: myPub })];
let size = 0;

View File

@ -13,6 +13,7 @@ import { Path } from './path';
import PubSub from './PubSub';
import Relays from './Relays';
import SocialNetwork from './SocialNetwork';
import { ID } from './UserIds';
try {
localStorage.setItem('gunPeers', JSON.stringify({})); // quick fix to not connect gun
@ -53,9 +54,10 @@ const Session = {
},
onLoggedIn() {
const myPub = Key.getPubKey();
SocialNetwork.followDistanceByUser.set(myPub, 0);
SocialNetwork.followersByUser.set(myPub, new Set());
SocialNetwork.usersByFollowDistance.set(0, new Set([myPub]));
const myId = ID(myPub);
SocialNetwork.followDistanceByUser.set(myId, 0);
SocialNetwork.followersByUser.set(myId, new Set());
SocialNetwork.usersByFollowDistance.set(0, new Set([myId]));
const subscribe = (filters: Filter[], callback: (event: Event) => void): string => {
const filter = filters[0];
const key = filter['#d']?.[0];

View File

@ -4,26 +4,49 @@ import Events from './Events';
import Key from './Key';
import LocalForage from './LocalForage';
import PubSub, { Unsubscribe } from './PubSub';
import { ID, PUB, UserId } from './UserIds';
export default {
followDistanceByUser: new Map<string, number>(),
usersByFollowDistance: new Map<number, Set<string>>(),
profiles: new Map<string, any>(), // JSON.parsed event.content of profiles
followedByUser: new Map<string, Set<string>>(),
followersByUser: new Map<string, Set<string>>(),
blockedUsers: new Set<string>(),
flaggedUsers: new Set<string>(),
followDistanceByUser: new Map<UserId, number>(),
usersByFollowDistance: new Map<number, Set<UserId>>(),
profiles: new Map<UserId, any>(), // JSON.parsed event.content of profile events
followedByUser: new Map<UserId, Set<UserId>>(),
followersByUser: new Map<UserId, Set<UserId>>(),
blockedUsers: new Set<UserId>(),
flaggedUsers: new Set<UserId>(),
isFollowing: function (follower: string, followedUser: string): boolean {
const followedUserId = ID(followedUser);
const followerId = ID(follower);
return !!this.followedByUser.get(followerId)?.has(followedUserId);
},
isBlocked: function (blockedUser: string): boolean {
const blockedUserId = ID(blockedUser);
return this.blockedUsers.has(blockedUserId);
},
getFollowDistance: function (user: string): number {
const userId = ID(user);
return this.followDistanceByUser.get(userId) || Infinity;
},
setFollowed: function (followedUsers: string | string[], follow = true) {
const myPub = Key.getPubKey();
const myId = ID(myPub);
if (typeof followedUsers === 'string') {
followedUsers = [followedUsers];
}
const myPub = Key.getPubKey();
followedUsers.forEach((followedUser) => {
followedUser = Key.toNostrHexAddress(followedUser) || '';
const followedUserId = ID(followedUser);
if (follow && followedUser && followedUser !== myPub) {
this.addFollower(followedUser, myPub);
this.addFollower(followedUserId, myId);
} else {
this.removeFollower(followedUser, myPub);
this.removeFollower(followedUserId, myId);
}
});
@ -32,8 +55,9 @@ export default {
const event = {
kind: 3,
content: existing?.content || '',
tags: Array.from(this.followedByUser.get(myPub) || []).map((address: string) => {
return ['p', address];
tags: Array.from(this.followedByUser.get(myId) || []).map((id: number) => {
const pubAddress = PUB(id);
return ['p', pubAddress];
}),
};
@ -41,19 +65,19 @@ export default {
},
setBlocked: function (blockedUser: string, block = true) {
blockedUser = Key.toNostrHexAddress(blockedUser) || '';
const myPub = Key.getPubKey();
const blockedUserId = ID(blockedUser);
const myId = ID(Key.getPubKey());
if (block) {
this.blockedUsers.add(blockedUser);
this.removeFollower(blockedUser, myPub);
this.blockedUsers.add(blockedUserId);
this.removeFollower(blockedUserId, myId);
Events.directMessagesByUser.delete(blockedUser);
} else {
this.blockedUsers.delete(blockedUser);
this.blockedUsers.delete(blockedUserId);
}
},
addUserByFollowDistance(distance: number, user: string) {
addUserByFollowDistance(distance: number, user: UserId) {
if (!this.usersByFollowDistance.has(distance)) {
this.usersByFollowDistance.set(distance, new Set());
}
@ -61,7 +85,7 @@ export default {
let unsub;
// TODO subscribe once param?
// eslint-disable-next-line prefer-const
unsub = PubSub.subscribe({ authors: [user], kinds: [0] }, () => unsub?.(), true);
unsub = PubSub.subscribe({ authors: [PUB(user)], kinds: [0] }, () => unsub?.(), true);
}
this.usersByFollowDistance.get(distance)?.add(user);
// remove from higher distances
@ -72,30 +96,24 @@ export default {
}
},
addFollower: function (followedUser: string, follower: string) {
if (followedUser.startsWith('npub')) {
console.error('addFollower: followedUser is not a hex address', followedUser);
followedUser = Key.toNostrHexAddress(followedUser) || '';
addFollower: function (followedUser: UserId, follower: UserId) {
if (typeof followedUser !== 'number' || typeof follower !== 'number') {
throw new Error('Invalid user id');
}
if (follower.startsWith('npub')) {
console.error('addFollower: follower is not a hex address', follower);
follower = Key.toNostrHexAddress(follower) || '';
}
if (!this.followersByUser.has(followedUser)) {
this.followersByUser.set(followedUser, new Set<string>());
this.followersByUser.set(followedUser, new Set<UserId>());
}
this.followersByUser.get(followedUser)?.add(follower);
if (!this.followedByUser.has(follower)) {
this.followedByUser.set(follower, new Set<string>());
this.followedByUser.set(follower, new Set<UserId>());
}
const myPub = Key.getPubKey();
const myId = ID(Key.getPubKey());
let newFollowDistance;
if (follower === myPub) {
if (follower === myId) {
// basically same as the next "else" block, but faster
if (followedUser === myPub) {
if (followedUser === myId) {
newFollowDistance = 0; // self-follow
} else {
newFollowDistance = 1;
@ -113,20 +131,20 @@ export default {
}
this.followedByUser.get(follower)?.add(followedUser);
if (followedUser === myPub) {
if (followedUser === myId) {
if (this.followersByUser.get(followedUser)?.size === 1) {
localState.get('hasNostrFollowers').put(true);
}
}
if (this.followedByUser.get(myPub)?.has(follower)) {
if (!PubSub.subscribedAuthors.has(followedUser)) {
if (this.followedByUser.get(myId)?.has(follower)) {
if (!PubSub.subscribedAuthors.has(PUB(followedUser))) {
setTimeout(() => {
PubSub.subscribe({ authors: [followedUser] }, undefined, true);
PubSub.subscribe({ authors: [PUB(followedUser)] }, undefined, true);
}, 0);
}
}
},
removeFollower: function (unfollowedUser: string, follower: string) {
removeFollower: function (unfollowedUser: UserId, follower: UserId) {
this.followersByUser.get(unfollowedUser)?.delete(follower);
this.followedByUser.get(follower)?.delete(unfollowedUser);
@ -154,26 +172,32 @@ export default {
// if resulting followersByUser(u).size is 0, remove that user as well
this.followDistanceByUser.delete(unfollowedUser);
this.followersByUser.delete(unfollowedUser);
PubSub.subscribedAuthors.delete(unfollowedUser);
PubSub.subscribedAuthors.delete(PUB(unfollowedUser));
}
LocalForage.saveEvents();
},
// TODO subscription methods for followersByUser and followedByUser. and maybe messagesByTime. and replies
followerCount: function (address: string) {
return this.followersByUser.get(address)?.size ?? 0;
const id = ID(address);
return this.followersByUser.get(id)?.size ?? 0;
},
followedByFriendsCount: function (address: string) {
let count = 0;
const myPub = Key.getPubKey();
for (const follower of this.followersByUser.get(address) ?? []) {
if (this.followedByUser.get(myPub)?.has(follower)) {
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;
},
block: async function (address: string, isBlocked: boolean) {
isBlocked ? this.blockedUsers.add(address) : this.blockedUsers.delete(address);
if (isBlocked) {
this.blockedUsers.add(ID(address));
} else {
this.blockedUsers.delete(ID(address));
}
let content: any = JSON.stringify(Array.from(this.blockedUsers));
content = await Key.encrypt(content);
Events.publish({
@ -184,7 +208,11 @@ export default {
});
},
flag: function (address: string, isFlagged: boolean) {
isFlagged ? this.flaggedUsers.add(address) : this.flaggedUsers.delete(address);
if (isFlagged) {
this.flaggedUsers.add(ID(address));
} else {
this.flaggedUsers.delete(ID(address));
}
Events.publish({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
@ -194,7 +222,13 @@ export default {
},
getBlockedUsers(cb?: (blocked: Set<string>) => void): Unsubscribe {
const callback = () => {
cb?.(this.blockedUsers);
if (cb) {
const set = new Set<string>();
for (const id of this.blockedUsers) {
set.add(PUB(id));
}
cb(set);
}
};
callback();
const myPub = Key.getPubKey();
@ -204,7 +238,13 @@ export default {
},
getFlaggedUsers(cb?: (flagged: Set<string>) => void): Unsubscribe {
const callback = () => {
cb?.(this.flaggedUsers);
if (cb) {
const set = new Set<string>();
for (const id of this.flaggedUsers) {
set.add(PUB(id));
}
cb(set);
}
};
callback();
const myPub = Key.getPubKey();
@ -216,33 +256,48 @@ export default {
user: string,
cb?: (followedUsers: Set<string>) => void,
): Unsubscribe {
const userId = ID(user);
const callback = () => {
cb?.(this.followedByUser.get(user) ?? new Set());
if (cb) {
const set = new Set<string>();
for (const id of this.followedByUser.get(userId) || []) {
set.add(PUB(id));
}
cb(set);
}
};
this.followedByUser.has(user) && callback();
this.followedByUser.has(userId) && callback();
return PubSub.subscribe({ kinds: [3], authors: [user] }, callback);
},
getFollowersByUser: function (
address: string,
cb?: (followers: Set<string>) => void,
): Unsubscribe {
const userId = ID(address);
const callback = () => {
cb?.(this.followersByUser.get(address) ?? new Set());
if (cb) {
const set = new Set<string>();
for (const id of this.followersByUser.get(userId) || []) {
set.add(PUB(id));
}
cb(set);
}
};
this.followersByUser.has(address) && callback();
this.followersByUser.has(userId) && callback();
return PubSub.subscribe({ kinds: [3], '#p': [address] }, callback); // TODO this doesn't fire when a user is unfollowed
},
// TODO param "proxyFirst" to skip relays if http proxy responds quickly
getProfile(
address,
address: string,
cb?: (profile: any, address: string) => void,
verifyNip05 = false,
): Unsubscribe {
const id = ID(address);
const callback = () => {
cb?.(this.profiles.get(address), address);
cb?.(this.profiles.get(id), address);
};
const profile = this.profiles.get(address);
const profile = this.profiles.get(id);
// TODO subscribe & callback
if (profile) {
callback();
@ -250,7 +305,7 @@ export default {
Key.verifyNip05Address(profile.nip05, address).then((isValid) => {
console.log('NIP05 address is valid?', isValid, profile.nip05, address);
profile.nip05valid = isValid;
this.profiles.set(address, profile);
this.profiles.set(id, profile);
callback();
});
}

43
src/js/nostr/UserIds.ts Normal file
View File

@ -0,0 +1,43 @@
import Key from './Key';
// should this be a class instead? convert all strings to internal representation, enable comparison
export type UserId = number;
// save space by mapping pubkeys to internal user ids
export class UserIds {
static pubKeyToUserId = new Map<string, number>();
static UserIdToPubKey = new Map<number, string>();
static currentUserId = 0;
static id(pubKey: string): number {
if (pubKey.startsWith('npub')) {
pubKey = Key.toNostrHexAddress(pubKey) || '';
if (!pubKey) {
throw new Error('addFollower: invalid pubKey ' + pubKey);
}
}
const existing = UserIds.pubKeyToUserId.get(pubKey);
if (existing) {
return existing;
}
const newId = UserIds.currentUserId++;
UserIds.pubKeyToUserId.set(pubKey, newId);
UserIds.UserIdToPubKey.set(newId, pubKey);
return newId;
}
static pub(id: number): string {
const pub = UserIds.UserIdToPubKey.get(id);
if (!pub) {
throw new Error('pub: invalid id ' + id);
}
return pub;
}
static has(pubKey: string): boolean {
return UserIds.pubKeyToUserId.has(pubKey);
}
}
export const PUB = UserIds.pub;
export const ID = UserIds.id;

View File

@ -226,7 +226,7 @@ class Profile extends View {
<b>${this.state.followerCount}</b> ${t('followers')}
</a>
</div>
${SocialNetwork.followedByUser.get(this.state.hexPub)?.has(Key.getPubKey())
${SocialNetwork.isFollowing(this.state.hexPub, Key.getPubKey())
? html` <div><small>${t('follows_you')}</small></div> `
: ''}
</div>

View File

@ -117,7 +117,7 @@ class PrivateChat extends PureComponent<PrivateChatProps, PrivateChatState> {
if (
this.chat &&
!this.chat.uuid &&
Key.toNostrHexAddress(this.props.id) !== Key.getPubKey()()
Key.toNostrHexAddress(this.props.id) !== Key.getPubKey()
) {
if ($('.msg.our').length && !$('.msg.their').length && !this.chat.theirMsgsLastSeenTime) {
$('#not-seen-by-them').slideDown();