Skip to content

Commit 201c56d

Browse files
committed
feat: CategoryHeader in Notifications screen
1 parent 9558ebe commit 201c56d

23 files changed

Lines changed: 254 additions & 0 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from 'react';
2+
import { ScrollView } from 'react-native';
3+
import {
4+
Box,
5+
ButtonFilter,
6+
ButtonSize,
7+
} from '@metamask/design-system-react-native';
8+
import { useTailwind } from '@metamask/design-system-twrnc-preset';
9+
10+
import { FilterTabBarProps } from './FilterTabBar.types';
11+
12+
/**
13+
* A horizontally scrollable row of filter pills built on the design-system
14+
* `ButtonFilter`. Renders a single active tab driven by `selectedKey` and
15+
* reports presses through `onSelect`.
16+
*/
17+
const FilterTabBar = ({
18+
tabs,
19+
selectedKey,
20+
onSelect,
21+
style,
22+
testID,
23+
}: FilterTabBarProps) => {
24+
const tw = useTailwind();
25+
26+
return (
27+
<Box twClassName="py-1" style={style} testID={testID}>
28+
<ScrollView
29+
horizontal
30+
showsHorizontalScrollIndicator={false}
31+
style={tw.style('flex-grow-0')}
32+
contentContainerStyle={tw.style('flex-row items-center gap-2 px-4')}
33+
>
34+
{tabs.map((tab) => (
35+
<ButtonFilter
36+
key={tab.key}
37+
isActive={tab.key === selectedKey}
38+
size={ButtonSize.Sm}
39+
onPress={() => onSelect(tab.key)}
40+
testID={tab.testID}
41+
>
42+
{tab.label}
43+
</ButtonFilter>
44+
))}
45+
</ScrollView>
46+
</Box>
47+
);
48+
};
49+
50+
export default FilterTabBar;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { StyleProp, ViewStyle } from 'react-native';
2+
3+
/**
4+
* A single selectable tab rendered inside the {@link FilterTabBar}.
5+
*/
6+
export interface FilterTab {
7+
/**
8+
* Stable, unique identifier for the tab. Returned via `onSelect` and compared
9+
* against `selectedKey` to determine the active state.
10+
*/
11+
key: string;
12+
/**
13+
* Visible label rendered inside the filter button.
14+
*/
15+
label: string;
16+
/**
17+
* Optional test identifier applied to the underlying button.
18+
*/
19+
testID?: string;
20+
}
21+
22+
/**
23+
* FilterTabBar component props.
24+
*/
25+
export interface FilterTabBarProps {
26+
/**
27+
* Ordered list of tabs to render.
28+
*/
29+
tabs: FilterTab[];
30+
/**
31+
* Key of the currently selected tab.
32+
*/
33+
selectedKey: string;
34+
/**
35+
* Called with the tab key when a tab is pressed.
36+
*/
37+
onSelect: (key: string) => void;
38+
/**
39+
* Optional style applied to the scrollable wrapper.
40+
*/
41+
style?: StyleProp<ViewStyle>;
42+
/**
43+
* Optional test identifier applied to the wrapper.
44+
*/
45+
testID?: string;
46+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default } from './FilterTabBar';
2+
export type { FilterTab, FilterTabBarProps } from './FilterTabBar.types';
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const NotificationsCategorySelectorsIDs = {
2+
CONTAINER: 'notifications-category',
3+
ALL: 'notifications-category-all',
4+
WALLET_ACTIVITY: 'notifications-category-wallet-activity',
5+
PERPS: 'notifications-category-perps',
6+
SOCIAL_AI: 'notifications-category-social-ai',
7+
MARKETING: 'notifications-category-marketing',
8+
} as const;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import React, { useCallback, useMemo, useState } from 'react';
2+
import { useSelector } from 'react-redux';
3+
4+
import { strings } from '../../../../../locales/i18n';
5+
import { selectIsMetamaskNotificationsEnabled } from '../../../../selectors/notifications';
6+
import { selectSocialLeaderboardEnabled } from '../../../../selectors/featureFlagController/socialLeaderboard';
7+
import FilterTabBar, {
8+
FilterTab,
9+
} from '../../../../component-library/components-temp/FilterTabBar';
10+
11+
import {
12+
NotificationCategoryId,
13+
NotificationsCategoryProps,
14+
} from './NotificationsCategory.types';
15+
import { NotificationsCategorySelectorsIDs } from './NotificationsCategory.testIds';
16+
17+
/**
18+
* Horizontally scrollable category filter for the notifications list. The set
19+
* of categories is gated by feature flags: the activity-related categories only
20+
* appear when MetaMask notifications are enabled, and "Trading Signals" only
21+
* when the social leaderboard flag is on.
22+
*/
23+
const NotificationsCategory = ({
24+
onSelect,
25+
testID,
26+
}: NotificationsCategoryProps) => {
27+
const isMetamaskNotificationsEnabled = useSelector(
28+
selectIsMetamaskNotificationsEnabled,
29+
);
30+
const isSocialLeaderboardEnabled = useSelector(
31+
selectSocialLeaderboardEnabled,
32+
);
33+
34+
const [selectedCategory, setSelectedCategory] =
35+
useState<NotificationCategoryId>(NotificationCategoryId.All);
36+
37+
const tabs = useMemo<FilterTab[]>(() => {
38+
const items: FilterTab[] = [
39+
{
40+
key: NotificationCategoryId.All,
41+
label: strings('app_settings.notifications_opts.all_title'),
42+
testID: NotificationsCategorySelectorsIDs.ALL,
43+
},
44+
];
45+
46+
if (isMetamaskNotificationsEnabled) {
47+
items.push(
48+
{
49+
key: NotificationCategoryId.WalletActivity,
50+
label: strings(
51+
'app_settings.notifications_opts.wallet_activity_title',
52+
),
53+
testID: NotificationsCategorySelectorsIDs.WALLET_ACTIVITY,
54+
},
55+
{
56+
key: NotificationCategoryId.Perps,
57+
label: strings('app_settings.notifications_opts.perps_title'),
58+
testID: NotificationsCategorySelectorsIDs.PERPS,
59+
},
60+
);
61+
62+
if (isSocialLeaderboardEnabled) {
63+
items.push({
64+
key: NotificationCategoryId.SocialAI,
65+
label: strings('app_settings.notifications_opts.social_ai_title'),
66+
testID: NotificationsCategorySelectorsIDs.SOCIAL_AI,
67+
});
68+
}
69+
70+
items.push({
71+
key: NotificationCategoryId.Marketing,
72+
label: strings('app_settings.notifications_opts.marketing_title'),
73+
testID: NotificationsCategorySelectorsIDs.MARKETING,
74+
});
75+
}
76+
77+
return items;
78+
}, [isMetamaskNotificationsEnabled, isSocialLeaderboardEnabled]);
79+
80+
const handleSelect = useCallback(
81+
(key: string) => {
82+
const category = key as NotificationCategoryId;
83+
setSelectedCategory(category);
84+
onSelect(category);
85+
},
86+
[onSelect],
87+
);
88+
89+
return (
90+
<FilterTabBar
91+
tabs={tabs}
92+
selectedKey={selectedCategory}
93+
onSelect={handleSelect}
94+
testID={testID ?? NotificationsCategorySelectorsIDs.CONTAINER}
95+
/>
96+
);
97+
};
98+
99+
export default NotificationsCategory;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Identifiers for the notification categories surfaced in the
3+
* {@link NotificationsCategory} tab bar.
4+
*/
5+
export enum NotificationCategoryId {
6+
All = 'all',
7+
WalletActivity = 'wallet-activity',
8+
Perps = 'perps',
9+
SocialAI = 'social-ai',
10+
Marketing = 'marketing',
11+
}
12+
13+
/**
14+
* NotificationsCategory component props.
15+
*/
16+
export interface NotificationsCategoryProps {
17+
/**
18+
* Called with the selected category whenever the user picks a tab.
19+
*/
20+
onSelect: (category: NotificationCategoryId) => void;
21+
/**
22+
* Optional test identifier applied to the tab bar wrapper.
23+
*/
24+
testID?: string;
25+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default } from './NotificationsCategory';
2+
export { NotificationCategoryId } from './NotificationsCategory.types';
3+
export type { NotificationsCategoryProps } from './NotificationsCategory.types';

