From dfafd39c32d4f224dcb14fa85b86a49bfc6b9645 Mon Sep 17 00:00:00 2001 From: Milad Raeisi Date: Wed, 2 Oct 2024 12:31:57 +0400 Subject: [PATCH] Add follow & unfollow functions --- .../components/profile/profile.component.ts | 53 +++++--- src/app/services/social.service.ts | 128 +++++++++++++++++- 2 files changed, 159 insertions(+), 22 deletions(-) diff --git a/src/app/components/profile/profile.component.ts b/src/app/components/profile/profile.component.ts index e7418b4..cff1415 100644 --- a/src/app/components/profile/profile.component.ts +++ b/src/app/components/profile/profile.component.ts @@ -60,8 +60,6 @@ export class ProfileComponent implements OnInit, OnDestroy { isCurrentUserProfile: Boolean=false; isFollowing = false; - - constructor( private _changeDetectorRef: ChangeDetectorRef, private _metadataService: MetadataService, @@ -77,18 +75,15 @@ export class ProfileComponent implements OnInit, OnDestroy { this._route.paramMap.subscribe((params) => { const routePubKey = params.get('pubkey'); + this.routePubKey= routePubKey; const userPubKey = this._signerService.getPublicKey(); - this.isCurrentUserProfile = routePubKey === userPubKey; - const pubKeyToLoad = routePubKey || userPubKey; this.loadProfile(pubKeyToLoad); - if (!routePubKey) { this.isCurrentUserProfile = true; } - - this.loadCurrentUserProfile(); + this.loadCurrentUserProfile(); }); @@ -111,7 +106,6 @@ export class ProfileComponent implements OnInit, OnDestroy { }); } - this._socialService.getFollowersObservable() .pipe(takeUntil(this._unsubscribeAll)) .subscribe((event) => { @@ -137,7 +131,7 @@ export class ProfileComponent implements OnInit, OnDestroy { this._unsubscribeAll.complete(); } - private async loadProfile(publicKey: string): Promise { + async loadProfile(publicKey: string): Promise { this.isLoading = true; this.errorMessage = null; @@ -159,8 +153,12 @@ export class ProfileComponent implements OnInit, OnDestroy { this.metadata = metadata; this._changeDetectorRef.detectChanges(); } - await this._socialService.getFollowers(publicKey); - await this._socialService.getFollowing(publicKey); + + await this._socialService.getFollowers(publicKey); + const currentUserPubKey = this._signerService.getPublicKey(); + this.isFollowing = this.followers.includes(currentUserPubKey); + + await this._socialService.getFollowing(publicKey); this._metadataService.getMetadataStream() .pipe(takeUntil(this._unsubscribeAll)) @@ -181,8 +179,6 @@ export class ProfileComponent implements OnInit, OnDestroy { } } - - private async loadCurrentUserProfile(): Promise { try { this.currentUserMetadata = null; @@ -225,7 +221,32 @@ export class ProfileComponent implements OnInit, OnDestroy { return this._sanitizer.bypassSecurityTrustUrl(url); } - toggleFollow() { - this.isFollowing = !this.isFollowing; - } + async toggleFollow(): Promise { + try { + const userPubKey = this._signerService.getPublicKey(); + const routePubKey = this.routePubKey || this.userPubKey; + + if (!routePubKey || !userPubKey) { + console.error('Public key missing. Unable to toggle follow.'); + return; + } + + if (this.isFollowing) { + await this._socialService.unfollow(routePubKey); + console.log(`Unfollowed ${routePubKey}`); + } else { + await this._socialService.follow(routePubKey); + console.log(`Followed ${routePubKey}`); + } + + this.isFollowing = !this.isFollowing; + + this._changeDetectorRef.detectChanges(); + + } catch (error) { + console.error('Failed to toggle follow:', error); + } + } + + } diff --git a/src/app/services/social.service.ts b/src/app/services/social.service.ts index 2fe4c51..a9d0ed5 100644 --- a/src/app/services/social.service.ts +++ b/src/app/services/social.service.ts @@ -1,19 +1,20 @@ import { Injectable } from '@angular/core'; -import { Filter, NostrEvent } from 'nostr-tools'; +import { Filter, NostrEvent, UnsignedEvent, Event } from 'nostr-tools'; import { RelayService } from './relay.service'; +import { SignerService } from './signer.service'; import { Subject, Observable } from 'rxjs'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class SocialService { - private followersSubject = new Subject(); private followingSubject = new Subject(); constructor( - private relayService: RelayService - ) {} + private relayService: RelayService, + private signerService: SignerService + ) { } getFollowersObservable(): Observable { return this.followersSubject.asObservable(); @@ -23,6 +24,7 @@ export class SocialService { return this.followingSubject.asObservable(); } + // Fetch followers async getFollowers(pubkey: string): Promise { await this.relayService.ensureConnectedRelays(); const pool = this.relayService.getPool(); @@ -49,6 +51,7 @@ export class SocialService { }); } + // Fetch who the user is following async getFollowing(pubkey: string): Promise { await this.relayService.ensureConnectedRelays(); const pool = this.relayService.getPool(); @@ -66,7 +69,7 @@ export class SocialService { onevent: (event: NostrEvent) => { const tags = event.tags.filter((tag) => tag[0] === 'p'); tags.forEach((tag) => { - following.push( tag[1] ); + following.push(tag[1]); this.followingSubject.next(event); }); }, @@ -77,4 +80,117 @@ export class SocialService { }); }); } + + // Follow a user + async follow(pubkeyToFollow: string): Promise { + await this.relayService.ensureConnectedRelays(); + const pool = this.relayService.getPool(); + const currentFollowing = this.getFollowingList(); + if (currentFollowing.includes(pubkeyToFollow)) { + console.log(`Already following ${pubkeyToFollow}`); + return; + } + + // Add the user to the following list + const newFollowingList = [...currentFollowing, pubkeyToFollow]; + this.setFollowingList(newFollowingList); + + const unsignedEvent: UnsignedEvent = this.signerService.getUnsignedEvent(3, newFollowingList.map(f => ['p', f]), ''); + + // Check if using Nostr extension + const isUsingExtension = await this.signerService.isUsingExtension(); + let signedEvent: Event; + + if (isUsingExtension) { + // Sign using Nostr extension + signedEvent = await this.signerService.signEventWithExtension(unsignedEvent); + } else { + // Sign using private key + const secretKey = await this.signerService.getDecryptedSecretKey(); + if (!secretKey) { + throw new Error('Secret key is missing. Unable to follow.'); + } + signedEvent = this.signerService.getSignedEvent(unsignedEvent, secretKey); + } + + // Publish the signed follow event + this.relayService.publishEventToRelays(signedEvent); + console.log(`Now following ${pubkeyToFollow}`); + } + + // Unfollow a user + async unfollow(pubkeyToUnfollow: string): Promise { + await this.relayService.ensureConnectedRelays(); + const currentFollowing = this.getFollowingList(); + if (!currentFollowing.includes(pubkeyToUnfollow)) { + console.log(`Not following ${pubkeyToUnfollow}`); + return; + } + + // Remove the user from the following list + const updatedFollowingList = currentFollowing.filter((pubkey) => pubkey !== pubkeyToUnfollow); + this.setFollowingList(updatedFollowingList); + + const unsignedEvent: UnsignedEvent = this.signerService.getUnsignedEvent(3, updatedFollowingList.map(f => ['p', f]), ''); + + // Check if using Nostr extension + const isUsingExtension = await this.signerService.isUsingExtension(); + let signedEvent: Event; + + if (isUsingExtension) { + // Sign using Nostr extension + signedEvent = await this.signerService.signEventWithExtension(unsignedEvent); + } else { + // Sign using private key + const secretKey = await this.signerService.getDecryptedSecretKey(); + if (!secretKey) { + throw new Error('Secret key is missing. Unable to unfollow.'); + } + signedEvent = this.signerService.getSignedEvent(unsignedEvent, secretKey); + } + + // Publish the signed unfollow event + this.relayService.publishEventToRelays(signedEvent); + console.log(`Unfollowed ${pubkeyToUnfollow}`); + } + + // Retrieve following list as tags for publishing follow/unfollow events + getFollowingListAsTags(): string[][] { + const following = this.getFollowingList(); + const tags: string[][] = []; + + const relays = this.relayService.getConnectedRelays(); + + following.forEach((f) => { + relays.forEach((relay) => { + tags.push(['p', f, relay, localStorage.getItem(`${f}`) || '']); + }); + }); + + return tags; + } + + + setFollowingListFromTags(tags: string[][]): void { + const following: string[] = []; + tags.forEach((t) => { + following.push(t[1]); + }); + this.setFollowingList(following); + } + + setFollowingList(following: string[]): void { + const followingSet = Array.from(new Set(following)); + const newFollowingList = followingSet.filter((s) => s).join(','); + localStorage.setItem('following', newFollowingList); + } + + getFollowingList(): string[] { + const followingRaw = localStorage.getItem('following'); + if (followingRaw === null || followingRaw === '') { + return []; + } + const following = followingRaw.split(','); + return following.filter((value) => /[a-f0-9]{64}/.test(value)); + } }