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 UI components to be used by Views #10363

Merged
merged 12 commits into from
Jul 24, 2024
5 changes: 5 additions & 0 deletions app/components/Nav/App/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ import OnboardingSuccess from '../../Views/OnboardingSuccess';
import DefaultSettings from '../../Views/OnboardingSuccess/DefaultSettings';
import BasicFunctionalityModal from '../../UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal';
import SmartTransactionsOptInModal from '../../Views/SmartTransactionsOptInModal/SmartTranactionsOptInModal';
import ProfileSyncingModal from '../../UI/ProfileSyncing/ProfileSyncingModal/ProfileSyncingModal';
import NFTAutoDetectionModal from '../../../../app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal';
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
import { SnapsExecutionWebView } from '../../../lib/snaps';
Expand Down Expand Up @@ -656,6 +657,10 @@ const App = ({ userLoggedIn }) => {
name={Routes.SHEET.BASIC_FUNCTIONALITY}
component={BasicFunctionalityModal}
/>
<Stack.Screen
name={Routes.SHEET.PROFILE_SYNCING}
component={ProfileSyncingModal}
/>
<Stack.Screen
name={Routes.SHEET.RETURN_TO_DAPP_MODAL}
component={ReturnToAppModal}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`NotificationsList should render correctly 1`] = `
<View
style={
{
"backgroundColor": "#ffffff",
"flex": 1,
"marginHorizontal": 8,
}
}
>
<View
style={
{
"height": "100%",
"position": "absolute",
"width": "100%",
"zIndex": 999,
}
}
>
<ActivityIndicator
color="#0376c9"
size="large"
/>
</View>
</View>
`;
24 changes: 24 additions & 0 deletions app/components/UI/Notification/List/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import NotificationsList from './';
import renderWithProvider from '../../../../util/test/renderWithProvider';
import MOCK_NOTIFICATIONS from '../__mocks__/mock_notifications';
import { NavigationProp, ParamListBase } from '@react-navigation/native';

const navigationMock = {
navigate: jest.fn(),
} as unknown as NavigationProp<ParamListBase>;

describe('NotificationsList', () => {
it('should render correctly', () => {
const { toJSON } = renderWithProvider(
<NotificationsList
navigation={navigationMock}
allNotifications={MOCK_NOTIFICATIONS}
walletNotifications={[MOCK_NOTIFICATIONS[1]]}
web3Notifications={[MOCK_NOTIFICATIONS[0]]}
loading
/>,
);
expect(toJSON()).toMatchSnapshot();
});
});
239 changes: 239 additions & 0 deletions app/components/UI/Notification/List/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { NavigationProp, ParamListBase } from '@react-navigation/native';
import React, { useCallback, useMemo } from 'react';
import { ActivityIndicator, FlatList, FlatListProps, View } from 'react-native';
import ScrollableTabView, {
DefaultTabBar,
DefaultTabBarProps,
TabBarProps,
} from 'react-native-scrollable-tab-view';
import { NotificationsViewSelectorsIDs } from '../../../../../e2e/selectors/NotificationsView.selectors';
import { strings } from '../../../../../locales/i18n';
import {
hasNotificationComponents,
hasNotificationModal,
NotificationComponentState,
} from '../../../../util/notifications/notification-states';
import Routes from '../../../../constants/navigation/Routes';
import { MetaMetricsEvents } from '../../../../core/Analytics';
import { Notification } from '../../../../util/notifications';
import { useMarkNotificationAsRead } from '../../../../util/notifications/hooks/useNotifications';
import { useMetrics } from '../../../hooks/useMetrics';
import Empty from '../Empty';
import { NotificationMenuItem } from '../NotificationMenuItem';
import useStyles from './useStyles';

interface NotificationsListProps {
navigation: NavigationProp<ParamListBase>;
allNotifications: Notification[];
walletNotifications: Notification[];
web3Notifications: Notification[];
loading: boolean;
}

interface NotificationsListItemProps {
navigation: NavigationProp<ParamListBase>;
notification: Notification;
}
interface NotificationsListItemProps {
navigation: NavigationProp<ParamListBase>;
notification: Notification;
}

function Loading() {
const {
theme: { colors },
styles,
} = useStyles();

return (
<View style={styles.loaderContainer}>
<ActivityIndicator color={colors.primary.default} size="large" />
</View>
);
}

