Skip to content

Social Features: Profile, Follow, Activity, Notifications, Chat & Presence + UI Overhaul#312

Merged
motirebuma merged 1 commit intomainfrom
311-social-features-profile-follow-activity-notifications-chat-presence
Mar 19, 2026
Merged

Social Features: Profile, Follow, Activity, Notifications, Chat & Presence + UI Overhaul#312
motirebuma merged 1 commit intomainfrom
311-social-features-profile-follow-activity-notifications-chat-presence

Conversation

@motirebuma
Copy link
Copy Markdown
Collaborator

Social Features

  • User Profile: ProfileController, ProfileHeader with cover banner + overlapping avatar, ProfileAvatar editor, ProfileBadge,
    ReputationDisplay, generateMetadata for SEO
  • Followers / Following: Dedicated pages with FollowInfo, UserFollowDisplay, NoFollowers empty state
  • FollowButton: Optimistic follow/unfollow with local state revert on error
  • SendInviteDialog & ReportUserDialog: Full implementations with form validation and mutations
  • Activity Feed: ActivityList, ActivityCard with icon mapping (FileText/MessageCircle/ThumbsUp/Bookmark), ActivitySkeleton,
    ActivityEmptyList, PaginatedActivityList
  • Notifications: NotificationMenu in navbar with badge count, full NotificationLists page, MobileDrawer (Sheet), real-time
    NEW_NOTIFICATION_SUBSCRIPTION
  • Chat & Messaging: ChatPanel (right Sheet), ChatTabs, ChatList, ChatContent, MessageItemList with react-scrollable-feed, MessageSend,
    TypingIndicator, message reactions, PostChat (discussion tab on post detail)
  • Buddy List & Presence: BuddyListWithPresence, AddBuddyDialog, accept/decline/block flows, PresenceIcon with status dots, StatusEditor
    dialog
  • Presence Heartbeat: usePresenceHeartbeat with visibilitychange pause/resume + exponential backoff retry
  • Real-time Subscriptions: Presence, roster, typing, notifications — all wired via Apollo subscriptions

