Skip to content

fix #591: Limit draw notifications to group owner#683

Open
jefische wants to merge 11 commits intodevelopfrom
jeremy/591-limit-draw-notifications-groupOwner
Open

fix #591: Limit draw notifications to group owner#683
jefische wants to merge 11 commits intodevelopfrom
jeremy/591-limit-draw-notifications-groupOwner

Conversation

@jefische
Copy link
Copy Markdown
Contributor

@jefische jefische commented Nov 13, 2025

Description

Before:

Previously any group that a user was part of, that user would receive toast notifications related to the drawing which only the owner can handle.

After:

All toasts for groups that pertain to the logged-in user are now filtered. Toasts will only display when the group owner id matches the session user id.

Closes #591

Testing instructions

Join groups for which you are not the owner, and also create groups for which you are the owner. Trigger a toast such as Draw date has passed or Draw Today to see the toasts only for the groups that you are the owner.

Screenshot of dashboard with error toast messages

image

Pre-submission checklist

  • Code builds and passes locally
  • PR title follows Conventional Commit format (e.g. test #001: created unit test for __ component)
  • Request reviews from the Peer Code Reviewers and Senior+ Code Reviewers groups
  • Thread has been created in Discord and PR is linked in gis-code-questions

Summary by CodeRabbit

  • Bug Fixes

    • Toast notifications now correctly display only for exchange groups you own, preventing unwanted alerts from groups outside your control and ensuring you see only relevant notifications for your active exchanges.
  • Infrastructure

    • Refined component structure and notification system architecture to improve maintainability.

@vercel
Copy link
Copy Markdown

vercel Bot commented Nov 13, 2025

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

Project Deployment Preview Comments Updated (UTC)
elecretanta Ready Ready Preview Comment Nov 18, 2025 4:45pm
elecretanta-storybook Ready Ready Preview Comment Nov 18, 2025 4:45pm
elecretanta-unit-test Ready Ready Preview Comment Nov 18, 2025 4:45pm

@shashilo shashilo requested review from a team and shashilo and removed request for a team November 13, 2025 16:43
@jefische jefische changed the title Jeremy/591 limit draw notifications group owner fix #591: Limit draw notifications group owner Nov 13, 2025
@jefische jefische changed the title fix #591: Limit draw notifications group owner fix #591: Limit draw notifications to group owner Nov 13, 2025
@jefische jefische marked this pull request as ready for review November 13, 2025 16:48
Copilot AI review requested due to automatic review settings November 13, 2025 16:48
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 addresses issue #591 by filtering toast notifications to only show those for groups where the logged-in user is the owner. Previously, all group members received draw-related notifications, but only owners could act on them.

Key Changes:

  • Added useExchangeGroups hook to fetch user's gift exchange groups
  • Modified Toaster component to filter toasts based on group ownership
  • Moved <Toaster /> inside AuthContextProvider in layout to enable access to session data

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
hooks/useExchangeGroups.ts New custom hook that fetches and manages the current user's gift exchange groups
components/Toaster/Toaster.tsx Added filtering logic to show toasts only for groups where user is owner; requires session and exchange group data
components/Toaster/Toaster.test.tsx Updated tests with mocks for new dependencies and added group filtering test scenarios
app/layout.tsx Moved Toaster component inside AuthContextProvider to enable access to session context

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

Comment on lines +17 to +39
async function fetchGiftExchanges() {
try {
const response = await fetch(`/api/gift-exchanges`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
setGiftExchanges(data);

} catch (error) {
console.error('Failed to fetch gift exchanges:', error);
}
};

useEffect(() => {
fetchGiftExchanges();
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The useExchangeGroups hook may cause race conditions when the component unmounts before the fetch completes. Consider adding cleanup logic to prevent state updates on unmounted components:

useEffect(() => {
  let isMounted = true;
  
  async function fetchGiftExchanges() {
    try {
      const response = await fetch(`/api/gift-exchanges`, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
        },
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const data = await response.json();
      if (isMounted) {
        setGiftExchanges(data);
      }
    } catch (error) {
      console.error('Failed to fetch gift exchanges:', error);
    }
  }
  
  fetchGiftExchanges();
  
  return () => {
    isMounted = false;
  };
}, []);
Suggested change
async function fetchGiftExchanges() {
try {
const response = await fetch(`/api/gift-exchanges`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setGiftExchanges(data);
} catch (error) {
console.error('Failed to fetch gift exchanges:', error);
}
};
useEffect(() => {
fetchGiftExchanges();
useEffect(() => {
let isMounted = true;
async function fetchGiftExchanges() {
try {
const response = await fetch(`/api/gift-exchanges`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (isMounted) {
setGiftExchanges(data);
}
} catch (error) {
console.error('Failed to fetch gift exchanges:', error);
}
}
fetchGiftExchanges();
return () => {
isMounted = false;
};

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +30
const filteredToasts = toasts.filter((toast) => {
return userExchangeGroups.some((group) => {
return (group.gift_exchange_id === toast.group && group.owner_id === session?.user.id)
})
})
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

[nitpick] The filtering logic runs on every render of the Toaster component. Since userExchangeGroups is fetched asynchronously and could change, consider memoizing the filtered result to avoid unnecessary recomputations:

const filteredToasts = useMemo(() => {
  return toasts.filter((toast) => {
    // Show toasts without a group (global toasts)
    if (!toast.group) return true;
    
    // For toasts with a group, only show if user is the owner
    return userExchangeGroups.some((group) => {
      return (group.gift_exchange_id === toast.group && group.owner_id === session?.user.id)
    })
  })
}, [toasts, userExchangeGroups, session?.user.id])

Copilot uses AI. Check for mistakes.
import { GiftExchangeWithMemberCount } from '../app/types/giftExchange';

/**
* A React hook that manages the current users exchange groups.
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The JSDoc comment has a grammatical error. Change "manages the current users exchange groups" to "manages the current user's exchange groups" (add apostrophe for possessive).

Suggested change
* A React hook that manages the current users exchange groups.
* A React hook that manages the current user's exchange groups.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +35
} catch (error) {
console.error('Failed to fetch gift exchanges:', error);
}
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

[nitpick] The error handling doesn't provide any feedback to the user when the fetch fails. The hook silently fails and returns an empty array, which could be confusing. Consider:

  1. Adding a loading state to distinguish between "no data" and "loading"
  2. Adding an error state to handle fetch failures
  3. Or at minimum, logging more context about the error

Example:

const useExchangeGroups = () => {
  const [giftExchanges, setGiftExchanges] = useState<GiftExchangeWithMemberCount[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  async function fetchGiftExchanges() {
    try {
      setIsLoading(true);
      const response = await fetch(`/api/gift-exchanges`, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
        },
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const data = await response.json();
      setGiftExchanges(data);
      setError(null);
    } catch (error) {
      console.error('Failed to fetch gift exchanges:', error);
      setError(error instanceof Error ? error : new Error('Unknown error'));
    } finally {
      setIsLoading(false);
    }
  }

  // ...

  return { giftExchanges, isLoading, error };
};

Copilot uses AI. Check for mistakes.
} catch (error) {
console.error('Failed to fetch gift exchanges:', error);
}
};
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

Remove the unnecessary semicolon after the closing brace. Function declarations don't require a semicolon.

Suggested change
};
}

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +29
return userExchangeGroups.some((group) => {
return (group.gift_exchange_id === toast.group && group.owner_id === session?.user.id)
})
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

The filtering logic will hide all toasts that don't have a group property. This breaks existing toasts like TOASTS.badLinkToast, TOASTS.expiredLinkToast, and TOASTS.errorToast which are used in the codebase but don't include a group field.

Consider updating the filter to show toasts without a group property, or only filter toasts that explicitly have a group property:

const filteredToasts = toasts.filter((toast) => {
  // Show toasts without a group (global toasts)
  if (!toast.group) return true;
  
  // For toasts with a group, only show if user is the owner
  return userExchangeGroups.some((group) => {
    return (group.gift_exchange_id === toast.group && group.owner_id === session?.user.id)
  })
})
Suggested change
return userExchangeGroups.some((group) => {
return (group.gift_exchange_id === toast.group && group.owner_id === session?.user.id)
})
// Show toasts without a group (global toasts)
if (!toast.group) return true;
// For toasts with a group, only show if user is the owner
return userExchangeGroups.some((group) => (
group.gift_exchange_id === toast.group && group.owner_id === session?.user.id
));

Copilot uses AI. Check for mistakes.
gift_exchange_id: 'test-group-2',
owner_id: 'test-member-2',
},
]
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

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

Avoid automated semicolon insertion (90% of all statements in the enclosing script have an explicit semicolon).

Suggested change
]
];

Copilot uses AI. Check for mistakes.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 18, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

The Toaster component is repositioned within the AuthContextProvider to access authentication context. A new hook fetches the user's exchange groups. Toaster filtering logic now displays only toasts belonging to groups owned by the current user, implementing ownership-based notification scoping.

Changes

Cohort / File(s) Summary
Layout context restructuring
app/layout.tsx
Moved Toaster component from outside to inside AuthContextProvider body, adjusting render order and context availability.
Toaster filtering logic
components/Toaster/Toaster.tsx
Added imports for useAuthContext and useExchangeGroups. Implemented filtering logic to display only toasts where group's gift_exchange_id matches toast.group and group's owner_id equals current session user ID.
Toaster test updates
components/Toaster/Toaster.test.tsx
Added mocks for useAuthContext and useExchangeGroups. Introduced group ownership test data. Modified assertions to conditionally render based on group ownership, with dismiss functionality tested only for owned groups.
Exchange groups hook
hooks/useExchangeGroups.ts
New hook that fetches current user's exchange groups from /api/gift-exchanges on mount and manages state as Array<GiftExchangeWithMemberCount>. Includes error logging for fetch failures.

Sequence Diagram

sequenceDiagram
    participant App as Root Layout
    participant Auth as AuthContextProvider
    participant Toast as Toaster Component
    participant UseExchangeGroups as useExchangeGroups Hook
    participant API as /api/gift-exchanges

    App->>Auth: Wrap content
    Auth->>Toast: Render (inside provider)
    Toast->>UseExchangeGroups: Call hook to get groups
    UseExchangeGroups->>API: Fetch exchange groups
    API-->>UseExchangeGroups: Return groups
    UseExchangeGroups-->>Toast: Return groups array
    
    Note over Toast: For each toast:<br/>Check if group owner_id<br/>matches current user_id
    
    Toast->>Toast: Filter toasts by ownership
    Toast-->>App: Render filtered toasts only
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Ownership filtering logic: Review the filtering condition in Toaster.tsx to ensure gift_exchange_id matching and owner_id comparison against session?.user.id is correct and handles edge cases (null session, missing data).
  • Hook implementation: Verify useExchangeGroups.ts properly handles API responses, error cases, and dependency arrays in useEffect.
  • Test coverage: Ensure conditional assertions in Toaster.test.tsx accurately reflect the dual-group ownership scenario and dismiss behavior is correctly validated.
  • Context positioning: Confirm that moving Toaster inside AuthContextProvider doesn't cause unintended re-renders or context access issues.

Poem

🐰 A toast flies through the AuthContext air,
Filtered by groups with the owner's care.
Only those blessed by the keeper's own hand,
Will shimmer and shine 'cross the exchange land.
Notifications so kind, so precise, so true—
Now showing their magic to only those who.

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly identifies the issue being fixed (#591) and succinctly describes the main change: limiting draw notifications to group owners.
Linked Issues check ✅ Passed The code changes implement the core requirement to filter toast notifications so they only display for group owners by matching group owner_id with current session user id.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the notification filtering feature: Toaster component moved into AuthContextProvider, filtering logic added, tests updated, and useExchangeGroups hook created to support the feature.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jeremy/591-limit-draw-notifications-groupOwner

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 43e0035 and 39d91da.

📒 Files selected for processing (4)
  • app/layout.tsx (1 hunks)
  • components/Toaster/Toaster.test.tsx (3 hunks)
  • components/Toaster/Toaster.tsx (1 hunks)
  • hooks/useExchangeGroups.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
hooks/useExchangeGroups.ts (2)
app/types/giftExchange.ts (1)
  • GiftExchangeWithMemberCount (17-27)
app/api/cron/functions/fetchGiftExchanges/fetchGiftExchanges.ts (1)
  • fetchGiftExchanges (13-33)
components/Toaster/Toaster.tsx (1)
hooks/use-toast.ts (2)
  • useToast (243-267)
  • toast (188-232)
🪛 ESLint
hooks/useExchangeGroups.ts

[error] 12-13: Missing JSDoc comment.

(jsdoc/require-jsdoc)


[error] 17-18: Missing JSDoc comment.

(jsdoc/require-jsdoc)


[error] 17-17: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 22-22: Expected indentation of 10 spaces but found 8.

(indent)


[error] 23-23: Expected indentation of 8 spaces but found 6.

(indent)

🔇 Additional comments (5)
components/Toaster/Toaster.tsx (2)

14-15: LGTM: Required imports for ownership-based filtering.

The imports are correctly added to support the new functionality.


23-24: LGTM: Hook invocations are correct.

The authentication session and user exchange groups are properly retrieved for filtering.

app/layout.tsx (1)

68-68: LGTM: Correct placement of Toaster within AuthContextProvider.

Moving the Toaster inside the AuthContextProvider is necessary since it now uses useAuthContext to access the current user session for ownership-based filtering.

components/Toaster/Toaster.test.tsx (2)

82-86: LGTM: Test setup correctly mocks required hooks.

The setup properly mocks useAuthContext and useExchangeGroups with appropriate test data for ownership-based testing.


90-125: LGTM: Test assertions correctly verify ownership-based rendering.

The updated test assertions properly verify that:

  • Toasts for owned groups (test-group-1) are rendered
  • Toasts for non-owned groups (test-group-2) are not rendered
  • Dismiss functionality works only for owned groups

Comment @coderabbitai help to get the list of available commands and usage tips.

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.

Limit Drawing Notifications to Group Owner

3 participants