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:
co-authored by
Rotimi Best
parent
88c4cf03b3
commit
a1c4ee720f
@@ -0,0 +1,76 @@
|
||||
import { Context, Next } from 'hono';
|
||||
import { getGroupMemberIdByCourseAndProfile } from '@cio/db/queries/group';
|
||||
|
||||
import { ErrorCodes } from '@api/utils/errors';
|
||||
import { getNewsfeedCommentAuthorAndCourse } from '@cio/db/queries/newsfeed';
|
||||
|
||||
/**
|
||||
* Middleware to check if user is strictly the comment author
|
||||
* Requires authMiddleware to be applied first
|
||||
* Expects commentId in route params as 'commentId'
|
||||
*/
|
||||
export const newsfeedCommentAuthorOnlyMiddleware = async (c: Context, next: Next) => {
|
||||
try {
|
||||
const user = c.get('user');
|
||||
if (!user) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
code: 'UNAUTHORIZED'
|
||||
},
|
||||
401
|
||||
);
|
||||
}
|
||||
|
||||
const commentId = c.req.param('commentId');
|
||||
if (!commentId) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Comment ID is required',
|
||||
code: 'COMMENT_ID_REQUIRED'
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const commentInfo = await getNewsfeedCommentAuthorAndCourse(Number(commentId));
|
||||
if (!commentInfo || !commentInfo.courseId) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Comment not found',
|
||||
code: ErrorCodes.COMMENT_NOT_FOUND
|
||||
},
|
||||
404
|
||||
);
|
||||
}
|
||||
|
||||
const userGroupMemberId = await getGroupMemberIdByCourseAndProfile(commentInfo.courseId, user.id);
|
||||
const isAuthor = Boolean(userGroupMemberId && commentInfo.authorId && commentInfo.authorId === userGroupMemberId);
|
||||
|
||||
if (!isAuthor) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Only the author of this comment can edit it',
|
||||
code: ErrorCodes.ORG_TEAM_NOT_AUTHORIZED
|
||||
},
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
await next();
|
||||
} catch (error) {
|
||||
console.error('Error in newsfeedCommentAuthorOnlyMiddleware:', error);
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Failed to verify comment authorization',
|
||||
code: 'COMMENT_AUTH_CHECK_FAILED'
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -49,7 +49,7 @@ export const newsfeedCommentAuthorOrTeamMiddleware = async (c: Context, next: Ne
|
||||
|
||||
// Check if user is the author of the comment
|
||||
const userGroupMemberId = await getGroupMemberIdByCourseAndProfile(commentInfo.courseId, user.id);
|
||||
const isAuthor = commentInfo.authorId === userGroupMemberId;
|
||||
const isAuthor = Boolean(userGroupMemberId && commentInfo.authorId && commentInfo.authorId === userGroupMemberId);
|
||||
|
||||
if (isAuthor) {
|
||||
await next();
|
||||
|
||||
@@ -28,6 +28,7 @@ import { courseMemberMiddleware } from '@api/middlewares/course-member';
|
||||
import { courseTeamMemberMiddleware } from '@api/middlewares/course-team-member';
|
||||
import { getGroupMemberIdByCourseAndProfile } from '@cio/db/queries/group';
|
||||
import { handleError } from '@api/utils/errors';
|
||||
import { newsfeedCommentAuthorOnlyMiddleware } from '@api/middlewares/newsfeed-comment-author-only';
|
||||
import { newsfeedCommentAuthorOrTeamMiddleware } from '@api/middlewares/newsfeed-comment-author-or-team';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
@@ -130,9 +131,9 @@ export const newsfeedRouter = new Hono()
|
||||
async (c) => {
|
||||
try {
|
||||
const { feedId } = c.req.valid('param');
|
||||
const { cursor, limit = 5 } = c.req.valid('query');
|
||||
const { parentId, cursor, limit = 5 } = c.req.valid('query');
|
||||
|
||||
const result = await getNewsfeedCommentsService(feedId, { cursor, limit });
|
||||
const result = await getNewsfeedCommentsService(feedId, { parentId, cursor, limit });
|
||||
|
||||
return c.json({ success: true, data: result }, 200);
|
||||
} catch (error) {
|
||||
@@ -151,14 +152,14 @@ export const newsfeedRouter = new Hono()
|
||||
const user = c.get('user')!;
|
||||
const courseId = c.req.param('courseId')!;
|
||||
const { feedId } = c.req.valid('param');
|
||||
const { content } = c.req.valid('json');
|
||||
const { content, parentId } = c.req.valid('json');
|
||||
|
||||
const groupMemberId = await getGroupMemberIdByCourseAndProfile(courseId, user.id);
|
||||
if (!groupMemberId) {
|
||||
return c.json({ success: false, error: 'User is not a member of this course' }, 403);
|
||||
}
|
||||
|
||||
const comment = await createNewsfeedCommentService(feedId, groupMemberId, content);
|
||||
const comment = await createNewsfeedCommentService(feedId, groupMemberId, content, parentId);
|
||||
|
||||
return c.json({ success: true, data: comment }, 201);
|
||||
} catch (error) {
|
||||
@@ -169,7 +170,7 @@ export const newsfeedRouter = new Hono()
|
||||
.put(
|
||||
'/comment/:commentId',
|
||||
authMiddleware,
|
||||
newsfeedCommentAuthorOrTeamMiddleware,
|
||||
newsfeedCommentAuthorOnlyMiddleware,
|
||||
zValidator('param', ZNewsfeedCommentGetParam),
|
||||
zValidator('json', ZNewsfeedCommentUpdate),
|
||||
async (c) => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getNewsfeedByCourseId,
|
||||
getNewsfeedByCourseIdPaginated,
|
||||
getNewsfeedById,
|
||||
getNewsfeedCommentById,
|
||||
getNewsfeedCommentsByFeedId,
|
||||
getNewsfeedCommentsByFeedIdPaginated,
|
||||
getNewsfeedForEmail,
|
||||
@@ -231,10 +232,13 @@ export async function getNewsfeedComments(feedId: string) {
|
||||
/**
|
||||
* Gets paginated comments for a newsfeed item
|
||||
* @param feedId Newsfeed ID
|
||||
* @param options Pagination options (cursor, limit)
|
||||
* @param options Pagination options (parentId, cursor, limit)
|
||||
* @returns Paginated comments with metadata
|
||||
*/
|
||||
export async function getNewsfeedCommentsService(feedId: string, options: { cursor?: string; limit: number }) {
|
||||
export async function getNewsfeedCommentsService(
|
||||
feedId: string,
|
||||
options: { parentId?: number; cursor?: string; limit: number }
|
||||
) {
|
||||
try {
|
||||
const feed = await getNewsfeedById(feedId);
|
||||
if (!feed) {
|
||||
@@ -260,16 +264,44 @@ export async function getNewsfeedCommentsService(feedId: string, options: { curs
|
||||
* @param feedId Newsfeed ID
|
||||
* @param authorId Group member ID (author)
|
||||
* @param content Comment content
|
||||
* @param parentId Optional parent comment ID for replies
|
||||
* @returns Created comment
|
||||
*/
|
||||
export async function createNewsfeedCommentService(feedId: string, authorId: string, content: string) {
|
||||
export async function createNewsfeedCommentService(
|
||||
feedId: string,
|
||||
authorId: string,
|
||||
content: string,
|
||||
parentId?: number
|
||||
) {
|
||||
try {
|
||||
// Validate parentId when provided
|
||||
if (parentId !== undefined) {
|
||||
const parentComment = await getNewsfeedCommentById(parentId);
|
||||
|
||||
if (!parentComment) {
|
||||
throw new AppError('Parent comment not found', ErrorCodes.COMMENT_NOT_FOUND, 404);
|
||||
}
|
||||
|
||||
if (parentComment.courseNewsfeedId !== feedId) {
|
||||
throw new AppError('Parent comment does not belong to this feed', ErrorCodes.VALIDATION_ERROR, 400);
|
||||
}
|
||||
|
||||
if (parentComment.parentId !== null) {
|
||||
throw new AppError(
|
||||
'Cannot reply to a reply — only top-level comments can be replied to',
|
||||
ErrorCodes.VALIDATION_ERROR,
|
||||
400
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizedContent = sanitizeHtml(content);
|
||||
|
||||
const commentData: TNewCourseNewsfeedComment = {
|
||||
courseNewsfeedId: feedId,
|
||||
authorId,
|
||||
content: sanitizedContent
|
||||
content: sanitizedContent,
|
||||
parentId: parentId ?? null
|
||||
};
|
||||
|
||||
const comment = await createNewsfeedComment(commentData);
|
||||
@@ -281,6 +313,10 @@ export async function createNewsfeedCommentService(feedId: string, authorId: str
|
||||
|
||||
return comment;
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new AppError(
|
||||
error instanceof Error ? error.message : 'Failed to create newsfeed comment',
|
||||
ErrorCodes.INTERNAL_ERROR,
|
||||
|
||||
Reference in New Issue
Block a user