function NotificationsListItem(props: NotificationsListItemProps) {
const { styles } = useStyles();
const { markNotificationAsRead } = useMarkNotificationAsRead();

const onNotificationClick = useCallback(
(item: Notification) => {
markNotificationAsRead([
{
id: item.id,
type: item.type,
isRead: item.isRead,
},
]);
if (hasNotificationModal(item.type)) {
props.navigation.navigate(Routes.NOTIFICATIONS.DETAILS, {
notification: item,
});
}
},
[markNotificationAsRead, props.navigation],
);

const menuItemState = useMemo(() => {
const notificationState =
NotificationComponentState[props.notification.type];
return notificationState.createMenuItem(props.notification);
}, [props.notification]);

if (!hasNotificationComponents(props.notification.type)) {
return null;
}

return (
<NotificationMenuItem.Root
handleOnPress={() => onNotificationClick(props.notification)}
styles={styles}
simultaneousHandlers={undefined}
>
<NotificationMenuItem.Icon {...menuItemState} />
<NotificationMenuItem.Content {...menuItemState} />
</NotificationMenuItem.Root>
);
}

function useNotificationListProps(props: {
navigation: NavigationProp<ParamListBase>;
}) {
const { styles } = useStyles();

const getListProps = useCallback(
(data: Notification[], tabLabel?: string) => {
const listProps: FlatListProps<Notification> = {
keyExtractor: (item) => item.id,
data,
ListEmptyComponent: (
<Empty
testID={NotificationsViewSelectorsIDs.NO_NOTIFICATIONS_CONTAINER}
/>
),
contentContainerStyle: styles.list,
renderItem: ({ item }) => (
<NotificationsListItem
notification={item}
// eslint-disable-next-line react/prop-types
navigation={props.navigation}
/>
),
initialNumToRender: 10,
maxToRenderPerBatch: 2,
onEndReachedThreshold: 0.5,
};

return { ...listProps, tabLabel: tabLabel ?? '' };
},
[props.navigation, styles.list],
);

return getListProps;
}

function SingleNotificationList(props: NotificationsListProps) {
const getListProps = useNotificationListProps(props);

return <FlatList {...getListProps(props.allNotifications)} />;
}

function TabbedNotificationList(props: NotificationsListProps) {
const {
theme: { colors },
styles,
} = useStyles();
const { trackEvent } = useMetrics();

const getListProps = useNotificationListProps(props);

const onTabClick = useCallback(
(tabLabel: string) => {
switch (tabLabel) {
case strings('notifications.list.0'):
trackEvent(MetaMetricsEvents.ALL_NOTIFICATIONS);
break;
case strings('notifications.list.1'):
trackEvent(MetaMetricsEvents.WALLET_NOTIFICATIONS);
break;
case strings('notifications.list.2'):
trackEvent(MetaMetricsEvents.WEB3_NOTIFICATIONS);
break;
default:
break;
}
},
[trackEvent],
);

return (
<ScrollableTabView
renderTabBar={(tabProps: TabBarProps<DefaultTabBarProps>) => (
<View>
<DefaultTabBar
underlineStyle={styles.tabUnderlineStyle}
activeTextColor={colors.primary.default}
inactiveTextColor={colors.text.default}
backgroundColor={colors.background.default}
tabStyle={styles.tabStyle}
textStyle={styles.textStyle}
style={styles.tabBar}
{...tabProps}
/>
</View>
)}
onChangeTab={(val) => onTabClick(val.ref.props.tabLabel)}
>
{/* Tab 1 - All Notifications */}
<FlatList
{...getListProps(
props.allNotifications,
strings(`notifications.list.0`),
)}
/>

{/* Tab 2 - Wallet Notifications */}
<FlatList
{...getListProps(
props.allNotifications,
strings(`notifications.list.1`),
)}
/>

{/* Tab 3 - Web 3 Notifications */}
<FlatList
{...getListProps(
props.web3Notifications,
strings(`notifications.list.2`),
)}
/>
</ScrollableTabView>
);
}

const Notifications = (props: NotificationsListProps) => {
const { styles } = useStyles();
if (props.loading) {
return (
<View style={styles.container}>
<Loading />
</View>
);
}

if (props.web3Notifications.length > 0) {
return (
<View style={styles.container}>
<TabbedNotificationList {...props} />
</View>
);
}

return (
<View style={styles.container}>
<SingleNotificationList {...props} />
</View>
);
};

export default Notifications;
Loading
Loading