app/components/Views/Notifications/index.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from '@metamask/design-system-react-native';
2121

2222
import DisabledNotifications from './DisabledNotifications';
23+
import NotificationsCategory from './NotificationsCategory';
2324
import { strings } from '../../../../locales/i18n';
2425
import Routes from '../../../constants/navigation/Routes';
2526
import {
@@ -143,6 +144,11 @@ const NotificationsView = ({
143144
>
144145
{isNotificationEnabled ? (
145146
<>
147+
<NotificationsCategory
148+
onSelect={() => {
149+
// TODO: filter the notifications list by the selected category
150+
}}
151+
/>
146152
<Notifications
147153
navigation={navigation}
148154
allNotifications={allNotifications}

locales/languages/de.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3437,6 +3437,7 @@
34373437
"enable_push_notifications": "Push-Benachrichtigungen aktivieren",
34383438
"allow_notifications_desc": "Bestimmen Sie, worüber Sie benachrichtigt werden und wie.",
34393439
"notifications_opts": {
3440+
"all_title": "Alle",
34403441
"preferences_title": "Einstellungen",
34413442
"push_recommended": "Push",
34423443
"in_app": "In-App",

locales/languages/el.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3437,6 +3437,7 @@
34373437
"enable_push_notifications": "Ενεργοποίηση ειδοποιήσεων ώθησης",
34383438
"allow_notifications_desc": "Επιλέξτε τι ειδοποιήσεις θα λαμβάνετε και πώς.",
34393439
"notifications_opts": {
3440+
"all_title": "Όλα",
34403441
"preferences_title": "Προτιμήσεις",
34413442
"push_recommended": "Ειδοποιήσεις push",
34423443
"in_app": "Εντός εφαρμογής",

0 commit comments

Comments
 (0)