diff --git a/apps/socialmon/src/components/posts/PostList/PostDetail.tsx b/apps/socialmon/src/components/posts/PostList/PostDetail.tsx index 7294dda8b..1ac296412 100644 --- a/apps/socialmon/src/components/posts/PostList/PostDetail.tsx +++ b/apps/socialmon/src/components/posts/PostList/PostDetail.tsx @@ -21,8 +21,8 @@ import { trpc } from '~/hooks/trpc'; import type { PostExtended } from '~/types'; -import PostCommentsList from '../comments/PostCommentsList'; import { parseMarkdown } from '../../common/ParseMarkdown'; +import PostCommentsList from '../comments/PostCommentsList'; import PostRelevanceActionButton from '../PostRelevanceActionButton'; import PostReplyStatusActionButton from '../PostReplyStatusActionButton'; import PostMetadata from './PostMetadata'; diff --git a/apps/socialmon/src/components/posts/PostList/PostList.tsx b/apps/socialmon/src/components/posts/PostList/PostList.tsx index 8e094d12d..26846ebd4 100644 --- a/apps/socialmon/src/components/posts/PostList/PostList.tsx +++ b/apps/socialmon/src/components/posts/PostList/PostList.tsx @@ -76,13 +76,6 @@ export default function PostList() { All - } - withArrow={true}> - - Pending - - } withArrow={true}> @@ -99,6 +92,13 @@ export default function PostList() { Irrelevant + } + withArrow={true}> + + Pending + +
diff --git a/apps/socialmon/src/components/posts/PostRelevanceActionButton.tsx b/apps/socialmon/src/components/posts/PostRelevanceActionButton.tsx index f5448f97f..82dd35272 100644 --- a/apps/socialmon/src/components/posts/PostRelevanceActionButton.tsx +++ b/apps/socialmon/src/components/posts/PostRelevanceActionButton.tsx @@ -7,6 +7,7 @@ import { trpc } from '~/hooks/trpc'; import useCurrentProjectSlug from '~/hooks/useCurrentProjectSlug'; import ShortcutDisplay from '~/components/common/ShortcutDisplay'; +import { useOptionalPostsContext } from '~/components/posts/PostsContext'; import { ShortcutAction } from '~/config/shortcuts'; import { PostRelevancy } from '~/prisma/client'; @@ -27,30 +28,42 @@ export default function PostRelevanceActionButton({ const markPostRelevancyMutation = trpc.socialPosts.markPostRelevancy.useMutation(); + // Try to get context (undefined if not in PostsProvider) + const context = useOptionalPostsContext(); + const { markPostRelevancy: contextMarkPostRelevancy } = context || {}; + + const targetRelevancy = + relevancy === PostRelevancy.IRRELEVANT + ? PostRelevancy.RELEVANT + : PostRelevancy.IRRELEVANT; + const onMarkPostRelevancy = () => { - markPostRelevancyMutation.mutate( - { - postId, - projectSlug, - relevancy: - relevancy === PostRelevancy.IRRELEVANT - ? PostRelevancy.RELEVANT - : PostRelevancy.IRRELEVANT, - }, - { - onSuccess() { - // Fast update for posts list (badge appears immediately) - utils.socialPosts.getPosts.invalidate(); - // Comprehensive update for everything else (button text updates) - router.refresh(); - toast.success( - relevancy === PostRelevancy.IRRELEVANT - ? 'Marked the post as relevant successfully!' - : 'Marked the post as irrelevant successfully!', - ); + if (contextMarkPostRelevancy) { + // Use context method with auto-navigation (when in post list) + contextMarkPostRelevancy(postId, targetRelevancy); + } else { + // Fall back to direct mutation (when in standalone post) + markPostRelevancyMutation.mutate( + { + postId, + projectSlug, + relevancy: targetRelevancy, }, - }, - ); + { + onSuccess() { + // Fast update for posts list (badge appears immediately) + utils.socialPosts.getPosts.invalidate(); + // Comprehensive update for everything else (button text updates) + router.refresh(); + toast.success( + relevancy === PostRelevancy.IRRELEVANT + ? 'Marked the post as relevant successfully!' + : 'Marked the post as irrelevant successfully!', + ); + }, + }, + ); + } }; const label = diff --git a/apps/socialmon/src/components/posts/PostReplyStatusActionButton.tsx b/apps/socialmon/src/components/posts/PostReplyStatusActionButton.tsx index 851633136..dae3916e9 100644 --- a/apps/socialmon/src/components/posts/PostReplyStatusActionButton.tsx +++ b/apps/socialmon/src/components/posts/PostReplyStatusActionButton.tsx @@ -7,6 +7,7 @@ import { trpc } from '~/hooks/trpc'; import useCurrentProjectSlug from '~/hooks/useCurrentProjectSlug'; import ShortcutDisplay from '~/components/common/ShortcutDisplay'; +import { useOptionalPostsContext } from '~/components/posts/PostsContext'; import { ShortcutAction } from '~/config/shortcuts'; import { PostRepliedStatus } from '~/prisma/client'; @@ -28,32 +29,48 @@ export default function PostReplyStatusActionButton({ const markPostReplyStatusMutation = trpc.socialPosts.markPostReplyStatus.useMutation(); - const onMarkPostReplyStatus = () => { - const newStatus = - replyStatus === PostRepliedStatus.NOT_REPLIED - ? PostRepliedStatus.REPLIED_MANUALLY - : PostRepliedStatus.NOT_REPLIED; + // Try to get context (undefined if not in PostsProvider) + const context = useOptionalPostsContext(); + const { markPostReplyStatus: contextMarkPostReplyStatus } = context || {}; - markPostReplyStatusMutation.mutate( - { - postId, - projectSlug, - replyStatus: newStatus, - }, - { - onSuccess() { - // Fast update for posts list (badge appears immediately) - utils.socialPosts.getPosts.invalidate(); - // Comprehensive update for everything else (button text updates) - router.refresh(); - toast.success( - newStatus === PostRepliedStatus.REPLIED_MANUALLY - ? 'Marked the post as replied successfully!' - : 'Marked the post as not replied successfully!', - ); + const newStatus = + replyStatus === PostRepliedStatus.NOT_REPLIED + ? PostRepliedStatus.REPLIED_MANUALLY + : PostRepliedStatus.NOT_REPLIED; + + // Convert to context type + const contextReplyStatus: 'NOT_REPLIED' | 'REPLIED_MANUALLY' = + newStatus === PostRepliedStatus.REPLIED_MANUALLY + ? 'REPLIED_MANUALLY' + : 'NOT_REPLIED'; + + const onMarkPostReplyStatus = () => { + if (contextMarkPostReplyStatus) { + // Use context method with auto-navigation (when in post list) + contextMarkPostReplyStatus(postId, contextReplyStatus); + } else { + // Fall back to direct mutation (when in standalone post) + markPostReplyStatusMutation.mutate( + { + postId, + projectSlug, + replyStatus: newStatus, }, - }, - ); + { + onSuccess() { + // Fast update for posts list (badge appears immediately) + utils.socialPosts.getPosts.invalidate(); + // Comprehensive update for everything else (button text updates) + router.refresh(); + toast.success( + newStatus === PostRepliedStatus.REPLIED_MANUALLY + ? 'Marked the post as replied successfully!' + : 'Marked the post as not replied successfully!', + ); + }, + }, + ); + } }; // Don't show button if post was replied via app diff --git a/apps/socialmon/src/components/posts/PostsContext.tsx b/apps/socialmon/src/components/posts/PostsContext.tsx index 58290149b..162d65399 100644 --- a/apps/socialmon/src/components/posts/PostsContext.tsx +++ b/apps/socialmon/src/components/posts/PostsContext.tsx @@ -1,7 +1,14 @@ 'use client'; import { useParams, useRouter } from 'next/navigation'; -import { createContext, useContext, useEffect, useMemo, useState } from 'react'; +import { + createContext, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { trpc } from '~/hooks/trpc'; @@ -52,6 +59,89 @@ export function PostsProvider({ const params = useParams(); const utils = trpc.useUtils(); + // Store navigation context before mutations to handle auto-navigation + const navigationContextRef = useRef<{ + currentIndex: number; + postsSnapshot: Array; + } | null>(null); + + // Helper function to find the next logical post after a state change + const findNextLogicalPost = ( + currentPostId: string, + postsSnapshot: Array, + ): string | null => { + const currentIndex = postsSnapshot.findIndex( + (post) => post.id === currentPostId, + ); + + if (currentIndex === -1) return null; + + // Try next post first + if (currentIndex < postsSnapshot.length - 1) { + return postsSnapshot[currentIndex + 1]!.id; + } + + // If no next post, try previous post + if (currentIndex > 0) { + return postsSnapshot[currentIndex - 1]!.id; + } + + // No adjacent posts available + return null; + }; + + // Helper function to determine if auto-navigation should happen based on current tab + const shouldAutoNavigateForTab = ( + currentTab: PostListTab, + actionType: 'relevancy' | 'reply', + ): boolean => { + switch (currentTab) { + case 'ALL': + // Posts always stay visible in ALL tab, no navigation needed + return false; + + case 'PENDING': + // Navigate when marking as replied or irrelevant (removes from pending) + return actionType === 'reply' || actionType === 'relevancy'; + + case 'REPLIED': + // Navigate when marking as not replied (removes from replied tab) + return actionType === 'reply'; + + case 'IRRELEVANT': + // Navigate when marking as relevant (removes from irrelevant tab) + return actionType === 'relevancy'; + + default: + // Conservative default: navigate for unknown tabs + return true; + } + }; + + // Helper function to handle auto-navigation after successful mutation + const handleAutoNavigation = ( + postId: string, + actionType: 'relevancy' | 'reply', + ): void => { + // Only navigate if current post was selected and we have navigation context + if (selectedPostId === postId && navigationContextRef.current) { + const shouldNavigate = shouldAutoNavigateForTab(activeTab, actionType); + + if (shouldNavigate) { + const nextPostId = findNextLogicalPost( + postId, + navigationContextRef.current.postsSnapshot, + ); + + if (nextPostId) { + // Navigate to next post immediately + setSelectedPostId(nextPostId); + router.push(`/projects/${projectSlug}/posts/${nextPostId}`); + } + } + } + }; + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = trpc.socialPosts.getPosts.useInfiniteQuery( { @@ -106,12 +196,18 @@ export function PostsProvider({ function handlePrevPost() { if (adjacentPosts.prev) { handlePostClick(adjacentPosts.prev.id); + } else if (posts.length > 0) { + // Fallback: if no prev post but we have posts, go to the first post + handlePostClick(posts[0]!.id); } } function handleNextPost() { if (adjacentPosts.next) { handlePostClick(adjacentPosts.next.id); + } else if (posts.length > 0) { + // Fallback: if no next post but we have posts, go to the last post + handlePostClick(posts[posts.length - 1]!.id); } } @@ -123,6 +219,14 @@ export function PostsProvider({ trpc.socialPosts.markPostReplyStatus.useMutation(); function markPostRelevancy(postId: string, relevancy: PostRelevancy) { + // Capture navigation context before mutation + const currentIndex = posts.findIndex((post) => post.id === postId); + + navigationContextRef.current = { + currentIndex, + postsSnapshot: [...posts], // Create a snapshot + }; + markPostRelevancyMutation.mutate( { postId, @@ -130,15 +234,35 @@ export function PostsProvider({ relevancy, }, { + onError() { + // Clear navigation context on error + navigationContextRef.current = null; + }, onSuccess() { + // First, invalidate and refresh the data utils.socialPosts.getPosts.invalidate(); + + // Handle auto-navigation if needed + handleAutoNavigation(postId, 'relevancy'); + router.refresh(); + + // Clear navigation context + navigationContextRef.current = null; }, }, ); } function markPostReplyStatus(postId: string, replyStatus: ManualReplyStatus) { + // Capture navigation context before mutation + const currentIndex = posts.findIndex((post) => post.id === postId); + + navigationContextRef.current = { + currentIndex, + postsSnapshot: [...posts], // Create a snapshot + }; + markPostReplyStatusMutation.mutate( { postId, @@ -146,9 +270,21 @@ export function PostsProvider({ replyStatus, }, { + onError() { + // Clear navigation context on error + navigationContextRef.current = null; + }, onSuccess() { + // First, invalidate and refresh the data utils.socialPosts.getPosts.invalidate(); + + // Handle auto-navigation if needed + handleAutoNavigation(postId, 'reply'); + router.refresh(); + + // Clear navigation context + navigationContextRef.current = null; }, }, ); @@ -188,3 +324,9 @@ export function usePostsContext() { return context; } + +export function useOptionalPostsContext() { + const context = useContext(PostsContext); + + return context; // Returns undefined if no provider, doesn't throw +}