feat(newsfeed): Improved UI flow for the course newsfeed (#791)

* feat(newsfeed): improved the newsfeed ui

* feat(newsfeed): updated the ui

* feat(newsfeed): some improvements

* feat(newsfeed): cleanup

* chore(newsfeed): added translations

* fix(newsfeed): fixing comments and reviews from greptile and coderabbit

* fix(newsfeed): fixing comments and reviews from greptile and coderabbit, improved replies to a reply

* fix(newsfeed): fixing reviews

* fix(ui): expose cancelLabel prop in comment-tree-input and pass localized text

---------

Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Nwosu Emmanuel
2026-07-31 00:26:59 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 88c4cf03b3
commit a1c4ee720f
40 changed files with 13639 additions and 280 deletions
@@ -0,0 +1,5 @@
ALTER TABLE "course_newsfeed_comment" ADD COLUMN "parent_id" bigint;
--> statement-breakpoint
ALTER TABLE "course_newsfeed_comment" ADD CONSTRAINT "course_newsfeed_comment_parent_id_fkey" FOREIGN KEY ("parent_id") REFERENCES "public"."course_newsfeed_comment"("id") ON DELETE cascade ON UPDATE no action;
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "course_newsfeed_comment_parent_id_idx" ON "course_newsfeed_comment" USING btree ("parent_id");
File diff suppressed because it is too large Load Diff
@@ -36,6 +36,13 @@
"when": 1783938329367,
"tag": "0004_organizationmember_email_notifications",
"breakpoints": true
},
{
"idx": 5,
"version": "7",
"when": 1785228000000,
"tag": "0005_add_parent_id_to_course_newsfeed_comment",
"breakpoints": true
}
]
}
+50 -15
View File
@@ -1,7 +1,7 @@
import * as schema from '@db/schema';
import type { TCourseNewsfeed, TCourseNewsfeedComment, TNewCourseNewsfeed, TNewCourseNewsfeedComment } from '@db/types';
import { and, db, desc, eq, lt, ne, sql } from '@db/drizzle';
import { and, db, desc, eq, isNull, lt, ne, sql } from '@db/drizzle';
import { ROLE } from '@cio/utils/constants';
/**
@@ -221,36 +221,50 @@ export async function getNewsfeedCommentsByFeedId(feedId: string): Promise<TCour
/**
* Gets paginated comments for a newsfeed item with author profile
* @param feedId Newsfeed ID
* @param options Pagination options (cursor, limit)
* @param options Pagination options (parentId, cursor, limit)
* @returns Object with comments array, total count, hasMore flag, and nextCursor
*/
export async function getNewsfeedCommentsByFeedIdPaginated(
feedId: string,
options: { cursor?: string; limit: number }
options: { parentId?: number; cursor?: string; limit: number }
) {
try {
const { cursor, limit } = options;
const { parentId, cursor, limit } = options;
// Build base parent-scoped conditions
const parentCondition =
parentId !== undefined
? eq(schema.courseNewsfeedComment.parentId, parentId)
: isNull(schema.courseNewsfeedComment.parentId);
const baseWhereConditions = [eq(schema.courseNewsfeedComment.courseNewsfeedId, feedId), parentCondition];
const whereConditions = [...baseWhereConditions];
// Build where conditions
const whereConditions = [eq(schema.courseNewsfeedComment.courseNewsfeedId, feedId)];
if (cursor) {
// Cursor is the last comment ID, fetch comments created after it
whereConditions.push(lt(schema.courseNewsfeedComment.id, Number(cursor)));
}
// Get total count
const totalCountResult = await db
.select({ count: sql<number>`count(*)`.as('count') })
.from(schema.courseNewsfeedComment)
.where(eq(schema.courseNewsfeedComment.courseNewsfeedId, feedId));
.where(and(...baseWhereConditions));
const totalCount = Number(totalCountResult[0]?.count || 0);
// Fetch comments with author profile (limit + 1 to check if there are more)
// Fetch comments with author profile & reply count
const comments = await db
.select({
comment: schema.courseNewsfeedComment,
groupmember: schema.groupmember,
profile: schema.profile
profile: schema.profile,
replyCount: sql<number>`
COALESCE(
(SELECT COUNT(*)::int
FROM ${schema.courseNewsfeedComment} c2
WHERE c2.parent_id = ${schema.courseNewsfeedComment.id}),
0
)
`.as('replyCount')
})
.from(schema.courseNewsfeedComment)
.leftJoin(schema.groupmember, eq(schema.courseNewsfeedComment.authorId, schema.groupmember.id))
@@ -259,11 +273,9 @@ export async function getNewsfeedCommentsByFeedIdPaginated(
.orderBy(desc(schema.courseNewsfeedComment.createdAt))
.limit(limit + 1);
// Check if there are more comments
const hasMore = comments.length > limit;
const items = comments.slice(0, limit);
// Get next cursor (ID of the oldest comment in this batch - for loading older comments)
const nextCursor = hasMore && items.length > 0 ? String(items[items.length - 1].comment.id) : null;
return {
@@ -272,7 +284,8 @@ export async function getNewsfeedCommentsByFeedIdPaginated(
authorProfileId: row.profile?.id || null,
authorFullname: row.profile?.fullname || null,
authorUsername: row.profile?.username || null,
authorAvatarUrl: row.profile?.avatarUrl || null
authorAvatarUrl: row.profile?.avatarUrl || null,
replyCount: Number(row.replyCount || 0)
})),
totalCount,
hasMore,
@@ -343,6 +356,27 @@ export async function deleteNewsfeedComment(commentId: number): Promise<TCourseN
}
}
/**
* Gets a single newsfeed comment by ID
* @param commentId Comment ID
* @returns Comment or null
*/
export async function getNewsfeedCommentById(commentId: number): Promise<TCourseNewsfeedComment | null> {
try {
const [comment] = await db
.select()
.from(schema.courseNewsfeedComment)
.where(eq(schema.courseNewsfeedComment.id, commentId))
.limit(1);
return comment || null;
} catch (error) {
console.error('getNewsfeedCommentById error:', error);
throw new Error(
`Failed to get newsfeed comment by ID "${commentId}": ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
/**
* Gets comment author and course ID for authorization checks
* @param commentId Comment ID
@@ -489,7 +523,8 @@ function getCommentCount() {
COALESCE(
(SELECT COUNT(*)::int
FROM ${schema.courseNewsfeedComment}
WHERE ${eq(schema.courseNewsfeedComment.courseNewsfeedId, schema.courseNewsfeed.id)}),
WHERE ${eq(schema.courseNewsfeedComment.courseNewsfeedId, schema.courseNewsfeed.id)}
AND ${isNull(schema.courseNewsfeedComment.parentId)}),
0
)
`.as('commentCount');
+9 -2
View File
@@ -1692,7 +1692,8 @@ export const courseNewsfeedComment = pgTable(
minValue: 1,
cache: 1
}),
courseNewsfeedId: uuid('course_newsfeed_id')
courseNewsfeedId: uuid('course_newsfeed_id'),
parentId: bigint('parent_id', { mode: 'number' })
},
(table) => [
foreignKey({
@@ -1704,7 +1705,13 @@ export const courseNewsfeedComment = pgTable(
columns: [table.courseNewsfeedId],
foreignColumns: [courseNewsfeed.id],
name: 'course_newsfeed_comment_course_newsfeed_id_fkey'
})
}),
foreignKey({
columns: [table.parentId],
foreignColumns: [table.id],
name: 'course_newsfeed_comment_parent_id_fkey'
}).onDelete('cascade'),
index('course_newsfeed_comment_parent_id_idx').on(table.parentId)
]
);