@replyke/core
Version:
Replyke: Build interactive apps with social features like comments, votes, feeds, user lists, notifications, and more.
358 lines • 15.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const react_1 = require("react");
const useCreateComment_1 = __importDefault(require("./useCreateComment"));
const handleError_1 = require("../../utils/handleError");
const useDeleteComment_1 = __importDefault(require("./useDeleteComment"));
const useUpdateComment_1 = __importDefault(require("./useUpdateComment"));
const useAddReaction_1 = __importDefault(require("../reactions/useAddReaction"));
const useEntityComments_1 = __importDefault(require("./useEntityComments"));
const useFetchComment_1 = __importDefault(require("./useFetchComment"));
const user_1 = require("../user");
const entities_1 = require("../entities");
const isUUID_1 = require("../../utils/isUUID");
const useStableObject_1 = require("../useStableObject");
function useCommentSectionData(props) {
const { entity: entityProp, entityId, foreignId, shortId, createIfNotFound, defaultSortBy = "top", limit = 15, callbacks: callbacksProp = {}, highlightedCommentId, mentionTriggers: mentionTriggersProp, } = props;
const mentionTriggers = {
user: mentionTriggersProp?.user ?? "@",
space: mentionTriggersProp?.space ?? "#",
};
// Stabilize callbacks reference to prevent unnecessary re-renders
const callbacks = (0, useStableObject_1.useStableObject)(callbacksProp);
const { entity: entityFromContext, setEntity: setContextEntity } = (0, entities_1.useEntity)();
const [entity, setEntity] = (0, react_1.useState)(entityProp ?? entityFromContext);
const { user } = (0, user_1.useUser)();
const { entityCommentsTree, comments, newComments, loading, hasMore, sortBy, setSortBy, loadMore, addCommentsToTree, removeCommentFromTree, markCommentAsDeleted, } = (0, useEntityComments_1.default)({
entityId: entity?.id,
defaultSortBy,
limit,
include: "user",
});
const createComment = (0, useCreateComment_1.default)();
const deleteComment = (0, useDeleteComment_1.default)();
const updateComment = (0, useUpdateComment_1.default)();
const addReaction = (0, useAddReaction_1.default)();
const fetchComment = (0, useFetchComment_1.default)();
const fetchEntity = (0, entities_1.useFetchEntity)();
const fetchEntityByForeignId = (0, entities_1.useFetchEntityByForeignId)();
const fetchEntityByShortId = (0, entities_1.useFetchEntityByShortId)();
const [highlightedComment, setHighlightedComment] = (0, react_1.useState)(null);
const fetchingCommentIdRef = (0, react_1.useRef)(null);
const fetchedStatus = (0, react_1.useRef)({}); // Track status by unique key
const submittingComment = (0, react_1.useRef)(false);
const [submittingCommentState, setSubmittingCommentState] = (0, react_1.useState)(false); // required to trigger rerenders
const [pushMention, setPushMention] = (0, react_1.useState)(null);
// const previousPushMention = useRef<null | User>(null);
const [repliedToComment, setRepliedToComment] = (0, react_1.useState)(null);
const [showReplyBanner, setShowReplyBanner] = (0, react_1.useState)(false);
const setShowReplyBannerHandler = (0, react_1.useCallback)(({ newState }) => {
setShowReplyBanner(newState);
}, []);
const [selectedComment, setSelectedComment] = (0, react_1.useState)(null);
// const handleSetPushMention = (user: User | null) => {
// if(!user?.username)
// setPushMention((prevMention) => {
// if (JSON.stringify(prevMention) === JSON.stringify(user)) {
// return prevMention;
// }
// return user;
// });
// };
// For replies that appear as a child of the comment they are replying to.
const handleDeepReply = (0, react_1.useCallback)((comment) => {
setRepliedToComment(comment);
setShowReplyBanner(true);
}, [setRepliedToComment]);
// For replies that appear at the same level as the comment they are replying to. Includes a mention (e.g. @username).
const handleShallowReply = (0, react_1.useCallback)((comment) => {
setRepliedToComment({ id: comment.parentId ?? undefined });
if (comment.user)
setPushMention(comment.user);
}, [setRepliedToComment]);
const handleCreateComment = (0, react_1.useCallback)(async (props) => {
const { parentId, content, gif, mentions, autoReaction } = props;
if (submittingComment.current)
return;
if (!entity) {
console.error("Invalid entity in useCommentSection");
return;
}
if (!user) {
callbacks?.loginRequiredCallback?.();
return;
}
if (callbacks?.usernameRequiredCallback && !user.username) {
callbacks?.usernameRequiredCallback();
return;
}
if (!gif && (!content || content.length <= 1)) {
callbacks?.commentTooShortCallback();
return;
}
submittingComment.current = true;
setSubmittingCommentState(true);
// Filter mentions to include only those whose trigger + identifier appears in the content
const filteredMentions = content
? (mentions || []).filter((mention) => {
if (mention.type === "space") {
return content.includes(mentionTriggers.space + mention.slug);
}
return content.includes(mentionTriggers.user + mention.username);
})
: [];
const TEMP_ID = Math.random().toString(36).substring(2, 7);
const tempNewComment = {
id: TEMP_ID,
foreignId: null,
projectId: "TEMP_PROJECT_ID",
userId: user.id,
parentId: parentId ?? repliedToComment?.id ?? null,
entityId: entity.id,
content: content ?? null,
gif: gif ?? null,
mentions: filteredMentions,
user: {
...user,
bio: null,
birthdate: new Date(),
location: null,
createdAt: new Date(),
avatarFileId: null,
bannerFileId: null,
},
upvotes: [],
downvotes: [],
userReaction: autoReaction ?? null,
reactionCounts: {
upvote: autoReaction === "upvote" ? 1 : 0,
downvote: autoReaction === "downvote" ? 1 : 0,
like: autoReaction === "like" ? 1 : 0,
love: autoReaction === "love" ? 1 : 0,
wow: autoReaction === "wow" ? 1 : 0,
sad: autoReaction === "sad" ? 1 : 0,
angry: autoReaction === "angry" ? 1 : 0,
funny: autoReaction === "funny" ? 1 : 0,
},
createdAt: new Date(),
updatedAt: new Date(),
deletedAt: null,
parentDeletedAt: null,
userDeletedAt: null,
repliesCount: 0,
metadata: {},
moderationStatus: null,
moderatedAt: null,
moderatedById: null,
moderatedByType: null,
moderationReason: null,
};
setRepliedToComment(null);
setShowReplyBanner(false);
setPushMention(null);
try {
addCommentsToTree([tempNewComment], true);
const newCommentData = await createComment({
entityId: entity.id,
parentCommentId: parentId ?? repliedToComment?.id ?? null,
content,
gif,
mentions: filteredMentions,
});
if (newCommentData) {
removeCommentFromTree({ commentId: TEMP_ID });
if (autoReaction) {
// Add comment with optimistic reaction data while the API call is in flight
addCommentsToTree([{ ...newCommentData, userReaction: autoReaction, reactionCounts: { ...newCommentData.reactionCounts, [autoReaction]: (newCommentData.reactionCounts?.[autoReaction] ?? 0) + 1 } }], true);
// Fire-and-forget: update the tree with server truth when the reaction API resolves
addReaction({
targetType: "comment",
targetId: newCommentData.id,
reactionType: autoReaction,
})
.then((updatedComment) => {
addCommentsToTree([updatedComment], true);
})
.catch(() => { });
}
else {
addCommentsToTree([newCommentData], true);
}
}
setContextEntity?.((prevEntity) => {
if (!prevEntity)
return prevEntity;
return { ...prevEntity, repliesCount: prevEntity.repliesCount + 1 };
});
return newCommentData;
}
catch (err) {
// TODO: currently we remove the temp comment from the tree but don't offer the user any option to retry. It's as if they've never sent anything and all they typed is gone. We need to add a flag for comment in the tree that says t failed so we can give he user a try again button
removeCommentFromTree({ commentId: TEMP_ID });
(0, handleError_1.handleError)(err, "Failed to submit a new comment: ");
return undefined;
}
finally {
submittingComment.current = false;
setSubmittingCommentState(false);
}
}, [
user,
addCommentsToTree,
removeCommentFromTree,
entity,
createComment,
addReaction,
repliedToComment,
callbacks,
]);
const handleDeleteComment = (0, react_1.useCallback)(async ({ commentId }) => {
if (!(0, isUUID_1.isUUID)(commentId))
return;
try {
// Reddit-style: mark as deleted placeholder instead of removing from tree
markCommentAsDeleted({ commentId });
await deleteComment({ commentId });
setContextEntity?.((prevEntity) => {
if (!prevEntity)
return prevEntity;
return { ...prevEntity, repliesCount: prevEntity.repliesCount - 1 };
});
}
catch (err) {
(0, handleError_1.handleError)(err, "Failed to delete comment");
}
}, [deleteComment, markCommentAsDeleted]);
const handleUpdateComment = (0, react_1.useCallback)(async ({ commentId, content }) => {
try {
const updatedComment = await updateComment({ commentId, content });
if (updatedComment) {
console.log("update comment in tree. Implement!");
}
}
catch (err) {
(0, handleError_1.handleError)(err, "Failed to update comment");
}
}, [updateComment]);
(0, react_1.useEffect)(() => {
const handleFetchSingleComment = async () => {
if (fetchingCommentIdRef.current === highlightedCommentId) {
return; // Skip if already fetching for this comment ID
}
fetchingCommentIdRef.current = highlightedCommentId;
try {
const fetchedCommentData = await fetchComment({
commentId: highlightedCommentId,
include: ["user", "parent"],
});
if (!fetchedCommentData) {
console.error("Issue fetching single comment comment not found");
return;
}
if (!fetchedCommentData.comment) {
console.error("Highlighted comment not found");
return;
}
const targetComment = fetchedCommentData.comment;
const parentComment = targetComment.parentComment ?? null;
// Maintain backward-compatible state structure
setHighlightedComment({
comment: targetComment,
parentComment: parentComment,
});
addCommentsToTree?.(parentComment ? [targetComment, parentComment] : [targetComment]);
}
catch (err) {
(0, handleError_1.handleError)(err, "Fetching single comment failed");
}
};
if (highlightedCommentId) {
handleFetchSingleComment();
}
}, [highlightedCommentId, fetchComment, addCommentsToTree]);
(0, react_1.useEffect)(() => {
const handleFetchEntity = async () => {
if (!foreignId && !entityId && !shortId) {
return;
}
if (entity && entityId && entity.id === entityId)
return;
if (entity && foreignId && entity.foreignId === foreignId)
return;
if (entity && shortId && entity.shortId === shortId)
return;
const uniqueKey = `${entityId ?? ""}-${foreignId ?? ""}-${shortId ?? ""}`;
if (fetchedStatus.current[uniqueKey])
return;
fetchedStatus.current[uniqueKey] = true;
try {
let fetchedEntity = null;
if (entityId) {
fetchedEntity = await fetchEntity({
entityId,
});
}
else if (foreignId) {
fetchedEntity = await fetchEntityByForeignId({
foreignId,
createIfNotFound,
});
}
else if (shortId) {
fetchedEntity = await fetchEntityByShortId({
shortId,
});
}
if (fetchedEntity) {
setEntity(fetchedEntity);
}
}
catch (err) {
(0, handleError_1.handleError)(err, "Fetching entity failed");
}
};
handleFetchEntity();
}, [
fetchEntity,
fetchEntityByForeignId,
fetchEntityByShortId,
entityId,
foreignId,
shortId,
entity,
createIfNotFound,
]);
return {
entity,
callbacks,
entityCommentsTree,
comments,
newComments,
highlightedComment,
loading,
hasMore,
submittingComment: submittingCommentState,
loadMore,
sortBy,
setSortBy,
pushMention,
selectedComment,
setSelectedComment,
repliedToComment,
setRepliedToComment,
showReplyBanner,
setShowReplyBanner: setShowReplyBannerHandler,
addCommentsToTree,
removeCommentFromTree,
handleShallowReply,
handleDeepReply,
createComment: handleCreateComment,
updateComment: handleUpdateComment,
deleteComment: handleDeleteComment,
};
}
exports.default = useCommentSectionData;
//# sourceMappingURL=useCommentSectionData.js.map