mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-02 19:02:28 +00:00
fix(ci): fix netlify deployment by building to dist
- Move from root publishing to clean `dist/` directory - Create `scripts/build-dist.mjs` to orchestrate build and copy assets - Update `package.json` build script - Update `netlify.toml` to publish `dist/` - Ignore `dist/` in `.gitignore` Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
ccb60c3a7e
commit
0bfa3b08e8
638 files changed
+193490
-2
No files matched your search
+962
@@ -0,0 +1,962 @@
|
||||
import { devLogger } from "../utils/logger.js";
|
||||
import { buildVideoAddressPointer } from "../utils/videoPointer.js";
|
||||
import { COMMENT_EVENT_KIND } from "../nostr/commentEvents.js";
|
||||
import { normalizeHexString, normalizeHexPubkey } from "../utils/hex.js";
|
||||
|
||||
const DEFAULT_LIMIT = 40;
|
||||
|
||||
function normalizeString(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
const normalizeHexId = normalizeHexString;
|
||||
|
||||
function normalizeRelay(value) {
|
||||
if (typeof value === "string") {
|
||||
return value.trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function normalizeKind(value) {
|
||||
if (Number.isFinite(value)) {
|
||||
return String(Math.floor(value));
|
||||
}
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
return value.trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export default class VideoModalCommentController {
|
||||
constructor({
|
||||
commentThreadService = null,
|
||||
videoModal = null,
|
||||
auth = {},
|
||||
callbacks = {},
|
||||
services = {},
|
||||
utils = {},
|
||||
} = {}) {
|
||||
this.commentThreadService = commentThreadService;
|
||||
this.videoModal = videoModal;
|
||||
|
||||
this.auth = {
|
||||
isLoggedIn:
|
||||
typeof auth.isLoggedIn === "function" ? () => auth.isLoggedIn() : () => false,
|
||||
initializeLoginModalController:
|
||||
typeof auth.initializeLoginModalController === "function"
|
||||
? (options) => auth.initializeLoginModalController(options)
|
||||
: () => {},
|
||||
getLoginModalController:
|
||||
typeof auth.getLoginModalController === "function"
|
||||
? () => auth.getLoginModalController()
|
||||
: () => null,
|
||||
requestLogin:
|
||||
typeof auth.requestLogin === "function"
|
||||
? (options) => auth.requestLogin(options)
|
||||
: () => Promise.resolve(false),
|
||||
};
|
||||
|
||||
this.callbacks = {
|
||||
showError:
|
||||
typeof callbacks.showError === "function"
|
||||
? (message) => callbacks.showError(message)
|
||||
: () => {},
|
||||
showStatus:
|
||||
typeof callbacks.showStatus === "function"
|
||||
? (message, options) => callbacks.showStatus(message, options)
|
||||
: () => {},
|
||||
muteAuthor:
|
||||
typeof callbacks.muteAuthor === "function"
|
||||
? (pubkey) => callbacks.muteAuthor(pubkey)
|
||||
: () => Promise.resolve(),
|
||||
shouldHideAuthor:
|
||||
typeof callbacks.shouldHideAuthor === "function"
|
||||
? (pubkey) => callbacks.shouldHideAuthor(pubkey)
|
||||
: () => false,
|
||||
};
|
||||
|
||||
this.services = {
|
||||
publishComment:
|
||||
typeof services.publishComment === "function"
|
||||
? (payload, eventData) => services.publishComment(payload, eventData)
|
||||
: () => Promise.resolve(null),
|
||||
};
|
||||
|
||||
this.utils = {
|
||||
normalizeHexPubkey:
|
||||
typeof utils.normalizeHexPubkey === "function"
|
||||
? (value) => utils.normalizeHexPubkey(value)
|
||||
: (value) => normalizeHexPubkey(value) || null,
|
||||
};
|
||||
|
||||
this.currentVideo = null;
|
||||
this.modalCommentState = {
|
||||
videoEventId: null,
|
||||
videoDefinitionAddress: null,
|
||||
videoKind: null,
|
||||
videoAuthorPubkey: null,
|
||||
videoRootId: null,
|
||||
videoRootRelay: null,
|
||||
parentCommentId: null,
|
||||
parentCommentKind: null,
|
||||
parentCommentPubkey: null,
|
||||
};
|
||||
this.modalCommentProfiles = new Map();
|
||||
this.modalCommentLimit = this.commentThreadService?.defaultLimit || DEFAULT_LIMIT;
|
||||
this.modalCommentLoadPromise = null;
|
||||
this.modalCommentPublishPromise = null;
|
||||
|
||||
this.boundThreadReadyHandler = (snapshot) =>
|
||||
this.handleCommentThreadReady(snapshot);
|
||||
this.boundThreadAppendHandler = (payload) =>
|
||||
this.handleCommentThreadAppend(payload);
|
||||
this.boundThreadErrorHandler = (error) =>
|
||||
this.handleCommentThreadError(error);
|
||||
|
||||
if (this.commentThreadService?.setCallbacks) {
|
||||
this.commentThreadService.setCallbacks({
|
||||
onThreadReady: this.boundThreadReadyHandler,
|
||||
onCommentsAppended: this.boundThreadAppendHandler,
|
||||
onError: this.boundThreadErrorHandler,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
load(video) {
|
||||
if (!this.videoModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!video) {
|
||||
this.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispose({ resetUi: false });
|
||||
this.currentVideo = video;
|
||||
|
||||
this.videoModal.setCommentSectionCallbacks?.({
|
||||
teardown: () => this.dispose(),
|
||||
});
|
||||
|
||||
if (!this.commentThreadService) {
|
||||
this.resetModalCommentState();
|
||||
return;
|
||||
}
|
||||
|
||||
this.videoModal.hideCommentsDisabledMessage?.();
|
||||
|
||||
if (video.enableComments === false) {
|
||||
this.resetModalCommentState();
|
||||
this.videoModal.showCommentsDisabledMessage?.(
|
||||
"Comments have been turned off for this video.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const videoEventId = normalizeHexId(video.id);
|
||||
let videoDefinitionAddress = buildVideoAddressPointer(video);
|
||||
if (!videoDefinitionAddress && this.modalCommentState.videoDefinitionAddress) {
|
||||
videoDefinitionAddress = normalizeString(
|
||||
this.modalCommentState.videoDefinitionAddress,
|
||||
);
|
||||
}
|
||||
if (videoDefinitionAddress) {
|
||||
videoDefinitionAddress = videoDefinitionAddress.trim().toLowerCase();
|
||||
}
|
||||
const videoKind = normalizeKind(video.kind);
|
||||
const videoAuthorPubkey = normalizeHexPubkey(video.pubkey);
|
||||
const pointerRootId = normalizeHexId(video.pointerIdentifiers?.videoRootId);
|
||||
const videoRootId = normalizeHexId(video.videoRootId) || pointerRootId;
|
||||
const videoRootRelay = normalizeRelay(
|
||||
video.videoRootRelay || video.rootIdentifierRelay,
|
||||
);
|
||||
|
||||
if (!videoEventId) {
|
||||
this.resetModalCommentState({ hide: false });
|
||||
this.videoModal.setCommentStatus?.(
|
||||
"Comments are unavailable for this video.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.modalCommentState = {
|
||||
videoEventId,
|
||||
videoDefinitionAddress: videoDefinitionAddress || null,
|
||||
videoKind: videoKind || null,
|
||||
videoAuthorPubkey: videoAuthorPubkey || null,
|
||||
videoRootId: videoRootId || null,
|
||||
videoRootRelay: videoRootRelay || null,
|
||||
parentCommentId: null,
|
||||
parentCommentKind: null,
|
||||
parentCommentPubkey: 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…");
|
||||
|
||||
const loadResult = this.commentThreadService.loadThread({
|
||||
video,
|
||||
parentCommentId: null,
|
||||
limit: this.modalCommentLimit,
|
||||
});
|
||||
|
||||
if (!loadResult) {
|
||||
this.applyCommentComposerAuthState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof loadResult.then !== "function") {
|
||||
const didRenderSnapshot = this.handleCommentThreadReady(loadResult);
|
||||
if (didRenderSnapshot === false) {
|
||||
this.applyCommentComposerAuthState();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.modalCommentLoadPromise = loadResult;
|
||||
loadResult
|
||||
.then((snapshot) => {
|
||||
if (this.modalCommentLoadPromise === loadResult) {
|
||||
this.modalCommentLoadPromise = null;
|
||||
}
|
||||
|
||||
const didRenderSnapshot = this.handleCommentThreadReady(snapshot);
|
||||
if (didRenderSnapshot === false) {
|
||||
this.applyCommentComposerAuthState();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (this.modalCommentLoadPromise === loadResult) {
|
||||
this.modalCommentLoadPromise = null;
|
||||
}
|
||||
this.handleCommentThreadError(error);
|
||||
});
|
||||
}
|
||||
|
||||
submit(detail = {}) {
|
||||
this.handleVideoModalCommentSubmit(detail);
|
||||
}
|
||||
|
||||
retry(detail = {}) {
|
||||
this.handleVideoModalCommentSubmit(detail);
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
this.handleVideoModalCommentLoadMore();
|
||||
}
|
||||
|
||||
handleLoginRequired(detail = {}) {
|
||||
this.handleVideoModalCommentLoginRequired(detail);
|
||||
}
|
||||
|
||||
async muteAuthor(detail = {}) {
|
||||
await this.handleVideoModalCommentMute(detail);
|
||||
}
|
||||
|
||||
refreshAuthState() {
|
||||
this.applyCommentComposerAuthState();
|
||||
}
|
||||
|
||||
dispose({ 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,
|
||||
videoKind: null,
|
||||
videoAuthorPubkey: null,
|
||||
videoRootId: null,
|
||||
videoRootRelay: null,
|
||||
parentCommentId: null,
|
||||
parentCommentKind: null,
|
||||
parentCommentPubkey: null,
|
||||
};
|
||||
this.modalCommentProfiles = new Map();
|
||||
if (resetUi) {
|
||||
this.resetModalCommentState();
|
||||
}
|
||||
this.videoModal?.setCommentSectionCallbacks?.({ teardown: null });
|
||||
this.currentVideo = null;
|
||||
}
|
||||
|
||||
destroy({ resetUi = true } = {}) {
|
||||
this.dispose({ resetUi });
|
||||
if (this.commentThreadService?.setCallbacks) {
|
||||
this.commentThreadService.setCallbacks({
|
||||
onThreadReady: null,
|
||||
onCommentsAppended: null,
|
||||
onError: null,
|
||||
});
|
||||
}
|
||||
this.commentThreadService = null;
|
||||
this.videoModal = null;
|
||||
}
|
||||
|
||||
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?.("");
|
||||
}
|
||||
|
||||
applyCommentComposerAuthState() {
|
||||
if (!this.videoModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.modalCommentState.videoEventId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentVideo?.enableComments === false) {
|
||||
this.videoModal.showCommentsDisabledMessage?.(
|
||||
"Comments have been turned off for this video.",
|
||||
);
|
||||
this.videoModal.setCommentComposerState?.({
|
||||
disabled: true,
|
||||
reason: "disabled",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.videoModal.hideCommentsDisabledMessage?.();
|
||||
|
||||
if (!this.auth.isLoggedIn()) {
|
||||
this.videoModal.setCommentComposerState?.({
|
||||
disabled: true,
|
||||
reason: "login-required",
|
||||
});
|
||||
this.videoModal.setCommentsVisibility?.(true);
|
||||
this.videoModal.setCommentStatus?.("");
|
||||
return;
|
||||
}
|
||||
|
||||
this.videoModal.setCommentComposerState?.({
|
||||
disabled: false,
|
||||
reason: "",
|
||||
});
|
||||
this.videoModal.setCommentStatus?.("");
|
||||
}
|
||||
|
||||
handleCommentThreadReady(snapshot) {
|
||||
if (!snapshot || !this.videoModal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const snapshotVideoEventId = normalizeHexId(snapshot.videoEventId);
|
||||
const modalVideoEventId = normalizeHexId(this.modalCommentState.videoEventId);
|
||||
|
||||
if (!snapshotVideoEventId || snapshotVideoEventId !== modalVideoEventId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.modalCommentProfiles = this.createMapFromInput(snapshot.profiles, {
|
||||
normalizeKey: this.utils.normalizeHexPubkey,
|
||||
});
|
||||
this.modalCommentState.parentCommentId =
|
||||
normalizeHexId(snapshot.parentCommentId) || null;
|
||||
if (typeof snapshot.videoDefinitionAddress === "string") {
|
||||
const pointer = normalizeString(snapshot.videoDefinitionAddress);
|
||||
if (pointer) {
|
||||
this.modalCommentState.videoDefinitionAddress = pointer;
|
||||
}
|
||||
}
|
||||
const snapshotVideoKind = normalizeKind(snapshot.videoKind);
|
||||
if (snapshotVideoKind) {
|
||||
this.modalCommentState.videoKind = snapshotVideoKind;
|
||||
}
|
||||
const snapshotVideoAuthor = normalizeHexPubkey(snapshot.videoAuthorPubkey);
|
||||
if (snapshotVideoAuthor) {
|
||||
this.modalCommentState.videoAuthorPubkey = snapshotVideoAuthor;
|
||||
}
|
||||
const snapshotRootIdentifier = normalizeHexId(snapshot.rootIdentifier);
|
||||
if (snapshotRootIdentifier) {
|
||||
this.modalCommentState.videoRootId = snapshotRootIdentifier;
|
||||
}
|
||||
const snapshotRootRelay = normalizeRelay(snapshot.rootIdentifierRelay);
|
||||
if (snapshotRootRelay) {
|
||||
this.modalCommentState.videoRootRelay = snapshotRootRelay;
|
||||
}
|
||||
const snapshotParentKind = normalizeKind(snapshot.parentCommentKind);
|
||||
if (snapshotParentKind) {
|
||||
this.modalCommentState.parentCommentKind = snapshotParentKind;
|
||||
}
|
||||
const snapshotParentPubkey = normalizeHexPubkey(snapshot.parentCommentPubkey);
|
||||
if (snapshotParentPubkey) {
|
||||
this.modalCommentState.parentCommentPubkey = snapshotParentPubkey;
|
||||
}
|
||||
|
||||
const sanitizedSnapshot = this.buildModalCommentSnapshot(snapshot);
|
||||
this.videoModal.setCommentsVisibility?.(true);
|
||||
this.videoModal.renderComments?.(sanitizedSnapshot);
|
||||
this.videoModal.setCommentStatus?.("");
|
||||
this.applyCommentComposerAuthState();
|
||||
return true;
|
||||
}
|
||||
|
||||
handleCommentThreadAppend(payload) {
|
||||
if (!payload || !this.videoModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payloadVideoEventId = normalizeHexId(payload.videoEventId);
|
||||
const modalVideoEventId = normalizeHexId(this.modalCommentState.videoEventId);
|
||||
|
||||
if (!payloadVideoEventId || payloadVideoEventId !== modalVideoEventId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const comments = this.createMapFromInput(payload.commentsById);
|
||||
const childrenByParent = this.createChildrenMapFromInput(
|
||||
payload.childrenByParent,
|
||||
);
|
||||
const profiles = this.createMapFromInput(payload.profiles, {
|
||||
normalizeKey: this.utils.normalizeHexPubkey,
|
||||
});
|
||||
profiles.forEach((profile, pubkey) => {
|
||||
this.modalCommentProfiles.set(pubkey, profile);
|
||||
});
|
||||
|
||||
const payloadRootIdentifier = normalizeHexId(payload.rootIdentifier);
|
||||
if (payloadRootIdentifier) {
|
||||
this.modalCommentState.videoRootId = payloadRootIdentifier;
|
||||
}
|
||||
const payloadRootRelay = normalizeRelay(payload.rootIdentifierRelay);
|
||||
if (payloadRootRelay) {
|
||||
this.modalCommentState.videoRootRelay = payloadRootRelay;
|
||||
}
|
||||
|
||||
const commentIds = Array.isArray(payload.commentIds)
|
||||
? payload.commentIds
|
||||
: [];
|
||||
|
||||
if (
|
||||
this.modalCommentState.parentCommentId &&
|
||||
comments.has(this.modalCommentState.parentCommentId)
|
||||
) {
|
||||
const parentEvent = comments.get(this.modalCommentState.parentCommentId);
|
||||
const eventKind = normalizeKind(parentEvent?.kind);
|
||||
if (eventKind) {
|
||||
this.modalCommentState.parentCommentKind = eventKind;
|
||||
}
|
||||
const eventPubkey = normalizeHexPubkey(parentEvent?.pubkey);
|
||||
if (eventPubkey) {
|
||||
this.modalCommentState.parentCommentPubkey = eventPubkey;
|
||||
}
|
||||
}
|
||||
|
||||
const parentByChild = new Map();
|
||||
childrenByParent.forEach((ids, parentId) => {
|
||||
ids.forEach((id) => {
|
||||
if (!parentByChild.has(id)) {
|
||||
parentByChild.set(id, parentId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let requiresFullRender = false;
|
||||
for (const commentId of commentIds) {
|
||||
if (!comments.has(commentId)) {
|
||||
requiresFullRender = true;
|
||||
break;
|
||||
}
|
||||
if (!parentByChild.has(commentId)) {
|
||||
requiresFullRender = true;
|
||||
break;
|
||||
}
|
||||
const parentId = parentByChild.get(commentId);
|
||||
if (parentId) {
|
||||
requiresFullRender = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (requiresFullRender) {
|
||||
const snapshot = this.buildModalCommentSnapshot({
|
||||
...payload,
|
||||
commentsById: comments,
|
||||
childrenByParent,
|
||||
});
|
||||
this.videoModal.renderComments?.(snapshot);
|
||||
return;
|
||||
}
|
||||
|
||||
commentIds.forEach((commentId) => {
|
||||
if (!comments.has(commentId)) {
|
||||
return;
|
||||
}
|
||||
const event = comments.get(commentId);
|
||||
if (!event || this.shouldHideModalComment(event)) {
|
||||
return;
|
||||
}
|
||||
const enriched = this.enrichCommentEvent(event);
|
||||
if (
|
||||
this.modalCommentState.parentCommentId &&
|
||||
commentId === this.modalCommentState.parentCommentId
|
||||
) {
|
||||
const eventKind = normalizeKind(event.kind);
|
||||
if (eventKind) {
|
||||
this.modalCommentState.parentCommentKind = eventKind;
|
||||
}
|
||||
const eventPubkey = normalizeHexPubkey(event.pubkey);
|
||||
if (eventPubkey) {
|
||||
this.modalCommentState.parentCommentPubkey = eventPubkey;
|
||||
}
|
||||
}
|
||||
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.auth.isLoggedIn()) {
|
||||
this.videoModal.setCommentComposerState?.({
|
||||
disabled: true,
|
||||
reason: "login-required",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createMapFromInput(input, { normalizeKey = null } = {}) {
|
||||
const map = new Map();
|
||||
const applyKey = (key) => {
|
||||
if (!normalizeKey) {
|
||||
return key;
|
||||
}
|
||||
const normalized = normalizeKey(key);
|
||||
return normalized === null || normalized === undefined ? null : normalized;
|
||||
};
|
||||
|
||||
const setEntry = (key, value) => {
|
||||
const normalizedKey = applyKey(key);
|
||||
if (normalizedKey === null || normalizedKey === undefined) {
|
||||
return;
|
||||
}
|
||||
map.set(normalizedKey, value);
|
||||
};
|
||||
|
||||
if (input instanceof Map) {
|
||||
input.forEach((value, key) => {
|
||||
setEntry(key, value);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
if (Array.isArray(input)) {
|
||||
input.forEach((entry) => {
|
||||
if (Array.isArray(entry) && entry.length >= 2) {
|
||||
setEntry(entry[0], entry[1]);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}
|
||||
if (input && typeof input === "object") {
|
||||
Object.entries(input).forEach(([key, value]) => {
|
||||
setEntry(key, value);
|
||||
});
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
createChildrenMapFromInput(input) {
|
||||
const source =
|
||||
input instanceof Map ? input : this.createMapFromInput(input);
|
||||
const result = new Map();
|
||||
source.forEach((value, key) => {
|
||||
let normalizedKey = key;
|
||||
if (normalizedKey === null || normalizedKey === undefined) {
|
||||
normalizedKey = null;
|
||||
} else if (typeof normalizedKey === "string") {
|
||||
const trimmed = normalizedKey.trim();
|
||||
if (
|
||||
!trimmed ||
|
||||
trimmed === "null" ||
|
||||
trimmed === "undefined" ||
|
||||
trimmed === "__root__"
|
||||
) {
|
||||
normalizedKey = null;
|
||||
} else {
|
||||
normalizedKey = normalizeHexId(trimmed) || trimmed;
|
||||
}
|
||||
}
|
||||
const list = Array.isArray(value) ? value.filter(Boolean) : [];
|
||||
result.set(normalizedKey, list);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
enrichCommentEvent(event) {
|
||||
const cloned = { ...(event || {}) };
|
||||
const normalized = this.utils.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 seen = new Set();
|
||||
const filtered = [];
|
||||
ids.forEach((id) => {
|
||||
if (!sanitizedComments.has(id) || seen.has(id)) {
|
||||
return;
|
||||
}
|
||||
seen.add(id);
|
||||
filtered.push(id);
|
||||
});
|
||||
sanitizedChildren.set(parentId, filtered);
|
||||
});
|
||||
|
||||
return {
|
||||
videoEventId: normalizeHexId(snapshot.videoEventId),
|
||||
parentCommentId: normalizeHexId(snapshot.parentCommentId) || null,
|
||||
rootIdentifier: normalizeHexId(snapshot.rootIdentifier) || null,
|
||||
rootIdentifierRelay: snapshot.rootIdentifierRelay || null,
|
||||
commentsById: sanitizedComments,
|
||||
childrenByParent: sanitizedChildren,
|
||||
profiles: this.modalCommentProfiles,
|
||||
};
|
||||
}
|
||||
|
||||
shouldHideModalComment(event) {
|
||||
const normalized = this.utils.normalizeHexPubkey(event?.pubkey);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return Boolean(this.callbacks.shouldHideAuthor(normalized));
|
||||
} catch (error) {
|
||||
devLogger.warn("[comment] Failed to evaluate comment visibility:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async handleVideoModalCommentSubmit(detail = {}) {
|
||||
if (this.modalCommentPublishPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = normalizeString(detail.text);
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.auth.isLoggedIn()) {
|
||||
this.videoModal.setCommentComposerState?.({
|
||||
disabled: true,
|
||||
reason: "login-required",
|
||||
});
|
||||
this.handleVideoModalCommentLoginRequired(detail);
|
||||
return;
|
||||
}
|
||||
|
||||
const video = this.currentVideo;
|
||||
if (!video || video.enableComments === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const videoEventId = normalizeHexId(video.id);
|
||||
let videoDefinitionAddress = buildVideoAddressPointer(video);
|
||||
if (!videoDefinitionAddress && this.modalCommentState.videoDefinitionAddress) {
|
||||
videoDefinitionAddress = normalizeString(
|
||||
this.modalCommentState.videoDefinitionAddress,
|
||||
);
|
||||
}
|
||||
const videoKind =
|
||||
normalizeKind(this.modalCommentState.videoKind) || normalizeKind(video.kind);
|
||||
const videoAuthorPubkey =
|
||||
normalizeHexPubkey(this.modalCommentState.videoAuthorPubkey) ||
|
||||
normalizeHexPubkey(video.pubkey);
|
||||
const fallbackPointerRootId = normalizeHexId(video.pointerIdentifiers?.videoRootId);
|
||||
const videoRootId =
|
||||
normalizeHexId(this.modalCommentState.videoRootId) ||
|
||||
normalizeHexId(video.videoRootId) ||
|
||||
fallbackPointerRootId;
|
||||
const videoRootRelay =
|
||||
normalizeRelay(this.modalCommentState.videoRootRelay) ||
|
||||
normalizeRelay(video.videoRootRelay || video.rootIdentifierRelay);
|
||||
|
||||
if (!videoEventId) {
|
||||
this.callbacks.showError("Comments are unavailable for this video.");
|
||||
return;
|
||||
}
|
||||
|
||||
const parentCommentId = normalizeHexId(detail.parentId) || null;
|
||||
let parentCommentKind = this.modalCommentState.parentCommentKind
|
||||
? normalizeKind(this.modalCommentState.parentCommentKind)
|
||||
: "";
|
||||
let parentCommentPubkey = this.modalCommentState.parentCommentPubkey
|
||||
? normalizeHexPubkey(this.modalCommentState.parentCommentPubkey)
|
||||
: "";
|
||||
|
||||
if (parentCommentId && typeof this.commentThreadService?.getCommentEvent === "function") {
|
||||
const parentEvent = this.commentThreadService.getCommentEvent(
|
||||
parentCommentId,
|
||||
);
|
||||
if (parentEvent) {
|
||||
const eventKind = normalizeKind(parentEvent.kind);
|
||||
if (eventKind) {
|
||||
parentCommentKind = eventKind;
|
||||
this.modalCommentState.parentCommentKind = eventKind;
|
||||
}
|
||||
const eventPubkey = normalizeHexPubkey(parentEvent.pubkey);
|
||||
if (eventPubkey) {
|
||||
parentCommentPubkey = eventPubkey;
|
||||
this.modalCommentState.parentCommentPubkey = eventPubkey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!parentCommentKind && parentCommentId) {
|
||||
parentCommentKind = String(COMMENT_EVENT_KIND);
|
||||
}
|
||||
|
||||
this.videoModal.setCommentComposerState?.({
|
||||
disabled: true,
|
||||
reason: "submitting",
|
||||
});
|
||||
|
||||
const publishPayload = {
|
||||
videoEventId,
|
||||
parentCommentId,
|
||||
};
|
||||
|
||||
if (videoDefinitionAddress) {
|
||||
publishPayload.videoDefinitionAddress = videoDefinitionAddress;
|
||||
}
|
||||
if (videoKind) {
|
||||
publishPayload.videoKind = videoKind;
|
||||
publishPayload.rootKind = videoKind;
|
||||
}
|
||||
if (videoAuthorPubkey) {
|
||||
publishPayload.videoAuthorPubkey = videoAuthorPubkey;
|
||||
publishPayload.rootAuthorPubkey = videoAuthorPubkey;
|
||||
}
|
||||
if (videoRootId) {
|
||||
publishPayload.rootIdentifier = videoRootId;
|
||||
}
|
||||
if (videoRootRelay) {
|
||||
publishPayload.rootIdentifierRelay = videoRootRelay;
|
||||
}
|
||||
if (parentCommentKind) {
|
||||
publishPayload.parentCommentKind = parentCommentKind;
|
||||
}
|
||||
if (parentCommentPubkey) {
|
||||
publishPayload.parentCommentPubkey = parentCommentPubkey;
|
||||
publishPayload.parentAuthorPubkey = parentCommentPubkey;
|
||||
}
|
||||
|
||||
const publishPromise = Promise.resolve(
|
||||
this.services.publishComment(
|
||||
publishPayload,
|
||||
{
|
||||
content: text,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
this.modalCommentPublishPromise = publishPromise;
|
||||
|
||||
try {
|
||||
const result = await publishPromise;
|
||||
if (!result?.ok || !result.event) {
|
||||
throw result || { 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) {
|
||||
const authRequired =
|
||||
(error && typeof error === "object" && error.error === "auth-required") ||
|
||||
error === "auth-required";
|
||||
|
||||
if (authRequired) {
|
||||
this.videoModal.setCommentComposerState?.({
|
||||
disabled: true,
|
||||
reason: "login-required",
|
||||
});
|
||||
this.handleVideoModalCommentLoginRequired(detail);
|
||||
this.videoModal.setCommentComposerState?.({
|
||||
disabled: true,
|
||||
reason: "login-required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
devLogger.warn("[comment] Failed to publish comment:", error);
|
||||
this.videoModal.setCommentComposerState?.({
|
||||
disabled: false,
|
||||
reason: "error",
|
||||
});
|
||||
this.callbacks.showError("Failed to post comment. Please try again.");
|
||||
} finally {
|
||||
if (this.modalCommentPublishPromise === publishPromise) {
|
||||
this.modalCommentPublishPromise = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleVideoModalCommentLoadMore() {
|
||||
if (!this.commentThreadService || !this.currentVideo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modalCommentLoadPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
const increment = this.commentThreadService.defaultLimit || DEFAULT_LIMIT;
|
||||
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.callbacks.showError(
|
||||
"Failed to load more comments. Please try again.",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
handleVideoModalCommentLoginRequired(detail = {}) {
|
||||
this.applyCommentComposerAuthState();
|
||||
try {
|
||||
this.auth.initializeLoginModalController({ logIfMissing: true });
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[comment] Failed to initialize login modal controller:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
const triggerElement = detail?.triggerElement || null;
|
||||
try {
|
||||
const loginModalController = this.auth.getLoginModalController();
|
||||
if (
|
||||
loginModalController &&
|
||||
typeof loginModalController.openModal === "function"
|
||||
) {
|
||||
const opened = loginModalController.openModal({ triggerElement });
|
||||
if (opened) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.warn("[comment] Failed to open login modal:", error);
|
||||
}
|
||||
|
||||
Promise.resolve(
|
||||
this.auth.requestLogin({ allowAccountSelection: true }),
|
||||
).catch((error) => {
|
||||
devLogger.warn("[comment] Login request failed:", error);
|
||||
});
|
||||
}
|
||||
|
||||
async handleVideoModalCommentMute(detail = {}) {
|
||||
const pubkey = normalizeHexPubkey(detail?.pubkey);
|
||||
if (!pubkey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.auth.isLoggedIn()) {
|
||||
this.handleVideoModalCommentLoginRequired(detail);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.resolve(this.callbacks.muteAuthor(pubkey));
|
||||
const snapshot = this.commentThreadService?.getSnapshot?.();
|
||||
if (snapshot) {
|
||||
this.handleCommentThreadReady(snapshot);
|
||||
}
|
||||
this.callbacks.showStatus?.("Author muted.");
|
||||
} catch (error) {
|
||||
devLogger.warn("[comment] Failed to mute author:", error);
|
||||
this.callbacks.showError("Failed to mute this author. Please try again.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user