Skip to content

Conversation

@julianbenegas
Copy link
Member

Summary

  • Adds RepoPermissionsProvider context at app/[owner]/[repo]/layout.tsx
  • checkCanModerate now runs once per repo navigation instead of in each component
  • Components use useRepoPermissions() hook to access permissions
  • Removes unused canModerate prop from PostMetadataProvider

Test plan

  • Navigate to a repo where you have moderator permissions
  • Verify moderation banner appears on posts
  • Verify stale banner shows re-run button for moderators
  • Navigate between posts within the same repo—permissions should persist without re-fetching

🤖 Generated with Claude Code

julianbenegas and others added 2 commits January 16, 2026 18:30
Extracts canModerate check to RepoPermissionsProvider at the repo layout level.
Now checkCanModerate only runs once when entering a repo, shared via context.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@vercel
Copy link
Contributor

vercel bot commented Jan 16, 2026

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

Project Deployment Review Updated (UTC)
forums Ready Ready Preview, Comment Jan 16, 2026 10:30pm

}) {
const session = authClient.useSession()
const [canModerate, setCanModerate] = useState(false)
const [isLoading, setIsLoading] = useState(true)
Copy link
Contributor

Choose a reason for hiding this comment

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

ModerationBanner and StaleBanner components don't wait for permission loading to complete, causing moderation UI to be hidden during initial load even when user has permissions

View Details
📝 Patch Details
diff --git a/app/[owner]/[repo]/[postNumber]/post-header.tsx b/app/[owner]/[repo]/[postNumber]/post-header.tsx
index 8c522c7..9ce76bf 100644
--- a/app/[owner]/[repo]/[postNumber]/post-header.tsx
+++ b/app/[owner]/[repo]/[postNumber]/post-header.tsx
@@ -151,7 +151,7 @@ function RefSelector() {
 
 function ModerationBanner() {
   const { postId, pinned } = usePostMetadata()
-  const { canModerate } = useRepoPermissions()
+  const { canModerate, isLoading } = useRepoPermissions()
   const [isPinned, setIsPinned] = useState(pinned)
   const [isPending, startTransition] = useTransition()
   const router = useRouter()
@@ -159,7 +159,7 @@ function ModerationBanner() {
     (s) => s.setModeratorDeletePostDialog
   )
 
-  if (!canModerate) {
+  if (!canModerate || isLoading) {
     return null
   }
 
@@ -231,7 +231,7 @@ function StaleBanner() {
   const session = authClient.useSession()
   const userId = session.data?.user.id
   const isAuthor = userId === authorId
-  const { canModerate } = useRepoPermissions()
+  const { canModerate, isLoading } = useRepoPermissions()
   const [isPending, startTransition] = useTransition()
   const router = useRouter()
 
@@ -239,7 +239,7 @@ function StaleBanner() {
     return null
   }
 
-  const canRerun = isAuthor || canModerate
+  const canRerun = (isAuthor || canModerate) && !isLoading
 
   function handleRerun() {
     startTransition(async () => {

Analysis

Bug Explanation:

The RepoPermissionsProvider context provides both canModerate and isLoading states. The canModerate state starts as false (line 24 in repo-permissions-context.tsx) and isLoading starts as true. After an async permission check completes, canModerate is updated and isLoading is set to false.

However, two components (ModerationBanner at line 154 and StaleBanner at line 234 in post-header.tsx) were only destructuring canModerate without checking isLoading. This causes:

  1. ModerationBanner: Returns null if !canModerate (line 159). During the loading phase, canModerate is false, so the entire moderation banner is hidden. When the permission check completes and the user actually has moderation permissions, the banner only appears after the component re-renders. This causes UI flickering or the banner appearing after a delay.

  2. StaleBanner: Computes canRerun = isAuthor || canModerate (line 244). During loading, canModerate is false, so the rerun button won't show for moderators until the permission check completes, even though they should have access.

Fix Explanation:

I modified both components to destructure isLoading from useRepoPermissions() and incorporate it into their rendering logic:

  1. ModerationBanner: Changed the early return condition from if (!canModerate) to if (!canModerate || isLoading). This ensures the moderation banner doesn't render until the permission check completes, preventing the flickering behavior caused by canModerate being initially false.

  2. StaleBanner: Changed canRerun computation from isAuthor || canModerate to (isAuthor || canModerate) && !isLoading. This ensures the rerun button only appears after the permission check completes, preventing premature hiding of the button for moderators.

The fix ensures that moderation UI correctly represents the user's permissions once the loading state completes, eliminating the false-negative display issue.

Comment on lines +34 to +37
checkCanModerate(owner, repo).then((result) => {
setCanModerate(result)
setIsLoading(false)
})
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
checkCanModerate(owner, repo).then((result) => {
setCanModerate(result)
setIsLoading(false)
})
checkCanModerate(owner, repo)
.then((result) => {
setCanModerate(result)
setIsLoading(false)
})
.catch((error) => {
console.error("Failed to check moderation permissions:", error)
setCanModerate(false)
setIsLoading(false)
})

Missing error handler on checkCanModerate promise causes loading state to persist indefinitely when the async operation fails

View Details
📝 Patch Details
diff --git a/app/[owner]/[repo]/repo-permissions-context.tsx b/app/[owner]/[repo]/repo-permissions-context.tsx
index 8145431..1c1f0d2 100644
--- a/app/[owner]/[repo]/repo-permissions-context.tsx
+++ b/app/[owner]/[repo]/repo-permissions-context.tsx
@@ -31,10 +31,16 @@ export function RepoPermissionsProvider({
       return
     }
     setIsLoading(true)
-    checkCanModerate(owner, repo).then((result) => {
-      setCanModerate(result)
-      setIsLoading(false)
-    })
+    checkCanModerate(owner, repo)
+      .then((result) => {
+        setCanModerate(result)
+        setIsLoading(false)
+      })
+      .catch((error) => {
+        console.error("Failed to check moderation permissions:", error)
+        setCanModerate(false)
+        setIsLoading(false)
+      })
   }, [owner, repo, session.data?.user])
 
   return (

Analysis

Bug Explanation

The checkCanModerate promise chain in repo-permissions-context.tsx (lines 34-37) was missing a .catch() error handler. The checkCanModerate server action performs multiple async operations that can fail:

  1. Authentication call: auth.api.getSession() can throw errors
  2. Database queries: canModerate() involves database operations that can fail due to network issues, database unavailability, or connectivity problems
  3. Authorization failures: Auth-related errors can be thrown

When any of these operations fail, the promise rejects. Without a .catch() handler:

  • The .then() callback never executes
  • setIsLoading(false) is never called
  • isLoading remains true indefinitely
  • The UI gets stuck in a loading state, providing poor user experience
  • There's no visibility into what went wrong (no error logging)

Fix Explanation

Added a .catch() handler that:

  1. Logs the error to console for debugging/monitoring
  2. Sets canModerate to false (fail-safe: deny access on error)
  3. Critically: Sets isLoading to false to unblock the UI

This ensures that regardless of whether the permission check succeeds or fails, the loading state is properly cleaned up, preventing the UI from hanging indefinitely. The fail-safe default of setCanModerate(false) ensures security by denying moderation permissions if we can't verify them.

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.

2 participants