diff --git a/apps/web/app/(with-contexts)/course/[slug]/[id]/[lesson]/page.tsx b/apps/web/app/(with-contexts)/course/[slug]/[id]/[lesson]/page.tsx
index 160ffeea5..d837fb494 100644
--- a/apps/web/app/(with-contexts)/course/[slug]/[id]/[lesson]/page.tsx
+++ b/apps/web/app/(with-contexts)/course/[slug]/[id]/[lesson]/page.tsx
@@ -1,10 +1,12 @@
"use client";
import { LessonViewer } from "@components/public/lesson-viewer";
+import { LessonDiscussionPanel } from "@components/public/lesson-discussion-panel";
import { redirect } from "next/navigation";
-import { useContext, use } from "react";
+import { useContext, use, useEffect, useState } from "react";
import { ProfileContext, AddressContext } from "@components/contexts";
import { Profile } from "@courselit/common-models";
+import { FetchBuilder } from "@courselit/utils";
export default function LessonPage(props: {
params: Promise<{
@@ -17,19 +19,135 @@ export default function LessonPage(props: {
const { slug, id, lesson } = params;
const { profile, setProfile } = useContext(ProfileContext);
const address = useContext(AddressContext);
+ const [discussionsEnabled, setDiscussionsEnabled] = useState(false);
+
+ useEffect(() => {
+ if (id) {
+ loadCourseDiscussionsStatus();
+ }
+ }, [id]);
+
+ const loadCourseDiscussionsStatus = async () => {
+ try {
+ const query = `
+ query {
+ course: getCourse(id: "${id}") {
+ discussions
+ }
+ }
+ `;
+ const fetch = new FetchBuilder()
+ .setUrl(`${address.backend}/api/graph`)
+ .setPayload(query)
+ .setIsGraphQLEndpoint(true)
+ .build();
+ const response = await fetch.exec();
+ if (response.course) {
+ setDiscussionsEnabled(!!response.course.discussions);
+ }
+ } catch (err) {
+ // ignore
+ }
+ };
if (!lesson) {
redirect(`/course/${slug}/${id}`);
}
return (
-
+
+
+
+
+ {discussionsEnabled && (
+ <>
+
+
+
+
+
+
+
+
+ >
+ )}
+
+ );
+}
+
+function MobileDiscussionDrawer({ children }: { children: React.ReactNode }) {
+ const [open, setOpen] = useState(false);
+
+ return (
+ <>
+
+ {open && (
+
+
setOpen(false)}
+ />
+
+
+
Discussions
+
+
+
{children}
+
+
+ )}
+ >
);
}
diff --git a/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/manage/components/course-discussions.tsx b/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/manage/components/course-discussions.tsx
new file mode 100644
index 000000000..e1683b956
--- /dev/null
+++ b/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/manage/components/course-discussions.tsx
@@ -0,0 +1,98 @@
+"use client";
+
+import { useState } from "react";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
+import { Separator } from "@/components/ui/separator";
+import { useToast } from "@courselit/components-library";
+import {
+ APP_MESSAGE_COURSE_SAVED,
+ TOAST_TITLE_ERROR,
+ TOAST_TITLE_SUCCESS,
+} from "@ui-config/strings";
+import { responses } from "@/config/strings";
+import { useGraphQLFetch } from "@/hooks/use-graphql-fetch";
+
+const MUTATION_UPDATE_DISCUSSIONS = `
+ mutation UpdateDiscussions($courseId: String!, $discussions: Boolean!) {
+ updateCourse(courseData: { id: $courseId, discussions: $discussions }) {
+ courseId
+ }
+ }
+`;
+
+interface CourseDiscussionsProps {
+ product: any;
+}
+
+export default function CourseDiscussions({ product }: CourseDiscussionsProps) {
+ const { toast } = useToast();
+ const fetch = useGraphQLFetch();
+ const [loading, setLoading] = useState(false);
+ const [discussionsEnabled, setDiscussionsEnabled] = useState(
+ product?.discussions || false,
+ );
+
+ const handleDiscussionsChange = async () => {
+ const newValue = !discussionsEnabled;
+ const previousValue = discussionsEnabled;
+ setDiscussionsEnabled(newValue);
+
+ if (!product?.courseId) return;
+
+ try {
+ setLoading(true);
+ const response = await fetch
+ .setPayload({
+ query: MUTATION_UPDATE_DISCUSSIONS,
+ variables: {
+ courseId: product.courseId,
+ discussions: newValue,
+ },
+ })
+ .build()
+ .exec();
+
+ if (response?.updateCourse) {
+ toast({
+ title: TOAST_TITLE_SUCCESS,
+ description: newValue
+ ? responses.discussions_enabled
+ : responses.discussions_disabled,
+ });
+ }
+ } catch (err: any) {
+ setDiscussionsEnabled(previousValue);
+ toast({
+ title: TOAST_TITLE_ERROR,
+ description: err.message,
+ variant: "destructive",
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ {responses.discussions_toggle_description}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/manage/page.tsx b/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/manage/page.tsx
index 75a1ec2bf..1eaf89b7c 100644
--- a/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/manage/page.tsx
+++ b/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/manage/page.tsx
@@ -19,6 +19,7 @@ import PaymentPlans from "./components/payment-plans";
import DownloadOptions from "./components/download-options";
import ProductPublishing from "./components/product-publishing";
import Certificates from "./components/certificates";
+import CourseDiscussions from "./components/course-discussions";
import ProductDeletion from "./components/product-deletion";
const { permissions } = UIConstants;
@@ -109,6 +110,7 @@ export default function SettingsPage() {
/>
+
diff --git a/apps/web/components/admin/settings/index.tsx b/apps/web/components/admin/settings/index.tsx
index 4bd92981c..59e5b9cae 100644
--- a/apps/web/components/admin/settings/index.tsx
+++ b/apps/web/components/admin/settings/index.tsx
@@ -813,6 +813,10 @@ const Settings = (props: SettingsProps) => {
newSettings.paymentMethod || PAYMENT_METHOD_NONE
}
options={[
+ {
+ label: SITE_SETTINGS_PAYMENT_METHOD_NONE_LABEL,
+ value: PAYMENT_METHOD_NONE,
+ },
{
label: capitalize(
PAYMENT_METHOD_STRIPE.toLowerCase(),
@@ -857,9 +861,6 @@ const Settings = (props: SettingsProps) => {
}),
)
}
- placeholderMessage={
- SITE_SETTINGS_PAYMENT_METHOD_NONE_LABEL
- }
disabled={!newSettings.currencyISOCode}
/>
diff --git a/apps/web/components/public/lesson-discussion-panel.tsx b/apps/web/components/public/lesson-discussion-panel.tsx
new file mode 100644
index 000000000..ebf17a9c7
--- /dev/null
+++ b/apps/web/components/public/lesson-discussion-panel.tsx
@@ -0,0 +1,520 @@
+"use client";
+
+import { useEffect, useState, useContext } from "react";
+import { FetchBuilder } from "@courselit/utils";
+import { useRouter } from "next/navigation";
+import {
+ AddressContext,
+ ProfileContext,
+ ThemeContext,
+} from "@components/contexts";
+import { Button } from "@components/ui/button";
+import { Textarea } from "@components/ui/textarea";
+import { Avatar, AvatarFallback } from "@components/ui/avatar";
+import { Skeleton } from "@components/ui/skeleton";
+import { useToast } from "@courselit/components-library";
+import { MessageSquare, X, Heart, Reply, ChevronRight } from "lucide-react";
+import {
+ LESSON_DISCUSSIONS_HEADER,
+ LESSON_DISCUSSIONS_WRITE_COMMENT,
+ LESSON_DISCUSSIONS_VIEW_ALL,
+ LESSON_DISCUSSIONS_EMPTY,
+ TOAST_TITLE_ERROR,
+ DELETED_COMMENT_PLACEHOLDER,
+} from "@ui-config/strings";
+import { truncate } from "@courselit/utils";
+import type { Profile } from "@courselit/common-models";
+
+interface LessonDiscussionPanelProps {
+ courseId: string;
+ lessonId: string;
+ slug: string;
+}
+
+interface Comment {
+ commentId: string;
+ content: string;
+ userId: string;
+ likesCount: number;
+ hasLiked: boolean;
+ updatedAt: string;
+ replies: Reply[];
+ deleted: boolean;
+ user?: {
+ userId: string;
+ name: string;
+ email: string;
+ avatar?: { file?: string };
+ };
+}
+
+interface Reply {
+ replyId: string;
+ content: string;
+ userId: string;
+ likesCount: number;
+ hasLiked: boolean;
+ parentReplyId?: string;
+ updatedAt: string;
+ deleted: boolean;
+ user?: {
+ userId: string;
+ name: string;
+ email: string;
+ avatar?: { file?: string };
+ };
+}
+
+interface Post {
+ postId: string;
+ communityId: string;
+ title: string;
+ lessonId: string;
+}
+
+export function LessonDiscussionPanel({
+ courseId,
+ lessonId,
+ slug,
+}: LessonDiscussionPanelProps) {
+ const [post, setPost] = useState(null);
+ const [comments, setComments] = useState([]);
+ const [newComment, setNewComment] = useState("");
+ const [replyTo, setReplyTo] = useState(null);
+ const [replyContent, setReplyContent] = useState("");
+ const [loading, setLoading] = useState(true);
+ const [submitting, setSubmitting] = useState(false);
+
+ const address = useContext(AddressContext);
+ const { profile } = useContext(ProfileContext);
+ const { toast } = useToast();
+ const router = useRouter();
+
+ useEffect(() => {
+ if (courseId && lessonId) {
+ loadDiscussionPost();
+ }
+ }, [courseId, lessonId]);
+
+ const loadDiscussionPost = async () => {
+ setLoading(true);
+ try {
+ const query = `
+ query {
+ posts: getCourseDiscussionPosts(
+ courseId: "${courseId}",
+ lessonId: "${lessonId}"
+ ) {
+ postId
+ communityId
+ title
+ lessonId
+ }
+ }
+ `;
+ const fetch = new FetchBuilder()
+ .setUrl(`${address.backend}/api/graph`)
+ .setPayload(query)
+ .setIsGraphQLEndpoint(true)
+ .build();
+ const response = await fetch.exec();
+ if (response.posts && response.posts.length > 0) {
+ const discussionPost = response.posts[0];
+ setPost(discussionPost);
+ await loadComments(
+ discussionPost.communityId,
+ discussionPost.postId,
+ );
+ }
+ } catch (err: any) {
+ // Discussion not available
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const loadComments = async (communityId: string, postId: string) => {
+ try {
+ const query = `
+ query {
+ comments: getComments(
+ communityId: "${communityId}",
+ postId: "${postId}"
+ ) {
+ commentId
+ content
+ userId
+ likesCount
+ hasLiked
+ updatedAt
+ deleted
+ user {
+ userId
+ name
+ email
+ avatar {
+ file
+ }
+ }
+ replies {
+ replyId
+ content
+ userId
+ likesCount
+ hasLiked
+ parentReplyId
+ updatedAt
+ deleted
+ user {
+ userId
+ name
+ email
+ avatar {
+ file
+ }
+ }
+ }
+ }
+ }
+ `;
+ const fetch = new FetchBuilder()
+ .setUrl(`${address.backend}/api/graph`)
+ .setPayload(query)
+ .setIsGraphQLEndpoint(true)
+ .build();
+ const response = await fetch.exec();
+ if (response.comments) {
+ setComments(response.comments);
+ }
+ } catch (err: any) {
+ // Comments unavailable
+ }
+ };
+
+ const handlePostComment = async () => {
+ if (!newComment.trim() || !post) return;
+ setSubmitting(true);
+ try {
+ const query = `
+ mutation {
+ comment: postComment(
+ communityId: "${post.communityId}",
+ postId: "${post.postId}",
+ content: "${escapeString(newComment)}"
+ ) {
+ commentId
+ }
+ }
+ `;
+ const fetch = new FetchBuilder()
+ .setUrl(`${address.backend}/api/graph`)
+ .setPayload(query)
+ .setIsGraphQLEndpoint(true)
+ .build();
+ await fetch.exec();
+ setNewComment("");
+ await loadComments(post.communityId, post.postId);
+ } catch (err: any) {
+ toast({
+ title: TOAST_TITLE_ERROR,
+ description: err.message,
+ variant: "destructive",
+ });
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handlePostReply = async (commentId: string) => {
+ if (!replyContent.trim() || !post) return;
+ setSubmitting(true);
+ try {
+ const query = `
+ mutation {
+ comment: postComment(
+ communityId: "${post.communityId}",
+ postId: "${post.postId}",
+ content: "${escapeString(replyContent)}",
+ parentCommentId: "${commentId}"
+ ) {
+ commentId
+ }
+ }
+ `;
+ const fetch = new FetchBuilder()
+ .setUrl(`${address.backend}/api/graph`)
+ .setPayload(query)
+ .setIsGraphQLEndpoint(true)
+ .build();
+ await fetch.exec();
+ setReplyTo(null);
+ setReplyContent("");
+ await loadComments(post.communityId, post.postId);
+ } catch (err: any) {
+ toast({
+ title: TOAST_TITLE_ERROR,
+ description: err.message,
+ variant: "destructive",
+ });
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleToggleLike = async (commentId: string) => {
+ if (!post) return;
+ try {
+ const query = `
+ mutation {
+ comment: toggleCommentLike(
+ communityId: "${post.communityId}",
+ postId: "${post.postId}",
+ commentId: "${commentId}"
+ ) {
+ commentId
+ }
+ }
+ `;
+ const fetch = new FetchBuilder()
+ .setUrl(`${address.backend}/api/graph`)
+ .setPayload(query)
+ .setIsGraphQLEndpoint(true)
+ .build();
+ await fetch.exec();
+ await loadComments(post.communityId, post.postId);
+ } catch (err: any) {
+ // ignore
+ }
+ };
+
+ const getInitials = (name?: string, email?: string) => {
+ const displayName = name || email || "U";
+ return displayName
+ .split(" ")
+ .map((n) => n[0])
+ .join("")
+ .substring(0, 2)
+ .toUpperCase();
+ };
+
+ return (
+
+
+
+
+
+ {LESSON_DISCUSSIONS_HEADER}
+
+
+
+
+
+ {loading && (
+
+
+
+
+ )}
+
+ {!loading && comments.length === 0 && (
+
+ {LESSON_DISCUSSIONS_EMPTY}
+
+ )}
+
+ {comments.map((comment) => (
+
+
+
+
+ {getInitials(
+ comment.user?.name,
+ comment.user?.email,
+ )}
+
+
+
+
+
+ {comment.user?.name ||
+ comment.user?.email ||
+ "User"}
+
+
+ {new Date(
+ comment.updatedAt,
+ ).toLocaleDateString()}
+
+
+
+ {comment.deleted
+ ? DELETED_COMMENT_PLACEHOLDER
+ : comment.content}
+
+ {!comment.deleted && (
+
+
+
+
+ )}
+
+ {replyTo === comment.commentId && (
+
+ )}
+
+ {comment.replies?.length > 0 && (
+
+ {comment.replies.map((reply) => (
+
+
+
+ {getInitials(
+ reply.user?.name,
+ reply.user?.email,
+ )}
+
+
+
+
+
+ {reply.user?.name ||
+ reply.user
+ ?.email ||
+ "User"}
+
+
+ {new Date(
+ reply.updatedAt,
+ ).toLocaleDateString()}
+
+
+
+ {reply.deleted
+ ? DELETED_COMMENT_PLACEHOLDER
+ : reply.content}
+
+
+
+ ))}
+
+ )}
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+}
+
+function escapeString(str: string): string {
+ return str
+ .replace(/\\/g, "\\\\")
+ .replace(/"/g, '\\"')
+ .replace(/\n/g, "\\n");
+}
diff --git a/apps/web/config/strings.ts b/apps/web/config/strings.ts
index fd57db2f7..7bdb22ff9 100644
--- a/apps/web/config/strings.ts
+++ b/apps/web/config/strings.ts
@@ -154,6 +154,12 @@ export const responses = {
"CourseID is required for demo certificate",
provider_not_configured: "Configure the provider before enabling",
provider_invalid_configuration: "Invalid provider configuration",
+ discussions_enabled:
+ "Discussions are active — students can comment on each lesson",
+ discussions_disabled: "Discussions are disabled",
+ discussions_toggle_label: "Course Discussions",
+ discussions_toggle_description:
+ "Allow students to discuss each lesson directly on the lesson page.",
};
export const internal = {
diff --git a/apps/web/graphql/communities/logic.ts b/apps/web/graphql/communities/logic.ts
index cde977f07..f7b291bd0 100644
--- a/apps/web/graphql/communities/logic.ts
+++ b/apps/web/graphql/communities/logic.ts
@@ -182,6 +182,7 @@ export async function getCommunities({
const query: Partial = {
domain: ctx.subdomain._id,
deleted: false,
+ courseId: { $exists: false },
};
if (
@@ -217,6 +218,7 @@ export async function getCommunities({
}),
defaultPaymentPlan: community.defaultPaymentPlan,
featuredImage: community.featuredImage,
+ courseId: community.courseId,
membersCount: await getMembersCount({
ctx,
communityId: community.communityId,
@@ -232,6 +234,7 @@ export async function getCommunitiesCount({
}): Promise {
const query: Partial = {
domain: ctx.subdomain._id,
+ courseId: { $exists: false },
};
if (
@@ -686,10 +689,10 @@ export async function deleteCommunityPost({
throw new Error(responses.item_not_found);
}
- post.deleted = true;
+ post.deleted = true as any;
await (post as any).save();
- return post;
+ return post as any;
}
export async function getPost({
@@ -953,6 +956,7 @@ async function formatCommunity(
joiningReasonText: community.joiningReasonText,
defaultPaymentPlan: community.defaultPaymentPlan,
featuredImage: community.featuredImage,
+ courseId: community.courseId,
membersCount: await getMembersCount({
ctx,
communityId: community.communityId,
@@ -1374,7 +1378,9 @@ export async function postComment({
await addNotification({
domain: ctx.subdomain._id.toString(),
entityId: replyId,
- entityAction: Constants.NotificationEntityAction.COMMUNITY_REPLIED,
+ entityAction: isCourseDiscussion
+ ? Constants.NotificationEntityAction.COURSE_DISCUSSION_REPLIED
+ : Constants.NotificationEntityAction.COMMUNITY_REPLIED,
forUserIds: postSubscribers.map((s) => s.userId),
userId: ctx.user.userId,
entityTargetId: comment.commentId,
@@ -1398,8 +1404,9 @@ export async function postComment({
await addNotification({
domain: ctx.subdomain._id.toString(),
entityId: post.postId,
- entityAction:
- Constants.NotificationEntityAction.COMMUNITY_COMMENTED,
+ entityAction: isCourseDiscussion
+ ? Constants.NotificationEntityAction.COURSE_DISCUSSION_COMMENTED
+ : Constants.NotificationEntityAction.COMMUNITY_COMMENTED,
forUserIds: postSubscribers.map((s) => s.userId),
userId: ctx.user.userId,
});
@@ -1491,6 +1498,13 @@ export async function toggleCommentLike({
throw new Error(responses.item_not_found);
}
+ const post = (await CommunityPostModel.findOne({
+ domain: ctx.subdomain._id,
+ communityId,
+ postId,
+ }).lean()) as unknown as CommunityPost;
+ const isCourseDiscussion = !!(post as any)?.lessonId;
+
const member = await getMembership(ctx, communityId);
if (!member || !hasPermission(member, Constants.MembershipRole.COMMENT)) {
@@ -1508,11 +1522,13 @@ export async function toggleCommentLike({
await comment.save();
if (liked && comment.userId !== ctx.user.userId) {
+ const entityAction = isCourseDiscussion
+ ? Constants.NotificationEntityAction.COURSE_DISCUSSION_COMMENT_LIKED
+ : Constants.NotificationEntityAction.COMMUNITY_COMMENT_LIKED;
const existingNotification = await NotificationModel.findOne({
domain: ctx.subdomain._id,
entityId: comment.commentId,
- entityAction:
- Constants.NotificationEntityAction.COMMUNITY_COMMENT_LIKED,
+ entityAction,
forUserId: comment.userId,
userId: ctx.user.userId,
});
@@ -1520,8 +1536,7 @@ export async function toggleCommentLike({
await addNotification({
domain: ctx.subdomain._id.toString(),
entityId: comment.commentId,
- entityAction:
- Constants.NotificationEntityAction.COMMUNITY_COMMENT_LIKED,
+ entityAction,
forUserIds: [comment.userId],
userId: ctx.user.userId,
});
@@ -1566,6 +1581,13 @@ export async function toggleCommentReplyLike({
throw new Error(responses.item_not_found);
}
+ const post = (await CommunityPostModel.findOne({
+ domain: ctx.subdomain._id,
+ communityId,
+ postId,
+ }).lean()) as unknown as CommunityPost;
+ const isCourseDiscussion = !!(post as any)?.lessonId;
+
const member = await getMembership(ctx, communityId);
if (!member || !hasPermission(member, Constants.MembershipRole.COMMENT)) {
@@ -1589,11 +1611,13 @@ export async function toggleCommentReplyLike({
await comment.save();
if (liked && reply.userId !== ctx.user.userId) {
+ const entityAction = isCourseDiscussion
+ ? Constants.NotificationEntityAction.COURSE_DISCUSSION_REPLY_LIKED
+ : Constants.NotificationEntityAction.COMMUNITY_REPLY_LIKED;
const existingNotification = await NotificationModel.findOne({
domain: ctx.subdomain._id,
entityId: reply.replyId,
- entityAction:
- Constants.NotificationEntityAction.COMMUNITY_REPLY_LIKED,
+ entityAction,
forUserId: reply.userId,
userId: ctx.user.userId,
});
@@ -1601,8 +1625,7 @@ export async function toggleCommentReplyLike({
await addNotification({
domain: ctx.subdomain._id.toString(),
entityId: reply.replyId,
- entityAction:
- Constants.NotificationEntityAction.COMMUNITY_REPLY_LIKED,
+ entityAction,
forUserIds: [reply.userId],
userId: ctx.user.userId,
entityTargetId: comment.commentId,
diff --git a/apps/web/graphql/communities/types.ts b/apps/web/graphql/communities/types.ts
index db8c5e0dd..278a67fc8 100644
--- a/apps/web/graphql/communities/types.ts
+++ b/apps/web/graphql/communities/types.ts
@@ -62,6 +62,7 @@ const community = new GraphQLObjectType({
defaultPaymentPlan: { type: GraphQLString },
featuredImage: { type: mediaTypes.mediaType },
membersCount: { type: new GraphQLNonNull(GraphQLInt) },
+ courseId: { type: GraphQLString },
},
});
diff --git a/apps/web/graphql/courses/logic.ts b/apps/web/graphql/courses/logic.ts
index d0c6ed860..ad57dc63a 100644
--- a/apps/web/graphql/courses/logic.ts
+++ b/apps/web/graphql/courses/logic.ts
@@ -35,7 +35,7 @@ import { deleteAllLessons } from "../lessons/logic";
import { deleteMedia } from "@/services/medialit";
import PageModel from "@/models/Page";
import { getPrevNextCursor } from "../lessons/helpers";
-import { checkPermission } from "@courselit/utils";
+import { checkPermission, generateUniqueId, slugify } from "@courselit/utils";
import { error } from "@/services/logger";
import {
deleteProductsFromPaymentPlans,
@@ -56,6 +56,10 @@ import getDeletedMediaIds, {
extractMediaIDs,
} from "@/lib/get-deleted-media-ids";
import { deletePageInternal } from "../pages/logic";
+import CommunityModel from "@models/Community";
+import CommunityPostModel from "@models/CommunityPost";
+import { getInternalPaymentPlan } from "../paymentplans/logic";
+import { internal } from "@config/strings";
const { open, itemsPerPage, blogPostSnippetLength, permissions } = constants;
@@ -231,6 +235,19 @@ export const updateCourse = async (
for (const mediaId of mediaIdsMarkedForDeletion) {
await deleteMedia(mediaId);
}
+
+ const discussionsToggled = Object.prototype.hasOwnProperty.call(
+ courseData,
+ "discussions",
+ );
+ if (discussionsToggled) {
+ if (courseData.discussions) {
+ await enableDiscussions(course, ctx);
+ } else {
+ await disableDiscussions(course, ctx);
+ }
+ }
+
course = await (course as any).save();
await PageModel.updateOne(
{ entityId: course.courseId, domain: ctx.subdomain._id },
@@ -310,6 +327,15 @@ export const deleteCourse = async (id: string, ctx: GQLContext) => {
},
);
await deletePageInternal(ctx, course.pageId!);
+ if (course.discussionCommunityId) {
+ await CommunityModel.updateOne(
+ {
+ domain: ctx.subdomain._id,
+ communityId: course.discussionCommunityId,
+ },
+ { $set: { deleted: true } },
+ );
+ }
await CourseModel.deleteOne({
domain: ctx.subdomain._id,
courseId: course.courseId,
@@ -1001,3 +1027,244 @@ export const updateCourseCertificateTemplate = async ({
logo: updatedTemplate.logo,
};
};
+
+async function enableDiscussions(course: InternalCourse, ctx: GQLContext) {
+ if (course.discussionCommunityId) {
+ await CommunityModel.updateOne(
+ {
+ domain: ctx.subdomain._id,
+ communityId: course.discussionCommunityId,
+ },
+ { $set: { deleted: false, enabled: true } },
+ );
+ return;
+ }
+
+ const communityId = generateUniqueId();
+ const pageId = `${slugify(`${course.title}-discussions`)}-${communityId.substring(0, 5)}`;
+
+ await PageModel.create({
+ domain: ctx.subdomain._id,
+ pageId,
+ type: "community",
+ creatorId: ctx.user.userId,
+ name: `${course.title} — Discussions`,
+ entityId: communityId,
+ layout: [
+ { name: "header", deleteable: false, shared: true },
+ { name: "banner" },
+ { name: "footer", deleteable: false, shared: true },
+ ],
+ title: `${course.title} — Discussions`,
+ });
+
+ await CommunityModel.create({
+ domain: ctx.subdomain._id,
+ communityId,
+ name: `${course.title} — Discussions`,
+ pageId,
+ courseId: course.courseId,
+ autoAcceptMembers: true,
+ enabled: true,
+ categories: ["General"],
+ });
+
+ course.discussionCommunityId = communityId;
+
+ const paymentPlan = await getInternalPaymentPlan(ctx);
+ await MembershipModel.create({
+ domain: ctx.subdomain._id,
+ userId: ctx.user.userId,
+ entityId: communityId,
+ entityType: Constants.MembershipEntityType.COMMUNITY,
+ status: Constants.MembershipStatus.ACTIVE,
+ joiningReason: internal.joining_reason_creator,
+ role: Constants.MembershipRole.MODERATE,
+ paymentPlanId: paymentPlan.planId,
+ });
+
+ await createDiscussionPostsForCourseLessons(course, communityId, ctx);
+
+ await addEnrolledStudentsToDiscussions(course, communityId, ctx);
+}
+
+async function disableDiscussions(course: InternalCourse, ctx: GQLContext) {
+ if (!course.discussionCommunityId) {
+ return;
+ }
+
+ await CommunityModel.updateOne(
+ {
+ domain: ctx.subdomain._id,
+ communityId: course.discussionCommunityId,
+ },
+ { $set: { deleted: true } },
+ );
+}
+
+async function createDiscussionPostsForCourseLessons(
+ course: InternalCourse,
+ communityId: string,
+ ctx: GQLContext,
+) {
+ const courseLessons = await Lesson.find({
+ courseId: course.courseId,
+ domain: ctx.subdomain._id,
+ });
+
+ for (const lesson of courseLessons) {
+ await CommunityPostModel.create({
+ domain: ctx.subdomain._id,
+ userId: course.creatorId,
+ communityId,
+ title: lesson.title,
+ content: "",
+ category: "General",
+ lessonId: lesson.lessonId,
+ });
+ }
+}
+
+async function addEnrolledStudentsToDiscussions(
+ course: InternalCourse,
+ communityId: string,
+ ctx: GQLContext,
+) {
+ const memberships = await MembershipModel.find({
+ domain: ctx.subdomain._id,
+ entityId: course.courseId,
+ entityType: Constants.MembershipEntityType.COURSE,
+ status: Constants.MembershipStatus.ACTIVE,
+ }).lean();
+
+ const paymentPlan = await getInternalPaymentPlan(ctx);
+
+ for (const membership of memberships) {
+ const existingCommunityMembership = await MembershipModel.findOne({
+ domain: ctx.subdomain._id,
+ userId: membership.userId,
+ entityId: communityId,
+ entityType: Constants.MembershipEntityType.COMMUNITY,
+ });
+
+ if (!existingCommunityMembership) {
+ await MembershipModel.create({
+ domain: ctx.subdomain._id,
+ userId: membership.userId,
+ entityId: communityId,
+ entityType: Constants.MembershipEntityType.COMMUNITY,
+ status: Constants.MembershipStatus.ACTIVE,
+ role:
+ course.creatorId === membership.userId
+ ? Constants.MembershipRole.MODERATE
+ : Constants.MembershipRole.COMMENT,
+ paymentPlanId: paymentPlan.planId,
+ joiningReason: "Enrolled in course",
+ });
+ }
+ }
+}
+
+export async function getCourseDiscussionPosts({
+ courseId,
+ lessonId,
+ ctx,
+}: {
+ courseId: string;
+ lessonId?: string;
+ ctx: GQLContext;
+}) {
+ const course = await CourseModel.findOne({
+ courseId,
+ domain: ctx.subdomain._id,
+ });
+
+ if (!course || !course.discussionCommunityId) {
+ return [];
+ }
+
+ const community = await CommunityModel.findOne({
+ domain: ctx.subdomain._id,
+ communityId: course.discussionCommunityId,
+ deleted: false,
+ });
+
+ if (!community) {
+ return [];
+ }
+
+ const query: Record = {
+ domain: ctx.subdomain._id,
+ communityId: community.communityId,
+ deleted: false,
+ };
+
+ if (lessonId) {
+ query.lessonId = lessonId;
+ }
+
+ const posts = await CommunityPostModel.find(query).lean();
+ return posts.map((post) => ({
+ ...post,
+ userId: post.userId,
+ likesCount: (post.likes || []).length,
+ hasLiked: ctx.user
+ ? (post.likes || []).includes(ctx.user.userId)
+ : false,
+ }));
+}
+
+export async function getCourseDiscussionStream({
+ courseId,
+ page = 1,
+ limit = 10,
+ ctx,
+}: {
+ courseId: string;
+ page?: number;
+ limit?: number;
+ ctx: GQLContext;
+}) {
+ const course = await CourseModel.findOne({
+ courseId,
+ domain: ctx.subdomain._id,
+ });
+
+ if (!course || !course.discussionCommunityId) {
+ return { posts: [], total: 0 };
+ }
+
+ const community = await CommunityModel.findOne({
+ domain: ctx.subdomain._id,
+ communityId: course.discussionCommunityId,
+ deleted: false,
+ });
+
+ if (!community) {
+ return { posts: [], total: 0 };
+ }
+
+ const query = {
+ domain: ctx.subdomain._id,
+ communityId: community.communityId,
+ deleted: false,
+ };
+
+ const total = await CommunityPostModel.countDocuments(query);
+ const rawPosts = await (CommunityPostModel as any).paginatedFind(query, {
+ page,
+ limit,
+ sort: -1,
+ });
+
+ const posts = rawPosts.map((post) => ({
+ ...post,
+ userId: post.userId,
+ likesCount: (post.likes || []).length,
+ hasLiked: ctx.user
+ ? (post.likes || []).includes(ctx.user.userId)
+ : false,
+ }));
+
+ return { posts, total };
+}
diff --git a/apps/web/graphql/courses/query.ts b/apps/web/graphql/courses/query.ts
index ad192bd1c..eb157c0a7 100644
--- a/apps/web/graphql/courses/query.ts
+++ b/apps/web/graphql/courses/query.ts
@@ -5,6 +5,7 @@ import {
GraphQLList,
GraphQLEnumType,
GraphQLBoolean,
+ GraphQLObjectType,
} from "graphql";
import types from "./types";
import {
@@ -15,6 +16,8 @@ import {
getProducts,
getProductsCount,
getCourseCertificateTemplate,
+ getCourseDiscussionPosts,
+ getCourseDiscussionStream,
} from "./logic";
import GQLContext from "../../models/GQLContext";
import Filter from "./models/filter";
@@ -22,6 +25,7 @@ import constants from "../../config/constants";
import { reports, courseMember } from "./types/reports";
import userTypes from "../users/types";
import { MembershipStatus } from "@courselit/common-models";
+import communityTypes from "../communities/types";
const { course, download, blog } = constants;
@@ -219,4 +223,59 @@ export default {
context: GQLContext,
) => getCourseCertificateTemplate(courseId, context),
},
+ getCourseDiscussionPosts: {
+ type: new GraphQLList(communityTypes.communityPost),
+ args: {
+ courseId: {
+ type: new GraphQLNonNull(GraphQLString),
+ },
+ lessonId: {
+ type: GraphQLString,
+ },
+ },
+ resolve: (
+ _: any,
+ {
+ courseId,
+ lessonId,
+ }: {
+ courseId: string;
+ lessonId?: string;
+ },
+ context: GQLContext,
+ ) => getCourseDiscussionPosts({ courseId, lessonId, ctx: context }),
+ },
+ getCourseDiscussionStream: {
+ type: new GraphQLObjectType({
+ name: "CourseDiscussionStream",
+ fields: {
+ posts: { type: new GraphQLList(communityTypes.communityPost) },
+ total: { type: GraphQLInt },
+ },
+ }),
+ args: {
+ courseId: {
+ type: new GraphQLNonNull(GraphQLString),
+ },
+ page: {
+ type: GraphQLInt,
+ },
+ limit: {
+ type: GraphQLInt,
+ },
+ },
+ resolve: (
+ _: any,
+ {
+ courseId,
+ page,
+ limit,
+ }: {
+ courseId: string;
+ page?: number;
+ limit?: number;
+ },
+ context: GQLContext,
+ ) => getCourseDiscussionStream({ courseId, page, limit, ctx: context }),
+ },
};
diff --git a/apps/web/graphql/courses/types/index.ts b/apps/web/graphql/courses/types/index.ts
index 7819ac89e..5fa81358c 100644
--- a/apps/web/graphql/courses/types/index.ts
+++ b/apps/web/graphql/courses/types/index.ts
@@ -166,6 +166,8 @@ const courseType = new GraphQLObjectType({
sales: { type: GraphQLFloat },
customers: { type: GraphQLInt },
certificate: { type: GraphQLBoolean },
+ discussions: { type: GraphQLBoolean },
+ discussionCommunityId: { type: GraphQLString },
},
});
@@ -191,6 +193,7 @@ const courseUpdateInput = new GraphQLInputObjectType({
featuredImage: { type: mediaTypes.mediaInputType },
leadMagnet: { type: GraphQLBoolean },
certificate: { type: GraphQLBoolean },
+ discussions: { type: GraphQLBoolean },
},
});
diff --git a/apps/web/graphql/lessons/logic.ts b/apps/web/graphql/lessons/logic.ts
index 2b6ad7f91..ed3c750ed 100644
--- a/apps/web/graphql/lessons/logic.ts
+++ b/apps/web/graphql/lessons/logic.ts
@@ -31,6 +31,7 @@ import getDeletedMediaIds, {
} from "@/lib/get-deleted-media-ids";
import ActivityModel from "@/models/Activity";
import UserModel from "../../models/User";
+import CommunityPostModel from "@models/CommunityPost";
const { permissions, quiz, scorm } = constants;
@@ -173,6 +174,18 @@ export const createLesson = async (
group?.lessonsOrder.push(lesson.lessonId);
await (course as any).save();
+ if (course.discussionCommunityId) {
+ await CommunityPostModel.create({
+ domain: ctx.subdomain._id,
+ userId: ctx.user.userId,
+ communityId: course.discussionCommunityId,
+ title: lessonData.title,
+ content: "",
+ category: "General",
+ lessonId: lesson.lessonId,
+ });
+ }
+
return lesson;
} catch (err: any) {
throw new Error(err.message);
@@ -220,6 +233,16 @@ export const updateLesson = async (
await deleteMedia(mediaId);
}
+ if (lessonData.title && lessonData.title !== lesson.title) {
+ await CommunityPostModel.updateMany(
+ {
+ domain: ctx.subdomain._id,
+ lessonId: lesson.lessonId,
+ },
+ { $set: { title: lessonData.title } },
+ );
+ }
+
lesson = await (lesson as any).save();
return lesson;
};
@@ -265,6 +288,15 @@ export const deleteLesson = async (id: string, ctx: GQLContext) => {
_id: lesson.id,
domain: ctx.subdomain._id,
});
+
+ await CommunityPostModel.updateMany(
+ {
+ domain: ctx.subdomain._id,
+ lessonId: lesson.lessonId,
+ },
+ { $set: { deleted: true } },
+ );
+
return true;
} catch (err: any) {
throw new Error(err.message);
diff --git a/apps/web/graphql/notifications/logic.ts b/apps/web/graphql/notifications/logic.ts
index b0de7726e..feb33245f 100644
--- a/apps/web/graphql/notifications/logic.ts
+++ b/apps/web/graphql/notifications/logic.ts
@@ -7,6 +7,7 @@ import {
import Community from "@models/Community";
import CommunityComment from "@models/CommunityComment";
import CommunityPost from "@models/CommunityPost";
+import CourseModel from "@models/Course";
import GQLContext from "@models/GQLContext";
import NotificationModel, { InternalNotification } from "@models/Notification";
import UserModel from "@models/User";
@@ -299,11 +300,101 @@ async function getMessage({
message: `${userName} granted your request to join ${community7.name}`,
href: `/dashboard/community/${community7.communityId}`,
};
+ case Constants.NotificationEntityAction.COURSE_DISCUSSION_COMMENTED: {
+ const post = await CommunityPost.findOne({ postId: entityId });
+ if (!post) return { message: "", href: "" };
+ const lessonUrl = await getCourseDiscussionLessonUrl(post);
+ if (!lessonUrl) return { message: "", href: "" };
+ return {
+ message: `${userName} commented on your post '${truncate(post.title, 20).trim()}'`,
+ href: lessonUrl,
+ };
+ }
+ case Constants.NotificationEntityAction.COURSE_DISCUSSION_REPLIED: {
+ const comment = await CommunityComment.findOne({
+ commentId: entityTargetId,
+ });
+ if (!comment) return { message: "", href: "" };
+ const reply = comment.replies.find((r) => r.replyId === entityId);
+ if (!reply) return { message: "", href: "" };
+ const post = await CommunityPost.findOne({
+ postId: comment.postId,
+ });
+ if (!post) return { message: "", href: "" };
+ let parentReply;
+ if (reply.parentReplyId) {
+ parentReply = comment.replies.find(
+ (r) => r.replyId === reply.parentReplyId,
+ );
+ }
+ const prefix = parentReply
+ ? loggedInUserId === parentReply.userId
+ ? "your"
+ : "a"
+ : loggedInUserId === comment.userId
+ ? "your"
+ : "a";
+ const lessonUrl = await getCourseDiscussionLessonUrl(post);
+ if (!lessonUrl) return { message: "", href: "" };
+ return {
+ message: `${userName} replied to ${prefix} comment on '${truncate(post.title, 20).trim()}'`,
+ href: lessonUrl,
+ };
+ }
+ case Constants.NotificationEntityAction
+ .COURSE_DISCUSSION_COMMENT_LIKED: {
+ const comment = await CommunityComment.findOne({
+ commentId: entityId,
+ });
+ if (!comment) return { message: "", href: "" };
+ const post = await CommunityPost.findOne({
+ postId: comment.postId,
+ });
+ if (!post) return { message: "", href: "" };
+ const lessonUrl = await getCourseDiscussionLessonUrl(post);
+ if (!lessonUrl) return { message: "", href: "" };
+ return {
+ message: `${userName} liked your comment '${truncate(comment.content, 20).trim()}' on '${truncate(post.title, 20).trim()}'`,
+ href: lessonUrl,
+ };
+ }
+ case Constants.NotificationEntityAction.COURSE_DISCUSSION_REPLY_LIKED: {
+ const comment = await CommunityComment.findOne({
+ commentId: entityTargetId,
+ });
+ if (!comment) return { message: "", href: "" };
+ const reply = comment.replies.find((r) => r.replyId === entityId);
+ if (!reply) return { message: "", href: "" };
+ const post = await CommunityPost.findOne({
+ postId: comment.postId,
+ });
+ if (!post) return { message: "", href: "" };
+ const lessonUrl = await getCourseDiscussionLessonUrl(post);
+ if (!lessonUrl) return { message: "", href: "" };
+ return {
+ message: `${userName} liked your reply '${truncate(reply.content, 20).trim()}' on '${truncate(post.title, 20).trim()}'`,
+ href: lessonUrl,
+ };
+ }
default:
return { message: "", href: "" };
}
}
+async function getCourseDiscussionLessonUrl(post: any): Promise {
+ if (!post.lessonId) return null;
+ const community = await Community.findOne({
+ communityId: post.communityId,
+ });
+ if (!community || !community.courseId) return null;
+ const course = await CourseModel.findOne({
+ domain: community.domain,
+ courseId: community.courseId,
+ });
+ if (!course) return null;
+ return `/course/${course.slug}/${course.courseId}/${post.lessonId}`;
+}
+
export async function markAsRead({
ctx,
notificationId,
diff --git a/apps/web/graphql/users/logic.ts b/apps/web/graphql/users/logic.ts
index 9669fefaf..edb1f724f 100644
--- a/apps/web/graphql/users/logic.ts
+++ b/apps/web/graphql/users/logic.ts
@@ -893,6 +893,14 @@ export async function runPostMembershipTasks({
user,
product,
});
+ if (product.discussionCommunityId) {
+ await addUserToDiscussionCommunity({
+ domain,
+ userId: user.userId,
+ discussionCommunityId: product.discussionCommunityId,
+ creatorId: product.creatorId,
+ });
+ }
}
await recordActivity({
domain,
@@ -1013,3 +1021,46 @@ export const getCertificateInternal = async (
productPageId: course?.pageId || null,
};
};
+
+async function addUserToDiscussionCommunity({
+ domain,
+ userId,
+ discussionCommunityId,
+ creatorId,
+}: {
+ domain: mongoose.Types.ObjectId;
+ userId: string;
+ discussionCommunityId: string;
+ creatorId: string;
+}) {
+ const existingMembership = await MembershipModel.findOne({
+ domain,
+ userId,
+ entityId: discussionCommunityId,
+ entityType: Constants.MembershipEntityType.COMMUNITY,
+ });
+
+ if (existingMembership) {
+ return;
+ }
+
+ const paymentPlan = await getInternalPaymentPlan({
+ subdomain: {
+ _id: domain,
+ },
+ });
+
+ await MembershipModel.create({
+ domain,
+ userId,
+ entityId: discussionCommunityId,
+ entityType: Constants.MembershipEntityType.COMMUNITY,
+ status: Constants.MembershipStatus.ACTIVE,
+ role:
+ userId === creatorId
+ ? Constants.MembershipRole.MODERATE
+ : Constants.MembershipRole.COMMENT,
+ paymentPlanId: paymentPlan.planId,
+ joiningReason: "Enrolled in course",
+ });
+}
diff --git a/apps/web/hooks/use-product.ts b/apps/web/hooks/use-product.ts
index 7e435a085..40216f230 100644
--- a/apps/web/hooks/use-product.ts
+++ b/apps/web/hooks/use-product.ts
@@ -102,6 +102,8 @@ export default function useProduct(id?: string | null): {
leadMagnet
defaultPaymentPlan
certificate
+ discussions
+ discussionCommunityId
}
}
`;
diff --git a/apps/web/models/Community.ts b/apps/web/models/Community.ts
index a770e1c91..e89949156 100644
--- a/apps/web/models/Community.ts
+++ b/apps/web/models/Community.ts
@@ -8,6 +8,7 @@ export interface InternalCommunity extends Omit {
createdAt: Date;
updatedAt: Date;
deleted: boolean;
+ courseId?: string;
}
const CommunitySchema = new mongoose.Schema(
@@ -30,6 +31,7 @@ const CommunitySchema = new mongoose.Schema(
// paymentPlans: [String],
defaultPaymentPlan: { type: String },
featuredImage: MediaSchema,
+ courseId: { type: String },
deleted: { type: Boolean, default: false },
},
{
diff --git a/apps/web/models/CommunityPost.ts b/apps/web/models/CommunityPost.ts
index 3c6c2823c..9b991b355 100644
--- a/apps/web/models/CommunityPost.ts
+++ b/apps/web/models/CommunityPost.ts
@@ -14,6 +14,7 @@ export interface InternalCommunityPost
| "media"
| "pinned"
| "deleted"
+ | "lessonId"
> {
domain: mongoose.Types.ObjectId;
userId: string;
@@ -39,6 +40,7 @@ const CommunityPostSchema = new mongoose.Schema(
media: [CommunityMediaSchema],
pinned: { type: Boolean, default: false },
likes: [String],
+ lessonId: { type: String },
deleted: { type: Boolean, default: false },
},
{
diff --git a/apps/web/ui-config/strings.ts b/apps/web/ui-config/strings.ts
index cdbd0bb7a..9b6ef732b 100644
--- a/apps/web/ui-config/strings.ts
+++ b/apps/web/ui-config/strings.ts
@@ -683,6 +683,23 @@ export const NEW_COMMUNITY_BUTTON = "New community";
export const COMMUNITY_FIELD_NAME = "Community name";
export const COMMUNITY_NEW_BTN_CAPTION = "Create";
export const COMMUNITY_SETTINGS = "Manage";
+export const COURSE_DISCUSSIONS_HEADER = "Course Discussions";
+export const COURSE_DISCUSSIONS_DESCRIPTION =
+ "Allow students to discuss each lesson directly on the lesson page.";
+export const COURSE_DISCUSSIONS_ACTIVE =
+ "Discussions are active — students can comment on each lesson";
+export const COURSE_DISCUSSIONS_INACTIVE = "Discussions are disabled";
+export const COURSE_DISCUSSIONS_MANAGE_COMMUNITY =
+ "Manage discussion community";
+export const LESSON_DISCUSSIONS_HEADER = "Discussions";
+export const LESSON_DISCUSSIONS_WRITE_COMMENT = "Write a comment...";
+export const LESSON_DISCUSSIONS_VIEW_ALL = "View all course discussions";
+export const LESSON_DISCUSSIONS_EMPTY =
+ "No comments yet. Start the discussion!";
+export const LESSON_DISCUSSIONS_POST = "Post";
+export const LESSON_DISCUSSIONS_REPLY = "Reply";
+export const LESSON_DISCUSSIONS_LOADING = "Loading discussions...";
+export const LESSON_DISCUSSIONS_BUTTON = "Discussion";
// Payment Plan strings
export const NEW_PAYMENT_PLAN_HEADER = "New Payment Plan";
diff --git a/packages/common-logic/src/models/course.ts b/packages/common-logic/src/models/course.ts
index 9a25eb883..6a033dab7 100644
--- a/packages/common-logic/src/models/course.ts
+++ b/packages/common-logic/src/models/course.ts
@@ -20,6 +20,8 @@ export interface InternalCourse extends Omit {
sales: number;
customers: string[];
certificate?: boolean;
+ discussions?: boolean;
+ discussionCommunityId?: string;
}
export const CourseSchema = new mongoose.Schema(
@@ -81,6 +83,8 @@ export const CourseSchema = new mongoose.Schema(
defaultPaymentPlan: { type: String },
leadMagnet: { type: Boolean, required: true, default: false },
certificate: Boolean,
+ discussions: { type: Boolean, default: false },
+ discussionCommunityId: { type: String },
},
{
timestamps: true,
diff --git a/packages/common-models/src/community-post.ts b/packages/common-models/src/community-post.ts
index d47bf9088..2d2874926 100644
--- a/packages/common-models/src/community-post.ts
+++ b/packages/common-models/src/community-post.ts
@@ -16,4 +16,5 @@ export interface CommunityPost {
createdAt: string;
hasLiked: boolean;
deleted: boolean;
+ lessonId?: string;
}
diff --git a/packages/common-models/src/community.ts b/packages/common-models/src/community.ts
index 2cb244ec2..5f4979fc6 100644
--- a/packages/common-models/src/community.ts
+++ b/packages/common-models/src/community.ts
@@ -15,4 +15,5 @@ export interface Community {
defaultPaymentPlan?: string;
featuredImage?: Media;
membersCount: number;
+ courseId?: string;
}
diff --git a/packages/common-models/src/constants.ts b/packages/common-models/src/constants.ts
index 42196f9b7..bbb00494a 100644
--- a/packages/common-models/src/constants.ts
+++ b/packages/common-models/src/constants.ts
@@ -102,6 +102,10 @@ export const NotificationEntityAction = {
COMMUNITY_REPLY_LIKED: "community:reply:liked",
COMMUNITY_MEMBERSHIP_REQUESTED: "community:membership:requested",
COMMUNITY_MEMBERSHIP_GRANTED: "community:membership:granted",
+ COURSE_DISCUSSION_COMMENTED: "course:discussion:commented",
+ COURSE_DISCUSSION_REPLIED: "course:discussion:replied",
+ COURSE_DISCUSSION_COMMENT_LIKED: "course:discussion:comment:liked",
+ COURSE_DISCUSSION_REPLY_LIKED: "course:discussion:reply:liked",
} as const;
export const ProductPriceType = {
FREE: "free",
diff --git a/packages/common-models/src/course.ts b/packages/common-models/src/course.ts
index 00fb53fb6..17d3300a9 100644
--- a/packages/common-models/src/course.ts
+++ b/packages/common-models/src/course.ts
@@ -34,4 +34,6 @@ export interface Course {
lessons?: Lesson[];
user: User;
paymentPlans?: PaymentPlan[];
+ discussions?: boolean;
+ discussionCommunityId?: string;
}