From 92962f3b3dcd857bfdf74497812d3e476a8d5f22 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 24 Oct 2025 15:54:25 -0400 Subject: [PATCH] Refine comment thread service wiring --- js/app.js | 651 ++++++++++++++++++++++++++ js/services/commentThreadService.js | 62 ++- tests/comment-thread-service.test.mjs | 14 +- 3 files changed, 707 insertions(+), 20 deletions(-) diff --git a/js/app.js b/js/app.js index 1408b97b..3033d980 100644 --- a/js/app.js +++ b/js/app.js @@ -47,6 +47,7 @@ import getAuthProvider, { import NwcSettingsService from "./services/nwcSettingsService.js"; import nostrService from "./services/nostrService.js"; import DiscussionCountService from "./services/discussionCountService.js"; +import CommentThreadService from "./services/commentThreadService.js"; import { initQuickR2Upload } from "./r2-quick.js"; import { createWatchHistoryRenderer } from "./historyView.js"; import WatchHistoryController from "./ui/watchHistoryController.js"; @@ -236,6 +237,11 @@ class Application { this.boundVideoModalCreatorHandler = null; this.boundVideoModalZapHandler = null; this.boundVideoModalZapWalletHandler = null; + this.boundVideoModalCommentSubmitHandler = null; + this.boundVideoModalCommentRetryHandler = null; + this.boundVideoModalCommentLoadMoreHandler = null; + this.boundVideoModalCommentLoginHandler = null; + this.boundVideoModalCommentMuteHandler = null; this.pendingModalZapOpen = false; this.videoListViewPlaybackHandler = null; this.videoListViewEditHandler = null; @@ -245,6 +251,15 @@ class Application { this.moreMenuController = null; this.latestFeedMetadata = null; this.lastModalTrigger = null; + this.modalCommentState = { + videoEventId: null, + videoDefinitionAddress: null, + parentCommentId: null, + }; + this.modalCommentProfiles = new Map(); + this.modalCommentLimit = 40; + this.modalCommentLoadPromise = null; + this.modalCommentPublishPromise = null; this.nostrService = services.nostrService || nostrService; this.r2Service = services.r2Service || r2Service; @@ -416,6 +431,39 @@ class Application { }) ); + this.commentThreadService = + services.commentThreadService || + new CommentThreadService({ + fetchVideoComments: (target, options) => + nostrClient?.fetchVideoComments?.(target, options), + subscribeVideoComments: (target, options) => + nostrClient?.subscribeVideoComments?.(target, options), + getProfileCacheEntry: (pubkey) => this.getProfileCacheEntry(pubkey), + batchFetchProfiles: (pubkeys) => this.batchFetchProfiles(pubkeys), + logger: { + warn: (...args) => devLogger.warn(...args), + }, + }); + this.boundCommentThreadReadyHandler = (snapshot) => + this.handleCommentThreadReady(snapshot); + this.boundCommentThreadAppendHandler = (payload) => + this.handleCommentThreadAppend(payload); + this.boundCommentThreadErrorHandler = (error) => + this.handleCommentThreadError(error); + if ( + this.commentThreadService && + typeof this.commentThreadService.setCallbacks === "function" + ) { + this.commentThreadService.setCallbacks({ + onThreadReady: this.boundCommentThreadReadyHandler, + onCommentsAppended: this.boundCommentThreadAppendHandler, + onError: this.boundCommentThreadErrorHandler, + }); + if (this.commentThreadService.defaultLimit) { + this.modalCommentLimit = this.commentThreadService.defaultLimit; + } + } + // Profile and login modal controller state this.profileController = null; this.loginModalController = null; @@ -882,6 +930,41 @@ class Application { "zap:wallet-link", this.boundVideoModalZapWalletHandler ); + this.boundVideoModalCommentSubmitHandler = (event) => { + this.handleVideoModalCommentSubmit(event?.detail || {}); + }; + this.videoModal.addEventListener( + "comment:submit", + this.boundVideoModalCommentSubmitHandler + ); + this.boundVideoModalCommentRetryHandler = (event) => { + this.handleVideoModalCommentRetry(event?.detail || {}); + }; + this.videoModal.addEventListener( + "comment:retry", + this.boundVideoModalCommentRetryHandler + ); + this.boundVideoModalCommentLoadMoreHandler = (event) => { + this.handleVideoModalCommentLoadMore(event?.detail || {}); + }; + this.videoModal.addEventListener( + "comment:load-more", + this.boundVideoModalCommentLoadMoreHandler + ); + this.boundVideoModalCommentLoginHandler = (event) => { + this.handleVideoModalCommentLoginRequired(event?.detail || {}); + }; + this.videoModal.addEventListener( + "comment:login-required", + this.boundVideoModalCommentLoginHandler + ); + this.boundVideoModalCommentMuteHandler = (event) => { + this.handleVideoModalCommentMute(event?.detail || {}); + }; + this.videoModal.addEventListener( + "comment:mute-author", + this.boundVideoModalCommentMuteHandler + ); this.moreMenuController = new MoreMenuController({ document, @@ -2185,6 +2268,515 @@ class Application { } } + resetModalCommentState({ hide = true } = {}) { + if (!this.videoModal) { + return; + } + + this.videoModal.clearComments?.(); + this.videoModal.resetCommentComposer?.(); + this.videoModal.setCommentComposerState?.({ + disabled: true, + reason: "disabled", + }); + if (hide) { + this.videoModal.setCommentsVisibility?.(false); + } + this.videoModal.setCommentStatus?.(""); + } + + teardownModalCommentSubscription({ resetUi = true } = {}) { + if (this.commentThreadService) { + try { + this.commentThreadService.teardown(); + } catch (error) { + devLogger.warn("[comment] Failed to teardown modal comment thread:", error); + } + } + this.modalCommentLoadPromise = null; + this.modalCommentPublishPromise = null; + this.modalCommentState = { + videoEventId: null, + videoDefinitionAddress: null, + parentCommentId: null, + }; + this.modalCommentProfiles = new Map(); + if (resetUi) { + this.resetModalCommentState(); + } + this.videoModal?.setCommentSectionCallbacks?.({ teardown: null }); + } + + subscribeModalComments(video) { + if (!this.videoModal) { + return; + } + + if (!video) { + this.teardownModalCommentSubscription(); + return; + } + + this.videoModal.setCommentSectionCallbacks?.({ + teardown: () => this.teardownModalCommentSubscription(), + }); + + if (!this.commentThreadService) { + this.resetModalCommentState(); + return; + } + + this.teardownModalCommentSubscription({ resetUi: false }); + + if (video.enableComments === false) { + this.resetModalCommentState(); + return; + } + + const videoEventId = + typeof video.id === "string" && video.id.trim() ? video.id.trim() : ""; + const videoDefinitionAddress = buildVideoAddressPointer(video); + + if (!videoEventId || !videoDefinitionAddress) { + this.resetModalCommentState({ hide: false }); + this.videoModal.setCommentStatus?.( + "Comments are unavailable for this video.", + ); + return; + } + + this.modalCommentState = { + videoEventId, + videoDefinitionAddress, + parentCommentId: null, + }; + this.modalCommentProfiles = new Map(); + if (this.commentThreadService.defaultLimit) { + this.modalCommentLimit = this.commentThreadService.defaultLimit; + } + + this.videoModal.setCommentsVisibility?.(true); + this.videoModal.clearComments?.(); + this.videoModal.resetCommentComposer?.(); + this.videoModal.setCommentStatus?.("Loading comments…"); + this.applyCommentComposerAuthState(); + + const loadPromise = this.commentThreadService.loadThread({ + video, + parentCommentId: null, + limit: this.modalCommentLimit, + }); + + if (!loadPromise || typeof loadPromise.then !== "function") { + this.applyCommentComposerAuthState(); + return; + } + + this.modalCommentLoadPromise = loadPromise; + loadPromise + .then(() => { + if (this.modalCommentLoadPromise === loadPromise) { + this.modalCommentLoadPromise = null; + } + this.applyCommentComposerAuthState(); + }) + .catch((error) => { + if (this.modalCommentLoadPromise === loadPromise) { + this.modalCommentLoadPromise = null; + } + this.handleCommentThreadError(error); + }); + } + + applyCommentComposerAuthState() { + if (!this.videoModal) { + return; + } + + if (!this.modalCommentState.videoEventId) { + return; + } + + if (this.currentVideo?.enableComments === false) { + this.videoModal.setCommentComposerState?.({ + disabled: true, + reason: "disabled", + }); + return; + } + + if (!this.isUserLoggedIn()) { + this.videoModal.setCommentComposerState?.({ + disabled: true, + reason: "login-required", + }); + return; + } + + this.videoModal.setCommentComposerState?.({ + disabled: false, + reason: "", + }); + this.videoModal.setCommentStatus?.(""); + } + + handleCommentThreadReady(snapshot) { + if (!snapshot || !this.videoModal) { + return; + } + + if (snapshot.videoEventId !== this.modalCommentState.videoEventId) { + return; + } + + this.modalCommentProfiles = this.createMapFromInput(snapshot.profiles); + this.modalCommentState.parentCommentId = + typeof snapshot.parentCommentId === "string" && + snapshot.parentCommentId.trim() + ? snapshot.parentCommentId.trim() + : null; + + const sanitizedSnapshot = this.buildModalCommentSnapshot(snapshot); + this.videoModal.renderComments?.(sanitizedSnapshot); + this.videoModal.setCommentStatus?.(""); + this.applyCommentComposerAuthState(); + } + + handleCommentThreadAppend(payload) { + if (!payload || !this.videoModal) { + return; + } + + if (payload.videoEventId !== this.modalCommentState.videoEventId) { + return; + } + + const comments = this.createMapFromInput(payload.commentsById); + const profiles = this.createMapFromInput(payload.profiles); + profiles.forEach((profile, pubkey) => { + this.modalCommentProfiles.set(pubkey, profile); + }); + + const commentIds = Array.isArray(payload.commentIds) + ? payload.commentIds + : []; + + commentIds.forEach((commentId) => { + if (!comments.has(commentId)) { + return; + } + const event = comments.get(commentId); + if (!event || this.shouldHideModalComment(event)) { + return; + } + const enriched = this.enrichCommentEvent(event); + this.videoModal.appendComment?.(enriched); + }); + } + + handleCommentThreadError(error) { + if (error) { + devLogger.warn("[comment]", error); + } + if (!this.videoModal) { + return; + } + this.videoModal.setCommentStatus?.( + "Failed to load comments. Please try again later.", + ); + if (!this.isUserLoggedIn()) { + this.videoModal.setCommentComposerState?.({ + disabled: true, + reason: "login-required", + }); + } + } + + createMapFromInput(input) { + if (input instanceof Map) { + return new Map(input); + } + const map = new Map(); + if (Array.isArray(input)) { + input.forEach((entry) => { + if (Array.isArray(entry) && entry.length >= 2) { + map.set(entry[0], entry[1]); + } + }); + return map; + } + if (input && typeof input === "object") { + Object.entries(input).forEach(([key, value]) => { + map.set(key, value); + }); + } + return map; + } + + createChildrenMapFromInput(input) { + const source = input instanceof Map ? input : this.createMapFromInput(input); + const result = new Map(); + source.forEach((value, key) => { + const list = Array.isArray(value) ? value.filter(Boolean) : []; + result.set(key, list); + }); + return result; + } + + enrichCommentEvent(event) { + const cloned = { ...(event || {}) }; + const normalized = this.normalizeHexPubkey(cloned.pubkey); + if (normalized && this.modalCommentProfiles.has(normalized)) { + cloned.profile = this.modalCommentProfiles.get(normalized); + } + return cloned; + } + + buildModalCommentSnapshot(snapshot) { + const comments = this.createMapFromInput(snapshot?.commentsById); + const children = this.createChildrenMapFromInput(snapshot?.childrenByParent); + const sanitizedComments = new Map(); + + comments.forEach((event, key) => { + if (!event || this.shouldHideModalComment(event)) { + return; + } + sanitizedComments.set(key, this.enrichCommentEvent(event)); + }); + + const sanitizedChildren = new Map(); + children.forEach((ids, parentId) => { + const filtered = ids.filter((id) => sanitizedComments.has(id)); + sanitizedChildren.set(parentId, filtered); + }); + + return { + videoEventId: snapshot.videoEventId, + parentCommentId: snapshot.parentCommentId || null, + commentsById: sanitizedComments, + childrenByParent: sanitizedChildren, + profiles: this.modalCommentProfiles, + }; + } + + shouldHideModalComment(event) { + const normalized = this.normalizeHexPubkey(event?.pubkey); + if (!normalized) { + return false; + } + + if (userBlocks?.isBlocked?.(normalized)) { + return true; + } + + try { + if (moderationService?.isAuthorMutedByViewer?.(normalized)) { + return true; + } + } catch (error) { + devLogger.warn("[comment] Failed to check viewer mute state:", error); + } + + try { + if (moderationService?.isAuthorMutedByTrusted?.(normalized)) { + return true; + } + } catch (error) { + devLogger.warn("[comment] Failed to check trusted mute state:", error); + } + + return false; + } + + async handleVideoModalCommentSubmit(detail = {}) { + if (this.modalCommentPublishPromise) { + return; + } + + const text = + typeof detail.text === "string" && detail.text.trim() + ? detail.text.trim() + : ""; + if (!text) { + return; + } + + if (!this.isUserLoggedIn()) { + this.videoModal.setCommentComposerState?.({ + disabled: true, + reason: "login-required", + }); + this.handleVideoModalCommentLoginRequired(detail); + return; + } + + const video = this.currentVideo; + if (!video || video.enableComments === false) { + return; + } + + const videoEventId = + typeof video.id === "string" && video.id.trim() ? video.id.trim() : ""; + const videoDefinitionAddress = buildVideoAddressPointer(video); + + if (!videoEventId || !videoDefinitionAddress) { + this.showError("Comments are unavailable for this video."); + return; + } + + const parentCommentId = + typeof detail.parentId === "string" && detail.parentId.trim() + ? detail.parentId.trim() + : null; + + this.videoModal.setCommentComposerState?.({ + disabled: true, + reason: "submitting", + }); + + const publishPromise = Promise.resolve( + nostrClient.publishVideoComment( + { + videoEventId, + videoDefinitionAddress, + parentCommentId, + }, + { + content: text, + }, + ), + ); + + this.modalCommentPublishPromise = publishPromise; + + try { + const result = await publishPromise; + if (!result?.ok || !result.event) { + throw result?.error || new Error("publish-failed"); + } + + const event = this.enrichCommentEvent(result.event); + if (this.commentThreadService) { + try { + this.commentThreadService.processIncomingEvent(event); + } catch (error) { + devLogger.warn( + "[comment] Failed to process optimistic comment event:", + error, + ); + this.videoModal.appendComment?.(event); + } + } else { + this.videoModal.appendComment?.(event); + } + + this.videoModal.resetCommentComposer?.(); + this.applyCommentComposerAuthState(); + this.videoModal.setCommentStatus?.("Comment posted."); + } catch (error) { + devLogger.warn("[comment] Failed to publish comment:", error); + this.videoModal.setCommentComposerState?.({ + disabled: false, + reason: "error", + }); + this.showError("Failed to post comment. Please try again."); + } finally { + if (this.modalCommentPublishPromise === publishPromise) { + this.modalCommentPublishPromise = null; + } + } + } + + handleVideoModalCommentRetry(detail = {}) { + this.handleVideoModalCommentSubmit(detail); + } + + handleVideoModalCommentLoadMore() { + if (!this.commentThreadService || !this.currentVideo) { + return; + } + + if (this.modalCommentLoadPromise) { + return; + } + + const increment = this.commentThreadService.defaultLimit || 40; + this.modalCommentLimit = (this.modalCommentLimit || increment) + increment; + + const loadPromise = this.commentThreadService.loadThread({ + video: this.currentVideo, + parentCommentId: this.modalCommentState.parentCommentId, + limit: this.modalCommentLimit, + }); + + if (!loadPromise || typeof loadPromise.then !== "function") { + return; + } + + this.modalCommentLoadPromise = loadPromise; + loadPromise + .then(() => { + if (this.modalCommentLoadPromise === loadPromise) { + this.modalCommentLoadPromise = null; + } + this.applyCommentComposerAuthState(); + }) + .catch((error) => { + if (this.modalCommentLoadPromise === loadPromise) { + this.modalCommentLoadPromise = null; + } + devLogger.warn("[comment] Failed to load more comments:", error); + this.showError("Failed to load more comments. Please try again."); + }); + } + + handleVideoModalCommentLoginRequired(detail = {}) { + this.applyCommentComposerAuthState(); + this.initializeLoginModalController({ logIfMissing: true }); + const triggerElement = detail?.triggerElement || null; + try { + const opened = this.loginModalController?.openModal?.({ triggerElement }); + if (opened) { + return; + } + } catch (error) { + devLogger.warn("[comment] Failed to open login modal:", error); + } + + this.authService + .requestLogin({ allowAccountSelection: true }) + .catch((error) => { + devLogger.warn("[comment] Login request failed:", error); + }); + } + + async handleVideoModalCommentMute(detail = {}) { + const pubkey = + typeof detail?.pubkey === "string" && detail.pubkey.trim() + ? detail.pubkey.trim() + : ""; + if (!pubkey) { + return; + } + + if (!this.isUserLoggedIn()) { + this.handleVideoModalCommentLoginRequired(detail); + return; + } + + try { + await userBlocks.addBlock(pubkey, this.pubkey); + const snapshot = this.commentThreadService?.getSnapshot?.(); + if (snapshot) { + this.handleCommentThreadReady(snapshot); + } + this.showStatus?.("Author muted."); + } catch (error) { + devLogger.warn("[comment] Failed to mute author:", error); + this.showError("Failed to mute this author. Please try again."); + } + } + subscribeModalViewCount(pointer, pointerKey) { const viewEl = this.videoModal?.getViewCountElement() || null; if (!viewEl) { @@ -4207,6 +4799,7 @@ class Application { } this.applyAuthenticatedUiState(); + this.applyCommentComposerAuthState(); const loginContext = { pubkey: detail?.pubkey || this.pubkey, @@ -4405,6 +4998,7 @@ class Application { } this.applyLoggedOutUiState(); + this.applyCommentComposerAuthState(); if (this.videoModal?.closeZapDialog) { try { @@ -8123,6 +8717,7 @@ class Application { this.currentVideoPointer, this.currentVideoPointerKey ); + this.subscribeModalComments(this.currentVideo); this.syncModalMoreMenuData(); @@ -8436,6 +9031,7 @@ class Application { this.currentVideoPointerKey = null; this.subscribeModalViewCount(null, null); this.subscribeModalReactions(null, null); + this.subscribeModalComments(null); const sanitizedUrl = typeof url === "string" ? url.trim() : ""; const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : ""; @@ -8991,6 +9587,7 @@ class Application { } if (this.videoModal) { + this.teardownModalCommentSubscription({ resetUi: false }); if (this.boundVideoModalCloseHandler) { this.videoModal.removeEventListener( "modal:close", @@ -9068,6 +9665,41 @@ class Application { ); this.boundVideoModalZapWalletHandler = null; } + if (this.boundVideoModalCommentSubmitHandler) { + this.videoModal.removeEventListener( + "comment:submit", + this.boundVideoModalCommentSubmitHandler + ); + this.boundVideoModalCommentSubmitHandler = null; + } + if (this.boundVideoModalCommentRetryHandler) { + this.videoModal.removeEventListener( + "comment:retry", + this.boundVideoModalCommentRetryHandler + ); + this.boundVideoModalCommentRetryHandler = null; + } + if (this.boundVideoModalCommentLoadMoreHandler) { + this.videoModal.removeEventListener( + "comment:load-more", + this.boundVideoModalCommentLoadMoreHandler + ); + this.boundVideoModalCommentLoadMoreHandler = null; + } + if (this.boundVideoModalCommentLoginHandler) { + this.videoModal.removeEventListener( + "comment:login-required", + this.boundVideoModalCommentLoginHandler + ); + this.boundVideoModalCommentLoginHandler = null; + } + if (this.boundVideoModalCommentMuteHandler) { + this.videoModal.removeEventListener( + "comment:mute-author", + this.boundVideoModalCommentMuteHandler + ); + this.boundVideoModalCommentMuteHandler = null; + } if (typeof this.videoModal.destroy === "function") { try { this.videoModal.destroy(); @@ -9131,6 +9763,25 @@ class Application { this.loginModalController = null; } + if (this.commentThreadService?.setCallbacks) { + this.commentThreadService.setCallbacks({ + onThreadReady: null, + onCommentsAppended: null, + onError: null, + }); + } + if (this.commentThreadService) { + try { + this.commentThreadService.teardown(); + } catch (error) { + devLogger.warn( + "[Application] Failed to destroy comment thread service:", + error, + ); + } + this.commentThreadService = null; + } + this.videoList = null; if (this.videoListPopularTags) { this.videoListPopularTags.textContent = ""; diff --git a/js/services/commentThreadService.js b/js/services/commentThreadService.js index 9ba92cd9..8d0a0feb 100644 --- a/js/services/commentThreadService.js +++ b/js/services/commentThreadService.js @@ -47,12 +47,42 @@ export default class CommentThreadService { constructor({ nostrClient = null, app = null, + fetchVideoComments = null, + subscribeVideoComments = null, + getProfileCacheEntry = null, + batchFetchProfiles = null, limit = DEFAULT_INITIAL_LIMIT, hydrationDebounceMs = DEFAULT_HYDRATION_DEBOUNCE_MS, logger = devLogger, } = {}) { this.nostrClient = nostrClient; this.app = app; + this.fetchVideoComments = + typeof fetchVideoComments === "function" + ? fetchVideoComments + : this.nostrClient && + typeof this.nostrClient.fetchVideoComments === "function" + ? (...args) => this.nostrClient.fetchVideoComments(...args) + : null; + this.subscribeVideoComments = + typeof subscribeVideoComments === "function" + ? subscribeVideoComments + : this.nostrClient && + typeof this.nostrClient.subscribeVideoComments === "function" + ? (...args) => this.nostrClient.subscribeVideoComments(...args) + : null; + this.getProfileCacheEntry = + typeof getProfileCacheEntry === "function" + ? getProfileCacheEntry + : this.app && typeof this.app.getProfileCacheEntry === "function" + ? (pubkey) => this.app.getProfileCacheEntry(pubkey) + : null; + this.batchFetchProfiles = + typeof batchFetchProfiles === "function" + ? batchFetchProfiles + : this.app && typeof this.app.batchFetchProfiles === "function" + ? (pubkeys) => this.app.batchFetchProfiles(pubkeys) + : null; this.defaultLimit = toPositiveInteger(limit, DEFAULT_INITIAL_LIMIT); this.hydrationDelay = Math.max( 0, @@ -118,8 +148,7 @@ export default class CommentThreadService { return { success: false }; } - if (!this.nostrClient || - typeof this.nostrClient.fetchVideoComments !== "function") { + if (typeof this.fetchVideoComments !== "function") { this.emitError( new Error("Nostr client is missing fetchVideoComments implementation."), ); @@ -135,7 +164,7 @@ export default class CommentThreadService { let events = []; try { - events = await this.nostrClient.fetchVideoComments(target, { + events = await this.fetchVideoComments(target, { limit: fetchLimit, relays: this.activeRelays, }); @@ -166,10 +195,7 @@ export default class CommentThreadService { } startSubscription(target) { - if ( - !this.nostrClient || - typeof this.nostrClient.subscribeVideoComments !== "function" - ) { + if (typeof this.subscribeVideoComments !== "function") { return; } @@ -179,7 +205,7 @@ export default class CommentThreadService { }; try { - const cleanup = this.nostrClient.subscribeVideoComments(target, options); + const cleanup = this.subscribeVideoComments(target, options); if (typeof cleanup === "function") { this.subscriptionCleanup = () => { try { @@ -335,7 +361,7 @@ export default class CommentThreadService { return; } - const cachedProfile = this.getProfileFromApp(pubkey); + const cachedProfile = this.getProfileFromCache(pubkey); if (cachedProfile) { this.profileCache.set(pubkey, cachedProfile); return; @@ -345,15 +371,19 @@ export default class CommentThreadService { this.scheduleProfileHydration(); } - getProfileFromApp(pubkey) { - if (!this.app || typeof this.app.getProfileCacheEntry !== "function") { + getProfileFromCache(pubkey) { + if (typeof this.getProfileCacheEntry !== "function") { return null; } try { - const entry = this.app.getProfileCacheEntry(pubkey); + const entry = this.getProfileCacheEntry(pubkey); + if (!entry) { + return null; + } if (entry && typeof entry === "object" && entry.profile) { return entry.profile; } + return entry; } catch (error) { if (this.logger?.warn) { this.logger.warn( @@ -387,12 +417,12 @@ export default class CommentThreadService { const pubkeys = Array.from(this.profileQueue); this.profileQueue.clear(); - if (!this.app || typeof this.app.batchFetchProfiles !== "function") { + if (typeof this.batchFetchProfiles !== "function") { return; } const hydrationPromise = Promise.resolve( - this.app.batchFetchProfiles(pubkeys), + this.batchFetchProfiles(pubkeys), ) .catch((error) => { if (this.logger?.warn) { @@ -405,7 +435,7 @@ export default class CommentThreadService { }) .then(() => { pubkeys.forEach((pubkey) => { - const profile = this.getProfileFromApp(pubkey); + const profile = this.getProfileFromCache(pubkey); if (profile) { this.profileCache.set(pubkey, profile); } @@ -488,7 +518,7 @@ export default class CommentThreadService { return this.profileCache.get(normalized); } - const profile = this.getProfileFromApp(normalized); + const profile = this.getProfileFromCache(normalized); if (profile) { this.profileCache.set(normalized, profile); return profile; diff --git a/tests/comment-thread-service.test.mjs b/tests/comment-thread-service.test.mjs index de43c3ba..3388de1d 100644 --- a/tests/comment-thread-service.test.mjs +++ b/tests/comment-thread-service.test.mjs @@ -82,8 +82,11 @@ async function testThreadFlow() { }; const service = new CommentThreadService({ - nostrClient, - app, + fetchVideoComments: (...args) => nostrClient.fetchVideoComments(...args), + subscribeVideoComments: (...args) => + nostrClient.subscribeVideoComments(...args), + getProfileCacheEntry: (pubkey) => app.getProfileCacheEntry(pubkey), + batchFetchProfiles: (pubkeys) => app.batchFetchProfiles(pubkeys), hydrationDebounceMs: 0, }); @@ -217,8 +220,11 @@ async function testTeardownCancelsHydration() { }; const service = new CommentThreadService({ - nostrClient, - app, + fetchVideoComments: (...args) => nostrClient.fetchVideoComments(...args), + subscribeVideoComments: (...args) => + nostrClient.subscribeVideoComments(...args), + getProfileCacheEntry: (pubkey) => app.getProfileCacheEntry(pubkey), + batchFetchProfiles: (pubkeys) => app.batchFetchProfiles(pubkeys), hydrationDebounceMs: 50, });