[socialmon] inbox: fix unusable prev/next buttons (#1572)

This commit is contained in:
Zhou Yuhang 2025-07-24 09:10:56 +08:00 committed by GitHub
parent 37210da6ce
commit d7efb99636
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 227 additions and 55 deletions

View File

@ -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';

View File

@ -76,13 +76,6 @@ export default function PostList() {
All
</Tabs.Tab>
</Tooltip>
<Tooltip
label={<ShortcutDisplay action={ShortcutAction.GO_TO_PENDING} />}
withArrow={true}>
<Tabs.Tab fw={500} value="PENDING">
Pending
</Tabs.Tab>
</Tooltip>
<Tooltip
label={<ShortcutDisplay action={ShortcutAction.GO_TO_REPLIED} />}
withArrow={true}>
@ -99,6 +92,13 @@ export default function PostList() {
Irrelevant
</Tabs.Tab>
</Tooltip>
<Tooltip
label={<ShortcutDisplay action={ShortcutAction.GO_TO_PENDING} />}
withArrow={true}>
<Tabs.Tab fw={500} value="PENDING">
Pending
</Tabs.Tab>
</Tooltip>
</Tabs.List>
</Tabs>
<div className="absolute right-2 top-2 flex items-center gap-2">

View File

@ -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 =

View File

@ -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

View File

@ -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<QueriedRedditPost>;
} | null>(null);
// Helper function to find the next logical post after a state change
const findNextLogicalPost = (
currentPostId: string,
postsSnapshot: Array<QueriedRedditPost>,
): 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
}