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 <best@classroomio.com>
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
@@ -10,7 +10,7 @@
import { currentOrg } from '$lib/utils/store/org';
import { profile } from '$lib/utils/store/user';
import type { Feed } from '$features/course/utils/types';
import PinIcon from '@lucide/svelte/icons/pin';
import { onMount } from 'svelte';
import type { AccountOrg } from '$features/app/types';
import Empty from '@cio/ui/custom/empty/empty.svelte';
@@ -44,8 +44,8 @@
avatarUrl: $profile.avatarUrl || ''
});
const deleteComment = async (feedId: string, commentId: string) => {
await newsfeedApi.deleteComment(courseId, feedId, commentId);
const deleteComment = async (feedId: string, commentId: string, parentId?: number) => {
await newsfeedApi.deleteComment(courseId, feedId, commentId, parentId);
};
const addNewReaction = async (reactionType: NewsfeedReactionType, feedId: string, authorId: string) => {
@@ -69,8 +69,8 @@
}
};
const addNewComment = async (comment: string, feedId: string) => {
await newsfeedApi.createComment(courseId, feedId, comment, author);
const addNewComment = async (comment: string, feedId: string, parentId?: number) => {
await newsfeedApi.createComment(courseId, feedId, comment, author, parentId);
if (!newsfeedApi.success) {
return snackbar.error('snackbar.course.error.commenting_error');
@@ -96,12 +96,12 @@
await newsfeedApi.delete(courseId, id);
};
const sortedFeeds = $derived.by(() => {
return [...newsfeedApi.feeds].sort((a, b) => {
const aPinned = (a as any).isPinned || (a as any).is_pinned || false;
const bPinned = (b as any).isPinned || (b as any).is_pinned || false;
return Number(bPinned) - Number(aPinned);
});
const pinnedFeeds = $derived.by(() => {
return newsfeedApi.feeds.filter((feed) => Boolean((feed as any).isPinned || (feed as any).is_pinned));
});
const unpinnedFeeds = $derived.by(() => {
return newsfeedApi.feeds.filter((feed) => !((feed as any).isPinned || (feed as any).is_pinned));
});
function getPageRoles(org: AccountOrg) {
@@ -138,7 +138,7 @@
<NewsFeedLoader />
<NewsFeedLoader />
</div>
{:else if !sortedFeeds.length}
{:else if !pinnedFeeds.length && !unpinnedFeeds.length}
<Empty
title={$t('course.navItem.news_feed.body_header')}
description={$t('course.navItem.news_feed.body_content')}
@@ -146,29 +146,57 @@
variant="page"
/>
{:else}
{#each sortedFeeds as feed}
{#if feed.isPinned}
<div class="flex items-center gap-2">
<PinIcon size={16} class="filled" />
{#if pinnedFeeds.length > 0}
<div class="text-muted-foreground mb-3 flex items-center gap-1.5">
<span class="text-xs font-medium tracking-wider uppercase">
{$t('course.navItem.news_feed.pinned') || 'Pinned'}
</span>
</div>
{#each pinnedFeeds as feed (feed.id)}
<NewsFeedCard
{feed}
comments={newsfeedApi.commentsByFeedId[feed.id]}
{courseId}
{deleteFeed}
{addNewComment}
{deleteComment}
{addNewReaction}
{onPin}
{author}
bind:edit
bind:editFeed
isActive={feedId === feed.id}
isReacting={Boolean(isReactingByFeedId[feed.id])}
/>
{/each}
{/if}
<p class="text-sm">{$t('course.navItem.news_feed.pinned')}</p>
</div>
{/if}
<NewsFeedCard
{feed}
comments={newsfeedApi.commentsByFeedId[feed.id]}
{courseId}
{deleteFeed}
{addNewComment}
{deleteComment}
{addNewReaction}
{onPin}
{author}
bind:edit
bind:editFeed
isActive={feedId === feed.id}
isReacting={Boolean(isReactingByFeedId[feed.id])}
/>
{/each}
{#if pinnedFeeds.length > 0 && unpinnedFeeds.length > 0}
<div class="text-muted-foreground mb-3 flex items-center gap-1.5">
<span class="text-xs font-medium tracking-wider uppercase">
{$t('course.navItem.news_feed.other_posts')}
</span>
</div>
{/if}
{#if unpinnedFeeds.length > 0}
{#each unpinnedFeeds as feed (feed.id)}
<NewsFeedCard
{feed}
comments={newsfeedApi.commentsByFeedId[feed.id]}
{courseId}
{deleteFeed}
{addNewComment}
{deleteComment}
{addNewReaction}
{onPin}
{author}
bind:edit
bind:editFeed
isActive={feedId === feed.id}
isReacting={Boolean(isReactingByFeedId[feed.id])}
/>
{/each}
{/if}
{/if}
</RoleBasedSecurity>