From 1c781a997820d031107332c41edefaa039e46871 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 26 Oct 2025 20:09:12 -0400 Subject: [PATCH] Add subscriptions tab to profile modal --- components/profile-modal.html | 27 +++ js/ui/profileModalController.js | 313 +++++++++++++++++++++++++++++++- 2 files changed, 336 insertions(+), 4 deletions(-) diff --git a/components/profile-modal.html b/components/profile-modal.html index 9c609603..84b49813 100644 --- a/components/profile-modal.html +++ b/components/profile-modal.html @@ -109,6 +109,15 @@ > Hashtags + + Subscriptions + + + + These are the creators you’ve subscribed to directly in bitvid. + + + You haven’t subscribed to any creators yet. + + + + { + if (!hex) { + return; + } + normalizedEntries.push({ hex, label }); + }; + + sourceEntries.forEach((entry) => { + if (!entry) { + return; + } + + if (typeof entry === "string") { + const trimmed = entry.trim(); + if (!trimmed) { + return; + } + + if (trimmed.startsWith("npub1")) { + const decoded = this.safeDecodeNpub(trimmed); + if (!decoded) { + return; + } + const label = this.safeEncodeNpub(decoded) || trimmed; + pushEntry(decoded, label); + return; + } + + if (/^[0-9a-f]{64}$/i.test(trimmed)) { + pushEntry(trimmed.toLowerCase(), trimmed); + } + return; + } + + if (typeof entry !== "object") { + return; + } + + const candidateHex = + typeof entry.pubkey === "string" ? entry.pubkey.trim() : ""; + if (candidateHex && /^[0-9a-f]{64}$/i.test(candidateHex)) { + pushEntry(candidateHex.toLowerCase(), candidateHex); + return; + } + + const candidateNpub = + typeof entry.npub === "string" ? entry.npub.trim() : ""; + if (candidateNpub && candidateNpub.startsWith("npub1")) { + const decoded = this.safeDecodeNpub(candidateNpub); + if (!decoded) { + return; + } + const label = this.safeEncodeNpub(decoded) || candidateNpub; + pushEntry(decoded, label); + } + }); + + const deduped = []; + const seenHex = new Set(); + normalizedEntries.forEach((entry) => { + if (!seenHex.has(entry.hex)) { + seenHex.add(entry.hex); + deduped.push(entry); + } + }); + + this.subscriptionList.innerHTML = ""; + + if (!deduped.length) { + this.subscriptionListEmpty.classList.remove("hidden"); + this.subscriptionList.classList.add("hidden"); + return; + } + + this.subscriptionListEmpty.classList.add("hidden"); + this.subscriptionList.classList.remove("hidden"); + + const formatNpub = + typeof this.formatShortNpub === "function" + ? (value) => this.formatShortNpub(value) + : (value) => (typeof value === "string" ? value : ""); + const entriesNeedingFetch = new Set(); + + deduped.forEach(({ hex, label }) => { + const item = document.createElement("li"); + item.className = "card flex items-center justify-between gap-4 p-4"; + + let cachedProfile = null; + if (hex && typeof this.services.getProfileCacheEntry === "function") { + const cacheEntry = this.services.getProfileCacheEntry(hex); + cachedProfile = cacheEntry?.profile || null; + if (!cacheEntry) { + entriesNeedingFetch.add(hex); + } + } + + const encodedNpub = + hex && typeof this.safeEncodeNpub === "function" + ? this.safeEncodeNpub(hex) + : label; + const displayNpub = formatNpub(encodedNpub) || encodedNpub || label; + const displayName = + (cachedProfile?.name && cachedProfile.name.trim()) || + (cachedProfile?.display_name && + typeof cachedProfile.display_name === "string" && + cachedProfile.display_name.trim()) || + displayNpub || + "Subscription"; + const avatarSrc = cachedProfile?.picture || FALLBACK_PROFILE_AVATAR; + + const summary = this.createCompactProfileSummary({ + displayName, + displayNpub, + avatarSrc, + }); + + const actions = document.createElement("div"); + actions.className = "flex flex-wrap items-center justify-end gap-2"; + + const viewButton = this.createViewChannelButton({ + targetNpub: encodedNpub, + displayNpub, + }); + if (viewButton) { + actions.appendChild(viewButton); + } + + const copyButton = this.createCopyNpubButton({ + targetNpub: encodedNpub, + displayNpub, + }); + if (copyButton) { + actions.appendChild(copyButton); + } + + if (hex) { + const unsubscribeButton = this.createRemoveButton({ + label: "Unsubscribe", + onRemove: () => this.handleUnsubscribeFromCreator(hex), + }); + if (unsubscribeButton) { + unsubscribeButton.dataset.subscriptionHex = hex; + actions.appendChild(unsubscribeButton); + } + } + + item.appendChild(summary); + if (actions.childElementCount > 0) { + item.appendChild(actions); + } + + this.subscriptionList.appendChild(item); + }); + + if ( + entriesNeedingFetch.size && + typeof this.services.batchFetchProfiles === "function" + ) { + try { + this.services.batchFetchProfiles(entriesNeedingFetch); + } catch (error) { + devLogger.warn( + "[profileModal] Failed to batch fetch profiles for subscriptions list:", + error, + ); + } + } + } catch (error) { + devLogger.warn("[profileModal] Failed to populate subscriptions list:", error); + } + } + + clearSubscriptionsList() { + if (this.subscriptionList instanceof HTMLElement) { + this.subscriptionList.innerHTML = ""; + this.subscriptionList.classList.add("hidden"); + } + + if (this.subscriptionListEmpty instanceof HTMLElement) { + this.subscriptionListEmpty.classList.remove("hidden"); + } + } + + async handleUnsubscribeFromCreator(candidate) { + const refresh = async () => { + try { + await this.populateSubscriptionsList(); + } catch (error) { + devLogger.warn( + "[profileModal] Failed to refresh subscriptions list after unsubscribe:", + error, + ); + } + + try { + await this.populateFriendsList(); + } catch (error) { + devLogger.warn( + "[profileModal] Failed to refresh friends list after unsubscribe:", + error, + ); + } + }; + + return this.handleRemoveFriend(candidate, { + successMessage: "You unsubscribed from this creator.", + refresh, + successReason: "unsubscribed", + }); + } + async populateFriendsList(friends = null) { if ( !(this.friendList instanceof HTMLElement) || @@ -3994,7 +4276,7 @@ export class ProfileModalController { } } - async handleRemoveFriend(candidate) { + async handleRemoveFriend(candidate, options = {}) { if ( this.moderationService && (!this.subscriptionsService || @@ -4021,6 +4303,14 @@ export class ProfileModalController { return { success: false, reason: "no-active-pubkey" }; } + const { + successMessage = "Creator removed from your friends list.", + refresh = async () => { + await this.populateFriendsList(); + }, + successReason = "removed", + } = typeof options === "object" && options ? options : {}; + let targetHex = ""; if (typeof candidate === "string") { const trimmed = candidate.trim(); @@ -4067,9 +4357,20 @@ export class ProfileModalController { return { success: false, reason: error?.code || "service-error", error }; } - this.showSuccess("Creator removed from your friends list."); - await this.populateFriendsList(); - return { success: true, reason: "removed" }; + this.showSuccess(successMessage); + + if (typeof refresh === "function") { + try { + await refresh(); + } catch (error) { + devLogger.warn( + "[profileModal] Failed to refresh lists after removing friend:", + error, + ); + } + } + + return { success: true, reason: successReason }; } async handleAddBlockedCreator() { @@ -7012,6 +7313,7 @@ export class ProfileModalController { } this.populateBlockedList(); + void this.populateSubscriptionsList(); void this.populateFriendsList(); this.populateProfileRelays(); this.refreshWalletPaneState(); @@ -7020,6 +7322,7 @@ export class ProfileModalController { postLoginPromise .then(() => { this.populateBlockedList(); + void this.populateSubscriptionsList(); void this.populateFriendsList(); this.populateProfileRelays(); this.refreshWalletPaneState(); @@ -7069,6 +7372,7 @@ export class ProfileModalController { } this.populateBlockedList(); + this.clearSubscriptionsList(); this.clearFriendsList(); this.populateProfileRelays(); this.refreshWalletPaneState(); @@ -7101,6 +7405,7 @@ export class ProfileModalController { } this.renderSavedProfiles(); + void this.populateSubscriptionsList(); } removeSavedProfile(pubkey) {
+ These are the creators you’ve subscribed to directly in bitvid. +