diff --git a/packages/mobile/src/screens/notifications-screen/NotificationBlock.tsx b/packages/mobile/src/screens/notifications-screen/NotificationBlock.tsx deleted file mode 100644 index caea3222c6..0000000000 --- a/packages/mobile/src/screens/notifications-screen/NotificationBlock.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import { useCallback, useContext } from 'react' - -import { BadgeTier } from 'audius-client/src/common/models/BadgeTier' -import { - getNotificationEntities, - getNotificationEntity, - getNotificationUser, - getNotificationUsers -} from 'audius-client/src/common/store/notifications/selectors' -import { - Notification, - NotificationType, - TierChange -} from 'audius-client/src/common/store/notifications/types' -import { setNotificationId } from 'audius-client/src/common/store/user-list/notifications/actions' -import { NOTIFICATION_PAGE } from 'audius-client/src/utils/route' -import { View, Text } from 'react-native' -import { SvgProps } from 'react-native-svg' -import { useDispatch } from 'react-redux' - -import IconBronzeBadge from 'app/assets/images/IconBronzeBadge.svg' -import IconGoldBadge from 'app/assets/images/IconGoldBadge.svg' -import IconPlatinumBadge from 'app/assets/images/IconPlatinumBadge.svg' -import IconSilverBadge from 'app/assets/images/IconSilverBadge.svg' -import IconAudius from 'app/assets/images/iconAudius.svg' -import IconHeart from 'app/assets/images/iconHeart.svg' -import IconPlaylists from 'app/assets/images/iconPlaylists.svg' -import IconRemix from 'app/assets/images/iconRemix.svg' -import IconRepost from 'app/assets/images/iconRepost.svg' -import IconStars from 'app/assets/images/iconStars.svg' -import IconTrending from 'app/assets/images/iconTrending.svg' -import IconTrophy from 'app/assets/images/iconTrophy.svg' -import IconUser from 'app/assets/images/iconUser.svg' -import { Tile } from 'app/components/core' -import { useDispatchWeb } from 'app/hooks/useDispatchWeb' -import { useNavigation } from 'app/hooks/useNavigation' -import { isEqual, useSelectorWeb } from 'app/hooks/useSelectorWeb' -import { AppTabScreenParamList } from 'app/screens/app-screen' -import { ProfileTabScreenParamList } from 'app/screens/app-screen/ProfileTabScreen' -import { NotificationsDrawerNavigationContext } from 'app/screens/notifications-screen/NotificationsDrawerNavigationContext' -import { close } from 'app/store/notifications/actions' -import { makeStyles } from 'app/styles' - -import NotificationContent from './content/NotificationContent' -import { getNotificationRoute, getNotificationScreen } from './routeUtil' - -// The maximum number of users to fetch along with a notification, -// which determines the number of profile pictures to show -const USER_LENGTH_LIMIT = 8 - -const tierInfoMap: Record< - BadgeTier, - { title: string; icon: React.FC } -> = { - none: { - title: 'NO TIER', - icon: IconBronzeBadge - }, - bronze: { - title: 'BRONZE TIER UNLOCKED', - icon: IconBronzeBadge - }, - silver: { - title: 'SILVER TIER UNLOCKED', - icon: IconSilverBadge - }, - gold: { - title: 'GOLD TIER UNLOCKED', - icon: IconGoldBadge - }, - platinum: { - title: 'PLATINUM TIER UNLOCKED', - icon: IconPlatinumBadge - } -} - -const typeIconMap: Record< - NotificationType, - (notification: any) => React.FC -> = { - [NotificationType.Announcement]: () => IconAudius, - [NotificationType.Follow]: () => IconUser, - [NotificationType.UserSubscription]: () => IconStars, - [NotificationType.Favorite]: () => IconHeart, - [NotificationType.Repost]: () => IconRepost, - [NotificationType.Milestone]: () => IconTrophy, - [NotificationType.RemixCosign]: () => IconRemix, - [NotificationType.RemixCreate]: () => IconRemix, - [NotificationType.TrendingTrack]: () => IconTrending, - [NotificationType.ChallengeReward]: () => IconAudius, - [NotificationType.TierChange]: (notification: TierChange) => - tierInfoMap[notification.tier].icon, - [NotificationType.Reaction]: () => IconTrending, - [NotificationType.TipReceive]: () => IconTrending, - [NotificationType.TipSend]: () => IconTrending, - [NotificationType.SupporterRankUp]: () => IconTrending, - [NotificationType.SupportingRankUp]: () => IconTrending, - [NotificationType.AddTrackToPlaylist]: () => IconPlaylists -} - -const typeTitleMap: Record string> = { - [NotificationType.Announcement]: () => "WHAT'S NEW", - [NotificationType.Follow]: () => 'NEW FOLLOWER', - [NotificationType.UserSubscription]: () => 'ARTIST UPDATE', - [NotificationType.Favorite]: () => 'NEW FAVORITES', - [NotificationType.Repost]: () => 'NEW REPOSTS', - [NotificationType.Milestone]: () => 'NEW MILESTONE', - [NotificationType.RemixCosign]: () => 'NEW COSIGN', - [NotificationType.RemixCreate]: () => 'NEW REMIX', - [NotificationType.TrendingTrack]: () => 'TRENDING', - [NotificationType.ChallengeReward]: () => "YOU'VE EARNED $AUDIO", - [NotificationType.TierChange]: (notification: TierChange) => - tierInfoMap[notification.tier].title, - [NotificationType.Reaction]: () => '', - [NotificationType.TipReceive]: () => '', - [NotificationType.TipSend]: () => '', - [NotificationType.SupporterRankUp]: () => '', - [NotificationType.SupportingRankUp]: () => '', - [NotificationType.AddTrackToPlaylist]: () => 'TRACK ADDED TO PLAYLIST' -} - -const useStyles = makeStyles(({ spacing, palette }, { isViewed }) => ({ - tile: { - borderColor: isViewed ? 'none' : palette.primary, - borderWidth: isViewed ? 0 : 2 - }, - content: { - padding: spacing(3), - paddingVertical: spacing(4) - }, - top: { - flexDirection: 'row', - marginLeft: spacing(3), - justifyContent: 'flex-start', - alignItems: 'center', - lineHeight: 20, - marginBottom: spacing(2) - }, - title: { - fontFamily: 'AvenirNextLTPro-Heavy', - fontSize: 18, - marginLeft: 12, - color: isViewed ? palette.neutralLight4 : palette.primary - }, - body: { - marginLeft: spacing(12) - }, - timestamp: { - fontFamily: 'AvenirNextLTPro-Regular', - fontSize: 12, - marginTop: spacing(2), - color: palette.neutralLight5 - }, - iconTierChange: { - color: isViewed ? palette.neutralLight4 : palette.primary - } -})) - -type NotificationBlockProps = { - notification: Notification -} - -export const NotificationBlock = (props: NotificationBlockProps) => { - const { notification } = props - const { isViewed, type } = notification - const styles = useStyles({ isViewed }) - const Icon = typeIconMap[type](notification) - const title = typeTitleMap[type](notification) - const dispatch = useDispatch() - const dispatchWeb = useDispatchWeb() - - const user = useSelectorWeb((state) => - getNotificationUser(state, notification) - ) - - const users = useSelectorWeb( - (state) => getNotificationUsers(state, notification, USER_LENGTH_LIMIT), - isEqual - ) - - const entity = useSelectorWeb( - (state) => getNotificationEntity(state, notification), - isEqual - ) - - const entities = useSelectorWeb( - (state) => getNotificationEntities(state, notification), - isEqual - ) - - // TODO: Type notifications & their selectors more strictly. - // The reason we ignore here is because user/users/entity/entities - // are specific to each type of notification, but they are handled by - // generic selectors. - // @ts-ignore - notification.user = user - // @ts-ignore - notification.users = users - // @ts-ignore - notification.entity = entity - // @ts-ignore - notification.entities = entities - - const notificationScreen = getNotificationScreen(notification, { - fromNotifications: true - }) - const notificationRoute = getNotificationRoute(notification) - const { drawerHelpers } = useContext(NotificationsDrawerNavigationContext) - const navigation = useNavigation< - AppTabScreenParamList & ProfileTabScreenParamList - >({ customNativeNavigation: drawerHelpers }) - - const onPress = useCallback(() => { - if (notificationRoute && notificationScreen) { - if (notificationScreen.screen === 'NotificationUsers') { - dispatchWeb(setNotificationId(notification.id)) - } - navigation.navigate({ - native: notificationScreen, - web: { - route: notificationRoute, - fromPage: NOTIFICATION_PAGE - } - }) - dispatch(close()) - } - }, [ - notification, - notificationScreen, - notificationRoute, - navigation, - dispatch, - dispatchWeb - ]) - - const iconHeightProps = - notification.type === NotificationType.TierChange - ? { height: 32, width: 32 } - : {} - - return ( - - - - {title} - - - - {notification.timeLabel} - - - ) -} diff --git a/packages/mobile/src/screens/notifications-screen/NotificationList.tsx b/packages/mobile/src/screens/notifications-screen/NotificationList.tsx index db3da9df9f..6b2f9f18b3 100644 --- a/packages/mobile/src/screens/notifications-screen/NotificationList.tsx +++ b/packages/mobile/src/screens/notifications-screen/NotificationList.tsx @@ -1,7 +1,6 @@ import { useCallback, useContext, useEffect, useState } from 'react' import Status from 'audius-client/src/common/models/Status' -import { FeatureFlags } from 'audius-client/src/common/services/remote-config' import { fetchNotifications, refreshNotifications @@ -17,13 +16,10 @@ import { View, ViewToken } from 'react-native' import { FlatList } from 'app/components/core' import LoadingSpinner from 'app/components/loading-spinner' import { useDispatchWeb } from 'app/hooks/useDispatchWeb' -import { useFeatureFlag } from 'app/hooks/useRemoteConfig' import { isEqual, useSelectorWeb } from 'app/hooks/useSelectorWeb' import { makeStyles } from 'app/styles' import { EmptyNotifications } from './EmptyNotifications' -import { NotificationBlock } from './NotificationBlock' -import { NotificationErrorBoundary } from './NotificationErrorBoundary' import { NotificationListItem } from './NotificationListItem' import { NotificationsDrawerNavigationContext } from './NotificationsDrawerNavigationContext' @@ -109,9 +105,6 @@ export const NotificationList = () => { const status = useSelectorWeb(getNotificationStatus) const hasMore = useSelectorWeb(getNotificationHasMore) const [isRefreshing, setIsRefreshing] = useState(false) - const { isEnabled: isTippingEnabled } = useFeatureFlag( - FeatureFlags.TIPPING_ENABLED - ) const { gesturesDisabled } = useContext(NotificationsDrawerNavigationContext) @@ -146,20 +139,12 @@ export const NotificationList = () => { onRefresh={handleRefresh} data={notifications} keyExtractor={(item: Notification, index) => `${item.id} ${index}`} - renderItem={({ item, index }) => - isTippingEnabled ? ( - - ) : ( - - - - - - ) - } + renderItem={({ item, index }) => ( + + )} ListFooterComponent={ status === Status.LOADING && !isRefreshing ? ( diff --git a/packages/mobile/src/screens/notifications-screen/Notifications/FavoriteNotification.tsx b/packages/mobile/src/screens/notifications-screen/Notifications/FavoriteNotification.tsx index e6ab37c290..df0cade1b3 100644 --- a/packages/mobile/src/screens/notifications-screen/Notifications/FavoriteNotification.tsx +++ b/packages/mobile/src/screens/notifications-screen/Notifications/FavoriteNotification.tsx @@ -4,10 +4,9 @@ import { } from 'audius-client/src/common/store/notifications/selectors' import { Favorite } from 'common/store/notifications/types' import { formatCount } from 'common/utils/formatUtil' -import { isEqual } from 'lodash' import IconHeart from 'app/assets/images/iconHeart.svg' -import { useSelectorWeb } from 'app/hooks/useSelectorWeb' +import { isEqual, useSelectorWeb } from 'app/hooks/useSelectorWeb' import { NotificationHeader, @@ -38,7 +37,7 @@ export const FavoriteNotification = (props: FavoriteNotificationProps) => { (state) => getNotificationUsers(state, notification, USER_LENGTH_LIMIT), isEqual ) - const [firstUser] = users + const firstUser = users?.[0] const otherUsersCount = userIds.length - 1 const entity = useSelectorWeb( @@ -48,6 +47,8 @@ export const FavoriteNotification = (props: FavoriteNotificationProps) => { const handlePress = useSocialActionHandler(notification, users) + if (!users || !firstUser || !entity) return null + return ( diff --git a/packages/mobile/src/screens/notifications-screen/Notifications/FollowNotification.tsx b/packages/mobile/src/screens/notifications-screen/Notifications/FollowNotification.tsx index cd12d9db7a..44e74a9bbc 100644 --- a/packages/mobile/src/screens/notifications-screen/Notifications/FollowNotification.tsx +++ b/packages/mobile/src/screens/notifications-screen/Notifications/FollowNotification.tsx @@ -1,10 +1,9 @@ import { getNotificationUsers } from 'audius-client/src/common/store/notifications/selectors' import { Follow } from 'common/store/notifications/types' import { formatCount } from 'common/utils/formatUtil' -import { isEqual } from 'lodash' import IconUser from 'app/assets/images/iconUser.svg' -import { useSelectorWeb } from 'app/hooks/useSelectorWeb' +import { isEqual, useSelectorWeb } from 'app/hooks/useSelectorWeb' import { NotificationHeader, @@ -34,11 +33,13 @@ export const FollowNotification = (props: FollowNotificationProps) => { (state) => getNotificationUsers(state, notification, USER_LENGTH_LIMIT), isEqual ) - const [firstUser] = users + const firstUser = users?.[0] const otherUsersCount = userIds.length - 1 const handlePress = useSocialActionHandler(notification, users) + if (!users || !firstUser) return null + return ( diff --git a/packages/mobile/src/screens/notifications-screen/Notifications/MilestoneNotification.tsx b/packages/mobile/src/screens/notifications-screen/Notifications/MilestoneNotification.tsx index 5d26603de7..56fbbabe77 100644 --- a/packages/mobile/src/screens/notifications-screen/Notifications/MilestoneNotification.tsx +++ b/packages/mobile/src/screens/notifications-screen/Notifications/MilestoneNotification.tsx @@ -132,7 +132,6 @@ export const MilestoneNotification = (props: MilestoneNotificationProps) => { const renderBody = () => { const { achievement, value } = notification if (achievement === Achievement.Followers) { - const { value } = notification return `${messages.follows} ${formatCount(value)} ${achievement}` } else if (entity) { const { entityType } = notification diff --git a/packages/mobile/src/screens/notifications-screen/Notifications/RemixCosignNotification.tsx b/packages/mobile/src/screens/notifications-screen/Notifications/RemixCosignNotification.tsx index 1fec869d0e..4568568eb8 100644 --- a/packages/mobile/src/screens/notifications-screen/Notifications/RemixCosignNotification.tsx +++ b/packages/mobile/src/screens/notifications-screen/Notifications/RemixCosignNotification.tsx @@ -4,12 +4,15 @@ import { getNotificationEntities, getNotificationUser } from 'audius-client/src/common/store/notifications/selectors' -import { RemixCosign } from 'audius-client/src/common/store/notifications/types' -import { isEqual } from 'lodash' +import { + RemixCosign, + TrackEntity +} from 'audius-client/src/common/store/notifications/types' +import { Nullable } from 'audius-client/src/common/utils/typeUtils' import { View } from 'react-native' import IconRemix from 'app/assets/images/iconRemix.svg' -import { useSelectorWeb } from 'app/hooks/useSelectorWeb' +import { isEqual, useSelectorWeb } from 'app/hooks/useSelectorWeb' import { EventNames } from 'app/types/analytics' import { make } from 'app/utils/analytics' import { getTrackRoute } from 'app/utils/routes' @@ -46,13 +49,15 @@ export const RemixCosignNotification = ( const user = useSelectorWeb((state) => getNotificationUser(state, notification) ) + // TODO: casting from EntityType to TrackEntity here, but + // getNotificationEntities should be smart enough based on notif type const tracks = useSelectorWeb( (state) => getNotificationEntities(state, notification), isEqual - ) + ) as Nullable - const childTrack = tracks.find(({ track_id }) => track_id === childTrackId) - const parentTrack = tracks.find( + const childTrack = tracks?.find(({ track_id }) => track_id === childTrackId) + const parentTrack = tracks?.find( ({ owner_id }) => owner_id === parentTrackUserId ) const parentTrackTitle = parentTrack?.title diff --git a/packages/mobile/src/screens/notifications-screen/Notifications/RepostNotification.tsx b/packages/mobile/src/screens/notifications-screen/Notifications/RepostNotification.tsx index 6425f367c0..a845fde94c 100644 --- a/packages/mobile/src/screens/notifications-screen/Notifications/RepostNotification.tsx +++ b/packages/mobile/src/screens/notifications-screen/Notifications/RepostNotification.tsx @@ -4,10 +4,9 @@ import { } from 'audius-client/src/common/store/notifications/selectors' import { Repost } from 'common/store/notifications/types' import { formatCount } from 'common/utils/formatUtil' -import { isEqual } from 'lodash' import IconRepost from 'app/assets/images/iconRepost.svg' -import { useSelectorWeb } from 'app/hooks/useSelectorWeb' +import { isEqual, useSelectorWeb } from 'app/hooks/useSelectorWeb' import { NotificationHeader, @@ -38,7 +37,7 @@ export const RepostNotification = (props: RepostNotificationProps) => { (state) => getNotificationUsers(state, notification, USER_LENGTH_LIMIT), isEqual ) - const [firstUser] = users + const firstUser = users?.[0] const otherUsersCount = userIds.length - 1 const entity = useSelectorWeb( @@ -48,6 +47,8 @@ export const RepostNotification = (props: RepostNotificationProps) => { const handlePress = useSocialActionHandler(notification, users) + if (!users || !firstUser || !entity) return null + return ( diff --git a/packages/mobile/src/screens/notifications-screen/Notifications/TrendingTrackNotification.tsx b/packages/mobile/src/screens/notifications-screen/Notifications/TrendingTrackNotification.tsx index 602d8b2d38..faf6e1e187 100644 --- a/packages/mobile/src/screens/notifications-screen/Notifications/TrendingTrackNotification.tsx +++ b/packages/mobile/src/screens/notifications-screen/Notifications/TrendingTrackNotification.tsx @@ -1,11 +1,14 @@ import { useCallback } from 'react' import { getNotificationEntity } from 'audius-client/src/common/store/notifications/selectors' -import { TrendingTrack } from 'audius-client/src/common/store/notifications/types' -import { isEqual } from 'lodash' +import { + TrackEntity, + TrendingTrack +} from 'audius-client/src/common/store/notifications/types' +import { Nullable } from 'audius-client/src/common/utils/typeUtils' import IconTrending from 'app/assets/images/iconTrending.svg' -import { useSelectorWeb } from 'app/hooks/useSelectorWeb' +import { isEqual, useSelectorWeb } from 'app/hooks/useSelectorWeb' import { getTrackRoute } from 'app/utils/routes' import { @@ -44,16 +47,20 @@ export const TrendingTrackNotification = ( const track = useSelectorWeb( (state) => getNotificationEntity(state, notification), isEqual - ) + ) as Nullable const navigation = useDrawerNavigation() const handlePress = useCallback(() => { - navigation.navigate({ - native: { screen: 'Track', params: { id: track.track_id } }, - web: { route: getTrackRoute(track) } - }) + if (track) { + navigation.navigate({ + native: { screen: 'Track', params: { id: track.track_id } }, + web: { route: getTrackRoute(track) } + }) + } }, [navigation, track]) + if (!track) return null + return ( diff --git a/packages/mobile/src/screens/notifications-screen/Notifications/UserSubscriptionNotification.tsx b/packages/mobile/src/screens/notifications-screen/Notifications/UserSubscriptionNotification.tsx index a1b494d09d..4fe2d6e29e 100644 --- a/packages/mobile/src/screens/notifications-screen/Notifications/UserSubscriptionNotification.tsx +++ b/packages/mobile/src/screens/notifications-screen/Notifications/UserSubscriptionNotification.tsx @@ -1,13 +1,18 @@ +import { useCallback } from 'react' + import { getNotificationEntities, getNotificationUser } from 'audius-client/src/common/store/notifications/selectors' -import { UserSubscription } from 'audius-client/src/common/store/notifications/types' -import { isEqual } from 'lodash' +import { + Entity, + UserSubscription +} from 'audius-client/src/common/store/notifications/types' +import { profilePage } from 'audius-client/src/utils/route' import { View } from 'react-native' import IconStars from 'app/assets/images/iconStars.svg' -import { useSelectorWeb } from 'app/hooks/useSelectorWeb' +import { isEqual, useSelectorWeb } from 'app/hooks/useSelectorWeb' import { NotificationHeader, @@ -18,6 +23,8 @@ import { UserNameLink, ProfilePicture } from '../Notification' +import { getEntityRoute, getEntityScreen } from '../Notification/utils' +import { useDrawerNavigation } from '../useDrawerNavigation' const messages = { title: 'New Release', @@ -34,6 +41,7 @@ export const UserSubscriptionNotification = ( ) => { const { notification } = props const { entityType } = notification + const navigation = useDrawerNavigation() const user = useSelectorWeb((state) => getNotificationUser(state, notification) ) @@ -42,13 +50,37 @@ export const UserSubscriptionNotification = ( isEqual ) - if (!user || !entities) return null - - const uploadCount = entities.length + const uploadCount = entities?.length ?? 0 const isSingleUpload = uploadCount === 1 + const handlePress = useCallback(() => { + if (entityType === Entity.Track && !isSingleUpload) { + if (user) { + navigation.navigate({ + native: { + screen: 'Profile', + params: { handle: user.handle, fromNotifications: true } + }, + web: { + route: profilePage(user.handle) + } + }) + } + } else { + if (entities) { + const [entity] = entities + navigation.navigate({ + native: getEntityScreen(entity), + web: { route: getEntityRoute(entity) } + }) + } + } + }, [entityType, isSingleUpload, navigation, user, entities]) + + if (!user || !entities) return null + return ( - + {messages.title} diff --git a/packages/mobile/src/screens/notifications-screen/Notifications/useSocialActionHandler.ts b/packages/mobile/src/screens/notifications-screen/Notifications/useSocialActionHandler.ts index dd6e1b1ed6..7353ff5f91 100644 --- a/packages/mobile/src/screens/notifications-screen/Notifications/useSocialActionHandler.ts +++ b/packages/mobile/src/screens/notifications-screen/Notifications/useSocialActionHandler.ts @@ -7,12 +7,12 @@ import { Repost } from 'audius-client/src/common/store/notifications/types' import { setNotificationId } from 'audius-client/src/common/store/user-list/notifications/actions' +import { Nullable } from 'audius-client/src/common/utils/typeUtils' import { NOTIFICATION_PAGE } from 'audius-client/src/utils/route' import { useDispatchWeb } from 'app/hooks/useDispatchWeb' import { getUserRoute } from 'app/utils/routes' -import { getUserListRoute } from '../routeUtil' import { useDrawerNavigation } from '../useDrawerNavigation' /** @@ -21,10 +21,10 @@ import { useDrawerNavigation } from '../useDrawerNavigation' */ export const useSocialActionHandler = ( notification: Follow | Repost | Favorite, - users: User[] + users: Nullable ) => { const { id, type, userIds } = notification - const [firstUser] = users + const firstUser = users?.[0] const isMultiUser = userIds.length > 1 const dispatchWeb = useDispatchWeb() const navigation = useDrawerNavigation() @@ -43,11 +43,11 @@ export const useSocialActionHandler = ( } }, web: { - route: getUserListRoute(notification), + route: `/notification/${id}/users`, fromPage: NOTIFICATION_PAGE } }) - } else { + } else if (firstUser) { navigation.navigate({ native: { screen: 'Profile', @@ -56,14 +56,5 @@ export const useSocialActionHandler = ( web: { route: getUserRoute(firstUser), fromPage: NOTIFICATION_PAGE } }) } - }, [ - isMultiUser, - id, - type, - userIds, - notification, - dispatchWeb, - navigation, - firstUser - ]) + }, [isMultiUser, id, type, userIds, dispatchWeb, navigation, firstUser]) } diff --git a/packages/mobile/src/screens/notifications-screen/content/AddTrackToPlaylist.tsx b/packages/mobile/src/screens/notifications-screen/content/AddTrackToPlaylist.tsx deleted file mode 100644 index dc50299c4b..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/AddTrackToPlaylist.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { - AddTrackToPlaylist as AddTrackToPlaylistNotification, - Entity as EntityType -} from 'audius-client/src/common/store/notifications/types' -import { Text, View } from 'react-native' - -import { makeStyles } from 'app/styles' -import { useTheme } from 'app/utils/theme' - -import Entity from './Entity' -import TwitterShare from './TwitterShare' -import User from './User' - -const useStyles = makeStyles(({ typography, spacing }) => ({ - text: { - fontFamily: typography.fontByWeight.bold, - fontSize: typography.fontSize.medium, - marginBottom: spacing(2) - } -})) - -type AddTrackToPlaylistProps = { - notification: AddTrackToPlaylistNotification -} - -const AddTrackToPlaylist = ({ notification }: AddTrackToPlaylistProps) => { - const styles = useStyles() - const { entities } = notification - const { track, playlist } = entities - const playlistOwner = playlist.user - - const textStyle = useTheme(styles.text, { - color: 'neutral' - }) - - if (!playlistOwner) return null - - return ( - - - - {' added your track '} - - {' to their playlist '} - - - - - ) -} - -export default AddTrackToPlaylist diff --git a/packages/mobile/src/screens/notifications-screen/content/Announcement.tsx b/packages/mobile/src/screens/notifications-screen/content/Announcement.tsx deleted file mode 100644 index 118e84ffeb..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/Announcement.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Announcement as AnnouncementNotification } from 'audius-client/src/common/store/notifications/types' -import { View } from 'react-native' -import Markdown from 'react-native-markdown-display' - -import { useTheme } from 'app/utils/theme' - -type AnnouncementProps = { - notification: AnnouncementNotification -} - -const Announcement = ({ notification }: AnnouncementProps) => { - const body = useTheme( - { - fontFamily: 'AvenirNextLTPro-Medium', - fontSize: 16 - }, - { - color: 'neutral' - } - ) - - const link = useTheme( - { - fontFamily: 'AvenirNextLTPro-Medium', - fontSize: 16 - }, - { - color: 'secondary' - } - ) - - return ( - - - {notification.shortDescription} - - - ) -} - -export default Announcement diff --git a/packages/mobile/src/screens/notifications-screen/content/ChallengeReward.tsx b/packages/mobile/src/screens/notifications-screen/content/ChallengeReward.tsx deleted file mode 100644 index 5dbac5e2e8..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/ChallengeReward.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { ChallengeRewardID } from 'audius-client/src/common/models/AudioRewards' -import { ChallengeReward as ChallengeRewardNotification } from 'audius-client/src/common/store/notifications/types' -import { StyleSheet, Text, View } from 'react-native' - -import { useTheme } from 'app/utils/theme' - -import TwitterShare from './TwitterShare' - -const styles = StyleSheet.create({ - wrapper: { - display: 'flex', - flexDirection: 'column' - }, - text: { - fontFamily: 'AvenirNextLTPro-Bold', - fontSize: 16, - marginBottom: 8 - } -}) - -type ChallengeRewardProps = { - notification: ChallengeRewardNotification -} - -const challengeInfoMap: Record< - ChallengeRewardID, - { title: string; amount: number } -> = { - 'profile-completion': { - title: '✅️ Complete your Profile', - amount: 1 - }, - 'listen-streak': { - title: '🎧 Listening Streak: 7 Days', - amount: 1 - }, - 'track-upload': { - title: '🎶 Upload 3 Tracks', - amount: 1 - }, - referrals: { - title: '📨 Invite your Friends', - amount: 1 - }, - 'ref-v': { - title: '📨 Invite your Fans', - amount: 1 - }, - referred: { - title: '📨 Invite your Friends', - amount: 1 - }, - 'connect-verified': { - title: '✅️ Link Verified Accounts', - amount: 5 - }, - 'mobile-install': { - title: '📲 Get the App', - amount: 1 - }, - 'send-first-tip': { - title: '🤑 Send Your First Tip', - amount: 2 - }, - 'first-playlist': { - title: '✨ Create Your First Playlist', - amount: 2 - } -} - -const ChallengeReward = (props: ChallengeRewardProps) => { - const { notification } = props - const { challengeId } = notification - - const mainTextStyle = useTheme(styles.text, { - color: 'secondary' - }) - - const infoTextStyle = useTheme(styles.text, { - color: 'neutral' - }) - - const rewardText = - challengeId === 'referred' - ? `You’ve earned ${challengeInfoMap[challengeId].amount} $AUDIO for being referred! Invite your friends to join to earn more!` - : `You’ve earned ${challengeInfoMap[challengeId].amount} $AUDIO for completing this challenge!` - - return ( - - {challengeInfoMap[challengeId].title} - {rewardText} - - - ) -} - -export default ChallengeReward diff --git a/packages/mobile/src/screens/notifications-screen/content/Cosign.tsx b/packages/mobile/src/screens/notifications-screen/content/Cosign.tsx deleted file mode 100644 index 9fd940af45..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/Cosign.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { - Entity as EntityType, - RemixCosign -} from 'audius-client/src/common/store/notifications/types' -import { StyleSheet, Text, View } from 'react-native' - -import { useTheme } from 'app/utils/theme' - -import Entity from './Entity' -import TwitterShare from './TwitterShare' -import User from './User' -import UserImages from './UserImages' - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'center', - flexWrap: 'wrap', - marginBottom: 8 - }, - textWrapper: { - fontFamily: 'AvenirNextLTPro-Medium', - fontSize: 16, - marginLeft: 4 - } -}) - -type CosignProps = { - notification: RemixCosign -} - -const Cosign = ({ notification }: CosignProps) => { - const textWrapperStyle = useTheme(styles.textWrapper, { - color: 'neutral' - }) - if (!notification.user) return null - - const user = notification.user - const entity = notification.entities.find( - (track) => track?.owner_id === notification.parentTrackUserId - ) - - return ( - - - - - - {` Co-signed your Remix of `} - - - - - - ) -} - -export default Cosign diff --git a/packages/mobile/src/screens/notifications-screen/content/Entity.tsx b/packages/mobile/src/screens/notifications-screen/content/Entity.tsx deleted file mode 100644 index 1a08955345..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/Entity.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useCallback, useContext } from 'react' - -import { Entity as EntityType } from 'audius-client/src/common/store/notifications/types' -import { StyleSheet, Text } from 'react-native' -import { useDispatch } from 'react-redux' - -import { useNavigation } from 'app/hooks/useNavigation' -import { NotificationsDrawerNavigationContext } from 'app/screens/notifications-screen/NotificationsDrawerNavigationContext' -import { close } from 'app/store/notifications/actions' -import { useTheme } from 'app/utils/theme' - -import { getEntityRoute, getEntityScreen } from '../routeUtil' - -const getEntityName = (entity: any) => entity.title || entity.playlist_name - -const styles = StyleSheet.create({ - text: { - fontFamily: 'AvenirNextLTPro-Bold', - fontSize: 16 - } -}) - -type EntityProps = { - entity: any - entityType: EntityType -} - -const Entity = ({ entity, entityType }: EntityProps) => { - const dispatch = useDispatch() - const { drawerHelpers } = useContext(NotificationsDrawerNavigationContext) - const navigation = useNavigation({ customNativeNavigation: drawerHelpers }) - const onPress = useCallback(() => { - navigation.navigate({ - native: getEntityScreen(entity, entityType, { - fromNotifications: true - }), - web: { route: getEntityRoute(entity, entityType) } - }) - dispatch(close()) - }, [entity, entityType, navigation, dispatch]) - - const textStyle = useTheme(styles.text, { - color: 'secondary' - }) - - return ( - - {getEntityName(entity)} - - ) -} - -export default Entity diff --git a/packages/mobile/src/screens/notifications-screen/content/Favorite.tsx b/packages/mobile/src/screens/notifications-screen/content/Favorite.tsx deleted file mode 100644 index f27a345625..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/Favorite.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Favorite as FavoriteNotification } from 'audius-client/src/common/store/notifications/types' -import { StyleSheet, Text, View } from 'react-native' - -import { formatCount } from 'app/utils/format' -import { useTheme } from 'app/utils/theme' - -import Entity from './Entity' -import User from './User' -import UserImages from './UserImages' - -const styles = StyleSheet.create({ - textWrapper: { - fontFamily: 'AvenirNextLTPro-Medium', - fontSize: 16 - } -}) - -type FavoriteProps = { - notification: FavoriteNotification -} - -const Favorite = ({ notification }: FavoriteProps) => { - const textWrapperStyle = useTheme(styles.textWrapper, { - color: 'neutral' - }) - - const firstUser = notification?.users?.[0] - if (!firstUser) return null - - let otherUsers = '' - if (notification.userIds.length > 1) { - const usersLen = notification.userIds.length - 1 - otherUsers = ` and ${formatCount(usersLen)} other${usersLen > 1 ? 's' : ''}` - } - const entityType = notification.entityType - const entity = notification.entity - - return ( - - {notification.users ? ( - - ) : null} - - - {`${otherUsers} favorited your ${entityType.toLowerCase()} `} - - - - ) -} - -export default Favorite diff --git a/packages/mobile/src/screens/notifications-screen/content/Follow.tsx b/packages/mobile/src/screens/notifications-screen/content/Follow.tsx deleted file mode 100644 index 0bc93d41bc..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/Follow.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Follow as FollowNotification } from 'audius-client/src/common/store/notifications/types' -import { StyleSheet, Text, View } from 'react-native' - -import { formatCount } from 'app/utils/format' -import { useTheme } from 'app/utils/theme' - -import User from './User' -import UserImages from './UserImages' - -const styles = StyleSheet.create({ - textWrapper: { - fontFamily: 'AvenirNextLTPro-Medium', - fontSize: 16 - } -}) - -type FollowProps = { - notification: FollowNotification -} - -const Follow = ({ notification }: FollowProps) => { - const textWrapperStyle = useTheme(styles.textWrapper, { - color: 'neutral' - }) - - const firstUser = notification?.users?.[0] - if (!firstUser) return null - let otherUsers = '' - if (notification.userIds.length > 1) { - const usersLen = notification.userIds.length - 1 - otherUsers = ` and ${formatCount(usersLen)} other${usersLen > 1 ? 's' : ''}` - } - - return ( - - {notification.users ? ( - - ) : null} - - - {`${otherUsers} followed you`} - - - ) -} - -export default Follow diff --git a/packages/mobile/src/screens/notifications-screen/content/Milestone.tsx b/packages/mobile/src/screens/notifications-screen/content/Milestone.tsx deleted file mode 100644 index fe6351a7c6..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/Milestone.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { ReactNode } from 'react' - -import { - Achievement, - Milestone as MilestoneNotification -} from 'audius-client/src/common/store/notifications/types' -import { StyleSheet, Text, View } from 'react-native' - -import { formatCount } from 'app/utils/format' -import { useTheme } from 'app/utils/theme' - -import Entity from './Entity' -import TwitterShare from './TwitterShare' - -const styles = StyleSheet.create({ - textWrapper: { - fontFamily: 'AvenirNextLTPro-Medium', - fontSize: 16, - marginBottom: 8 - } -}) - -type MilestoneProps = { - notification: MilestoneNotification -} - -const Milestone = ({ notification }: MilestoneProps) => { - let body: ReactNode - if (notification.achievement === Achievement.Followers) { - body = ( - - {`You have reached over ${formatCount(notification.value ?? 0)} ${ - notification.achievement - }`} - - ) - } else { - const entity = notification.entity - const entityType = notification.entityType - const achievementText = - notification.achievement === Achievement.Listens - ? 'Plays' - : notification.achievement - body = ( - <> - {`Your ${notification.entityType.toLowerCase()} `} - - - {` has reached over ${formatCount( - notification.value ?? 0 - )} ${achievementText}`} - - - ) - } - - const textWrapperStyle = useTheme(styles.textWrapper, { - color: 'neutral' - }) - - return ( - - {body} - - - ) -} - -export default Milestone diff --git a/packages/mobile/src/screens/notifications-screen/content/NotificationContent.tsx b/packages/mobile/src/screens/notifications-screen/content/NotificationContent.tsx deleted file mode 100644 index 00d4a4422b..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/NotificationContent.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { - Notification, - NotificationType -} from 'audius-client/src/common/store/notifications/types' - -import AddTrackToPlaylist from './AddTrackToPlaylist' -import Announcement from './Announcement' -import ChallengeReward from './ChallengeReward' -import Cosign from './Cosign' -import Favorite from './Favorite' -import Follow from './Follow' -import Milestone from './Milestone' -import Remix from './Remix' -import Repost from './Repost' -import Subscription from './Subscription' -import TierChange from './TierChange' -import Trending from './Trending' - -type NotificationContentProps = { - notification: Notification -} - -const NotificationContent = ({ notification }: NotificationContentProps) => { - switch (notification.type) { - case NotificationType.Announcement: - return - case NotificationType.Follow: - return - case NotificationType.UserSubscription: - return - case NotificationType.Favorite: - return - case NotificationType.Repost: - return - case NotificationType.Milestone: - return - case NotificationType.RemixCosign: - return - case NotificationType.RemixCreate: - return - case NotificationType.TrendingTrack: - return - case NotificationType.ChallengeReward: - return - case NotificationType.TierChange: - return - case NotificationType.AddTrackToPlaylist: - return - default: - return null - } -} - -export default NotificationContent diff --git a/packages/mobile/src/screens/notifications-screen/content/Remix.tsx b/packages/mobile/src/screens/notifications-screen/content/Remix.tsx deleted file mode 100644 index 96b1035c3f..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/Remix.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Track } from 'audius-client/src/common/models/Track' -import { - Entity as EntityType, - RemixCreate -} from 'audius-client/src/common/store/notifications/types' -import { StyleSheet, Text, View } from 'react-native' - -import Entity from './Entity' -import TwitterShare from './TwitterShare' -import User from './User' -import UserImages from './UserImages' - -const styles = StyleSheet.create({ - titleText: { - fontFamily: 'AvenirNextLTPro-Medium', - fontSize: 16, - color: '#858199' - }, - body: { - flexDirection: 'row', - alignItems: 'center', - marginTop: 8, - marginBottom: 8, - flexWrap: 'wrap' - }, - bodyText: { - fontFamily: 'AvenirNextLTPro-Medium', - fontSize: 16, - color: '#858199', - marginLeft: 4 - } -}) - -type RemixProps = { - notification: RemixCreate -} - -const Remix = ({ notification }: RemixProps) => { - const user = notification.user - if (!user) return null - - const entity = notification.entities.find( - (track: Track) => track.track_id === notification.childTrackId - ) - const original = notification.entities.find( - (track: Track) => track.track_id === notification.parentTrackId - ) - - return ( - - - {`New remix of your track `} - - - - - - - - {` by `} - - - - - - ) -} - -export default Remix diff --git a/packages/mobile/src/screens/notifications-screen/content/Repost.tsx b/packages/mobile/src/screens/notifications-screen/content/Repost.tsx deleted file mode 100644 index f187b4c47e..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/Repost.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Repost as RepostNotification } from 'audius-client/src/common/store/notifications/types' -import { StyleSheet, Text, View } from 'react-native' - -import { formatCount } from 'app/utils/format' -import { useTheme } from 'app/utils/theme' - -import Entity from './Entity' -import User from './User' -import UserImages from './UserImages' - -const styles = StyleSheet.create({ - textWrapper: { - fontFamily: 'AvenirNextLTPro-Medium', - fontSize: 16 - } -}) - -type RepostProps = { - notification: RepostNotification -} - -const Repost = ({ notification }: RepostProps) => { - const textWrapperStyle = useTheme(styles.textWrapper, { - color: 'neutral' - }) - - const firstUser = notification?.users?.[0] - if (!firstUser) return null - - let otherUsers = '' - if (notification.userIds.length > 1) { - const usersLen = notification.userIds.length - 1 - otherUsers = ` and ${formatCount(usersLen)} other${usersLen > 1 ? 's' : ''}` - } - const entityType = notification.entityType - const entity = notification.entity - - return ( - - {notification.users ? ( - - ) : null} - - - {`${otherUsers} Reposted your ${entityType.toLowerCase()} `} - - - - ) -} - -export default Repost diff --git a/packages/mobile/src/screens/notifications-screen/content/Subscription.tsx b/packages/mobile/src/screens/notifications-screen/content/Subscription.tsx deleted file mode 100644 index 7cf628717a..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/Subscription.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { ReactNode } from 'react' - -import { UserSubscription } from 'audius-client/src/common/store/notifications/types' -import { StyleSheet, Text, View } from 'react-native' - -import { useTheme } from 'app/utils/theme' - -import Entity from './Entity' -import User from './User' -import UserImages from './UserImages' - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'center', - flexWrap: 'wrap' - }, - textWrapper: { - marginLeft: 4, - fontFamily: 'AvenirNextLTPro-Medium', - fontSize: 16 - } -}) - -type SubscriptionProps = { - notification: UserSubscription -} - -const Subscription = ({ notification }: SubscriptionProps) => { - const textWrapperStyle = useTheme(styles.textWrapper, { - color: 'neutral' - }) - - const user = notification.user - if (!user || !notification.entities) return null - - const isMultipleUploads = notification.entities.length > 1 - let body: ReactNode - if (isMultipleUploads) { - body = ( - - {` posted ${ - (notification as any).entities.length - } new ${notification.entityType.toLowerCase()}s `} - - ) - } else { - body = ( - <> - {` posted a new ${notification.entityType.toLowerCase()} `} - - - ) - } - - return ( - - - - - {body} - - - ) -} - -export default Subscription diff --git a/packages/mobile/src/screens/notifications-screen/content/TierChange.tsx b/packages/mobile/src/screens/notifications-screen/content/TierChange.tsx deleted file mode 100644 index 369a0c4877..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/TierChange.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { BadgeTier } from 'audius-client/src/common/models/BadgeTier' -import { TierChange as TierChangeNotification } from 'audius-client/src/common/store/notifications/types' -import { StyleSheet, Text, View } from 'react-native' - -import { useTheme } from 'app/utils/theme' - -import TwitterShare from './TwitterShare' - -const styles = StyleSheet.create({ - wrapper: { - display: 'flex', - flexDirection: 'column' - }, - text: { - fontFamily: 'AvenirNextLTPro-Bold', - fontSize: 16, - marginBottom: 8 - } -}) - -export const tierInfoMap: Record = - { - none: { - amount: 0, - label: 'None' - }, - bronze: { - amount: 10, - label: 'Bronze' - }, - silver: { - amount: 100, - label: 'Silver' - }, - gold: { - amount: 10000, - label: 'Gold' - }, - platinum: { - amount: 100000, - label: 'Platinum' - } - } - -type TierChangeProps = { - notification: TierChangeNotification -} - -const TierChange = ({ notification }: TierChangeProps) => { - const { tier } = notification - - const textStyle = useTheme(styles.text, { - color: 'neutral' - }) - - const notifText = `Congrats, you’ve reached ${tierInfoMap[tier].label} Tier by having over ${tierInfoMap[tier].amount} $AUDIO! You now have access to exclusive features & a shiny new badge by your name.` - - return ( - - {notifText} - - - ) -} - -export default TierChange diff --git a/packages/mobile/src/screens/notifications-screen/content/Trending.tsx b/packages/mobile/src/screens/notifications-screen/content/Trending.tsx deleted file mode 100644 index d49290a163..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/Trending.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { TrendingTrack } from 'audius-client/src/common/store/notifications/types' -import { StyleSheet, Text, View } from 'react-native' - -import { useTheme } from 'app/utils/theme' - -import Entity from './Entity' -import TwitterShare from './TwitterShare' - -export const getRankSuffix = (rank: number) => { - if (rank === 1) return 'st' - if (rank === 2) return 'nd' - if (rank === 3) return 'rd' - return 'th' -} - -const styles = StyleSheet.create({ - textWrapper: { - fontFamily: 'AvenirNextLTPro-Medium', - fontSize: 16, - marginBottom: 8 - } -}) - -type TrendingProps = { - notification: TrendingTrack -} - -const Trending = ({ notification }: TrendingProps) => { - const entityType = notification.entityType - const { rank, entity } = notification - const rankSuffix = getRankSuffix(rank) - - const textWrapperStyle = useTheme(styles.textWrapper, { - color: 'neutral' - }) - - return ( - - - {`Your track `} - - {` is ${rank}${rankSuffix} on Trending Right Now! `} - - - - ) -} - -export default Trending diff --git a/packages/mobile/src/screens/notifications-screen/content/TwitterShare.tsx b/packages/mobile/src/screens/notifications-screen/content/TwitterShare.tsx deleted file mode 100644 index 6d3ff63c6e..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/TwitterShare.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import { useCallback } from 'react' - -import { BadgeTier } from 'audius-client/src/common/models/BadgeTier' -import { User } from 'audius-client/src/common/models/User' -import { - Achievement, - Notification, - Entity, - NotificationType, - TrendingTrack, - RemixCreate, - RemixCosign, - ChallengeReward, - TierChange, - AddTrackToPlaylist -} from 'audius-client/src/common/store/notifications/types' -import { Linking, StyleSheet, Text, TouchableOpacity, View } from 'react-native' - -import IconTwitterBird from 'app/assets/images/iconTwitterBird.svg' -import { getUserRoute } from 'app/utils/routes' -import { getTwitterLink } from 'app/utils/twitter' - -import { getEntityRoute } from '../routeUtil' - -import { getRankSuffix } from './Trending' - -export const formatAchievementText = ( - type: string, - name: string, - value: number, - achievement: string, - link: string -) => { - const achievementText = - achievement === Achievement.Listens ? 'Plays' : achievement - return `My ${type} ${name} has more than ${value} ${achievementText} on @AudiusProject #Audius -Check it out!` -} - -const getAchievementText = (notification: any) => { - switch (notification.achievement) { - case Achievement.Followers: { - const link = getUserRoute(notification.user, true) - const text = `I just hit over ${notification.value} followers on @AudiusProject #Audius!` - return { text, link } - } - case Achievement.Favorites: - case Achievement.Listens: - case Achievement.Reposts: { - const link = getEntityRoute( - notification.entity, - notification.entityType, - true - ) - const text = formatAchievementText( - notification.entityType, - notification.entity.title || notification.entity.playlist_name, - notification.value, - notification.achievement, - link - ) - return { text, link } - } - default: { - return { text: '', link: '' } - } - } -} - -const getTrendingTrackText = (notification: TrendingTrack) => { - const link = getEntityRoute( - notification.entity, - notification.entityType, - true - ) - const text = `My track ${notification.entity.title} is trending ${ - notification.rank - }${getRankSuffix( - notification.rank - )} on @AudiusProject! #AudiusTrending #Audius` - return { link, text } -} - -const getRemixCreateText = async (notification: RemixCreate) => { - const track = notification.entities.find( - (t) => t?.track_id === notification.parentTrackId - ) - if (!track) return - const link = getEntityRoute(track, Entity.Track, true) - - let twitterHandle = notification.user.twitter_handle - if (!twitterHandle) twitterHandle = notification.user.name - else twitterHandle = `@${twitterHandle}` - - return { - text: `New remix of ${track.title} by ${twitterHandle} on @AudiusProject #Audius`, - link - } -} - -const getRemixCosignText = async (notification: RemixCosign) => { - const parentTrack = notification.entities.find( - (t) => t?.owner_id === notification.parentTrackUserId - ) - const childtrack = notification.entities.find( - (t) => t?.track_id === notification.childTrackId - ) - - if (!parentTrack || !childtrack) return { text: '', link: '' } - - let twitterHandle = notification.user.twitter_handle - if (!twitterHandle) twitterHandle = notification.user.name - else twitterHandle = `@${twitterHandle}` - - const link = getEntityRoute(childtrack, Entity.Track) - - return { - text: `My remix of ${parentTrack.title} was Co-Signed by ${twitterHandle} on @AudiusProject #Audius`, - link - } -} - -export const getRewardsText = (notification: ChallengeReward) => ({ - text: `I earned $AUDIO for completing challenges on @AudiusProject #AudioRewards`, - link: null -}) - -const tierInfoMap: Record = { - none: { label: 'None', icon: '' }, - bronze: { label: 'Bronze', icon: '🥉' }, - silver: { label: 'Silver', icon: '🥈' }, - gold: { label: 'Gold', icon: '🥇' }, - platinum: { label: 'Platinum', icon: '🥇' } -} - -export const getTierChangeText = (notif: TierChange & { user: User }) => { - const { label, icon } = tierInfoMap[notif.tier] - return { - link: getUserRoute(notif.user, true), - text: `I’ve reached ${label} Tier on @AudiusProject! Check out the shiny new badge next to my name ${icon}` - } -} - -export const getAddTrackToPlaylistText = (notif: AddTrackToPlaylist) => { - const { track, playlist } = notif.entities - const playlistOwner = playlist.user - - const entityLink = getEntityRoute(playlist, Entity.Playlist, true) - const shareText = playlistOwner - ? `Listen to my track ${track.title} on ${playlist.playlist_name} by ${playlistOwner.handle} on @AudiusProject #Audius` - : '' - - return { link: entityLink, text: shareText } -} - -export const getNotificationTwitterText = async (notification: any) => { - if (notification.type === NotificationType.Milestone) { - return getAchievementText(notification) - } else if (notification.type === NotificationType.TrendingTrack) { - return getTrendingTrackText(notification) - } else if (notification.type === NotificationType.RemixCreate) { - return getRemixCreateText(notification) - } else if (notification.type === NotificationType.RemixCosign) { - return getRemixCosignText(notification) - } else if (notification.type === NotificationType.ChallengeReward) { - return getRewardsText(notification) - } else if (notification.type === NotificationType.TierChange) { - return getTierChangeText(notification) - } else if (notification.type === NotificationType.AddTrackToPlaylist) { - return getAddTrackToPlaylistText(notification) - } -} - -export const getTwitterButtonText = (notification: any) => { - switch (notification.type) { - case NotificationType.TrendingTrack: - case NotificationType.Milestone: - return 'Share this Milestone' - case NotificationType.RemixCreate: - case NotificationType.RemixCosign: - case NotificationType.ChallengeReward: - return 'Share With Your Fans' - case NotificationType.TierChange: - return 'Share to Twitter' - default: - return '' - } -} - -const styles = StyleSheet.create({ - button: { - flexDirection: 'row', - alignSelf: 'flex-start', - alignItems: 'center', - justifyContent: 'flex-start', - backgroundColor: '#1BA1F1', - paddingTop: 8, - paddingBottom: 8, - paddingLeft: 16, - paddingRight: 16, - borderRadius: 4 - }, - text: { - fontFamily: 'AvenirNextLTPro-Bold', - fontSize: 16, - color: '#FFFFFF', - marginLeft: 4 - } -}) - -type TwitterShareProps = { - notification: Notification -} - -const TwitterShare = ({ notification }: TwitterShareProps) => { - const buttonText = getTwitterButtonText(notification) - const onPress = useCallback(async () => { - const twitterText = await getNotificationTwitterText(notification) - if (!twitterText) return - const url = getTwitterLink(twitterText.link, twitterText.text) - Linking.canOpenURL(url).then((supported) => { - if (supported) { - Linking.openURL(url) - } else { - console.error(`Can't open: ${url}`) - } - }) - }, [notification]) - - return ( - - - - {buttonText} - - - ) -} - -export default TwitterShare diff --git a/packages/mobile/src/screens/notifications-screen/content/User.tsx b/packages/mobile/src/screens/notifications-screen/content/User.tsx deleted file mode 100644 index 85d2fa79f9..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/User.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useCallback, useContext } from 'react' - -import { User as UserType } from 'audius-client/src/common/models/User' -import { NOTIFICATION_PAGE } from 'audius-client/src/utils/route' -import { StyleSheet, Text } from 'react-native' -import { useDispatch } from 'react-redux' - -import { useNavigation } from 'app/hooks/useNavigation' -import { NotificationsDrawerNavigationContext } from 'app/screens/notifications-screen/NotificationsDrawerNavigationContext' -import { close } from 'app/store/notifications/actions' -import { getUserRoute } from 'app/utils/routes' -import { useTheme } from 'app/utils/theme' - -const styles = StyleSheet.create({ - text: { - fontFamily: 'AvenirNextLTPro-Bold', - fontSize: 16 - } -}) - -type UserProps = { - user: UserType -} - -const User = ({ user }: UserProps) => { - const dispatch = useDispatch() - const { drawerHelpers } = useContext(NotificationsDrawerNavigationContext) - const navigation = useNavigation({ customNativeNavigation: drawerHelpers }) - - const onPress = useCallback(() => { - navigation.navigate({ - native: { - screen: 'Profile', - params: { handle: user.handle, fromNotifications: true } - }, - web: { route: getUserRoute(user), fromPage: NOTIFICATION_PAGE } - }) - dispatch(close()) - }, [user, navigation, dispatch]) - - const textStyle = useTheme(styles.text, { - color: 'secondary' - }) - - return ( - - {user.name} - - ) -} - -export default User diff --git a/packages/mobile/src/screens/notifications-screen/content/UserImages.tsx b/packages/mobile/src/screens/notifications-screen/content/UserImages.tsx deleted file mode 100644 index 1410ae6147..0000000000 --- a/packages/mobile/src/screens/notifications-screen/content/UserImages.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useCallback, useContext } from 'react' - -import { SquareSizes } from 'audius-client/src/common/models/ImageSizes' -import { User } from 'audius-client/src/common/models/User' -import { Notification } from 'audius-client/src/common/store/notifications/types' -import { setNotificationId } from 'audius-client/src/common/store/user-list/notifications/actions' -import { NOTIFICATION_PAGE } from 'audius-client/src/utils/route' -import { StyleSheet, TouchableOpacity, View } from 'react-native' -import { useDispatch } from 'react-redux' - -import { DynamicImage } from 'app/components/core' -import { useDispatchWeb } from 'app/hooks/useDispatchWeb' -import { useNavigation } from 'app/hooks/useNavigation' -import { useUserProfilePicture } from 'app/hooks/useUserProfilePicture' -import { NotificationsDrawerNavigationContext } from 'app/screens/notifications-screen/NotificationsDrawerNavigationContext' -import { close } from 'app/store/notifications/actions' -import { getUserRoute } from 'app/utils/routes' -import { useTheme } from 'app/utils/theme' - -import { getUserListRoute } from '../routeUtil' - -const styles = StyleSheet.create({ - touchable: { - alignSelf: 'flex-start' - }, - container: { - marginBottom: 8, - flexDirection: 'row' - }, - image: { - height: 32, - width: 32, - borderRadius: 16, - marginRight: 4, - overflow: 'hidden' - } -}) - -type UserImagesProps = { - notification: Notification - users: User[] -} - -const UserImage = ({ - user, - allowPress = true -}: { - user: User - allowPress?: boolean -}) => { - const dispatch = useDispatch() - const { drawerHelpers } = useContext(NotificationsDrawerNavigationContext) - const navigation = useNavigation({ customNativeNavigation: drawerHelpers }) - - const handlePress = useCallback(() => { - navigation.navigate({ - native: { - screen: 'Profile', - params: { handle: user.handle, fromNotifications: true } - }, - web: { route: getUserRoute(user), fromPage: NOTIFICATION_PAGE } - }) - dispatch(close()) - }, [navigation, user, dispatch]) - - const profilePicture = useUserProfilePicture({ - id: user?.user_id, - sizes: user?._profile_picture_sizes, - size: SquareSizes.SIZE_150_BY_150 - }) - - const imageStyle = useTheme(styles.image, { - backgroundColor: 'neutralLight4' - }) - return ( - - - - ) -} - -const UserImages = ({ notification, users }: UserImagesProps) => { - const dispatch = useDispatch() - const navigation = useNavigation() - const dispatchWeb = useDispatchWeb() - - const isMultiUser = users.length > 1 - - const handlePress = useCallback(() => { - dispatchWeb(setNotificationId(notification.id)) - navigation.navigate({ - native: { - screen: 'NotificationUsers', - params: { - notificationType: notification.type, - count: users.length, - id: notification.id - } - }, - web: { - route: getUserListRoute(notification), - fromPage: NOTIFICATION_PAGE - } - }) - dispatch(close()) - }, [navigation, notification, dispatch, dispatchWeb, users.length]) - - const renderUsers = () => ( - - {users.map((user) => { - return ( - - ) - })} - - ) - - return isMultiUser ? ( - - {renderUsers()} - - ) : ( - renderUsers() - ) -} - -export default UserImages diff --git a/packages/mobile/src/screens/notifications-screen/routeUtil.ts b/packages/mobile/src/screens/notifications-screen/routeUtil.ts deleted file mode 100644 index 3db5805841..0000000000 --- a/packages/mobile/src/screens/notifications-screen/routeUtil.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { Track } from 'audius-client/src/common/models/Track' -import { - Achievement, - Notification, - Entity, - NotificationType -} from 'audius-client/src/common/store/notifications/types' -import Config from 'react-native-config' - -import { ContextualParams } from 'app/hooks/useNavigation' -import { - getTrackRoute, - getUserRoute, - getCollectionRoute, - getAudioPageRoute -} from 'app/utils/routes' - -const AUDIUS_URL = Config.AUDIUS_URL - -export const getUserListRoute = ( - notification: Notification, - fullUrl = false -) => { - const route = `/notification/${notification.id}/users` - return fullUrl ? `${AUDIUS_URL}${route}` : route -} - -export const getEntityRoute = ( - entity: any, - entityType: Entity, - fullUrl = false -) => { - switch (entityType) { - case Entity.Track: - return getTrackRoute(entity, fullUrl) - case Entity.User: - return getUserRoute(entity, fullUrl) - case Entity.Album: - case Entity.Playlist: - return getCollectionRoute(entity, fullUrl) - } -} - -export const getEntityScreen = ( - entity: any, - entityType: Entity, - contextualParams: ContextualParams -) => { - switch (entityType) { - case Entity.Track: - return { - screen: 'Track' as const, - params: { id: entity?.track_id, ...contextualParams } - } - case Entity.User: - return { - screen: 'Profile' as const, - params: { handle: entity?.handle, ...contextualParams } - } - case Entity.Album: - case Entity.Playlist: - return { - screen: 'Collection' as const, - params: { id: entity?.playlist_id, ...contextualParams } - } - } -} - -export const getNotificationRoute = (notification: Notification) => { - switch (notification.type) { - case NotificationType.Announcement: - return null - case NotificationType.Follow: { - const users = notification.users - const isMultiUser = !!users && users.length > 1 - if (isMultiUser) { - return getUserListRoute(notification) - } - const firstUser = notification?.users?.[0] - if (!firstUser) return null - return getUserRoute(firstUser) - } - case NotificationType.UserSubscription: - if (!notification?.entities?.[0]) return null - return getEntityRoute(notification.entities[0], notification.entityType) - case NotificationType.Favorite: - return getEntityRoute(notification.entity, notification.entityType) - case NotificationType.Repost: - return getEntityRoute(notification.entity, notification.entityType) - case NotificationType.Milestone: - if (notification.achievement === Achievement.Followers) { - if (!notification.user) return '' - return getUserRoute(notification.user) - } - return getEntityRoute(notification.entity, notification.entityType) - case NotificationType.RemixCosign: { - const original = notification.entities.find( - (track: Track) => track.owner_id === notification.parentTrackUserId - ) - return getEntityRoute(original, Entity.Track) - } - case NotificationType.RemixCreate: { - const remix = notification.entities.find( - (track: Track) => track.track_id === notification.childTrackId - ) - return getEntityRoute(remix, Entity.Track) - } - case NotificationType.TrendingTrack: - return getEntityRoute(notification.entity, notification.entityType) - case NotificationType.ChallengeReward: - case NotificationType.TierChange: - return getAudioPageRoute() - case NotificationType.AddTrackToPlaylist: - return getEntityRoute(notification.entities.playlist, Entity.Playlist) - } -} - -export const getNotificationScreen = ( - notification: Notification, - contextualParams: ContextualParams -) => { - switch (notification.type) { - case NotificationType.Announcement: - return null - case NotificationType.Follow: { - const { userIds, users } = notification - const isMultiUser = !!users && users.length > 1 - if (isMultiUser) { - return { - screen: 'NotificationUsers' as const, - params: { - ...contextualParams, - notificationType: notification.type, - count: userIds.length, - id: notification.id - } - } - } - const firstUser = notification?.users?.[0] - if (!firstUser) return null - return { - screen: 'Profile' as const, - params: { - ...contextualParams, - handle: firstUser.handle - } - } - } - case NotificationType.UserSubscription: - if (!notification?.entities?.[0]) return null - return getEntityScreen( - notification.entities[0], - notification.entityType, - contextualParams - ) - case NotificationType.Favorite: - return getEntityScreen( - notification.entity, - notification.entityType, - contextualParams - ) - case NotificationType.Repost: - return getEntityScreen( - notification.entity, - notification.entityType, - contextualParams - ) - case NotificationType.Milestone: - if (notification.achievement === Achievement.Followers) { - if (!notification.user) return '' - const { handle } = notification.user - return { - screen: 'Profile' as const, - params: { ...contextualParams, handle } - } - } - return getEntityScreen( - notification.entity, - notification.entityType, - contextualParams - ) - case NotificationType.RemixCosign: { - const original = notification.entities.find( - (track: Track) => track.owner_id === notification.parentTrackUserId - ) - return getEntityScreen(original, Entity.Track, contextualParams) - } - case NotificationType.RemixCreate: { - const remix = notification.entities.find( - (track: Track) => track.track_id === notification.childTrackId - ) - return getEntityScreen(remix, Entity.Track, contextualParams) - } - case NotificationType.TrendingTrack: - return getEntityScreen( - notification.entity, - notification.entityType, - contextualParams - ) - case NotificationType.ChallengeReward: - case NotificationType.TierChange: - return { - screen: 'AudioScreen' as const, - params: contextualParams - } - case NotificationType.AddTrackToPlaylist: - return getEntityScreen( - notification.entities.playlist, - Entity.Playlist, - contextualParams - ) - } -} diff --git a/packages/web/src/common/store/notifications/selectors.ts b/packages/web/src/common/store/notifications/selectors.ts index f84dbee5a1..d9e2f97cf8 100644 --- a/packages/web/src/common/store/notifications/selectors.ts +++ b/packages/web/src/common/store/notifications/selectors.ts @@ -10,6 +10,7 @@ import { } from 'common/store/cache/collections/selectors' import { getTrack, getTracks } from 'common/store/cache/tracks/selectors' import { getUser, getUsers } from 'common/store/cache/users/selectors' +import { Nullable } from 'common/utils/typeUtils' import { Entity, @@ -17,7 +18,10 @@ import { NotificationType, Achievement, Announcement, - EntityType + EntityType, + AddTrackToPlaylist, + CollectionEntity, + TrackEntity } from './types' const getBaseState = (state: CommonState) => state.pages.notifications @@ -135,10 +139,28 @@ export const getNotificationEntity = ( return null } -export const getNotificationEntities = ( +type EntityTypes = + T extends AddTrackToPlaylist + ? { track: TrackEntity; playlist: CollectionEntity } + : Nullable + +export const getNotificationEntities = < + T extends AddTrackToPlaylist | Notification +>( state: CommonState, - notification: Notification -) => { + notification: T +): EntityTypes => { + if (notification.type === NotificationType.AddTrackToPlaylist) { + const track = getTrack(state, { id: notification.trackId }) + const currentUser = getAccountUser(state) + const playlist = getCollection(state, { id: notification.playlistId }) + const playlistOwner = getUser(state, { id: notification.playlistOwnerId }) + return { + track: { ...track, user: currentUser }, + playlist: { ...playlist, user: playlistOwner } + } as EntityTypes + } + if ('entityIds' in notification && 'entityType' in notification) { const getEntities = notification.entityType === Entity.Track ? getTracks : getCollections @@ -157,12 +179,7 @@ export const getNotificationEntities = ( return null }) .filter((entity): entity is EntityType => !!entity) - return entities - } else if (notification.type === NotificationType.AddTrackToPlaylist) { - const track = getTrack(state, { id: notification.trackId }) - const playlist = getCollection(state, { id: notification.playlistId }) - const playlistOwner = getUser(state, { id: notification.playlistOwnerId }) - return { track, playlist: { ...playlist, user: playlistOwner } } + return entities as EntityTypes } - return null + return null as EntityTypes } diff --git a/packages/web/src/common/store/notifications/types.ts b/packages/web/src/common/store/notifications/types.ts index 3c096d0a43..7b809f3519 100644 --- a/packages/web/src/common/store/notifications/types.ts +++ b/packages/web/src/common/store/notifications/types.ts @@ -60,55 +60,33 @@ export type UserSubscription = BaseNotification & { type: NotificationType.UserSubscription userId: ID entityIds: ID[] - user: User } & ( | { entityType: Entity.Track - entities: Array } | { entityType: Entity.Playlist | Entity.Album - entities: Array } ) export type Follow = BaseNotification & { type: NotificationType.Follow userIds: ID[] - users: User[] } export type Repost = BaseNotification & { type: NotificationType.Repost entityId: ID userIds: ID[] - users: User[] -} & ( - | { - entityType: Entity.Playlist | Entity.Album - entity: CollectionEntity - } - | { - entityType: Entity.Track - entity: TrackEntity - } - ) + entityType: Entity.Playlist | Entity.Album | Entity.Track +} export type Favorite = BaseNotification & { type: NotificationType.Favorite entityId: ID userIds: ID[] - users: User[] -} & ( - | { - entityType: Entity.Playlist | Entity.Album - entity: CollectionEntity - } - | { - entityType: Entity.Track - entity: TrackEntity - } - ) + entityType: Entity.Playlist | Entity.Album | Entity.Track +} export enum Achievement { Listens = 'Listens', @@ -118,12 +96,12 @@ export enum Achievement { Followers = 'Followers' } -export type Milestone = BaseNotification & { user: User } & ( +export type Milestone = BaseNotification & + ( | { type: NotificationType.Milestone entityType: Entity entityId: ID - entity: EntityType achievement: Exclude value: number } @@ -142,8 +120,6 @@ export type RemixCreate = BaseNotification & { childTrackId: ID entityType: Entity.Track entityIds: ID[] - user: User - entities: Array } export type RemixCosign = BaseNotification & { @@ -153,8 +129,6 @@ export type RemixCosign = BaseNotification & { childTrackId: ID entityType: Entity.Track entityIds: ID[] - user: User - entities: Array } export type TrendingTrack = BaseNotification & { @@ -164,7 +138,6 @@ export type TrendingTrack = BaseNotification & { time: 'week' | 'month' | 'year' entityType: Entity.Track entityId: ID - entity: TrackEntity } export type ChallengeReward = BaseNotification & { @@ -176,7 +149,6 @@ export type TierChange = BaseNotification & { type: NotificationType.TierChange userId: ID tier: BadgeTier - user: User } // TODO: when we support multiple reaction types, reactedToEntity type @@ -192,7 +164,6 @@ export type Reaction = BaseNotification & { amount: StringWei tip_sender_id: ID } - user: User } export type TipReceive = BaseNotification & { @@ -202,7 +173,6 @@ export type TipReceive = BaseNotification & { entityId: ID entityType: Entity.User tipTxSignature: string - user: User } export type TipSend = BaseNotification & { @@ -210,7 +180,6 @@ export type TipSend = BaseNotification & { amount: StringWei entityId: ID entityType: Entity.User - user: User } export type SupporterRankUp = BaseNotification & { @@ -218,7 +187,6 @@ export type SupporterRankUp = BaseNotification & { rank: number entityId: ID entityType: Entity.User - user: User } export type SupportingRankUp = BaseNotification & { @@ -226,7 +194,6 @@ export type SupportingRankUp = BaseNotification & { rank: number entityId: ID entityType: Entity.User - user: User } export type AddTrackToPlaylist = BaseNotification & { @@ -234,10 +201,6 @@ export type AddTrackToPlaylist = BaseNotification & { trackId: ID playlistId: ID playlistOwnerId: ID - entities: { - playlist: CollectionEntity - track: TrackEntity - } } export type Notification = diff --git a/packages/web/src/components/notification/Notification/AddTrackToPlaylistNotification.tsx b/packages/web/src/components/notification/Notification/AddTrackToPlaylistNotification.tsx index 0bd11c4ca5..7ad99dd9d7 100644 --- a/packages/web/src/components/notification/Notification/AddTrackToPlaylistNotification.tsx +++ b/packages/web/src/components/notification/Notification/AddTrackToPlaylistNotification.tsx @@ -1,16 +1,18 @@ -import React, { useCallback } from 'react' +import { useCallback } from 'react' import { push } from 'connected-react-router' import { useDispatch } from 'react-redux' import { Name } from 'common/models/Analytics' +import { Track } from 'common/models/Track' +import { getNotificationEntities } from 'common/store/notifications/selectors' import { AddTrackToPlaylist, CollectionEntity, - Entity, - TrackEntity + Entity } from 'common/store/notifications/types' import { make } from 'store/analytics/actions' +import { useSelector } from 'utils/reducer' import styles from './TipSentNotification.module.css' import { EntityLink } from './components/EntityLink' @@ -29,7 +31,7 @@ const messages = { title: 'Track Added to Playlist', shareTwitterText: ( handle: string, - track: TrackEntity, + track: Track, playlist: CollectionEntity ) => `My track ${track.title} was added to the playlist ${playlist.playlist_name} by ${handle} on @audiusproject! #Audius` @@ -43,8 +45,10 @@ export const AddTrackToPlaylistNotification = ( props: AddTrackToPlaylistNotificationProps ) => { const { notification } = props - const { entities, timeLabel, isViewed } = notification - const { track, playlist } = entities + const { timeLabel, isViewed } = notification + const { track, playlist } = useSelector((state) => + getNotificationEntities(state, notification) + ) const playlistOwner = playlist.user const dispatch = useDispatch() @@ -72,7 +76,7 @@ export const AddTrackToPlaylistNotification = ( dispatch(push(getEntityLink(playlist))) }, [playlist, dispatch]) - if (!playlistOwner) return null + if (!playlistOwner || !track) return null return ( diff --git a/packages/web/src/components/notification/Notification/FavoriteNotification.tsx b/packages/web/src/components/notification/Notification/FavoriteNotification.tsx index 1a1002e0fb..af6348e9d3 100644 --- a/packages/web/src/components/notification/Notification/FavoriteNotification.tsx +++ b/packages/web/src/components/notification/Notification/FavoriteNotification.tsx @@ -3,6 +3,10 @@ import { MouseEventHandler, useCallback } from 'react' import { push } from 'connected-react-router' import { useDispatch } from 'react-redux' +import { + getNotificationEntity, + getNotificationUsers +} from 'common/store/notifications/selectors' import { Favorite } from 'common/store/notifications/types' import { setUsers as setUserListUsers, @@ -10,6 +14,7 @@ import { } from 'store/application/ui/userListModal/slice' import { UserListType } from 'store/application/ui/userListModal/types' import { isMobile } from 'utils/clientUtil' +import { useSelector } from 'utils/reducer' import { EntityLink, useGoToEntity } from './components/EntityLink' import { NotificationBody } from './components/NotificationBody' @@ -20,7 +25,7 @@ import { OthersLink } from './components/OthersLink' import { UserNameLink } from './components/UserNameLink' import { UserProfilePictureList } from './components/UserProfilePictureList' import { IconFavorite } from './components/icons' -import { entityToUserListEntity } from './utils' +import { entityToUserListEntity, USER_LENGTH_LIMIT } from './utils' const messages = { favorited: ' favorited your ' @@ -31,11 +36,18 @@ type FavoriteNotificationProps = { } export const FavoriteNotification = (props: FavoriteNotificationProps) => { const { notification } = props - const { id, users, userIds, entity, entityType, timeLabel, isViewed } = - notification - const [firstUser] = users + const { id, userIds, entityType, timeLabel, isViewed } = notification + const users = useSelector((state) => + getNotificationUsers(state, notification, USER_LENGTH_LIMIT) + ) + const firstUser = users?.[0] const otherUsersCount = userIds.length - 1 const isMultiUser = userIds.length > 1 + + const entity = useSelector((state) => + getNotificationEntity(state, notification) + ) + const dispatch = useDispatch() const handleGoToEntity = useGoToEntity(entity, entityType) @@ -62,6 +74,8 @@ export const FavoriteNotification = (props: FavoriteNotificationProps) => { [isMultiUser, dispatch, entityType, id, handleGoToEntity] ) + if (!users || !firstUser || !entity) return null + return ( }> diff --git a/packages/web/src/components/notification/Notification/FollowNotification.tsx b/packages/web/src/components/notification/Notification/FollowNotification.tsx index 9168ca9b0d..ca3139ec93 100644 --- a/packages/web/src/components/notification/Notification/FollowNotification.tsx +++ b/packages/web/src/components/notification/Notification/FollowNotification.tsx @@ -3,6 +3,7 @@ import { useCallback } from 'react' import { push } from 'connected-react-router' import { useDispatch } from 'react-redux' +import { getNotificationUsers } from 'common/store/notifications/selectors' import { Follow } from 'common/store/notifications/types' import { setUsers as setUserListUsers, @@ -13,6 +14,7 @@ import { UserListType } from 'store/application/ui/userListModal/types' import { isMobile } from 'utils/clientUtil' +import { useSelector } from 'utils/reducer' import { profilePage } from 'utils/route' import { NotificationBody } from './components/NotificationBody' @@ -23,6 +25,7 @@ import { OthersLink } from './components/OthersLink' import { UserNameLink } from './components/UserNameLink' import { UserProfilePictureList } from './components/UserProfilePictureList' import { IconFollow } from './components/icons' +import { USER_LENGTH_LIMIT } from './utils' const messages = { and: 'and', @@ -35,8 +38,11 @@ type FollowNotificationProps = { export const FollowNotification = (props: FollowNotificationProps) => { const { notification } = props - const { id, users, userIds, timeLabel, isViewed } = notification - const [firstUser] = users + const { id, userIds, timeLabel, isViewed } = notification + const users = useSelector((state) => + getNotificationUsers(state, notification, USER_LENGTH_LIMIT) + ) + const firstUser = users?.[0] const otherUsersCount = userIds.length - 1 const isMultiUser = userIds.length > 1 const dispatch = useDispatch() @@ -56,9 +62,13 @@ export const FollowNotification = (props: FollowNotificationProps) => { dispatch(openUserListModal(true)) } } else { - dispatch(push(profilePage(firstUser.handle))) + if (firstUser) { + dispatch(push(profilePage(firstUser.handle))) + } } - }, [isMultiUser, dispatch, id, firstUser.handle]) + }, [isMultiUser, dispatch, id, firstUser]) + + if (!users || !firstUser) return null return ( diff --git a/packages/web/src/components/notification/Notification/MilestoneNotification.tsx b/packages/web/src/components/notification/Notification/MilestoneNotification.tsx index 92dedb8acd..9b1a0edfbd 100644 --- a/packages/web/src/components/notification/Notification/MilestoneNotification.tsx +++ b/packages/web/src/components/notification/Notification/MilestoneNotification.tsx @@ -4,9 +4,20 @@ import { push } from 'connected-react-router' import { useDispatch } from 'react-redux' import { Name } from 'common/models/Analytics' -import { Achievement, Milestone } from 'common/store/notifications/types' +import { User } from 'common/models/User' +import { + getNotificationEntity, + getNotificationUser +} from 'common/store/notifications/selectors' +import { + Achievement, + EntityType, + Milestone +} from 'common/store/notifications/types' import { formatCount } from 'common/utils/formatUtil' +import { Nullable } from 'common/utils/typeUtils' import { make } from 'store/analytics/actions' +import { useSelector } from 'utils/reducer' import { fullProfilePage, profilePage } from 'utils/route' import { EntityLink } from './components/EntityLink' @@ -39,26 +50,36 @@ Check it out!` } } -const getAchievementText = (notification: Milestone) => { - const { achievement, user, value } = notification +const getAchievementText = ( + notification: Milestone, + entity?: Nullable, + user?: Nullable +) => { + const { achievement, value } = notification switch (achievement) { case Achievement.Followers: { - const link = fullProfilePage(user.handle) - const text = messages.followerAchievementText(value) - return { text, link } + if (user) { + const link = fullProfilePage(user.handle) + const text = messages.followerAchievementText(value) + return { text, link } + } + return { text: '', link: '' } } case Achievement.Favorites: case Achievement.Listens: case Achievement.Reposts: { - const { entity, entityType } = notification - const link = getEntityLink(entity, true) - const text = messages.achievementText( - entityType, - 'title' in entity ? entity.title : entity.playlist_name, - value, - achievement - ) - return { text, link } + if (entity) { + const { entityType } = notification + const link = getEntityLink(entity, true) + const text = messages.achievementText( + entityType, + 'title' in entity ? entity.title : entity.playlist_name, + value, + achievement + ) + return { text, link } + } + return { text: '', link: '' } } default: { return { text: '', link: '' } @@ -72,39 +93,51 @@ type MilestoneNotificationProps = { export const MilestoneNotification = (props: MilestoneNotificationProps) => { const { notification } = props - const { timeLabel, isViewed, user } = notification + const { timeLabel, isViewed, achievement } = notification + const entity = useSelector((state) => + getNotificationEntity(state, notification) + ) + const user = useSelector((state) => getNotificationUser(state, notification)) const dispatch = useDispatch() const renderBody = () => { - if (notification.achievement === Achievement.Followers) { - const { achievement, value } = notification + const { achievement, value } = notification + if (achievement === Achievement.Followers) { return `${messages.follows} ${formatCount(value)} ${achievement}` - } else { - const { entity, entityType, achievement, value, user } = notification - const entityWithUser = { ...entity, user } + } else if (entity) { + const { entityType } = notification const achievementText = achievement === Achievement.Listens ? 'plays' : achievement return ( {messages.your} {entityType}{' '} - {' '} + {' '} {messages.reached} {formatCount(value)} {achievementText} ) } } - const { link, text } = getAchievementText(notification) - const handleClick = useCallback(() => { if (notification.achievement === Achievement.Followers) { - dispatch(push(profilePage(user.handle))) - } else { - const { entity } = notification + if (user) { + dispatch(push(profilePage(user.handle))) + } + } else if (entity) { dispatch(push(getEntityLink(entity))) } - }, [notification, user, dispatch]) + }, [notification, user, entity, dispatch]) + + const isMissingRequiredUser = achievement === Achievement.Followers && !user + const isMissingRequiredEntity = + achievement !== Achievement.Followers && !entity + + if (isMissingRequiredUser || isMissingRequiredEntity) { + return null + } + + const { link, text } = getAchievementText(notification, entity, user) return ( diff --git a/packages/web/src/components/notification/Notification/RemixCosignNotification.tsx b/packages/web/src/components/notification/Notification/RemixCosignNotification.tsx index 368598bf04..78ddde9bd0 100644 --- a/packages/web/src/components/notification/Notification/RemixCosignNotification.tsx +++ b/packages/web/src/components/notification/Notification/RemixCosignNotification.tsx @@ -4,8 +4,14 @@ import { push } from 'connected-react-router' import { useDispatch } from 'react-redux' import { Name } from 'common/models/Analytics' +import { + getNotificationEntities, + getNotificationUser +} from 'common/store/notifications/selectors' import { RemixCosign, TrackEntity } from 'common/store/notifications/types' +import { Nullable } from 'common/utils/typeUtils' import { make } from 'store/analytics/actions' +import { useSelector } from 'utils/reducer' import { EntityLink } from './components/EntityLink' import { NotificationBody } from './components/NotificationBody' @@ -22,8 +28,8 @@ import { getEntityLink } from './utils' const messages = { title: 'Remix Co-sign', cosign: 'Co-signed your Remix of', - shareTwitterText: (track: TrackEntity, handle: string) => - `My remix of ${track.title} was Co-Signed by ${handle} on @AudiusProject #Audius` + shareTwitterText: (trackTitle: string, handle: string) => + `My remix of ${trackTitle} was Co-Signed by ${handle} on @AudiusProject #Audius` } type RemixCosignNotificationProps = { @@ -34,32 +40,35 @@ export const RemixCosignNotification = ( props: RemixCosignNotificationProps ) => { const { notification } = props - const { - user, - entities, - entityType, - timeLabel, - isViewed, - childTrackId, - parentTrackUserId - } = notification + const { entityType, timeLabel, isViewed, childTrackId, parentTrackUserId } = + notification + + const user = useSelector((state) => getNotificationUser(state, notification)) + + // TODO: casting from EntityType to TrackEntity here, but + // getNotificationEntities should be smart enough based on notif type + const tracks = useSelector((state) => + getNotificationEntities(state, notification) + ) as Nullable + const dispatch = useDispatch() - const childTrack = entities.find( - (track) => track.track_id === childTrackId - ) as TrackEntity + const childTrack = tracks?.find((track) => track.track_id === childTrackId) - const parentTrack = entities.find( + const parentTrack = tracks?.find( (track) => track.owner_id === parentTrackUserId - ) as TrackEntity + ) + const parentTrackTitle = parentTrack?.title const handleClick = useCallback(() => { + if (!childTrack) return dispatch(push(getEntityLink(childTrack))) }, [childTrack, dispatch]) const handleTwitterShare = useCallback( (handle: string) => { - const shareText = messages.shareTwitterText(parentTrack, handle) + if (!parentTrackTitle) return null + const shareText = messages.shareTwitterText(parentTrackTitle, handle) const analytics = make( Name.NOTIFICATIONS_CLICK_REMIX_COSIGN_TWITTER_SHARE, { @@ -68,9 +77,11 @@ export const RemixCosignNotification = ( ) return { shareText, analytics } }, - [parentTrack] + [parentTrackTitle] ) + if (!user || !parentTrack || !childTrack) return null + return ( }> diff --git a/packages/web/src/components/notification/Notification/RemixCreateNotification.tsx b/packages/web/src/components/notification/Notification/RemixCreateNotification.tsx index ada015d8a6..f83b46eee2 100644 --- a/packages/web/src/components/notification/Notification/RemixCreateNotification.tsx +++ b/packages/web/src/components/notification/Notification/RemixCreateNotification.tsx @@ -4,8 +4,14 @@ import { push } from 'connected-react-router' import { useDispatch } from 'react-redux' import { Name } from 'common/models/Analytics' +import { + getNotificationEntities, + getNotificationUser +} from 'common/store/notifications/selectors' import { RemixCreate, TrackEntity } from 'common/store/notifications/types' +import { Nullable } from 'common/utils/typeUtils' import { make } from 'store/analytics/actions' +import { useSelector } from 'utils/reducer' import { EntityLink } from './components/EntityLink' import { NotificationBody } from './components/NotificationBody' @@ -33,31 +39,30 @@ export const RemixCreateNotification = ( props: RemixCreateNotificationProps ) => { const { notification } = props - const { - user, - entities, - entityType, - timeLabel, - isViewed, - childTrackId, - parentTrackId - } = notification + const { entityType, timeLabel, isViewed, childTrackId, parentTrackId } = + notification const dispatch = useDispatch() + const user = useSelector((state) => getNotificationUser(state, notification)) - const childTrack = entities.find( - (track) => track.track_id === childTrackId - ) as TrackEntity + // TODO: casting from EntityType to TrackEntity here, but + // getNotificationEntities should be smart enough based on notif type + const tracks = useSelector((state) => + getNotificationEntities(state, notification) + ) as Nullable - const parentTrack = entities.find( - (track) => track.track_id === parentTrackId - ) as TrackEntity + const childTrack = tracks?.find((track) => track.track_id === childTrackId) + + const parentTrack = tracks?.find((track) => track.track_id === parentTrackId) const handleClick = useCallback(() => { - dispatch(push(getEntityLink(childTrack))) + if (childTrack) { + dispatch(push(getEntityLink(childTrack))) + } }, [childTrack, dispatch]) const handleShare = useCallback( (twitterHandle: string) => { + if (!parentTrack) return null const shareText = messages.shareTwitterText(parentTrack, twitterHandle) const analytics = make( Name.NOTIFICATIONS_CLICK_REMIX_CREATE_TWITTER_SHARE, @@ -68,6 +73,8 @@ export const RemixCreateNotification = ( [parentTrack] ) + if (!user || !parentTrack || !childTrack) return null + return ( }> diff --git a/packages/web/src/components/notification/Notification/RepostNotification.tsx b/packages/web/src/components/notification/Notification/RepostNotification.tsx index 0fab02b4d8..817112d4e5 100644 --- a/packages/web/src/components/notification/Notification/RepostNotification.tsx +++ b/packages/web/src/components/notification/Notification/RepostNotification.tsx @@ -3,6 +3,10 @@ import { MouseEventHandler, useCallback } from 'react' import { push } from 'connected-react-router' import { useDispatch } from 'react-redux' +import { + getNotificationEntity, + getNotificationUsers +} from 'common/store/notifications/selectors' import { Repost } from 'common/store/notifications/types' import { setUsers as setUserListUsers, @@ -10,6 +14,7 @@ import { } from 'store/application/ui/userListModal/slice' import { UserListType } from 'store/application/ui/userListModal/types' import { isMobile } from 'utils/clientUtil' +import { useSelector } from 'utils/reducer' import { EntityLink, useGoToEntity } from './components/EntityLink' import { NotificationBody } from './components/NotificationBody' @@ -20,7 +25,7 @@ import { OthersLink } from './components/OthersLink' import { UserNameLink } from './components/UserNameLink' import { UserProfilePictureList } from './components/UserProfilePictureList' import { IconRepost } from './components/icons' -import { entityToUserListEntity } from './utils' +import { entityToUserListEntity, USER_LENGTH_LIMIT } from './utils' const messages = { reposted: 'reposted your' @@ -32,11 +37,18 @@ type RepostNotificationProps = { export const RepostNotification = (props: RepostNotificationProps) => { const { notification } = props - const { id, users, userIds, entity, entityType, timeLabel, isViewed } = - notification - const [firstUser] = users + const { id, userIds, entityType, timeLabel, isViewed } = notification + const users = useSelector((state) => + getNotificationUsers(state, notification, USER_LENGTH_LIMIT) + ) + const firstUser = users?.[0] const otherUsersCount = userIds.length - 1 const isMultiUser = userIds.length > 1 + + const entity = useSelector((state) => + getNotificationEntity(state, notification) + ) + const dispatch = useDispatch() const handleGoToEntity = useGoToEntity(entity, entityType) @@ -63,6 +75,8 @@ export const RepostNotification = (props: RepostNotificationProps) => { [isMultiUser, dispatch, entityType, id, handleGoToEntity] ) + if (!users || !firstUser || !entity) return null + return ( { const { notification } = props - const { tier, timeLabel, isViewed, user } = notification + const { tier, timeLabel, isViewed } = notification + const user = useSelector((state) => getNotificationUser(state, notification)) const tierInfo = badgeTiers.find( (info) => info.tier === tier @@ -50,6 +53,8 @@ export const TierChangeNotification = (props: TierChangeNotificationProps) => { const { label, icon } = tierInfoMap[tier] const shareText = messages.twitterShareText(label, icon) + if (!user) return null + return ( {audioTierMapPng[tier]}}> diff --git a/packages/web/src/components/notification/Notification/TipReactionNotification.tsx b/packages/web/src/components/notification/Notification/TipReactionNotification.tsx index 12ecfa4450..d853220472 100644 --- a/packages/web/src/components/notification/Notification/TipReactionNotification.tsx +++ b/packages/web/src/components/notification/Notification/TipReactionNotification.tsx @@ -2,9 +2,11 @@ import { useCallback } from 'react' import { useUIAudio } from 'common/hooks/useUIAudio' import { Name } from 'common/models/Analytics' +import { getNotificationUser } from 'common/store/notifications/selectors' import { Reaction } from 'common/store/notifications/types' import { getReactionFromRawValue } from 'common/store/ui/reactions/slice' import { make } from 'store/analytics/actions' +import { useSelector } from 'utils/reducer' import styles from './TipReactionNotification.module.css' import { AudioText } from './components/AudioText' @@ -36,7 +38,6 @@ export const TipReactionNotification = ( ) => { const { notification } = props const { - user, reactionValue, timeLabel, isViewed, @@ -45,6 +46,7 @@ export const TipReactionNotification = ( const uiAmount = useUIAudio(amount) + const user = useSelector((state) => getNotificationUser(state, notification)) const handleClick = useGoToProfile(user) const handleShare = useCallback((twitterHandle: string) => { @@ -56,6 +58,8 @@ export const TipReactionNotification = ( return { shareText, analytics } }, []) + if (!user) return null + const userLinkElement = ( { const [isTileDisabled, setIsTileDisabled] = useState(false) const { notification } = props - const { user, amount, timeLabel, isViewed, tipTxSignature } = notification + const { amount, timeLabel, isViewed, tipTxSignature } = notification + + const user = useSelector((state) => getNotificationUser(state, notification)) const reactionValue = useSelector(makeGetReactionForSignature(tipTxSignature)) const setReaction = useSetReaction(tipTxSignature) @@ -87,6 +91,8 @@ export const TipReceivedNotification = ( const handleMouseEnter = useCallback(() => setIsTileDisabled(true), []) const handleMouseLeave = useCallback(() => setIsTileDisabled(false), []) + if (!user) return null + return ( { const { notification } = props - const { user, amount, timeLabel, isViewed } = notification + const { amount, timeLabel, isViewed } = notification const uiAmount = useUIAudio(amount) + const user = useSelector((state) => getNotificationUser(state, notification)) const handleClick = useGoToProfile(user) const handleShare = useCallback( @@ -50,6 +53,8 @@ export const TipSentNotification = (props: TipSentNotificationProps) => { [uiAmount] ) + if (!user) return null + return ( }> diff --git a/packages/web/src/components/notification/Notification/TopSupporterNotification.tsx b/packages/web/src/components/notification/Notification/TopSupporterNotification.tsx index 7c74aef732..d86ca861d3 100644 --- a/packages/web/src/components/notification/Notification/TopSupporterNotification.tsx +++ b/packages/web/src/components/notification/Notification/TopSupporterNotification.tsx @@ -2,8 +2,10 @@ import { useCallback } from 'react' import { ReactComponent as IconTrending } from 'assets/img/iconTrending.svg' import { Name } from 'common/models/Analytics' +import { getNotificationUser } from 'common/store/notifications/selectors' import { SupporterRankUp } from 'common/store/notifications/types' import { make } from 'store/analytics/actions' +import { useSelector } from 'utils/reducer' import styles from './TopSupporterNotification.module.css' import { NotificationBody } from './components/NotificationBody' @@ -33,7 +35,9 @@ export const TopSupporterNotification = ( props: TopSupporterNotificationProps ) => { const { notification } = props - const { user, rank, timeLabel, isViewed } = notification + const { rank, timeLabel, isViewed } = notification + + const user = useSelector((state) => getNotificationUser(state, notification)) const handleClick = useGoToProfile(user) @@ -51,6 +55,8 @@ export const TopSupporterNotification = ( [rank] ) + if (!user) return null + return ( }> diff --git a/packages/web/src/components/notification/Notification/TopSupportingNotification.tsx b/packages/web/src/components/notification/Notification/TopSupportingNotification.tsx index 5ada68c108..c8b8fcfd42 100644 --- a/packages/web/src/components/notification/Notification/TopSupportingNotification.tsx +++ b/packages/web/src/components/notification/Notification/TopSupportingNotification.tsx @@ -2,8 +2,10 @@ import { useCallback } from 'react' import { ReactComponent as IconTrending } from 'assets/img/iconTrending.svg' import { Name } from 'common/models/Analytics' +import { getNotificationUser } from 'common/store/notifications/selectors' import { SupportingRankUp } from 'common/store/notifications/types' import { make } from 'store/analytics/actions' +import { useSelector } from 'utils/reducer' import styles from './TopSupportingNotification.module.css' import { NotificationBody } from './components/NotificationBody' @@ -33,7 +35,9 @@ export const TopSupportingNotification = ( props: TopSupportingNotificationProps ) => { const { notification } = props - const { user, rank, timeLabel, isViewed } = notification + const { rank, timeLabel, isViewed } = notification + + const user = useSelector((state) => getNotificationUser(state, notification)) const handleClick = useGoToProfile(user) @@ -52,6 +56,8 @@ export const TopSupportingNotification = ( [rank] ) + if (!user) return null + return ( }> diff --git a/packages/web/src/components/notification/Notification/TrendingTrackNotification.tsx b/packages/web/src/components/notification/Notification/TrendingTrackNotification.tsx index 479123c36e..6b2c0b8c8d 100644 --- a/packages/web/src/components/notification/Notification/TrendingTrackNotification.tsx +++ b/packages/web/src/components/notification/Notification/TrendingTrackNotification.tsx @@ -4,8 +4,11 @@ import { push } from 'connected-react-router' import { useDispatch } from 'react-redux' import { Name } from 'common/models/Analytics' -import { TrendingTrack } from 'common/store/notifications/types' +import { getNotificationEntity } from 'common/store/notifications/selectors' +import { TrackEntity, TrendingTrack } from 'common/store/notifications/types' +import { Nullable } from 'common/utils/typeUtils' import { make } from 'store/analytics/actions' +import { useSelector } from 'utils/reducer' import { EntityLink } from './components/EntityLink' import { NotificationBody } from './components/NotificationBody' @@ -36,15 +39,22 @@ export const TrendingTrackNotification = ( props: TrendingTrackNotificationProps ) => { const { notification } = props - const { entity, entityType, rank, timeLabel, isViewed } = notification + const { entityType, rank, timeLabel, isViewed } = notification const rankSuffix = getRankSuffix(rank) const dispatch = useDispatch() - - const shareText = messages.twitterShareText(entity.title, rank) + const track = useSelector((state) => + getNotificationEntity(state, notification) + ) as Nullable const handleClick = useCallback(() => { - dispatch(push(getEntityLink(entity))) - }, [dispatch, entity]) + if (track) { + dispatch(push(getEntityLink(track))) + } + }, [dispatch, track]) + + if (!track) return null + + const shareText = messages.twitterShareText(track.title, rank) return ( @@ -52,13 +62,13 @@ export const TrendingTrackNotification = ( {messages.title} - {messages.your} {' '} + {messages.your} {' '} {messages.is} {rank} {rankSuffix} {messages.trending} { const { notification } = props - const { user, entities, entityType, timeLabel, isViewed, type } = notification - const uploadCount = entities.length + const { entityType, entityIds, timeLabel, isViewed, type } = notification + const user = useSelector((state) => getNotificationUser(state, notification)) + const entities = useSelector((state) => + getNotificationEntities(state, notification) + ) + const uploadCount = entityIds.length const isSingleUpload = uploadCount === 1 const dispatch = useDispatch() @@ -43,16 +52,25 @@ export const UserSubscriptionNotification = ( const handleClick = useCallback(() => { if (entityType === Entity.Track && !isSingleUpload) { - dispatch(push(profilePage(user.handle))) + if (user) { + dispatch(push(profilePage(user.handle))) + } } else { - const entityLink = getEntityLink(entities[0]) - dispatch(push(entityLink)) - record( - make(Name.NOTIFICATIONS_CLICK_TILE, { kind: type, link_to: entityLink }) - ) + if (entities) { + const entityLink = getEntityLink(entities[0]) + dispatch(push(entityLink)) + record( + make(Name.NOTIFICATIONS_CLICK_TILE, { + kind: type, + link_to: entityLink + }) + ) + } } }, [entityType, isSingleUpload, user, entities, dispatch, record, type]) + if (!user || !entities) return null + return ( }> diff --git a/packages/web/src/components/notification/Notification/components/EntityLink.tsx b/packages/web/src/components/notification/Notification/components/EntityLink.tsx index bb8c11134d..71c0450eb9 100644 --- a/packages/web/src/components/notification/Notification/components/EntityLink.tsx +++ b/packages/web/src/components/notification/Notification/components/EntityLink.tsx @@ -22,12 +22,16 @@ type EntityLinkProps = { entityType: Entity } -export const useGoToEntity = (entity: EntityType, entityType: Entity) => { +export const useGoToEntity = ( + entity: Nullable, + entityType: Entity +) => { const dispatch = useDispatch() const record = useRecord() const handleClick: MouseEventHandler = useCallback( (event) => { + if (!entity) return event.stopPropagation() event.preventDefault() const link = getEntityLink(entity) diff --git a/packages/web/src/components/notification/Notification/utils.ts b/packages/web/src/components/notification/Notification/utils.ts index 4490bee4af..69b13f7764 100644 --- a/packages/web/src/components/notification/Notification/utils.ts +++ b/packages/web/src/components/notification/Notification/utils.ts @@ -10,6 +10,7 @@ import { } from 'utils/route' export const getEntityLink = (entity: EntityType, fullRoute = false) => { + if (!entity.user) return '' if ('track_id' in entity) { return fullRoute ? fullTrackPage(entity.permalink) : entity.permalink } else if (entity.user && entity.playlist_id && entity.is_album) {