From b13b97efebcd7675c6683ae19804a5f1d60f6b10 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Fri, 1 Nov 2024 17:55:32 -0700 Subject: [PATCH] WIP --- src/actions/DeviceSettingsActions.ts | 10 ++++ src/components/Main.tsx | 10 ++++ src/components/icons/IconBadge.tsx | 42 +++++++--------- src/components/navigation/SideMenuButton.tsx | 6 ++- .../notification/NotificationCenterCard.tsx | 48 +++++++++++++++++++ src/components/scenes/DevTestScene.tsx | 5 ++ .../scenes/NotificationCenterScene.tsx | 32 +++++++++++++ src/components/themed/SideMenu.tsx | 22 ++++++++- src/locales/en_US.ts | 3 ++ src/locales/strings/enUS.json | 1 + src/types/routerTypes.tsx | 1 + 11 files changed, 154 insertions(+), 26 deletions(-) create mode 100644 src/components/notification/NotificationCenterCard.tsx create mode 100644 src/components/scenes/NotificationCenterScene.tsx diff --git a/src/actions/DeviceSettingsActions.ts b/src/actions/DeviceSettingsActions.ts index 25355cf5a77..425f849c325 100644 --- a/src/actions/DeviceSettingsActions.ts +++ b/src/actions/DeviceSettingsActions.ts @@ -96,6 +96,16 @@ export const modifyDeviceNotifInfo = async (deviceNotifStateKey: string, deviceN }) } +/** Returns 0 if any priority notifications exist, number of incomplete + * notifications otherwise. */ +export const getNotifNumber = (): number | undefined => { + const { deviceNotifState } = getDeviceSettings() + const priorityNotifs = Object.values(deviceNotifState).filter(deviceNotifInfo => deviceNotifInfo.isPriority).length + const incompleteNotifs = Object.values(deviceNotifState).filter(deviceNotifInfo => !deviceNotifInfo.isCompleted).length + + return priorityNotifs > 0 ? 0 : incompleteNotifs +} + /** * Track the state of whether the "How did you Discover Edge" modal was shown. **/ diff --git a/src/components/Main.tsx b/src/components/Main.tsx index 58316790381..8fadd7ac48b 100644 --- a/src/components/Main.tsx +++ b/src/components/Main.tsx @@ -100,6 +100,7 @@ import { ManageTokensScene as ManageTokensSceneComponent } from './scenes/Manage import { MigrateWalletCalculateFeeScene as MigrateWalletCalculateFeeSceneComponent } from './scenes/MigrateWalletCalculateFeeScene' import { MigrateWalletCompletionScene as MigrateWalletCompletionSceneComponent } from './scenes/MigrateWalletCompletionScene' import { MigrateWalletSelectCryptoScene as MigrateWalletSelectCryptoSceneComponent } from './scenes/MigrateWalletSelectCryptoScene' +import { NotificationCenterScene as NotificationCenterSceneComponent } from './scenes/NotificationCenterScene' import { NotificationScene as NotificationSceneComponent } from './scenes/NotificationScene' import { OtpRepairScene as OtpRepairSceneComponent } from './scenes/OtpRepairScene' import { OtpSettingsScene as OtpSettingsSceneComponent } from './scenes/OtpSettingsScene' @@ -195,6 +196,7 @@ const SweepPrivateKeyProcessingScene = ifLoggedIn(SweepPrivateKeyProcessingScene const MigrateWalletCalculateFeeScene = ifLoggedIn(MigrateWalletCalculateFeeSceneComponent) const MigrateWalletCompletionScene = ifLoggedIn(MigrateWalletCompletionSceneComponent) const MigrateWalletSelectCryptoScene = ifLoggedIn(MigrateWalletSelectCryptoSceneComponent) +const NotificationCenterScene = ifLoggedIn(NotificationCenterSceneComponent) const NotificationScene = ifLoggedIn(NotificationSceneComponent) const OtpRepairScene = ifLoggedIn(OtpRepairSceneComponent) const OtpSettingsScene = ifLoggedIn(OtpSettingsSceneComponent) @@ -685,6 +687,14 @@ const EdgeAppStack = () => { }} /> + null + }} + /> void | Promise /** * - If undefined, renders only the children, without a badge. * - If 0, renders a red dot with a white circle inside. * - All other cases: renders a red dot with a white number inside. */ number?: number - testID?: string } /** * Maybe renders a red dot badge on top of the supplied `children,` with a white * number or dot inside. Visibility of the red dot depends on the `number` prop. + * + * For backwards compatibility, takes a style prop and provides no built-in margins. */ export const IconBadge = (props: Props) => { - const { number, children, sizeRem, testID, onPress } = props + const { number, children, sizeRem } = props const theme = useTheme() const styles = getStyles(theme) @@ -42,25 +39,25 @@ export const IconBadge = (props: Props) => { ) return ( - + {children} {number == null ? null : ( {number === 0 ? ( ) : ( - + {number} )} )} - + ) } const getStyles = cacheStyles((theme: Theme) => { - const badgeSize = theme.rem(0.75) * SCALE + const badgeSize = theme.rem(0.75) return { iconContainer: { @@ -71,30 +68,27 @@ const getStyles = cacheStyles((theme: Theme) => { position: 'absolute', alignItems: 'center', justifyContent: 'center', - top: 0, - right: 0, + top: -badgeSize / 2, + right: -badgeSize / 2, height: badgeSize, minWidth: badgeSize, borderRadius: badgeSize / 2, - paddingLeft: theme.rem(0.25) / 2, - paddingRight: theme.rem(0.25) / 2, backgroundColor: 'red' }, - label: { - textAlign: 'center', - marginBottom: theme.rem(0.5) - }, - superscriptLabel: { + // TODO: Adjust platform-specific styles + textIos: { fontSize: theme.rem(0.5) }, - androidAdjust: { + textAndroid: { + fontSize: theme.rem(0.5), marginTop: 2, - marginLeft: 1 + marginLeft: 1, + marginRight: 1 }, circle: { - width: theme.rem(0.25), - height: theme.rem(0.25), - borderRadius: theme.rem(0.125), + width: theme.rem(0.2), + height: theme.rem(0.2), + borderRadius: theme.rem(0.1), backgroundColor: 'white', alignSelf: 'center' } diff --git a/src/components/navigation/SideMenuButton.tsx b/src/components/navigation/SideMenuButton.tsx index 70c3025a01d..6bd89b20a0a 100644 --- a/src/components/navigation/SideMenuButton.tsx +++ b/src/components/navigation/SideMenuButton.tsx @@ -2,9 +2,11 @@ import { DrawerActions, useNavigation } from '@react-navigation/native' import * as React from 'react' import { Keyboard } from 'react-native' +import { getNotifNumber } from '../../actions/DeviceSettingsActions' import { Fontello } from '../../assets/vector/index' import { useHandler } from '../../hooks/useHandler' import { triggerHaptic } from '../../util/haptic' +import { IconBadge } from '../icons/IconBadge' import { useTheme } from '../services/ThemeContext' import { NavigationButton } from './NavigationButton' @@ -20,7 +22,9 @@ export const SideMenuButton = () => { return ( - + + + ) } diff --git a/src/components/notification/NotificationCenterCard.tsx b/src/components/notification/NotificationCenterCard.tsx new file mode 100644 index 00000000000..1cc37055338 --- /dev/null +++ b/src/components/notification/NotificationCenterCard.tsx @@ -0,0 +1,48 @@ +import * as React from 'react' +import { View } from 'react-native' +import { cacheStyles } from 'react-native-patina' + +import { getThemedIconUri } from '../../util/CdnUris' +import { Theme, useTheme } from '../services/ThemeContext' +import { NotificationCard } from './NotificationCard' + +interface Props { + message: string + title: string + type: 'warning' | 'info' + isComplete: boolean + iconUri?: string + + onPress: () => void | Promise +} + +export const NotificationCenterCard = (props: Props) => { + const theme = useTheme() + const styles = getStyles(theme) + + const { title, type, message, isComplete, onPress } = props + const { iconUri = type === 'warning' ? getThemedIconUri(theme, 'notifications/icon-warning') : getThemedIconUri(theme, 'notifications/icon-info') } = props + + return ( + + + + + ) +} + +const getStyles = cacheStyles((theme: Theme) => ({ + container: { + flexDirection: 'row', + flexShrink: 1, + flexGrow: 1 + }, + dot: { + width: theme.rem(0.75), + height: theme.rem(0.75), + backgroundColor: 'red' + }, + noDot: { + backgroundColor: 'transparent' + } +})) diff --git a/src/components/scenes/DevTestScene.tsx b/src/components/scenes/DevTestScene.tsx index 901abb4444c..af61cd853ac 100644 --- a/src/components/scenes/DevTestScene.tsx +++ b/src/components/scenes/DevTestScene.tsx @@ -135,6 +135,10 @@ export function DevTestScene(props: Props) { }) } + const handleNotificationCenterPress = () => { + navigation2.navigate('notificationCenter') + } + const coreWallet = selectedWallet?.wallet let balance = coreWallet?.balanceMap.get(tokenId) ?? '' if (eq(balance, '0')) balance = '' @@ -158,6 +162,7 @@ export function DevTestScene(props: Props) { <> + <> Galore} /> diff --git a/src/components/scenes/NotificationCenterScene.tsx b/src/components/scenes/NotificationCenterScene.tsx new file mode 100644 index 00000000000..8613435d661 --- /dev/null +++ b/src/components/scenes/NotificationCenterScene.tsx @@ -0,0 +1,32 @@ +import * as React from 'react' + +import { getDeviceSettings } from '../../actions/DeviceSettingsActions' +import { getLocalAccountSettings } from '../../actions/LocalSettingsActions' +import { useAsyncEffect } from '../../hooks/useAsyncEffect' +import { lstrings } from '../../locales/strings' +import { useSelector } from '../../types/reactRedux' +import { EdgeAppSceneProps } from '../../types/routerTypes' +import { DeviceNotifInfo } from '../../types/types' +import { SceneWrapper } from '../common/SceneWrapper' +import { SectionHeader } from '../common/SectionHeader' +import { useTheme } from '../services/ThemeContext' + +interface Props extends EdgeAppSceneProps<'notificationCenter'> {} + +export const NotificationCenterScene = (props: Props) => { + const { navigation } = props + const theme = useTheme() + const { deviceNotifState } = getDeviceSettings() + const accountNotifDismissInfo = getLocalAccountSettings().accountNotifDismissInfo + + const pinnedNotifInfos = Object.values(deviceNotifState).filter(deviceNotifInfo => deviceNotifInfo.isPriority) + const otherNotifInfos = Object.values(deviceNotifState).filter(deviceNotifInfo => !deviceNotifInfo.isPriority) + + return ( + + + <> + + + ) +} diff --git a/src/components/themed/SideMenu.tsx b/src/components/themed/SideMenu.tsx index d1d9d5d7169..a9493c4eed9 100644 --- a/src/components/themed/SideMenu.tsx +++ b/src/components/themed/SideMenu.tsx @@ -11,11 +11,13 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context' import Share from 'react-native-share' import Feather from 'react-native-vector-icons/Feather' import FontAwesome5Icon from 'react-native-vector-icons/FontAwesome5' +import Ionicons from 'react-native-vector-icons/Ionicons' import MaterialIcon from 'react-native-vector-icons/MaterialIcons' import { sprintf } from 'sprintf-js' import { showBackupModal } from '../../actions/BackupModalActions' import { launchDeepLink } from '../../actions/DeepLinkingActions' +import { getNotifNumber } from '../../actions/DeviceSettingsActions' import { getRootNavigation, logoutRequest } from '../../actions/LoginActions' import { executePluginAction } from '../../actions/PluginActions' import { Fontello } from '../../assets/vector' @@ -32,6 +34,7 @@ import { getDisplayUsername } from '../../util/utils' import { IONIA_SUPPORTED_FIATS } from '../cards/VisaCardCard' import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity' import { styled } from '../hoc/styled' +import { IconBadge } from '../icons/IconBadge' import { ButtonsModal } from '../modals/ButtonsModal' import { ScanModal } from '../modals/ScanModal' import { Airship, showError } from '../services/AirshipInstance' @@ -216,6 +219,14 @@ export function SideMenu(props: DrawerContentComponentProps) { iconNameFontAwesome?: string title: string }> = [ + { + pressHandler: () => { + navigation.navigate('edgeAppStack', { screen: 'notificationCenter' }) + navigation.dispatch(DrawerActions.closeDrawer()) + }, + iconName: 'notifications', + title: lstrings.notifications + }, { pressHandler: () => { navigation.navigate('edgeAppStack', { screen: 'fioAddressList' }) @@ -349,7 +360,16 @@ export function SideMenu(props: DrawerContentComponentProps) { {rowDatas.map(rowData => ( - {rowData.iconName != null ? : null} + { + // TODO: No red dot ever renders for some reason... + rowData.iconName === 'notifications' ? ( + + + + ) : rowData.iconName != null ? ( + + ) : null + } {rowData.iconNameFontAwesome != null ? ( ) : null} diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index 31fb80d6b0f..d671ba3952e 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -1648,6 +1648,9 @@ const strings = { split_description: 'This action creates wallets from pre-existing wallets.', add_custom_token: 'Add Custom Token', choose_custom_token_wallet: 'Select Wallet for Custom Token', + notifications: 'Notifications', + pinned_notifications: 'Pinned', + other_notifications: 'Other', // Currency Labels currency_label_AFN: 'Afghani', diff --git a/src/locales/strings/enUS.json b/src/locales/strings/enUS.json index 677d544fba7..4b36018cdec 100644 --- a/src/locales/strings/enUS.json +++ b/src/locales/strings/enUS.json @@ -1424,6 +1424,7 @@ "split_description": "This action creates wallets from pre-existing wallets.", "add_custom_token": "Add Custom Token", "choose_custom_token_wallet": "Select Wallet for Custom Token", + "notifications": "Notifications", "currency_label_AFN": "Afghani", "currency_label_ALL": "Lek", "currency_label_DZD": "Algerian Dinar", diff --git a/src/types/routerTypes.tsx b/src/types/routerTypes.tsx index 89b1e03cb6f..e3ee9f60e6c 100644 --- a/src/types/routerTypes.tsx +++ b/src/types/routerTypes.tsx @@ -178,6 +178,7 @@ export type EdgeAppStackParamList = {} & { migrateWalletCompletion: MigrateWalletCompletionParams migrateWalletSelectCrypto: MigrateWalletSelectCryptoParams notificationSettings: undefined + notificationCenter: undefined otpRepair: OtpRepairParams otpSetup: undefined passwordRecovery: undefined