Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Notifications #1689

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open

Conversation

AMIRKHANEF
Copy link
Member

@AMIRKHANEF AMIRKHANEF commented Dec 18, 2024

Close: #1688

Summary by CodeRabbit

  • New Features

    • Introduced a comprehensive notification system with components for managing and displaying notifications.
    • Added a new route for managing notification settings.
    • Enhanced the HideBalance component with improved accessibility features.
    • Implemented a new SelectAccounts component for account selection.
    • Added a SelectNetwork component for selecting networks.
    • Introduced NotificationItem and Notifications components for displaying notification messages.
  • Bug Fixes

    • Improved error handling and logging for notification-related functionalities.
  • Documentation

    • Updated routing configuration to include notification settings.
  • Style

    • Enhanced visual appearance of components, including the AccountMenu and YouHave components.
  • Chores

    • Organized and refactored existing components for better maintainability.

Copy link
Contributor

coderabbitai bot commented Dec 18, 2024

Walkthrough

This pull request introduces a comprehensive notification system for the Polkadot extension, adding multiple components and utilities to manage and display notifications related to referenda, staking rewards, and received funds. The implementation spans several files across the extension, introducing new hooks, components, and routing configurations to support a robust notification mechanism with user-configurable settings.

Changes

File Change Summary
packages/extension-polkagate/src/hooks/index.ts Added export for useNotifications hook
packages/extension-polkagate/src/popup/home/YouHave.tsx Integrated notifications icon in header
packages/extension-polkagate/src/partials/SettingSubMenu.tsx Added menu item for managing notifications
packages/extension-polkagate/src/popup/notification/NotificationItem.tsx New component for displaying individual notifications
packages/extension-polkagate/src/popup/notification/NotificationSettings.tsx New component for managing notification settings
packages/extension-polkagate/src/popup/notification/Notifications.tsx New component for displaying notification list
packages/extension-polkagate/src/util/workers/utils/newReferendaNotification.js New utility for handling referenda notifications
packages/extension-ui/src/Popup/routes/RouteDefinitions.tsx Added route for managing notifications

Assessment against linked issues

Objective Addressed Explanation
Implement notification system Comprehensive notification system added with multiple components
Support referenda notifications Referenda tracking and notification generation implemented
User-configurable notification settings NotificationSettings component allows configuring notification preferences
Visual notification display Notifications and NotificationItem components provide UI for notifications

Suggested Labels

new feature, change requested

Poem

🐰 Hop, hop, notifications bloom!
In Polkadot's digital room
Referenda dance, rewards take flight
Messages ping with pure delight
A rabbit's guide to staying bright! 🔔

Tip

CodeRabbit's docstrings feature is now available as part of our Early Access Program! Simply use the command @coderabbitai generate docstrings to have CodeRabbit automatically generate docstrings for your pull request. We would love to hear your feedback on Discord.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (30)
packages/extension-polkagate/src/partials/AccountMenu.tsx (2)

101-116: Consider performance impact of backdrop blur effect.

The blur effect (backdropFilter: 'blur(5px)') can impact performance on lower-end devices. Consider:

  1. Using a semi-transparent background without blur for such devices
  2. Adding a CSS @supports check for backdrop-filter

Example implementation:

/* Add to your styles */
@supports not (backdrop-filter: blur(5px)) {
  .dialog-backdrop {
    background-color: rgba(0, 0, 0, 0.5); /* fallback */
  }
}

120-121: Use theme colors for box shadow.

The box shadow color is hardcoded to white (rgba(255, 255, 255, 0.25)). For better theme support, consider using the theme's color system.

-sx={{ borderRadius: '10px 10px 0px 0px', boxShadow: '0px 0px 10px rgba(255, 255, 255, 0.25)', height: 'parent.innerHeight' }}
+sx={(theme) => ({
+  borderRadius: '10px 10px 0px 0px',
+  boxShadow: `0px 0px 10px ${theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.25)' : 'rgba(0, 0, 0, 0.25)'}`,
+  height: 'parent.innerHeight'
+})}
packages/extension-polkagate/src/partials/Menu.tsx (2)

60-65: Consider handling errors more robustly.
While calling “catch(console.error)” is sufficient for logging errors, consider whether more granular error handling or user-facing error reporting may be beneficial.


143-176: Remove the unnecessary fragment around a single child.
Static analysis identified lines 162–174 as an unneeded React fragment. You can remove it to simplify the JSX.

Proposed diff to remove the redundant fragment:

-                  <>
                   <Grid item>
                     <Infotip2
                       text={t('Switch Theme')}
                     >
                       <IconButton
                         sx={{ height: '35px', p: 0, width: '35px' }}
                       >
                         <ThemeChanger color='secondary.light' noBorder />
                       </IconButton>
                     </Infotip2>
                   </Grid>
-                  </>
🧰 Tools
🪛 Biome (1.9.4)

[error] 162-174: Avoid using unnecessary Fragment.

A fragment is redundant if it contains only one child, or if it is the child of a html element, and is not a keyed fragment.
Unsafe fix: Remove the Fragment

(lint/complexity/noUselessFragments)

packages/extension-polkagate/src/util/workers/utils/newReferendaNotification.js (1)

22-30: Improve type safety and error handling in getRefStatus

The function could benefit from proper TypeScript typing and explicit error handling.