UI Overhaul

  • Typography: Tighter letter-spacing on headings, font smoothing, near-white background (#FAFBFC)
  • PostCard redesign: Author row at top (avatar + name + @handle + time), engagement bar at bottom (thumbs up/down, comments, quotes, views),
    hover lift animation
  • Profile redesign: Gradient cover banner, overlapping avatar with ring, Twitter-style follower stats, tabbed layout (Posts/Activity/About)
  • Notifications redesign: Readable action text ("started following you"), card-based items, responsive width
  • Search page: Welcome hero section, quick action cards (Write/Trending/Featured/People), single search field (removed duplicate from
    navbar)
  • Mock data: 8 realistic posts with varied interactions so the feed is always populated

Code Quality

  • Fixed all Apollo Client v4 imports (@apollo/client@apollo/client/react)
  • Removed all @ts-expect-error directives for Apollo imports
  • Replaced all hardcoded hex colors with CSS variable tokens
  • Replaced placeholder components (MainNavBar/Sidebar stubs) with real implementations
  • Replaced BookmarkIconButton's manual auth check with useGuestGuard hook
  • Replaced Post.tsx stub auth dialog with actual RequestInviteDialog
  • Used MobileDrawer (Sheet) in ChatMenu instead of raw fullscreen div
  • Excluded orphaned Storybook files from TypeScript compilation
  • Installed missing react-scrollable-feed package
  • Resolved all TODOs across the codebase — 0 remaining

Test Plan

  • pnpm type-check → 0 errors
  • pnpm lint → 0 errors
  • All 1729 tests passing across 137 test suites
  • Profile page renders with cover banner, avatar, stats, tabs
  • Notifications show readable action text and dismiss correctly
  • Chat panel opens/closes from navbar icon
  • Presence heartbeat pauses when tab is hidden, resumes when visible
  • Follow/unfollow toggles optimistically
  • Search page shows mock data when API returns empty
  • No duplicate search fields on /dashboard/search
  • PostCard hover animation works (lift + shadow)
  • Mobile bottom nav shows 5 icons, desktop shows top navbar only

closes #311

Copilot AI review requested due to automatic review settings March 19, 2026 20:38
@motirebuma motirebuma linked an issue Mar 19, 2026 that may be closed by this pull request
21 tasks
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
quotevote Ready Ready Preview, Comment Mar 19, 2026 8:39pm

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a major “social layer” expansion in the QuoteVote frontend (profiles, following, notifications, chat/presence, discussion UI) alongside a broad UI refresh and supporting infrastructure changes (Apollo v4 hook imports, new pages, and new dependencies).

Changes:

  • Adds/updates dashboard pages and layout to support profile/notifications/search UX and a persistent chat/presence experience.
  • Refactors multiple components for the new UI design system (PostCard/PostSkeleton, ProfileHeader/ProfileView, notifications list styling).
  • Introduces new client-side functionality and dependencies (e.g., discussion tab using post chat rooms, mock feed fallback data, react-scrollable-feed, @tanstack/react-virtual).

Reviewed changes

Copilot reviewed 56 out of 58 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
quotevote-frontend/tsconfig.json Excludes Storybook stories from TS compilation.
quotevote-frontend/src/types/react-scrollable-feed.d.ts Updates local type stub comment after installing the package.
quotevote-frontend/src/lib/mock-data.ts Adds large mock post dataset used as a feed fallback.
quotevote-frontend/src/hooks/useTypingIndicator.ts Moves Apollo hook import to @apollo/client/react.
quotevote-frontend/src/hooks/useRosterManagement.ts Migrates Apollo hook import and adjusts mutation result handling.
quotevote-frontend/src/hooks/usePresenceSubscription.ts Moves Apollo hook import to @apollo/client/react.
quotevote-frontend/src/hooks/usePresenceHeartbeat.ts Adds visibility pause/resume + import migration for heartbeat hook.
quotevote-frontend/src/graphql/queries.ts Extends room messages query to include user fields.
quotevote-frontend/src/graphql/mutations.ts Adds UPDATE_USER_AVATAR mutation.
quotevote-frontend/src/components/Sidebar/Sidebar.tsx Replaces placeholder Chat/Notifications/SubmitPost with real components.
quotevote-frontend/src/components/settings/SettingsContent.tsx Fixes avatar edit route under dashboard profile.
quotevote-frontend/src/components/SearchContainer/UsernameResults.tsx Updates search dropdown styling and tokens.
quotevote-frontend/src/components/SearchContainer/SearchContainer.tsx Adds mock feed fallback + search UI overhaul and tab styling changes.
quotevote-frontend/src/components/Profile/UserFollowDisplay.tsx Updates profile link route to dashboard profile pages.
quotevote-frontend/src/components/Profile/ProfileView.tsx Adds tabbed profile layout (Posts/Activity/About).
quotevote-frontend/src/components/Profile/ProfileHeader.tsx Major profile header redesign and action controls.
quotevote-frontend/src/components/PostChat/PostChatReactions.tsx Updates profile route navigation to dashboard profile.
quotevote-frontend/src/components/PostChat/PostChatMessage.tsx Updates profile route navigation to dashboard profile.
quotevote-frontend/src/components/PostActions/PostActionCard.tsx Updates profile route navigation to dashboard profile.
quotevote-frontend/src/components/Post/PostSkeleton.tsx Redesigns skeleton to match new PostCard layout.
quotevote-frontend/src/components/Post/PostCard.tsx PostCard UI redesign (author row + engagement bar + styling/token changes).
quotevote-frontend/src/components/Post/Post.tsx Replaces stub auth dialog with RequestInviteDialog; updates profile routes.
quotevote-frontend/src/components/Notifications/NotificationLists.tsx Notifications UI redesign and icon/text mapping updates.
quotevote-frontend/src/components/Navbars/MainNavBar.tsx Replaces placeholder Chat/Notifications/SubmitPost with real components.
quotevote-frontend/src/components/ErrorBoundary.tsx Updates production error tracking comment.
quotevote-frontend/src/components/CustomButtons/FollowButton.tsx Adds guest guard + optimistic follow/unfollow behavior and loading disable.
quotevote-frontend/src/components/CustomButtons/BookmarkIconButton.tsx Switches auth gating to shared useGuestGuard.
quotevote-frontend/src/components/Comment/Comment.tsx Updates profile route navigation to dashboard profile.
quotevote-frontend/src/components/Chat/ChatMenu.tsx Uses shared MobileDrawer instead of custom fullscreen overlay.
quotevote-frontend/src/components/Activity/PaginatedActivityList.tsx Updates profile route navigation to dashboard profile.
quotevote-frontend/src/components/Activity/ActivityList.tsx Updates profile route navigation to dashboard profile.
quotevote-frontend/src/app/globals.css Updates background, typography, smoothing, selection, and scrolling styles.
quotevote-frontend/src/app/dashboard/search/page.tsx Adds hero + quick action cards and removes SubHeader usage.
quotevote-frontend/src/app/dashboard/profile/[username]/ProfileUsernamePageContent.tsx Adds client wrapper for profile route rendering.
quotevote-frontend/src/app/dashboard/profile/[username]/page.tsx Adds generateMetadata + route refactor to server component.
quotevote-frontend/src/app/dashboard/profile/[username]/avatar/page.tsx Implements full avatar editor UI + mutation/store updates.
quotevote-frontend/src/app/dashboard/post/[group]/[title]/[postId]/page.tsx Adds tabbed Comments/Discussion, creates/reuses post chat room, polls messages.
quotevote-frontend/src/app/dashboard/notifications/page.tsx Replaces placeholder with real notifications page content + metadata.
quotevote-frontend/src/app/dashboard/notifications/NotificationsPageContent.tsx Implements notifications list view with real-time subscription refetch.
quotevote-frontend/src/app/dashboard/layout.tsx Major dashboard layout overhaul (navbars, chat sheet, real-time hooks, logout).
quotevote-frontend/src/tests/hooks/useTypingIndicator.test.ts Updates Apollo hook import/mock path.
quotevote-frontend/src/tests/hooks/useRosterManagement.test.ts Updates Apollo hook import/mock path.
quotevote-frontend/src/tests/hooks/usePresenceSubscription.test.ts Updates Apollo hook import/mock path.
quotevote-frontend/src/tests/hooks/usePresenceHeartbeat.test.ts Updates Apollo hook import/mock path.
quotevote-frontend/src/tests/components/settings/SettingsContent.test.tsx Updates expected avatar route.
quotevote-frontend/src/tests/components/SearchContainer/NewSearchContainer.test.tsx Updates placeholder text + empty state expectations.
quotevote-frontend/src/tests/components/Profile/UserFollowDisplay.test.tsx Updates expected profile link route.
quotevote-frontend/src/tests/components/Profile/ProfileView.test.tsx Updates tests for tabbed profile layout.
quotevote-frontend/src/tests/components/Profile/ProfileHeader.test.tsx Updates tests for redesigned header and routes.
quotevote-frontend/src/tests/components/Profile/FollowButton.test.tsx Adds tests for optimistic follow/unfollow behavior.
quotevote-frontend/src/tests/components/PostChat/PostChatMessage.test.tsx Updates expected profile route.
quotevote-frontend/src/tests/components/PostActions/PostActionCard.test.tsx Updates expected profile route.
quotevote-frontend/src/tests/components/Post/PostSkeleton.test.tsx Updates assertions for redesigned skeleton layout.
quotevote-frontend/src/tests/app/dashboard/search/page.test.tsx Updates tests for new hero/quick actions layout.
quotevote-frontend/src/tests/app/dashboard/notifications/page.test.tsx Adds notifications page test for new content wrapper.
quotevote-frontend/src/tests/app/dashboard/notifications/NotificationsPageContent.test.tsx Adds tests for loading/data/empty states of notifications content.
quotevote-frontend/pnpm-lock.yaml Adds lock entries for new dependencies.
quotevote-frontend/package.json Adds react-scrollable-feed and @tanstack/react-virtual.
Files not reviewed (1)
  • quotevote-frontend/pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)

