From 99c0e7a948f50e1644250e493f1db6ba0f1c67f3 Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Thu, 14 Dec 2023 23:41:10 -0800 Subject: [PATCH 1/3] Finalize --- packages/common/src/audius-query/index.ts | 1 + packages/common/src/messages/sign-on/pages.ts | 9 ++ packages/common/src/schemas/index.ts | 1 + .../schemas/sign-on/selectArtistsSchema.ts | 5 + .../harmony/src/components/avatar/Avatar.tsx | 3 +- .../harmony/src/components/avatar/types.ts | 2 +- .../audio-rewards/TiersExplainerDrawer.tsx | 2 +- .../src/components/audio-rewards/index.ts | 2 +- .../mobile/src/components/core/CardList.tsx | 8 +- .../mobile/src/components/core/FlatList.tsx | 24 ++-- .../IconAudioBadge.tsx | 0 .../src/components/core/ProfilePicture.tsx | 36 ++++++ .../src/components/core/UserCoverPhoto.tsx | 25 ++++ .../src/components/core/UserDisplayName.tsx | 42 +++++++ packages/mobile/src/components/core/index.ts | 3 + .../src/components/user/ProfilePicture.tsx | 4 + .../components/Avatar/Avatar.tsx | 5 +- .../Button/FollowButton/FollowButton.tsx | 19 +-- .../components/Button/FollowButton/types.ts | 5 + .../components/CoverPhoto/CoverPhoto.tsx | 72 ++++++++++++ .../harmony-native/components/Text/Text.tsx | 2 + .../src/harmony-native/components/index.ts | 2 + .../screens/sign-on-screen/SignOnStack.tsx | 7 +- .../components/AccountHeader.tsx | 46 ++------ .../SelectArtistScreen/FollowArtistField.tsx | 108 +++++++++++++++++ .../SelectArtistsScreen.tsx | 107 +++++++++++++++++ .../SelectedGenresTabBar.tsx | 62 ++++++++++ .../SelectArtistScreen/TopArtistsCardList.tsx | 49 ++++++++ .../screens/SelectArtistScreen/index.ts | 1 + .../screens/SelectArtistsScreen.tsx | 111 ------------------ .../sign-up-page/pages/SelectArtistsPage.tsx | 20 +--- 31 files changed, 589 insertions(+), 194 deletions(-) create mode 100644 packages/common/src/schemas/sign-on/selectArtistsSchema.ts rename packages/mobile/src/components/{audio-rewards => core}/IconAudioBadge.tsx (100%) create mode 100644 packages/mobile/src/components/core/ProfilePicture.tsx create mode 100644 packages/mobile/src/components/core/UserCoverPhoto.tsx create mode 100644 packages/mobile/src/components/core/UserDisplayName.tsx create mode 100644 packages/mobile/src/harmony-native/components/CoverPhoto/CoverPhoto.tsx create mode 100644 packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/FollowArtistField.tsx create mode 100644 packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/SelectArtistsScreen.tsx create mode 100644 packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/SelectedGenresTabBar.tsx create mode 100644 packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/TopArtistsCardList.tsx create mode 100644 packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/index.ts delete mode 100644 packages/mobile/src/screens/sign-on-screen/screens/SelectArtistsScreen.tsx diff --git a/packages/common/src/audius-query/index.ts b/packages/common/src/audius-query/index.ts index 130eaa98cf2..eea23dfcf26 100644 --- a/packages/common/src/audius-query/index.ts +++ b/packages/common/src/audius-query/index.ts @@ -1,3 +1,4 @@ export * from './AudiusQueryContext' export * from './createApi' export * from './hooks' +export * from './types' diff --git a/packages/common/src/messages/sign-on/pages.ts b/packages/common/src/messages/sign-on/pages.ts index a5c63f48e17..96d0bb1c8e2 100644 --- a/packages/common/src/messages/sign-on/pages.ts +++ b/packages/common/src/messages/sign-on/pages.ts @@ -76,3 +76,12 @@ export const selectGenresPageMessages = { description: 'Start by picking some of your favorite genres.', continue: 'Continue' } + +export const selectArtstsPageMessages = { + header: 'Follow At Least 3 Artists', + description: + 'Curate your feed with tracks uploaded or reposted by anyone you follow. Click the artist’s photo to preview their music.', + genresLabel: 'Genre', + pickArtists: (genre: string) => `Pick ${genre} Artists`, + selected: 'Selected' +} diff --git a/packages/common/src/schemas/index.ts b/packages/common/src/schemas/index.ts index c3c09ed2948..180f0c84873 100644 --- a/packages/common/src/schemas/index.ts +++ b/packages/common/src/schemas/index.ts @@ -4,3 +4,4 @@ export * from './sign-on/passwordSchema' export * from './sign-on/pickHandleSchema' export * from './sign-on/finishProfileSchema' export * from './sign-on/selectGenresSchema' +export * from './sign-on/selectArtistsSchema' diff --git a/packages/common/src/schemas/sign-on/selectArtistsSchema.ts b/packages/common/src/schemas/sign-on/selectArtistsSchema.ts new file mode 100644 index 00000000000..99014c2dab6 --- /dev/null +++ b/packages/common/src/schemas/sign-on/selectArtistsSchema.ts @@ -0,0 +1,5 @@ +import { z } from 'zod' + +export const selectArtistsSchema = z.object({ + selectedArtists: z.array(z.string()).min(3) +}) diff --git a/packages/harmony/src/components/avatar/Avatar.tsx b/packages/harmony/src/components/avatar/Avatar.tsx index ac61d6bb33d..63235213b9b 100644 --- a/packages/harmony/src/components/avatar/Avatar.tsx +++ b/packages/harmony/src/components/avatar/Avatar.tsx @@ -20,7 +20,8 @@ export const Avatar = (props: AvatarProps) => { const sizeMap = { auto: '100%', small: '24px', - large: '40px', + medium: '40px', + large: '72px', xl: '80px' } diff --git a/packages/harmony/src/components/avatar/types.ts b/packages/harmony/src/components/avatar/types.ts index 9f2ef1efec4..efdce533259 100644 --- a/packages/harmony/src/components/avatar/types.ts +++ b/packages/harmony/src/components/avatar/types.ts @@ -17,7 +17,7 @@ export type AvatarProps = PropsWithChildren<{ * Size * @default auto */ - size?: 'auto' | 'small' | 'large' | 'xl' + size?: 'auto' | 'small' | 'medium' | 'large' | 'xl' /** * Stroke Width diff --git a/packages/mobile/src/components/audio-rewards/TiersExplainerDrawer.tsx b/packages/mobile/src/components/audio-rewards/TiersExplainerDrawer.tsx index 4ef3948bd5d..45103366d33 100644 --- a/packages/mobile/src/components/audio-rewards/TiersExplainerDrawer.tsx +++ b/packages/mobile/src/components/audio-rewards/TiersExplainerDrawer.tsx @@ -9,9 +9,9 @@ import { useSelector } from 'react-redux' import { makeStyles } from 'app/styles' +import { IconAudioBadge } from '../core/IconAudioBadge' import { AppDrawer } from '../drawer/AppDrawer' -import { IconAudioBadge } from './IconAudioBadge' import { TierText } from './TierText' const { getProfileUserId } = profilePageSelectors diff --git a/packages/mobile/src/components/audio-rewards/index.ts b/packages/mobile/src/components/audio-rewards/index.ts index 132ec9a7c00..ab5fcbfc160 100644 --- a/packages/mobile/src/components/audio-rewards/index.ts +++ b/packages/mobile/src/components/audio-rewards/index.ts @@ -1,4 +1,4 @@ -export * from './IconAudioBadge' +export * from '../core/IconAudioBadge' export * from './TiersExplainerDrawer' export * from './RewardsBanner' export * from './TierText' diff --git a/packages/mobile/src/components/core/CardList.tsx b/packages/mobile/src/components/core/CardList.tsx index f8792f609fb..63333425928 100644 --- a/packages/mobile/src/components/core/CardList.tsx +++ b/packages/mobile/src/components/core/CardList.tsx @@ -1,17 +1,13 @@ import type { ComponentType } from 'react' import { useMemo, useCallback, useRef } from 'react' -import type { - FlatListProps, - ListRenderItem, - ListRenderItemInfo -} from 'react-native' +import type { ListRenderItem, ListRenderItemInfo } from 'react-native' import { View } from 'react-native' import { useScrollToTop } from 'app/hooks/useScrollToTop' import { makeStyles } from 'app/styles' -import type { FlatListT } from './FlatList' +import type { FlatListT, FlatListProps } from './FlatList' import { FlatList } from './FlatList' export type CardListProps = Omit, 'data'> & { diff --git a/packages/mobile/src/components/core/FlatList.tsx b/packages/mobile/src/components/core/FlatList.tsx index 24867650913..207131d941c 100644 --- a/packages/mobile/src/components/core/FlatList.tsx +++ b/packages/mobile/src/components/core/FlatList.tsx @@ -1,7 +1,10 @@ import type { MutableRefObject, Ref } from 'react' import { forwardRef, useContext, useRef } from 'react' -import type { FlatListProps, FlatList as RNFlatList } from 'react-native' +import type { + FlatListProps as RNFlatListProps, + FlatList as RNFlatList +} from 'react-native' import { Animated, Platform, RefreshControl, View } from 'react-native' import { useCollapsibleScene } from 'react-native-collapsible-tab-view' @@ -17,7 +20,7 @@ export type AnimatedFlatListT = Animated.FlatList type CollapsibleFlatListProps = { sceneName: string -} & Animated.AnimatedProps> +} & Animated.AnimatedProps> function CollapsibleFlatList(props: CollapsibleFlatListProps) { const { sceneName, onScroll, ...other } = props @@ -48,7 +51,7 @@ function CollapsibleFlatList(props: CollapsibleFlatListProps) { } const AnimatedFlatList = forwardRef(function AnimatedFlatList( - props: Animated.AnimatedProps>, + props: Animated.AnimatedProps>, ref: MutableRefObject | null> ) { const { refreshing, onRefresh, onScroll, ...other } = props @@ -102,6 +105,10 @@ const AnimatedFlatList = forwardRef(function AnimatedFlatList( ) }) +export type FlatListProps = RNFlatListProps & { + sceneName?: string +} + /** * Provides either a FlatList or an animated FlatList * depending on whether or not the list is found in a "collapsible" header tab @@ -110,8 +117,11 @@ export const FlatList = forwardRef(function FlatList( props: FlatListProps, ref: Ref> ) { - const { ListFooterComponent, ...other } = props - const { sceneName } = useContext(CollapsibleTabNavigatorContext) + const { ListFooterComponent, sceneName: sceneNameProp, ...other } = props + const { sceneName: sceneNameContext } = useContext( + CollapsibleTabNavigatorContext + ) + const sceneName = sceneNameProp ?? sceneNameContext const FooterComponent = ListFooterComponent ? ( <> {ListFooterComponent} @@ -130,14 +140,14 @@ export const FlatList = forwardRef(function FlatList( return ( >)} + {...(flatListProps as Animated.AnimatedProps>)} /> ) } return ( >} - {...(flatListProps as Animated.AnimatedProps>)} + {...(flatListProps as Animated.AnimatedProps>)} /> ) }) diff --git a/packages/mobile/src/components/audio-rewards/IconAudioBadge.tsx b/packages/mobile/src/components/core/IconAudioBadge.tsx similarity index 100% rename from packages/mobile/src/components/audio-rewards/IconAudioBadge.tsx rename to packages/mobile/src/components/core/IconAudioBadge.tsx diff --git a/packages/mobile/src/components/core/ProfilePicture.tsx b/packages/mobile/src/components/core/ProfilePicture.tsx new file mode 100644 index 00000000000..bdfcb0f705b --- /dev/null +++ b/packages/mobile/src/components/core/ProfilePicture.tsx @@ -0,0 +1,36 @@ +import { SquareSizes, type ID, cacheUsersSelectors } from '@audius/common' +import { useSelector } from 'react-redux' + +import type { AvatarProps } from '@audius/harmony-native' +import { Avatar } from '@audius/harmony-native' + +import { useProfilePicture } from '../image/UserImage' +const { getUser } = cacheUsersSelectors + +type ProfilePictureProps = Omit< + AvatarProps, + 'source' | 'accessibilityLabel' +> & { + userId: ID +} + +export const ProfilePicture = (props: ProfilePictureProps) => { + const { userId, ...other } = props + + const userName = useSelector((state) => getUser(state, { id: userId })?.name) + const accessibilityLabel = `Profile picture for ${userName}` + + const { source, handleError } = useProfilePicture( + userId, + SquareSizes.SIZE_150_BY_150 + ) + + return ( + + ) +} diff --git a/packages/mobile/src/components/core/UserCoverPhoto.tsx b/packages/mobile/src/components/core/UserCoverPhoto.tsx new file mode 100644 index 00000000000..aecba090216 --- /dev/null +++ b/packages/mobile/src/components/core/UserCoverPhoto.tsx @@ -0,0 +1,25 @@ +import type { ID } from '@audius/common' + +import type { CoverPhotoProps } from '@audius/harmony-native' +import { CoverPhoto } from '@audius/harmony-native' + +import { useCoverPhoto } from '../image/CoverPhoto' + +type UserCoverPhotoProps = { + userId: ID +} & Pick + +export const UserCoverPhoto = (props: UserCoverPhotoProps) => { + const { userId, ...other } = props + + const { source, handleError, shouldBlur } = useCoverPhoto(userId) + + return ( + + ) +} diff --git a/packages/mobile/src/components/core/UserDisplayName.tsx b/packages/mobile/src/components/core/UserDisplayName.tsx new file mode 100644 index 00000000000..e74f965dfd5 --- /dev/null +++ b/packages/mobile/src/components/core/UserDisplayName.tsx @@ -0,0 +1,42 @@ +import { useSelectTierInfo, type ID, cacheUsersSelectors } from '@audius/common' +import { useSelector } from 'react-redux' + +import type { TextProps } from '@audius/harmony-native' +import { + Flex, + IconVerified, + Text, + useTheme, + variantStylesMap +} from '@audius/harmony-native' + +import { IconAudioBadge } from './IconAudioBadge' + +const { getUser } = cacheUsersSelectors + +type UserDisplayProps = TextProps & { + userId: ID +} + +export const UserDisplayName = (props: UserDisplayProps) => { + const { userId, variant = 'title', size = 's', style, ...other } = props + const { tier, isVerified } = useSelectTierInfo(userId) + const displayName = useSelector( + (state) => getUser(state, { id: userId })?.name + ) + const { typography } = useTheme() + const fontSize = typography.size[variantStylesMap[variant].fontSize[size]] + const badgeSize = fontSize - 2 + + return ( + + + {displayName} + + {isVerified ? ( + + ) : null} + + + ) +} diff --git a/packages/mobile/src/components/core/index.ts b/packages/mobile/src/components/core/index.ts index f1a914da1b4..df9e3014620 100644 --- a/packages/mobile/src/components/core/index.ts +++ b/packages/mobile/src/components/core/index.ts @@ -38,3 +38,6 @@ export * from './InputErrorMessage' export * from './DogEar' export * from './LockedStatusBadge' export * from './ModalScreen' +export * from './ProfilePicture' +export * from './UserCoverPhoto' +export * from './UserDisplayName' diff --git a/packages/mobile/src/components/user/ProfilePicture.tsx b/packages/mobile/src/components/user/ProfilePicture.tsx index 555fc79517a..0c805d50434 100644 --- a/packages/mobile/src/components/user/ProfilePicture.tsx +++ b/packages/mobile/src/components/user/ProfilePicture.tsx @@ -30,6 +30,10 @@ export type ProfilePictureProps = Partial & } ) +/** + * @deprecated + * Use image/ProfilePicture instead + */ export const ProfilePicture = (props: ProfilePictureProps) => { const { style: styleProp, ...other } = props const userId = 'userId' in other ? other.userId : other.profile.user_id diff --git a/packages/mobile/src/harmony-native/components/Avatar/Avatar.tsx b/packages/mobile/src/harmony-native/components/Avatar/Avatar.tsx index 55a827deefc..afa646a942a 100644 --- a/packages/mobile/src/harmony-native/components/Avatar/Avatar.tsx +++ b/packages/mobile/src/harmony-native/components/Avatar/Avatar.tsx @@ -11,7 +11,8 @@ export type AvatarProps = Omit & const sizeMap = { auto: '100%', small: 24, - large: 40, + medium: 40, + large: 72, xl: 80 } @@ -28,7 +29,7 @@ export const Avatar = (props: AvatarProps) => { const { children, source, - size = 'auto', + size = 'medium', strokeWidth = 'default', variant = 'default', style, diff --git a/packages/mobile/src/harmony-native/components/Button/FollowButton/FollowButton.tsx b/packages/mobile/src/harmony-native/components/Button/FollowButton/FollowButton.tsx index b9ed23dd3ac..8776ffafa54 100644 --- a/packages/mobile/src/harmony-native/components/Button/FollowButton/FollowButton.tsx +++ b/packages/mobile/src/harmony-native/components/Button/FollowButton/FollowButton.tsx @@ -1,4 +1,5 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' +import type { ChangeEvent } from 'react' +import { useCallback, useEffect, useState } from 'react' import { css } from '@emotion/native' import { Pressable } from 'react-native' @@ -20,6 +21,8 @@ export const FollowButton = (props: FollowButtonProps) => { onUnfollow, onFollow, size = 'default', + value, + onChange, ...other } = props const { disabled } = other @@ -30,9 +33,7 @@ export const FollowButton = (props: FollowButtonProps) => { setFollowing(isFollowing) }, [isFollowing]) - const Icon = useMemo(() => { - return following ? IconUserFollowing : IconUserFollow - }, [following]) + const Icon = following ? IconUserFollowing : IconUserFollow const handlePress = useCallback(() => { if (following) { @@ -40,8 +41,11 @@ export const FollowButton = (props: FollowButtonProps) => { } else { onFollow?.() } + onChange?.({ + target: { value, checked: true, type: 'checkbox' } + } as ChangeEvent) setFollowing(!following) - }, [following, onUnfollow, onFollow]) + }, [following, onChange, value, onUnfollow, onFollow]) return ( @@ -52,12 +56,11 @@ export const FollowButton = (props: FollowButtonProps) => { justifyContent='center' gap='xs' pv='s' + border='default' style={css({ opacity: disabled ? 0.45 : 1, borderRadius: variant === 'pill' ? cornerRadius['2xl'] : cornerRadius.s, - borderWidth: 1, - borderStyle: 'solid', borderColor: color.primary.primary, backgroundColor: following ? color.primary.primary @@ -67,7 +70,7 @@ export const FollowButton = (props: FollowButtonProps) => { diff --git a/packages/mobile/src/harmony-native/components/Button/FollowButton/types.ts b/packages/mobile/src/harmony-native/components/Button/FollowButton/types.ts index 813957fc61c..3ad9495e518 100644 --- a/packages/mobile/src/harmony-native/components/Button/FollowButton/types.ts +++ b/packages/mobile/src/harmony-native/components/Button/FollowButton/types.ts @@ -1,3 +1,5 @@ +import type { ChangeEvent } from 'react' + import type { PressableProps } from 'react-native/types' export type FollowButtonProps = { @@ -27,4 +29,7 @@ export type FollowButtonProps = { * Callback for when an unfollow is triggered. */ onUnfollow?: () => void + value?: any + onChange?: (e: ChangeEvent) => void + // onChange?: (e: { target: { value: any; checked: boolean } }) => void } & Pick diff --git a/packages/mobile/src/harmony-native/components/CoverPhoto/CoverPhoto.tsx b/packages/mobile/src/harmony-native/components/CoverPhoto/CoverPhoto.tsx new file mode 100644 index 00000000000..22d4d12c756 --- /dev/null +++ b/packages/mobile/src/harmony-native/components/CoverPhoto/CoverPhoto.tsx @@ -0,0 +1,72 @@ +import type { ReactNode } from 'react' + +import type { CornerRadiusOptions } from '@audius/harmony' +import { css } from '@emotion/native' +import { BlurView } from '@react-native-community/blur' +import type { + ImageBackgroundProps, + ImageSourcePropType, + ImageStyle, + StyleProp +} from 'react-native' +import { ImageBackground, View } from 'react-native' +import LinearGradient from 'react-native-linear-gradient' + +import { useTheme } from '../../foundations/theme' + +export type CoverPhotoProps = { + profilePicture?: ImageSourcePropType + coverPhoto?: ImageSourcePropType + style?: StyleProp + children?: ReactNode + topCornerRadius?: CornerRadiusOptions +} & Omit + +export const CoverPhoto = (props: CoverPhotoProps) => { + const { + profilePicture, + coverPhoto, + style, + children, + topCornerRadius, + ...other + } = props + const { color, cornerRadius } = useTheme() + + const rootStyle = css({ + backgroundColor: color.neutral.n300, + height: 96, + ...(topCornerRadius + ? { + borderTopLeftRadius: cornerRadius[topCornerRadius], + borderTopRightRadius: cornerRadius[topCornerRadius], + overflow: 'hidden' + } + : {}) + }) + + const fullHeightStyle = css({ height: '100%' }) + + return ( + + {!profilePicture && !coverPhoto ? ( + + ) : null} + {profilePicture && !coverPhoto ? ( + + + + ) : null} + {children} + + ) +} diff --git a/packages/mobile/src/harmony-native/components/Text/Text.tsx b/packages/mobile/src/harmony-native/components/Text/Text.tsx index db44529902b..2587228643b 100644 --- a/packages/mobile/src/harmony-native/components/Text/Text.tsx +++ b/packages/mobile/src/harmony-native/components/Text/Text.tsx @@ -60,3 +60,5 @@ export const Text = forwardRef((props, ref) => { /> ) }) + +export { variantStylesMap } diff --git a/packages/mobile/src/harmony-native/components/index.ts b/packages/mobile/src/harmony-native/components/index.ts index 984759ace04..87565758f63 100644 --- a/packages/mobile/src/harmony-native/components/index.ts +++ b/packages/mobile/src/harmony-native/components/index.ts @@ -4,3 +4,5 @@ export * from './layout' export * from './Avatar/Avatar' export * from './Hint/Hint' export * from './input/SelectablePill/SelectablePill' +export * from './CoverPhoto/CoverPhoto' +export * from './Button/FollowButton/FollowButton' diff --git a/packages/mobile/src/screens/sign-on-screen/SignOnStack.tsx b/packages/mobile/src/screens/sign-on-screen/SignOnStack.tsx index 541332de5cd..b54821b1880 100644 --- a/packages/mobile/src/screens/sign-on-screen/SignOnStack.tsx +++ b/packages/mobile/src/screens/sign-on-screen/SignOnStack.tsx @@ -5,7 +5,7 @@ import { useScreenOptions } from 'app/app/navigation' import { CreatePasswordScreen } from './screens/CreatePasswordScreen' import { FinishProfileScreen } from './screens/FinishProfileScreen' import { PickHandleScreen } from './screens/PickHandleScreen' -import { SelectArtistsScreen } from './screens/SelectArtistsScreen' +import { SelectArtistsScreen } from './screens/SelectArtistScreen' import { SelectGenreScreen } from './screens/SelectGenreScreen' import { SignOnScreen } from './screens/SignOnScreen' @@ -15,7 +15,10 @@ const screenOptionsOverrides = { animationTypeForReplace: 'pop' as const } export const SignOnStack = () => { const screenOptions = useScreenOptions(screenOptionsOverrides) return ( - + { const { value: handle } = useSelector(getHandleField) const { value: coverPhoto } = useSelector(getCoverPhotoField) ?? {} const { value: displayName } = useSelector(getNameField) - const { value: profileImage } = useSelector(getProfileImageField) + const { value: profileImage } = useSelector(getProfileImageField) ?? {} const isVerified = useSelector(getIsVerified) return ( @@ -104,42 +102,14 @@ type CoverPhotoProps = { const CoverPhoto = (props: CoverPhotoProps) => { const { onSelectCoverPhoto, profilePicture, coverPhoto } = props - const { color, cornerRadius, spacing } = useTheme() - const isEdit = onSelectCoverPhoto - - const borderRadiusStyle = css({ - borderTopLeftRadius: cornerRadius.m, - borderTopRightRadius: cornerRadius.m, - overflow: 'hidden' - }) - - const rootCss = css({ - backgroundColor: color.neutral.n300, - height: 96 - }) + const { spacing } = useTheme() return ( - - {!profilePicture && !coverPhoto ? ( - - ) : null} - {profilePicture && !coverPhoto ? ( - - - - ) : null} {onSelectCoverPhoto ? ( { onPress={onSelectCoverPhoto} /> ) : null} - + ) } diff --git a/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/FollowArtistField.tsx b/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/FollowArtistField.tsx new file mode 100644 index 00000000000..646c00fa1f3 --- /dev/null +++ b/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/FollowArtistField.tsx @@ -0,0 +1,108 @@ +import type { UserMetadata } from '@audius/common' +import { css } from '@emotion/native' +import { useField } from 'formik' + +import { + Box, + Flex, + FollowButton, + IconNote, + IconUser, + Paper, + Text, + useTheme +} from '@audius/harmony-native' +import { + Divider, + ProfilePicture, + UserCoverPhoto, + UserDisplayName +} from 'app/components/core' +import { StaticSkeleton } from 'app/components/skeleton' + +type FollowArtistFieldProps = { + artist: UserMetadata +} + +export const FollowArtistField = (props: FollowArtistFieldProps) => { + const { artist } = props + const { user_id, track_count, follower_count } = artist + const { spacing } = useTheme() + const [{ onChange }] = useField({ name: 'selectedArtists', type: 'checkbox' }) + + return ( + + + + + + + + + + + + + {track_count} + + + + + + + {follower_count} + + + + + + + + + + ) +} + +export const FollowArtistTileSkeleton = () => { + return ( + + + + + + + + + ) +} diff --git a/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/SelectArtistsScreen.tsx b/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/SelectArtistsScreen.tsx new file mode 100644 index 00000000000..dadf609ae74 --- /dev/null +++ b/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/SelectArtistsScreen.tsx @@ -0,0 +1,107 @@ +import { useCallback } from 'react' + +import { + selectArtstsPageMessages as messages, + selectArtistsSchema +} from '@audius/common' +import { addFollowArtists } from 'common/store/pages/signon/actions' +import { getGenres } from 'common/store/pages/signon/selectors' +import { Formik } from 'formik' +import { createMaterialCollapsibleTopTabNavigator } from 'react-native-collapsible-tab-view' +import { useDispatch, useSelector } from 'react-redux' +import { toFormikValidationSchema } from 'zod-formik-adapter' + +import { Flex, Text } from '@audius/harmony-native' + +import { ReadOnlyAccountHeader } from '../../components/AccountHeader' +import { Heading, PageFooter } from '../../components/layout' + +import { SelectedGenresTabBar } from './SelectedGenresTabBar' +import { TopArtistsCardList } from './TopArtistsCardList' + +const Tab = createMaterialCollapsibleTopTabNavigator() + +type SelectArtistsValues = { + selectedArtists: string[] +} + +const initialValues: SelectArtistsValues = { + selectedArtists: [] +} + +export const SelectArtistsScreen = () => { + const genres = useSelector((state: any) => [ + 'Featured', + ...(getGenres(state) ?? []) + ]) + const dispatch = useDispatch() + + const renderHeader = useCallback( + () => ( + + + + + + + ), + [] + ) + + const handleSubmit = useCallback( + (values: SelectArtistsValues) => { + const { selectedArtists } = values + dispatch( + addFollowArtists(selectedArtists.map((artist) => parseInt(artist))) + ) + }, + [dispatch] + ) + + return ( + + {({ dirty, isValid, values, errors }) => { + const { selectedArtists } = values + return ( + + + {genres.map((genre) => ( + + ))} + + + {messages.selected} {selectedArtists.length || 0}/3 + + } + /> + + ) + }} + + ) +} diff --git a/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/SelectedGenresTabBar.tsx b/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/SelectedGenresTabBar.tsx new file mode 100644 index 00000000000..c92aa99a316 --- /dev/null +++ b/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/SelectedGenresTabBar.tsx @@ -0,0 +1,62 @@ +import { useCallback } from 'react' + +import type { Genre } from '@audius/common' +import { convertGenreLabelToValue } from '@audius/common' +import { css } from '@emotion/native' +import type { MaterialTopTabBarProps } from '@react-navigation/material-top-tabs' +import { ScrollView } from 'react-native' + +import { Flex, SelectablePill, useTheme } from '@audius/harmony-native' + +type SelectedGenresTabBarProps = MaterialTopTabBarProps + +export const SelectedGenresTabBar = (props: SelectedGenresTabBarProps) => { + const { state, navigation } = props + const { color } = useTheme() + + const { routes } = state + + const onPress = useCallback( + (route: any, tabIndex: number) => { + const event = navigation.emit({ + type: 'tabPress', + target: route.key, + canPreventDefault: true + }) + + if (state.index !== tabIndex && !event.defaultPrevented) { + navigation.navigate({ + name: route.name, + merge: true, + params: { tabIndex } + }) + } + }, + [navigation, state.index] + ) + + return ( + + + {routes.map((route, index) => { + const { name, key } = route + const isFocused = state.index === index + return ( + onPress(route, index)} + /> + ) + })} + + + ) +} diff --git a/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/TopArtistsCardList.tsx b/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/TopArtistsCardList.tsx new file mode 100644 index 00000000000..144de1993ec --- /dev/null +++ b/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/TopArtistsCardList.tsx @@ -0,0 +1,49 @@ +import { memo } from 'react' + +import type { QueryHookOptions } from '@audius/common' +import { useGetFeaturedArtists, useGetTopArtistsInGenre } from '@audius/common' +import { css } from '@emotion/native' +import { useIsFocused, type RouteProp } from '@react-navigation/native' + +import { Box, useTheme } from '@audius/harmony-native' +import { CardList } from 'app/components/core' + +import { + FollowArtistField, + FollowArtistTileSkeleton +} from './FollowArtistField' + +export const useGetTopArtists = (genre: string, options?: QueryHookOptions) => { + const useGetArtistsHook = + genre === 'Featured' ? useGetFeaturedArtists : useGetTopArtistsInGenre + + return useGetArtistsHook({ genre }, options) +} + +const MemoizedFollowArtistField = memo(FollowArtistField) + +type Props = { + route: RouteProp +} + +export const TopArtistsCardList = (props: Props) => { + const { name: genre } = props.route + const isFocused = useIsFocused() + const { spacing } = useTheme() + + const { data: artists } = useGetTopArtists(genre, { + disabled: !isFocused + }) + + return ( + } + sceneName={genre} + numColumns={2} + ListFooterComponent={} + LoadingCardComponent={FollowArtistTileSkeleton} + /> + ) +} diff --git a/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/index.ts b/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/index.ts new file mode 100644 index 00000000000..a0e5d45b8e8 --- /dev/null +++ b/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistScreen/index.ts @@ -0,0 +1 @@ +export { SelectArtistsScreen } from './SelectArtistsScreen' diff --git a/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistsScreen.tsx b/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistsScreen.tsx deleted file mode 100644 index 9fe4135cb8e..00000000000 --- a/packages/mobile/src/screens/sign-on-screen/screens/SelectArtistsScreen.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { useCallback, useState } from 'react' - -import { - useGetTopArtistsInGenre, - type ID, - useGetFeaturedArtists, - Status -} from '@audius/common' -import { addFollowArtists } from 'common/store/pages/signon/actions' -import { getGenres } from 'common/store/pages/signon/selectors' -import { Formik } from 'formik' -import { Pressable, View } from 'react-native' -import { useDispatch, useSelector } from 'react-redux' - -import { Button, Text } from 'app/components/core' - -import { ArtistTile } from '../components/ArtistTile' - -const messages = { - header: 'Follow At Least 3 Artists', - description: - 'Curate your feed with tracks uploaded or reposted by anyone you follow. Click the artist’s photo to preview their music.', - genresLabel: 'Selected genres', - continue: 'Continue' -} - -type SelectArtistsValues = { - artists: ID[] -} - -const initialValues: SelectArtistsValues = { - artists: [] -} - -export const SelectArtistsScreen = () => { - const genres = useSelector((state: any) => ['Featured', ...getGenres(state)]) - const [currentGenre, setCurrentGenre] = useState('Featured') - const dispatch = useDispatch() - - const handleSubmit = useCallback( - (values: SelectArtistsValues) => { - const { artists } = values - dispatch(addFollowArtists(artists)) - }, - [dispatch] - ) - - const isFeaturedArtists = currentGenre === 'Featured' - - const { data: topArtists, status: topArtistsStatus } = - useGetTopArtistsInGenre( - { genre: currentGenre }, - { disabled: isFeaturedArtists } - ) - - const { data: featuredArtists, status: featuredArtistsStatus } = - useGetFeaturedArtists(undefined, { - disabled: !isFeaturedArtists - }) - - const artists = isFeaturedArtists ? featuredArtists : topArtists - const isLoading = - (isFeaturedArtists ? topArtistsStatus : featuredArtistsStatus) === - Status.LOADING - - return ( - - - {messages.header} - {messages.description} - - - {genres.map((genre) => { - const checked = genre === currentGenre - return ( - setCurrentGenre(genre)} - style={{ backgroundColor: checked ? 'purple' : undefined }} - > - {genre} - - ) - })} - - - {({ handleSubmit }) => { - return ( - - - {isLoading - ? null - : artists?.map((user) => ( - - ))} - -