Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions apps/web/.migrations/20-06-26_00-00-convert-likes-to-reactions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/**
* Converts existing `likes: string[]` fields to `reactions: { "❤️": string[] }` format
* on CommunityPost and CommunityComment documents.
*
* This migration is idempotent - safe to re-run.
*
* Usage:
* DB_CONNECTION_STRING=<mongodb-connection-string> node 20-06-26_00-00-convert-likes-to-reactions.js
*/
import mongoose from "mongoose";

const DB_CONNECTION_STRING = process.env.DB_CONNECTION_STRING;

if (!DB_CONNECTION_STRING) {
throw new Error("DB_CONNECTION_STRING is not set");
}

(async () => {
try {
await mongoose.connect(DB_CONNECTION_STRING);

const db = mongoose.connection.db;
if (!db) {
throw new Error("Could not connect to database");
}

const BATCH_SIZE = 500;
const HEART_EMOJI = "❤️";

// --- Migrate CommunityPost documents ---
console.log("Migrating CommunityPost documents...");
let postCursor = db
.collection("communityposts")
.find({
$or: [
{
likes: { $exists: true },
$expr: { $gt: [{ $size: "$likes" }, 0] },
},
],
})
.batchSize(BATCH_SIZE);

let postBatch = [];
let postCount = 0;

while (await postCursor.hasNext()) {
const doc = await postCursor.next();
if (!doc) continue;

const update = {
$unset: { likes: "" },
$set: {
reactions: { [HEART_EMOJI]: doc.likes || [] },
},
};

postBatch.push({
updateOne: {
filter: { _id: doc._id },
update,
},
});

if (postBatch.length >= BATCH_SIZE) {
const result = await db
.collection("communityposts")
.bulkWrite(postBatch);
postCount += result.modifiedCount;
console.log(
` Processed ${postCount} CommunityPost documents...`,
);
postBatch = [];
}
}

if (postBatch.length > 0) {
const result = await db
.collection("communityposts")
.bulkWrite(postBatch);
postCount += result.modifiedCount;
}

console.log(`✅ Migrated ${postCount} CommunityPost documents.`);

// --- Migrate CommunityComment documents ---
console.log("Migrating CommunityComment documents...");
let commentCursor = db
.collection("communitycomments")
.find({
$or: [
{
likes: { $exists: true },
$expr: { $gt: [{ $size: "$likes" }, 0] },
},
],
})
.batchSize(BATCH_SIZE);

let commentBatch = [];
let commentCount = 0;

while (await commentCursor.hasNext()) {
const doc = await commentCursor.next();
if (!doc) continue;

const update = {
$unset: { likes: "" },
$set: {
reactions: { [HEART_EMOJI]: doc.likes || [] },
},
};

// Also migrate nested reply likes
if (doc.replies && Array.isArray(doc.replies)) {
const hasReplyLikes = doc.replies.some(
(reply) =>
reply.likes &&
Array.isArray(reply.likes) &&
reply.likes.length > 0,
);

if (hasReplyLikes) {
update.$set["replies"] = doc.replies.map((reply) => {
if (
reply.likes &&
Array.isArray(reply.likes) &&
reply.likes.length > 0
) {
const { likes, ...rest } = reply;
return {
...rest,
reactions: { [HEART_EMOJI]: likes },
};
}
return reply;
});
}
}

commentBatch.push({
updateOne: {
filter: { _id: doc._id },
update,
},
});

if (commentBatch.length >= BATCH_SIZE) {
const result = await db
.collection("communitycomments")
.bulkWrite(commentBatch);
commentCount += result.modifiedCount;
console.log(
` Processed ${commentCount} CommunityComment documents...`,
);
commentBatch = [];
}
}

if (commentBatch.length > 0) {
const result = await db
.collection("communitycomments")
.bulkWrite(commentBatch);
commentCount += result.modifiedCount;
}

console.log(`✅ Migrated ${commentCount} CommunityComment documents.`);

// --- Also migrate docs that have both likes and no reactions ---
console.log(
"Setting default reactions on documents without reactions...",
);
const postResult = await db
.collection("communityposts")
.updateMany(
{ reactions: { $exists: false } },
{ $set: { reactions: {} } },
);
console.log(
` Set default reactions on ${postResult.modifiedCount} CommunityPost documents.`,
);

const commentResult = db
.collection("communitycomments")
.updateMany(
{ reactions: { $exists: false } },
{ $set: { reactions: {} } },
);
const commentResult2 = await commentResult;
console.log(
` Set default reactions on ${commentResult2.modifiedCount} CommunityComment documents.`,
);

console.log("✅ Migration complete!");
} catch (err) {
console.error("Migration failed:", err);
process.exit(1);
} finally {
await mongoose.connection.close();
}
})();
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import NotFound from "@components/admin/not-found";
import { CommunityInfo } from "@components/community/info";
import MembershipStatus from "@components/community/membership-status";
import CommentSection from "@components/community/comment-section";
import { ReactionsBar } from "@components/community/reactions-bar";
import dynamic from "next/dynamic";
import { useMediaLit, useToast } from "@courselit/components-library";
import {
Expand All @@ -35,7 +36,6 @@ import {
Trash,
FlagTriangleRight,
MessageSquare,
ThumbsUp,
} from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Expand Down Expand Up @@ -125,6 +125,20 @@ export default function CommunityPostPage({
}
}
likesCount
reactions {
emoji
count
hasReacted
reactors {
userId
name
avatar {
mediaId
file
thumbnail
}
}
}
commentsCount
updatedAt
hasLiked
Expand Down Expand Up @@ -210,6 +224,58 @@ export default function CommunityPostPage({
}
};

const handleReact = async (targetPostId: string, emoji: string) => {
const query = `
mutation ($communityId: String!, $postId: String!, $emoji: String!) {
togglePostReaction(communityId: $communityId, postId: $postId, emoji: $emoji) {
postId
reactions {
emoji
count
hasReacted
reactors {
userId
name
avatar {
mediaId
file
thumbnail
}
}
}
}
}
`;

try {
const fetch = new FetchBuilder()
.setUrl(`${address.backend}/api/graph`)
.setPayload({
query,
variables: { communityId, postId: targetPostId, emoji },
})
.setIsGraphQLEndpoint(true)
.build();
const response = await fetch.exec();
if (response.togglePostReaction) {
setPost((prev) =>
prev && prev.postId === targetPostId
? {
...prev,
reactions: response.togglePostReaction.reactions,
}
: prev,
);
}
} catch (err: any) {
toast({
title: TOAST_TITLE_ERROR,
description: err.message,
variant: "destructive",
});
}
};

const uploadAttachments = useCallback(
async (media: MediaItem[]) => {
for (const i in media) {
Expand Down Expand Up @@ -644,15 +710,12 @@ export default function CommunityPostPage({
</div>
)}
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="sm"
className={`text-muted-foreground ${currentPost.hasLiked ? "bg-accent" : ""}`}
onClick={() => handleLike(currentPost.postId)}
>
<ThumbsUp className="mr-2 h-4 w-4" />
{currentPost.likesCount}
</Button>
<ReactionsBar
reactions={currentPost.reactions || []}
onReact={(emoji) =>
handleReact(currentPost.postId, emoji)
}
/>
<Button
variant="ghost"
size="sm"
Expand Down
8 changes: 6 additions & 2 deletions apps/web/app/api/graph/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import schema from "@/graphql";
import { graphql } from "graphql";
import { getAddress } from "@/lib/utils";
import User from "@models/User";
import { auth } from "@/auth";
import { getAuth } from "@/auth";
import { als } from "@/async-local-storage";
import { getCachedDomain } from "@/lib/domain-cache";
import { getBackendAddress } from "@/app/actions";

async function updateLastActive(user: any) {
const dateNow = new Date();
Expand All @@ -28,9 +29,12 @@ export async function POST(req: NextRequest) {
);
}

const backendAddress = await getBackendAddress(req.headers);
const currentAuth = getAuth(backendAddress);

const [domain, session, body] = await Promise.all([
getCachedDomain(domainName),
auth.api.getSession({ headers: req.headers }),
currentAuth.api.getSession({ headers: req.headers }),
req.json(),
]);

Expand Down
Loading
Loading