-const getRefStatus = (/** @type {{ [x: string]: any; }} */ item) => {
+const getRefStatus = (item: Referenda): string => {
   for (const status of REFERENDA_STATUS) {
     if (item[status]) {
       return status;
     }
   }
-  return 'ongoing';
+  return 'ongoing' as const;
 };
packages/extension-polkagate/src/popup/notification/Notifications.tsx (3)

38-70: Provide user-friendly guidance when no notifications exist
Currently, the user sees the "Introducing notifications!" message in the "FirstNotification" component when there are no notifications. Consider providing a brief step-by-step guide or a tooltip explaining how notifications are generated or triggered.


72-92: Use React Fragments to avoid extra DOM elements
In the "NotificationList" map, multiple adjacent elements (NotificationItem and Divider) are wrapped in a <> fragment. To ensure better React rendering performance and avoid unintentional keys assigned to the Fragment, replace the short syntax with a proper <React.Fragment key={unique}> when necessary, or move the 'key' to the wrapper element.


94-124: Allow dynamic sizing or scrolling
"NotificationBox" sets a fixed width of "320px" and a fixed layout. Consider using a more responsive or scrolling approach if there are numerous notifications.

packages/extension-polkagate/src/popup/notification/NotificationSettings.tsx (2)

25-36: Maintain consistent naming
"OptionButton" is a descriptive name. For readability and consistent naming, consider either using "OptionToggle" or "OptionSelector" if it is used to open selection popups. This helps unify naming patterns across the codebase, especially around toggles/selectors elsewhere.


91-100: Handle scenario when there are no accounts
When "accounts" is empty in the context, ensure the UI gracefully handles toggling notification settings to avoid creating a mismatch in the state. For example, disable or hide toggles until at least one account is selected.

packages/extension-polkagate/src/hooks/useNotifications.ts (3)

54-72: Clarify first-time initialization vs. returning visitors
In the reducer, "INITIALIZE" sets isFirstTime to true, while "CHECK_FIRST_TIME" also sets it to true. Consider consolidating their usage or documenting the difference to avoid confusion about how the "isFirstTime" property is determined.


271-300: Elaborate on error handling in "getReceivedFundsInformation"
When the API call returns a non-zero code, you raise a generic error and then retry. Consider logging the actual status code or response to help debugging issues related to partial or invalid data.


325-352: Optimize final save
The "beforeunload" event is used to save notifications, but it may not always provide enough time on slow networks. Consider triggering earlier saves or listening to other lifecycle events (like tab switching) to improve reliability.

packages/extension-polkagate/src/popup/notification/util.ts (1)

266-336: Document or limit concurrency
"getReceivedFundsInformation" handles batched requests. Consider restricting concurrency at the batch or address level to avoid saturating subscan with small intervals between attempts, or consider providing user feedback if the calls are extensive.

packages/extension-polkagate/src/popup/notification/constant.ts (3)

6-6: Consider expanding ReferendaStatus type with more descriptive statuses

The current type definition might be too simplistic. Consider adding more descriptive statuses like 'executionFailed', 'scheduled', etc., to better represent the complete lifecycle of referenda.

-export type ReferendaStatus = 'ongoing' | 'approved' | 'timedOut' | 'rejected' | 'cancelled';
+export type ReferendaStatus = 
+  | 'ongoing'
+  | 'approved'
+  | 'timedOut'
+  | 'rejected'
+  | 'cancelled'
+  | 'scheduled'
+  | 'executionFailed'
+  | 'executed';

8-11: Consider making tracking limits configurable

The hardcoded tracking limits (REFERENDA_COUNT_TO_TRACK_DOT and REFERENDA_COUNT_TO_TRACK_KSM) might need adjustment based on user feedback or system performance. Consider making these configurable through user settings.


33-61: Consider organizing SUBSCAN_SUPPORTED_CHAINS more efficiently

The current list of supported chains could be better organized for maintainability.

-export const SUBSCAN_SUPPORTED_CHAINS = [
+export const SUBSCAN_SUPPORTED_CHAINS = Object.freeze({
+  RELAY_CHAINS: ['Polkadot', 'Kusama', 'Westend'],
+  ASSET_HUBS: ['PolkadotAssethub', 'KusamaAssethub', 'WestendAssethub', 'PaseoAssethub'],
+  PARACHAINS: [
   'Acala',
   'Ajuna',
   'Astar',
   // ... other parachains
-];
+]});
packages/extension-polkagate/src/popup/notification/partial/SelectAccounts.tsx (2)

14-18: Consider improving Props interface documentation

The Props interface could benefit from JSDoc comments explaining the purpose of each prop.

 interface Props {
+  /** Array of previously selected account addresses */
   previousState: string[];
+  /** Callback function triggered when selection is applied */
   onApply: (items: string[]) => () => void;
+  /** Callback function to close the selection dialog */
   onClose: () => void;
 }

68-72: Add loading state to PButton

Consider adding a loading state to prevent multiple submissions while the apply action is processing.

 <PButton
   _onClick={onApply(selected)}
   disabled={isSelectionUnchanged || selected.length > MAX_ACCOUNT_COUNT_NOTIFICATION}
+  loading={isApplying}
   text={t('Apply')}
 />
packages/extension-polkagate/src/popup/notification/partial/SelectNetwork.tsx (4)

23-24: Move RELAY_CHAINS constant to constants file

The RELAY_CHAINS constant should be moved to the constants file for better maintainability and reuse.


45-54: Optimize isRelayChain function

The isRelayChain function can be simplified and optimized.

-const isRelayChain = useCallback((chain: string) => {
-  const itIs = RELAY_CHAINS.includes(chain);
-  const base = chain.charAt(0).toUpperCase() + chain.slice(1);
-
-  if (!itIs) {
-    return base;
-  } else {
-    return base + ' Relay Chain';
-  }
-}, []);
+const isRelayChain = useCallback((chain: string) => {
+  const base = chain.charAt(0).toUpperCase() + chain.slice(1);
+  return RELAY_CHAINS.includes(chain) ? `${base} Relay Chain` : base;
+}, []);

67-83: Consider extracting network option component

The network option rendering logic could be extracted into a separate component for better maintainability.

+const NetworkOption = React.memo(({ option, isSelected, onSelect }) => (
+  <Grid alignItems='center' container item justifyContent='space-between'>
+    <Grid alignItems='center' columnGap='10px' container item width='fit-content'>
+      <ChainLogo chainName={option.text} />
+      <Typography fontSize='16px' fontWeight={400}>
+        {isRelayChain(option.text)}
+      </Typography>
+    </Grid>
+    <Switch
+      isChecked={isSelected}
+      onChange={onSelect}
+      theme={theme}
+    />
+  </Grid>
+));

1-112: Consider unifying selection components

Both SelectAccounts and SelectNetwork share similar patterns and could potentially be unified into a single, more generic selection component.

Consider creating a generic SelectionList component that can be configured for different types of items (accounts, networks) while maintaining specific rendering logic through render props or children functions.

packages/extension-polkagate/src/partials/SettingSubMenu.tsx (1)

118-126: Consider consistent spacing with other MenuItems.

While the implementation is correct, consider maintaining consistent vertical spacing with other menu items for better visual hierarchy.

 <MenuItem
   fontSize='17px'
   iconComponent={
     <NotificationsIcon sx={{ color: theme.palette.text.primary, fontSize: '23px' }} />
   }
   onClick={onManageNotifications}
-  py='0px'
+  py='5px'
   text={t('Manage notifications')}
 />
packages/extension-polkagate/src/components/SVG/HideBalance.tsx (1)

88-93: LGTM: Improved accessibility with IconButton.

Good use of Material-UI's IconButton component. Consider adding an aria-label to the IconButton for better accessibility.

-<IconButton onClick={onClick} sx={{ m: 0 }}>
+<IconButton 
+  aria-label={hide ? 'Show balance' : 'Hide balance'}
+  onClick={onClick} 
+  sx={{ m: 0 }}
+>
packages/extension-polkagate/src/popup/home/YouHave.tsx (2)

94-111: Consider responsive design for mobile views.

The header icons are well-organized, but consider adding responsive spacing and potentially stacking icons vertically on smaller screens.

 <Grid container item width='fit-content'>
+  <Grid 
+    container 
+    spacing={{ xs: 1, sm: 2 }}
+    direction={{ xs: 'column', sm: 'row' }}
+    alignItems='center'
+  >
     <HideBalance
       darkColor={theme.palette.secondary.light}
       hide={isHideNumbers}
       lightColor={theme.palette.secondary.light}
       onClick={toggleHideNumbers}
       size={20}
     />
     <Notifications />
     <Infotip2 placement='bottom' text={t('Menu options')}>
       <IconButton
         onClick={onMenuClick}
         sx={{ p: 0 }}
       >
         <MoreVertIcon sx={{ color: 'secondary.light', fontSize: '33px' }} />
       </IconButton>
     </Infotip2>
+  </Grid>
 </Grid>

89-112: Consider extracting header actions into a separate component.

The header section with multiple actions (hide balance, notifications, menu) could be extracted into a separate component for better maintainability and reusability.

Consider creating a new component like HeaderActions to encapsulate these controls:

interface HeaderActionsProps {
  isHideNumbers: boolean;
  toggleHideNumbers: () => void;
  onMenuClick: () => void;
}

const HeaderActions: React.FC<HeaderActionsProps> = ({
  isHideNumbers,
  toggleHideNumbers,
  onMenuClick
}) => {
  const theme = useTheme();
  const { t } = useTranslation();

  return (
    <Grid container item width='fit-content'>
      <HideBalance
        darkColor={theme.palette.secondary.light}
        hide={isHideNumbers}
        lightColor={theme.palette.secondary.light}
        onClick={toggleHideNumbers}
        size={20}
      />
      <Notifications />
      <Infotip2 placement='bottom' text={t('Menu options')}>
        <IconButton onClick={onMenuClick} sx={{ p: 0 }}>
          <MoreVertIcon sx={{ color: 'secondary.light', fontSize: '33px' }} />
        </IconButton>
      </Infotip2>
    </Grid>
  );
};
packages/extension-polkagate/src/popup/notification/NotificationItem.tsx (3)

54-164: Consider extracting notification type renderers

The renderText function is quite large and handles multiple notification types. Consider extracting each notification type renderer into a separate component for better maintainability and reusability.

Example structure:

const ReceivedFundNotification = ({ message }) => {
  // receivedFund renderer logic
};

const ReferendaNotification = ({ message }) => {
  // referenda renderer logic
};

const StakingRewardNotification = ({ message }) => {
  // stakingReward renderer logic
};

77-79: Remove commented code

There's commented code that should be removed if it's no longer needed.


147-153: Remove commented code block

There's a large commented code block that should be removed if it's no longer needed.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13d2cad and 0ca0644.

📒 Files selected for processing (18)
  • packages/extension-polkagate/src/components/SVG/HideBalance.tsx (3 hunks)
  • packages/extension-polkagate/src/hooks/index.ts (1 hunks)
  • packages/extension-polkagate/src/hooks/useNotifications.ts (1 hunks)
  • packages/extension-polkagate/src/partials/AccountMenu.tsx (2 hunks)
  • packages/extension-polkagate/src/partials/Menu.tsx (6 hunks)
  • packages/extension-polkagate/src/partials/SettingSubMenu.tsx (3 hunks)
  • packages/extension-polkagate/src/popup/home/YouHave.tsx (3 hunks)
  • packages/extension-polkagate/src/popup/notification/NotificationItem.tsx (1 hunks)
  • packages/extension-polkagate/src/popup/notification/NotificationSettings.tsx (1 hunks)
  • packages/extension-polkagate/src/popup/notification/Notifications.tsx (1 hunks)
  • packages/extension-polkagate/src/popup/notification/constant.ts (1 hunks)
  • packages/extension-polkagate/src/popup/notification/partial/SelectAccounts.tsx (1 hunks)
  • packages/extension-polkagate/src/popup/notification/partial/SelectNetwork.tsx (1 hunks)
  • packages/extension-polkagate/src/popup/notification/util.ts (1 hunks)
  • packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnRelayChain.js (2 hunks)
  • packages/extension-polkagate/src/util/workers/utils/index.ts (1 hunks)
  • packages/extension-polkagate/src/util/workers/utils/newReferendaNotification.js (1 hunks)
  • packages/extension-ui/src/Popup/routes/RouteDefinitions.tsx (2 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
packages/extension-polkagate/src/popup/notification/NotificationItem.tsx

[error] 33-33: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.

See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.

(lint/suspicious/noGlobalIsNan)

packages/extension-polkagate/src/partials/Menu.tsx

[error] 162-174: Avoid using unnecessary Fragment.

A fragment is redundant if it contains only one child, or if it is the child of a html element, and is not a keyed fragment.
Unsafe fix: Remove the Fragment

(lint/complexity/noUselessFragments)

packages/extension-polkagate/src/popup/notification/util.ts

[error] 108-108: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.

See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.

(lint/suspicious/noGlobalIsNan)

🔇 Additional comments (22)
packages/extension-polkagate/src/partials/AccountMenu.tsx (1)

101-121: Clarify relationship to notifications feature.

The PR objectives mention adding notifications, but the changes in this file are purely styling-related. Could you clarify how these styling changes relate to the notifications feature implementation?

packages/extension-polkagate/src/partials/Menu.tsx (10)

8-8: Lock Icon import looks good.
No issues found with the addition of "LockIcon." Usage is correct.


13-19: New imports are consistent.
The newly introduced imports appear to be correctly referenced and in line with the usage within the file.


37-40: Enum usage appears clear and maintainable.
Defining an enum for popup states is a good approach.


42-42: Transition component is well-defined.
Forward ref usage aligns with Material UI’s best practices.


52-53: Hooks usage is correctly integrated.
“isLoginEnabled” and “setExtensionLock” are well-placed for extension lock functionality.


57-57: Popup state initialization is straightforward.
Using a dedicated enum to switch popup modes is a clean solution.


94-105: Testnet confirmation and rejection logic is coherent.
Setting the popup back to NONE and updating storage/toggling testnet usage is logically consistent and aligned with the intended feature.


120-126: Dialog Paper and backdrop styling changes look fine.
This ensures a transparent overlay with a blur effect, enhancing the UI design.

Also applies to: 128-135


139-141: Grid layout update is consistent with the new design.
The code correctly conditions the main menu content on “POPUP_MENUS.NONE.”


229-254: Warning popup logic is well-structured.
The multi-step display for confirming or rejecting testnet enabling is clear and user-friendly.

packages/extension-polkagate/src/util/workers/utils/index.ts (1)

9-9: LGTM!

The export follows the established pattern and is correctly configured.

packages/extension-polkagate/src/popup/notification/Notifications.tsx (1)

126-176: Validate popover anchor element reference
A potential edge case occurs if onClick is triggered while the component unmounts or if anchorEl is null after re-renders. Ensure the anchorEl reference is valid before leveraging it in the Popover.

Run the script below to search for all references to "setAnchorEl" to ensure consistent usage and no stale references:

✅ Verification successful

Based on the search results, I can see that the setAnchorEl usage in the Notifications component follows a consistent and safe pattern that's commonly used across the codebase for managing popover anchors. Specifically:

  1. The anchorEl state is properly initialized as null
  2. It's set to the current target element in the click handler
  3. It's properly cleared in the close handler
  4. The Popover's open state is controlled by Boolean(anchorEl)
  5. The same pattern is used consistently across multiple components in the codebase

The edge cases are properly handled
The implementation follows React's best practices for managing popover anchors and includes all necessary safeguards:

  • Null checking through Boolean(anchorEl)
  • Proper cleanup in the close handler
  • Proper event target reference in the click handler
  • Component state is managed through React's useState hook
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
rg -A 5 'setAnchorEl'

Length of output: 25864

packages/extension-polkagate/src/popup/notification/NotificationSettings.tsx (2)

38-52: Consider more type-safety for storing chain values
"NotificationSettingType" allows arrays of "DropdownOption[]". However, nested checks are required. Consider narrowing the chain references to a known type or an enum for more robust type-safety.


168-171: Clarify the navigation flow
"onNotificationApply" redirects to "/" after saving settings. Confirm that this redirect is desired and doesn't unexpectedly skip any steps if the user intended to continue configuring other settings.

packages/extension-polkagate/src/hooks/useNotifications.ts (1)

152-156: Avoid accidental duplication
Multiple booleans and references track whether certain calls have occurred (e.g., isGettingReceivedFundRef, isGettingPayoutsRef, isSavingRef, etc.). Double-check there's no scenario where these references cause the logic to skip necessary calls or block subsequent updates.

packages/extension-polkagate/src/popup/notification/util.ts (1)

167-203: Ensure "relay chain" and "network" are removed as expected
In "formatChainName", you remove " relay chain" and " network". Confirm that edge cases like "Acala Network" or multi-word suffixes still get sanitized correctly. If necessary, add more robust checks or expansions (e.g., " networks", " Relay Chains").

packages/extension-polkagate/src/popup/notification/constant.ts (1)

14-17: Review retry and batch size parameters

The current values for MAX_RETRIES (2) and BATCH_SIZE (3) seem arbitrary. Please verify if these values are optimal for the notification system's performance and reliability.

packages/extension-polkagate/src/partials/SettingSubMenu.tsx (1)

8-8: LGTM: Clean implementation of notification management navigation.

The notification icon import and callback implementation follow React best practices.

Also applies to: 54-56

packages/extension-polkagate/src/popup/notification/NotificationItem.tsx (1)

166-191: LGTM: Component rendering looks good

The component's rendering logic is well-structured, using proper Material-UI components and conditional styling based on the read status.

packages/extension-polkagate/src/hooks/index.ts (1)

82-82: LGTM: Clean hook export

The new hook export follows the existing pattern and maintains alphabetical ordering.

packages/extension-ui/src/Popup/routes/RouteDefinitions.tsx (1)

314-318: LGTM: Route configuration is well-structured

The new route for notification settings follows the existing pattern and is correctly placed in the FEATURE_ROUTES array.

Comment on lines +37 to +71
export async function newRefNotif (api, chainName, port) {
if (!NOTIFICATION_GOVERNANCE_CHAINS.includes(chainName)) {
return undefined;
}

const isDot = chainName === 'polkadot';

const latestRefId = Number((await api.query['referenda']['referendumCount']()).toPrimitive());

const referendaInfoRequests = Array.from({ length: isDot ? REFERENDA_COUNT_TO_TRACK_DOT : REFERENDA_COUNT_TO_TRACK_KSM }, (_, index) =>
api.query['referenda']['referendumInfoFor'](latestRefId - (index + 1)));

const referendaInfo = await Promise.all(referendaInfoRequests);

const info = referendaInfo.map((item, index) => {
const status = getRefStatus(item);

return {
refId: latestRefId - index,
status
};
});

const message = {
functionName: NOTIFICATIONS_KEY,
message: {
chainGenesis: api.genesisHash.toHex(),
data: info,
type: 'referenda'
}
};

// console.log('message sent :', JSON.stringify(message));
port.postMessage(JSON.stringify(message));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error handling and rate limiting to newRefNotif

The function has several potential issues that need to be addressed:

  1. Missing error handling for API calls and Promise.all
  2. No rate limiting for parallel requests
  3. Potential performance impact with large number of tracked referenda
 export async function newRefNotif (api, chainName, port) {
   if (!NOTIFICATION_GOVERNANCE_CHAINS.includes(chainName)) {
     return undefined;
   }
 
   const isDot = chainName === 'polkadot';
 
+  try {
     const latestRefId = Number((await api.query['referenda']['referendumCount']()).toPrimitive());
 
     const referendaInfoRequests = Array.from({ length: isDot ? REFERENDA_COUNT_TO_TRACK_DOT : REFERENDA_COUNT_TO_TRACK_KSM }, (_, index) =>
       api.query['referenda']['referendumInfoFor'](latestRefId - (index + 1)));
 
-    const referendaInfo = await Promise.all(referendaInfoRequests);
+    // Add rate limiting to avoid overwhelming the node
+    const referendaInfo = await Promise.all(
+      referendaInfoRequests.map((promise, index) =>
+        new Promise(resolve =>
+          setTimeout(() => resolve(promise), index * 100)
+        )
+      )
+    );
 
     const info = referendaInfo.map((item, index) => {
       const status = getRefStatus(item);
 
       return {
         refId: latestRefId - index,
         status
       };
     });
 
     const message = {
       functionName: NOTIFICATIONS_KEY,
       message: {
         chainGenesis: api.genesisHash.toHex(),
         data: info,
         type: 'referenda'
       }
     };
 
     port.postMessage(JSON.stringify(message));
+  } catch (error) {
+    console.error('Error in newRefNotif:', error);
+    port.postMessage(JSON.stringify({
+      functionName: NOTIFICATIONS_KEY,
+      error: 'Failed to fetch referenda information'
+    }));
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function newRefNotif (api, chainName, port) {
if (!NOTIFICATION_GOVERNANCE_CHAINS.includes(chainName)) {
return undefined;
}
const isDot = chainName === 'polkadot';
const latestRefId = Number((await api.query['referenda']['referendumCount']()).toPrimitive());
const referendaInfoRequests = Array.from({ length: isDot ? REFERENDA_COUNT_TO_TRACK_DOT : REFERENDA_COUNT_TO_TRACK_KSM }, (_, index) =>
api.query['referenda']['referendumInfoFor'](latestRefId - (index + 1)));
const referendaInfo = await Promise.all(referendaInfoRequests);
const info = referendaInfo.map((item, index) => {
const status = getRefStatus(item);
return {
refId: latestRefId - index,
status
};
});
const message = {
functionName: NOTIFICATIONS_KEY,
message: {
chainGenesis: api.genesisHash.toHex(),
data: info,
type: 'referenda'
}
};
// console.log('message sent :', JSON.stringify(message));
port.postMessage(JSON.stringify(message));
}
export async function newRefNotif (api, chainName, port) {
if (!NOTIFICATION_GOVERNANCE_CHAINS.includes(chainName)) {
return undefined;
}
const isDot = chainName === 'polkadot';
try {
const latestRefId = Number((await api.query['referenda']['referendumCount']()).toPrimitive());
const referendaInfoRequests = Array.from({ length: isDot ? REFERENDA_COUNT_TO_TRACK_DOT : REFERENDA_COUNT_TO_TRACK_KSM }, (_, index) =>
api.query['referenda']['referendumInfoFor'](latestRefId - (index + 1)));
// Add rate limiting to avoid overwhelming the node
const referendaInfo = await Promise.all(
referendaInfoRequests.map((promise, index) =>
new Promise(resolve =>
setTimeout(() => resolve(promise), index * 100)
)
)
);
const info = referendaInfo.map((item, index) => {
const status = getRefStatus(item);
return {
refId: latestRefId - index,
status
};
});
const message = {
functionName: NOTIFICATIONS_KEY,
message: {
chainGenesis: api.genesisHash.toHex(),
data: info,
type: 'referenda'
}
};
port.postMessage(JSON.stringify(message));
} catch (error) {
console.error('Error in newRefNotif:', error);
port.postMessage(JSON.stringify({
functionName: NOTIFICATIONS_KEY,
error: 'Failed to fetch referenda information'
}));
}
}

Comment on lines +103 to +139
export function timestampToDate (timestamp: number | string, format: 'full' | 'short' | 'relative' = 'full'): string {
// Ensure timestamp is a number and convert if it's a string
const timestampNum = Number(timestamp);

// Check if timestamp is valid
if (isNaN(timestampNum)) {
return 'Invalid Timestamp';
}

// Create a Date object (multiply by 1000 if it's a Unix timestamp in seconds)
const date = new Date(timestampNum * 1000);

// Different formatting options
switch (format) {
case 'full':
return date.toLocaleString('en-US', {
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
month: 'long',
second: '2-digit'
});

case 'short':
return date.toLocaleString('en-US', {
day: 'numeric',
month: 'short'
});

case 'relative':
return getRelativeTime(date);

default:
return date.toLocaleString();
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use Number.isNaN for type safety
Static analysis warns that global isNaN() is unsafe due to type coercion. Replace:
if (isNaN(timestampNum)) { ... }
with:
if (Number.isNaN(timestampNum)) { ... }
This ensures a more robust check consistent with modern JavaScript best practices.

Apply this diff:

-  if (isNaN(timestampNum)) {
+  if (Number.isNaN(timestampNum)) {
     return 'Invalid Timestamp';
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function timestampToDate (timestamp: number | string, format: 'full' | 'short' | 'relative' = 'full'): string {
// Ensure timestamp is a number and convert if it's a string
const timestampNum = Number(timestamp);
// Check if timestamp is valid
if (isNaN(timestampNum)) {
return 'Invalid Timestamp';
}
// Create a Date object (multiply by 1000 if it's a Unix timestamp in seconds)
const date = new Date(timestampNum * 1000);
// Different formatting options
switch (format) {
case 'full':
return date.toLocaleString('en-US', {
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
month: 'long',
second: '2-digit'
});
case 'short':
return date.toLocaleString('en-US', {
day: 'numeric',
month: 'short'
});
case 'relative':
return getRelativeTime(date);
default:
return date.toLocaleString();
}
}
export function timestampToDate (timestamp: number | string, format: 'full' | 'short' | 'relative' = 'full'): string {
// Ensure timestamp is a number and convert if it's a string
const timestampNum = Number(timestamp);
// Check if timestamp is valid
if (Number.isNaN(timestampNum)) {
return 'Invalid Timestamp';
}
// Create a Date object (multiply by 1000 if it's a Unix timestamp in seconds)
const date = new Date(timestampNum * 1000);
// Different formatting options
switch (format) {
case 'full':
return date.toLocaleString('en-US', {
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
month: 'long',
second: '2-digit'
});
case 'short':
return date.toLocaleString('en-US', {
day: 'numeric',
month: 'short'
});
case 'relative':
return getRelativeTime(date);
default:
return date.toLocaleString();
}
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 108-108: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.

See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.

(lint/suspicious/noGlobalIsNan)

Comment on lines +23 to +29
export const DEFAULT_NOTIFICATION_SETTING = {
accounts: [],
enable: true,
governance: DEFAULT_SUPPORTED_CHAINS,
receivedFunds: true,
stakingRewards: DEFAULT_SUPPORTED_CHAINS
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding validation for notification settings

The DEFAULT_NOTIFICATION_SETTING object should be validated when used. Consider adding runtime type checking or validation.

+export interface NotificationSetting {
+  accounts: string[];
+  enable: boolean;
+  governance: typeof DEFAULT_SUPPORTED_CHAINS;
+  receivedFunds: boolean;
+  stakingRewards: typeof DEFAULT_SUPPORTED_CHAINS;
+}

 export const DEFAULT_NOTIFICATION_SETTING = {
   accounts: [],
   enable: true,
   governance: DEFAULT_SUPPORTED_CHAINS,
   receivedFunds: true,
   stakingRewards: DEFAULT_SUPPORTED_CHAINS
-};
+} as const satisfies NotificationSetting;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const DEFAULT_NOTIFICATION_SETTING = {
accounts: [],
enable: true,
governance: DEFAULT_SUPPORTED_CHAINS,
receivedFunds: true,
stakingRewards: DEFAULT_SUPPORTED_CHAINS
};
export interface NotificationSetting {
accounts: string[];
enable: boolean;
governance: typeof DEFAULT_SUPPORTED_CHAINS;
receivedFunds: boolean;
stakingRewards: typeof DEFAULT_SUPPORTED_CHAINS;
}
export const DEFAULT_NOTIFICATION_SETTING = {
accounts: [],
enable: true,
governance: DEFAULT_SUPPORTED_CHAINS,
receivedFunds: true,
stakingRewards: DEFAULT_SUPPORTED_CHAINS
} as const satisfies NotificationSetting;

Comment on lines +52 to +67
<Grid alignItems='center' container item sx={{ maxHeight: '320px', overflowY: 'scroll', p: '30px 15px 0' }}>
{accounts.map(({ address }, index) => (
<Grid container item key={index}>
<Grid alignItems='center' container item justifyContent='left' xs={10}>
<Identity address={address} identiconSize={25} showShortAddress showSocial={false} style={{ fontSize: '14px' }} subIdOnly />
</Grid>
<Grid alignItems='center' container height='100%' item justifyContent='center' xs={2}>
<Switch
isChecked={selected.includes(address)}
onChange={onSelect(address)}
theme={theme}
/>
</Grid>
</Grid>
))}
</Grid>
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add keyboard navigation support

The account selection list should support keyboard navigation for better accessibility.

 <Grid alignItems='center' container item sx={{ maxHeight: '320px', overflowY: 'scroll', p: '30px 15px 0' }}>
+  <div role="listbox" tabIndex={0}>
   {accounts.map(({ address }, index) => (
-    <Grid container item key={index}>
+    <Grid
+      container
+      item
+      key={address}
+      role="option"
+      aria-selected={selected.includes(address)}
+      tabIndex={-1}
+      onKeyPress={(e) => e.key === 'Enter' && onSelect(address)()}
+    >

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +30 to +36
const sanitizeBNAmount = useCallback((amount: string, decimal: number) => {
const bnAmount = Number(amount);

return isNaN(bnAmount)
? amount
: (bnAmount / (10 ** decimal)).toString();
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Replace global isNaN with Number.isNaN

The global isNaN function performs type coercion which can lead to unexpected results. Use Number.isNaN for more reliable number validation.

-    return isNaN(bnAmount)
+    return Number.isNaN(bnAmount)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const sanitizeBNAmount = useCallback((amount: string, decimal: number) => {
const bnAmount = Number(amount);
return isNaN(bnAmount)
? amount
: (bnAmount / (10 ** decimal)).toString();
}, []);
const sanitizeBNAmount = useCallback((amount: string, decimal: number) => {
const bnAmount = Number(amount);
return Number.isNaN(bnAmount)
? amount
: (bnAmount / (10 ** decimal)).toString();
}, []);
🧰 Tools
🪛 Biome (1.9.4)

[error] 33-33: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.

See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.

(lint/suspicious/noGlobalIsNan)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
packages/extension-polkagate/src/hooks/useNotifications.ts (2)

72-129: Consider extracting timestamp conversion to a constant.

The timestamp conversion from milliseconds to seconds appears in multiple places. Consider extracting 1000 to a named constant like MILLISECONDS_TO_SECONDS for better maintainability and clarity.

+const MILLISECONDS_TO_SECONDS = 1000;
+
 const notificationReducer = (
   state: NotificationsType,
   action: NotificationActionType
 ): NotificationsType => {
   switch (action.type) {
     case 'INITIALIZE':
       return {
         isFirstTime: true,
-        latestLoggedIn: Math.floor(Date.now() / 1000),
+        latestLoggedIn: Math.floor(Date.now() / MILLISECONDS_TO_SECONDS),
         notificationMessages: [],
         // ... rest of the code

257-301: Refactor worker message handling for better maintainability.

The message handling logic has multiple nested conditions. Consider extracting the message validation into a separate function.

+const isValidWorkerMessage = (data: unknown): data is WorkerMessage => {
+  if (!data) return false;
+  const msg = data as WorkerMessage;
+  return msg.functionName === NOTIFICATIONS_KEY && 
+         msg.message?.type === 'referenda';
+};
+
 useEffect(() => {
   if (notificationIsOff || settings?.governance?.length === 0) {
     return;
   }
 
   const handelMessage = (event: MessageEvent<string>) => {
     try {
-      if (!event.data) {
-        return;
-      }
-
       const parsedMessage = JSON.parse(event.data) as WorkerMessage;
-
-      if (parsedMessage.functionName !== NOTIFICATIONS_KEY) {
-        return;
-      }
-
-      const { message } = parsedMessage;
-
-      if (message.type !== 'referenda') {
+      if (!isValidWorkerMessage(parsedMessage)) {
         return;
       }
packages/extension-polkagate/src/popup/notification/util.ts (2)

176-204: Enhance formatChainName function robustness.

The function could be more robust by handling edge cases and providing better type safety.

 const formatChainName = (chainName: string | undefined, suffixes: string[] = ['asset hub']): string => {
   if (!chainName) {
     return '';
   }
 
+  // Trim whitespace and convert to lowercase
+  const normalizedName = chainName.trim().toLowerCase();
+  
+  if (normalizedName.length === 0) {
+    return '';
+  }
 
   // Find and remove the invalid suffixes
-  const sanitized = chainName.toLowerCase().replace(' relay chain', '').replace(' network', '');
+  const sanitized = normalizedName
+    .replace(/\s+relay\s+chain\b/g, '')
+    .replace(/\s+network\b/g, '');

550-554: Improve type safety and naming in updateReferendas function.

The function could benefit from better parameter naming and type handling.

-export const updateReferendas = (preciousRefs: ReferendaNotificationType[] | null | undefined, newRefs: ReferendaNotificationType[], network: string) => {
+export const updateReferendas = (
+  previousRefs: ReferendaNotificationType[] | null | undefined,
+  newRefs: ReferendaNotificationType[],
+  networkName: string
+): ReferendaNotificationType[] => {
+  const normalizedNetworkName = networkName.toLowerCase();
+  const filteredRefs = previousRefs?.filter(
+    ({ chainName }) => chainName.toLowerCase() !== normalizedNetworkName
+  ) ?? [];
+
+  return [...filteredRefs, ...newRefs];
+};
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d40db41 and 5cbdfb7.

📒 Files selected for processing (2)
  • packages/extension-polkagate/src/hooks/useNotifications.ts (1 hunks)
  • packages/extension-polkagate/src/popup/notification/util.ts (1 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
packages/extension-polkagate/src/popup/notification/util.ts

[error] 108-108: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.

See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.

(lint/suspicious/noGlobalIsNan)

🔇 Additional comments (3)
packages/extension-polkagate/src/hooks/useNotifications.ts (1)

28-70: LGTM! Well-structured interfaces and initial state.

The interfaces provide good type safety and the initial state is properly structured with undefined values.

packages/extension-polkagate/src/popup/notification/util.ts (2)

103-139: Use Number.isNaN for type-safe number validation.

The global isNaN() is unsafe due to type coercion. Replace with Number.isNaN() for type safety.

-  if (isNaN(timestampNum)) {
+  if (Number.isNaN(timestampNum)) {
     return 'Invalid Timestamp';
   }
🧰 Tools
🪛 Biome (1.9.4)

[error] 108-108: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.

See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.

(lint/suspicious/noGlobalIsNan)


272-408: Extract common retry logic into a reusable function.

Both API functions contain similar retry logic. Consider extracting it into a reusable utility function.

+async function fetchWithRetry<T>(
+  operation: () => Promise<T>,
+  context: string,
+  maxRetries: number = MAX_RETRIES
+): Promise<T | null> {
+  let lastError: unknown = null;
+
+  for (let attempt = 1; attempt <= maxRetries; attempt++) {
+    try {
+      return await operation();
+    } catch (error) {
+      lastError = error;
+      console.warn(`Attempt ${attempt} failed for ${context}. Retrying...`);
+      await new Promise(resolve => setTimeout(resolve, attempt * 1000));
+    }
+  }
+
+  console.error(`Failed after ${maxRetries} attempts for ${context}`, lastError);
+  return null;
+}

Comment on lines +206 to +220
useEffect(() => {
const getSettings = async () => {
const savedSettings = await getStorage(NOTIFICATION_SETTING_KEY) as NotificationSettingType;

if (!savedSettings) {
setDefaultSettingFlag(true);

return;
}

setSettings(savedSettings);
};

getSettings().catch(console.error);
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling in settings retrieval.

The error is only logged to console. Consider implementing proper error handling and user feedback for settings retrieval failures.

-    getSettings().catch(console.error);
+    getSettings().catch((error) => {
+      console.error('Failed to load notification settings:', error);
+      // TODO: Implement user feedback mechanism
+      setSettings(DEFAULT_NOTIFICATION_SETTING);
+    });

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +319 to +346
useEffect(() => {
const handleBeforeUnload = () => {
const notificationIsInitializing = notifications.isFirstTime && notifications.referendas?.length === 0;

if (!isSavingRef.current && !notificationIsInitializing) {
isSavingRef.current = true;

const dataToSave = notifications;

dataToSave.latestLoggedIn = Math.floor(Date.now() / 1000); // timestamp must be in seconds not in milliseconds

setStorage(NOTIFICATIONS_KEY, dataToSave)
.then(() => {
console.log('Notifications saved successfully on unload.');
})
.catch((error) => {
console.error('Failed to save notifications on unload:', error);
});
}
};

// Add event listener for the 'beforeunload' event
window.addEventListener('beforeunload', handleBeforeUnload);

return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, [notifications]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential race condition in beforeunload handler.

The beforeunload handler might have a race condition when saving notifications. Consider using a synchronous approach or implementing a queue mechanism.

 const handleBeforeUnload = () => {
   const notificationIsInitializing = notifications.isFirstTime && notifications.referendas?.length === 0;
 
   if (!isSavingRef.current && !notificationIsInitializing) {
     isSavingRef.current = true;
 
     const dataToSave = notifications;
     dataToSave.latestLoggedIn = Math.floor(Date.now() / 1000);
 
-    setStorage(NOTIFICATIONS_KEY, dataToSave)
-      .then(() => {
-        console.log('Notifications saved successfully on unload.');
-      })
-      .catch((error) => {
-        console.error('Failed to save notifications on unload:', error);
-      });
+    try {
+      // Use synchronous storage API if available
+      localStorage.setItem(NOTIFICATIONS_KEY, JSON.stringify(dataToSave));
+    } catch (error) {
+      console.error('Failed to save notifications on unload:', error);
+    } finally {
+      isSavingRef.current = false;
+    }
   }
 };

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +238 to +264
const transformPayouts = (address: string, payouts: Payout[], network: DropdownOption) => {
// Initialize the accumulator for the reduce function
const initialAccumulator = {
address,
data: [] as PayoutsProp[],
network
};
const decimal = selectableNetworks.find(({ genesisHash }) => (genesisHash[0] as unknown as string) === network.value)?.decimals[0];

// Sanitize each transfer item and accumulate results
const result = payouts.reduce((accumulator, payout) => {
const sanitizedTransfer = {
amount: payout.amount,
date: timestampToDate(payout.block_timestamp),
decimal,
era: payout.era,
timestamp: payout.block_timestamp,
validatorStash: payout.validator_stash
} as PayoutsProp;

accumulator.data.push(sanitizedTransfer);

return accumulator;
}, initialAccumulator);

return result;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve decimal handling and error cases in transformPayouts.

The function should handle cases where decimal is undefined and provide better type safety.

 const transformPayouts = (address: string, payouts: Payout[], network: DropdownOption) => {
   const initialAccumulator = {
     address,
     data: [] as PayoutsProp[],
     network
   };
-  const decimal = selectableNetworks.find(({ genesisHash }) => 
+  const networkInfo = selectableNetworks.find(({ genesisHash }) => 
     (genesisHash[0] as unknown as string) === network.value)?.decimals[0];
+  
+  if (!networkInfo) {
+    console.warn(`Network decimals not found for ${network.value}`);
+    return initialAccumulator;
+  }
 
   const result = payouts.reduce((accumulator, payout) => {
     const sanitizedTransfer = {
       amount: payout.amount,
       date: timestampToDate(payout.block_timestamp),
-      decimal,
+      decimal: networkInfo,
       era: payout.era,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const transformPayouts = (address: string, payouts: Payout[], network: DropdownOption) => {
// Initialize the accumulator for the reduce function
const initialAccumulator = {
address,
data: [] as PayoutsProp[],
network
};
const decimal = selectableNetworks.find(({ genesisHash }) => (genesisHash[0] as unknown as string) === network.value)?.decimals[0];
// Sanitize each transfer item and accumulate results
const result = payouts.reduce((accumulator, payout) => {
const sanitizedTransfer = {
amount: payout.amount,
date: timestampToDate(payout.block_timestamp),
decimal,
era: payout.era,
timestamp: payout.block_timestamp,
validatorStash: payout.validator_stash
} as PayoutsProp;
accumulator.data.push(sanitizedTransfer);
return accumulator;
}, initialAccumulator);
return result;
};
const transformPayouts = (address: string, payouts: Payout[], network: DropdownOption) => {
const initialAccumulator = {
address,
data: [] as PayoutsProp[],
network
};
const networkInfo = selectableNetworks.find(({ genesisHash }) => (genesisHash[0] as unknown as string) === network.value)?.decimals[0];
if (!networkInfo) {
console.warn(`Network decimals not found for ${network.value}`);
return initialAccumulator;
}
const result = payouts.reduce((accumulator, payout) => {
const sanitizedTransfer = {
amount: payout.amount,
date: timestampToDate(payout.block_timestamp),
decimal: networkInfo,
era: payout.era,
timestamp: payout.block_timestamp,
validatorStash: payout.validator_stash
} as PayoutsProp;
accumulator.data.push(sanitizedTransfer);
return accumulator;
}, initialAccumulator);
return result;
};

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.

feat: notifications
1 participant