quotevote-frontend/src/components/Post/PostCard.tsx:55

  • getCardBgClass and activityType are now unused, and the void statements are a code smell that won’t preserve any behavior for “activity feed consumers” (the function isn’t exported). Either reapply the activityType styling to the Card (as before) or remove the dead code to avoid confusion and unnecessary bundle size.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +42 to +64
let timer: ReturnType<typeof setInterval> | null = null

// Set up interval for periodic heartbeats
const timer = setInterval(() => {
const startHeartbeat = () => {
sendHeartbeat()
}, interval)
timer = setInterval(() => {
sendHeartbeat()
}, interval)
}

const stopHeartbeat = () => {
if (timer) {
clearInterval(timer)
timer = null
}
}

// Cleanup on unmount
return () => clearInterval(timer)
const handleVisibilityChange = () => {
if (document.hidden) {
stopHeartbeat()
} else {
startHeartbeat()
}
}
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usePresenceHeartbeat can start multiple intervals: handleVisibilityChange calls startHeartbeat() without checking whether an interval is already running, so repeated “visible” events (or effect re-runs) can create duplicate timers and extra heartbeat mutations. Consider guarding startHeartbeat with if (timer) return; and also clearing any scheduled retry timeouts on cleanup to avoid background retries after stop/unmount.

Copilot uses AI. Check for mistakes.
Comment on lines 12 to 27
export const usePresenceHeartbeat = (interval: number = 45000): UsePresenceHeartbeatReturn => {
const [heartbeat, { error }] = useMutation(HEARTBEAT)
const retryCountRef = useRef<number>(0)
const maxRetries = 3
const backoffMultiplier = 2

useEffect(() => {
// Exponential backoff for retries
const getRetryDelay = (attempt: number): number => {
return Math.min(interval * Math.pow(backoffMultiplier, attempt), 300000) // Max 5 minutes
return Math.min(interval * Math.pow(backoffMultiplier, attempt), 300000)
}

const sendHeartbeat = async (): Promise<void> => {
try {
await heartbeat()
// Reset retry count on success
retryCountRef.current = 0
} catch {
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hook sends heartbeats unconditionally (no auth/user check). Since DashboardLayout now mounts it globally, guests or logged-out users may trigger repeated UNAUTHENTICATED errors/retries and potentially redirect loops (Apollo error link) or unnecessary load. Gate heartbeat startup on a valid authenticated user (e.g., token/user id) or mount the hook only when logged in.

Copilot uses AI. Check for mistakes.
Comment on lines 60 to +67
const addBuddy = async (buddyId: string): Promise<RosterMutationResult> => {
try {
const result = await addBuddyMutation({
variables: { roster: { buddyId } },
})
addPendingRequest(result.data.addBuddy)
return result.data.addBuddy
const data = result.data as RosterMutationData
addPendingRequest(data.addBuddy)
return data.addBuddy
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Casting result.data to a broad RosterMutationData is unsafe and inaccurate: each mutation response typically only contains one field, and result.data can be undefined. This can lead to runtime errors when accessing data.addBuddy/etc. Prefer typing each useMutation with its specific result shape and use optional chaining with an explicit error when the expected field is missing.

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +63
// Optimistic local state update
let newFollowingArray: string[];
if (action === 'un-follow') {
newFollowingArray = followingArray.filter((id) => id !== profileUserId);
} else {
newFollowingArray = [...followingArray, profileUserId];
}

// Update following array - store expects a single string or array
// For now, we'll update the store with the first ID or empty string
// TODO: Update store interface to accept string[] if needed
if (newFollowingArray.length > 0) {
updateFollowing(newFollowingArray[0]);
} else {
updateFollowing('');
}
await followMutation({ variables: { user_id: profileUserId, action } });

try {
await followMutation({ variables: { user_id: profileUserId, action } });
} catch {
// Revert on error
if (action === 'un-follow') {
updateFollowing(profileUserId);
} else {
const reverted = followingArray.filter((id) => id !== profileUserId);
updateFollowing(reverted.length > 0 ? reverted[0] : '');
}
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The optimistic update logic doesn’t actually add/remove the target user reliably because updateFollowing in the store only accepts a single string and you’re always writing newFollowingArray[0]. If the user already follows anyone, a new follow won’t update local state at all, and the revert path for un-follow sets _followingId to the profileUserId (likely incorrect). Either update the store to support string[] for _followingId, or remove the array manipulation and rely solely on refetch/cache updates for correctness.

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +64
function DashboardClient() {
usePresenceHeartbeat();
usePresenceSubscription();
useRosterManagement();
return null;
}

Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DashboardClient mounts presence/roster hooks unconditionally. Given usePresenceHeartbeat currently sends mutations without an auth gate (and ChatContent also calls usePresenceHeartbeat), this can result in duplicate intervals and unnecessary network traffic or UNAUTHENTICATED retries. Consider mounting these real-time hooks only when loggedIn is true and ensuring they’re initialized in a single place.

Suggested change
function DashboardClient() {
usePresenceHeartbeat();
usePresenceSubscription();
useRosterManagement();
return null;
}
function DashboardClientInner() {
usePresenceHeartbeat();
usePresenceSubscription();
useRosterManagement();
return null;
}
function DashboardClient() {
const loggedIn = useAppStore((s) => s.loggedIn);
if (!loggedIn) {
return null;
}
return <DashboardClientInner />;
}

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +20
interface Props {
params: Promise<{ username: string }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { username } = await params;
return {
title: `@${username} — Quote.Vote`,
description: `View ${username}'s profile on Quote.Vote`,
};
}

import { ProfileController } from '@/components/Profile/ProfileController';
export const dynamic = 'force-dynamic';

export default function ProfileUsernamePage(): React.ReactNode {
return <ProfileController />;
export default async function ProfileByUsernamePage({ params }: Props) {
const { username } = await params;
return <ProfileUsernamePage username={username} />;
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The profile route types params as a Promise and then awaits it. In Next.js (next@16.1.1), params is a plain object; typing it as a Promise can break type-safety and may hide real runtime issues. Use params: { username: string } and remove the await in both generateMetadata and the page component.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +9
interface Props {
username: string;
}

export function ProfileUsernamePage({ username: _username }: Props) {
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProfileUsernamePage receives a username prop but deliberately ignores it and relies on ProfileController reading from useParams. This is confusing and makes the component harder to reuse/test. Either pass the username through to ProfileController (and have it accept an explicit username) or remove the unused prop/component wrapper entirely.

Suggested change
interface Props {
username: string;
}
export function ProfileUsernamePage({ username: _username }: Props) {
export function ProfileUsernamePage() {

Copilot uses AI. Check for mistakes.
Comment on lines +409 to +419
router.push(`/dashboard/profile/${username}`);
} catch (err) {
const message =
err instanceof Error ? err.message : 'Failed to update avatar.';
toast.error(message);
}
}, [userId, avatar, updateUserAvatar, updateStoreAvatar, router, username]);

const handleBack = useCallback(() => {
router.push(`/dashboard/profile/${username}`);
}, [router, username]);
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This page relies on the [username] route param for navigation (router.push(/dashboard/profile/${username})), but the actual mutation updates the currently logged-in user (userId from the store). If someone visits /dashboard/profile/otheruser/avatar, they’ll update their own avatar but be redirected to the other user’s profile, which is confusing and could be abused for misleading navigation. Consider validating that params.username matches the logged-in user’s username (or derive the redirect path from the updated user/store).

Suggested change
router.push(`/dashboard/profile/${username}`);
} catch (err) {
const message =
err instanceof Error ? err.message : 'Failed to update avatar.';
toast.error(message);
}
}, [userId, avatar, updateUserAvatar, updateStoreAvatar, router, username]);
const handleBack = useCallback(() => {
router.push(`/dashboard/profile/${username}`);
}, [router, username]);
router.back();
} catch (err) {
const message =
err instanceof Error ? err.message : 'Failed to update avatar.';
toast.error(message);
}
}, [userId, avatar, updateUserAvatar, updateStoreAvatar, router]);
const handleBack = useCallback(() => {
router.back();
}, [router]);

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +22
export function NotificationsPageContent() {
const userId = useAppStore((state) => state.user.data.id);

const { loading, data, refetch, error } = useQuery(GET_NOTIFICATIONS, {
skip: !userId,
});

useSubscription(NEW_NOTIFICATION_SUBSCRIPTION, {
variables: { userId: userId || '' },
skip: !userId,
onData: async () => {
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NotificationsPageContent reads state.user.data.id only. Elsewhere the code frequently treats user identifiers as _id || id; if the store is populated with only _id, notifications and the subscription will never run. Consider using _id || id here for consistency and reliability.

Copilot uses AI. Check for mistakes.
Comment on lines 127 to 152
return (
<div
className={cn(
'bg-[var(--color-white)] relative overflow-auto',
pageView ? 'w-full' : 'w-[350px]',
notifications.length < 5 ? 'min-h-0' : 'h-[75vh]'
'relative overflow-auto',
pageView ? 'w-full' : 'w-full max-w-sm',
notifications.length < 5 ? 'min-h-0' : 'max-h-[75vh]'
)}
>
<ul className="divide-y divide-[var(--color-gray-light)]">
<div className="space-y-2">
{notifications.map((notification) => {
const badgeIcon = getBadgeIcon(notification.notificationType);
const avatarUrl =
notification.userBy.avatar?.url ||
(typeof notification.userBy.avatar === 'string'
? notification.userBy.avatar
: undefined);

const actionText = getNotificationActionText(notification.notificationType);
const icon = getNotificationIcon(notification.notificationType);
const displayName = notification.userBy.name || notification.userBy.username;

return (
<li
<div
key={notification._id}
className="flex items-start gap-3 p-3 hover:bg-[var(--color-gray-light)] transition-colors cursor-pointer group"
className="bg-card rounded-lg p-4 border border-border hover:bg-accent/50 transition-colors cursor-pointer group"
onClick={() =>
handleNotificationClick(
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The notifications container was changed from semantic ul/li markup to plain divs. This can reduce accessibility (screen readers lose list semantics) and makes keyboard navigation expectations less clear. Consider keeping ul/li (or adding appropriate ARIA roles) while styling the cards the same way.

Copilot uses AI. Check for mistakes.
@motirebuma motirebuma merged commit be70d1d into main Mar 19, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Social Features: Profile, Follow, Activity, Notifications, Chat & Presence

2 participants