diff --git a/packages/common/src/context/appContext.ts b/packages/common/src/context/appContext.ts index 930890ce48..44bc00cc6a 100644 --- a/packages/common/src/context/appContext.ts +++ b/packages/common/src/context/appContext.ts @@ -3,6 +3,7 @@ import { createContext, useContext } from 'react' import { StorageNodeSelectorService } from '@audius/sdk' import { AnalyticsEvent, AllTrackingEvents } from 'models/Analytics' +import { AudiusBackend } from 'services/audius-backend' type AppContextType = { analytics: { @@ -15,6 +16,12 @@ type AppContextType = { } } storageNodeSelector?: StorageNodeSelectorService + imageUtils: { + generatePlaylistArtwork: ( + imageUrls: string[] + ) => Promise<{ url: string; file: File }> + } + audiusBackend: AudiusBackend } export const AppContext = createContext(null) diff --git a/packages/common/src/hooks/index.ts b/packages/common/src/hooks/index.ts index bb3e174313..5a09e1cbe6 100644 --- a/packages/common/src/hooks/index.ts +++ b/packages/common/src/hooks/index.ts @@ -18,3 +18,4 @@ export * from './useThrottledCallback' export * from './useDebouncedCallback' export * from './useSavedCollections' export * from './chats' +export * from './useGeneratePlaylistArtwork' diff --git a/packages/common/src/hooks/useGeneratePlaylistArtwork.ts b/packages/common/src/hooks/useGeneratePlaylistArtwork.ts new file mode 100644 index 0000000000..f108135cab --- /dev/null +++ b/packages/common/src/hooks/useGeneratePlaylistArtwork.ts @@ -0,0 +1,53 @@ +import { useCallback } from 'react' + +import { useSelector } from 'react-redux' + +import { ID } from 'models/Identifiers' +import { SquareSizes } from 'models/ImageSizes' +import { useAppContext } from 'src/context' +import { getCollection } from 'store/cache/collections/selectors' +import { getTrack } from 'store/cache/tracks/selectors' +import { CommonState } from 'store/index' +import { removeNullable } from 'utils/typeUtils' + +export const useGeneratePlaylistArtwork = (collectionId: ID) => { + const collection = useSelector( + (state: CommonState) => getCollection(state, { id: collectionId })! + ) + + const collectionTracks = useSelector((state: CommonState) => { + const trackIds = collection.playlist_contents.track_ids.map( + ({ track }) => track + ) + const tracks = trackIds + .map((trackId) => getTrack(state, { id: trackId })) + .filter(removeNullable) + .slice(0, 4) + + if (tracks.length < 4) { + return tracks.slice(0, 1) + } + + return tracks + }) + + const { imageUtils, audiusBackend } = useAppContext() + + return useCallback(async () => { + if (collectionTracks.length === 0) { + return { url: '', file: undefined } + } + + const trackArtworkUrls = await Promise.all( + collectionTracks.map(async (track) => { + const { cover_art_sizes, cover_art } = track + return await audiusBackend.getImageUrl( + cover_art_sizes || cover_art, + SquareSizes.SIZE_1000_BY_1000 + ) + }) + ) + + return await imageUtils.generatePlaylistArtwork(trackArtworkUrls) + }, [collectionTracks, audiusBackend, imageUtils]) +} diff --git a/packages/common/src/models/Collection.ts b/packages/common/src/models/Collection.ts index 97cecdc98a..1303c5370e 100644 --- a/packages/common/src/models/Collection.ts +++ b/packages/common/src/models/Collection.ts @@ -35,6 +35,7 @@ export type CollectionMetadata = { has_current_user_saved: boolean is_album: boolean is_delete: boolean + is_image_autogenerated?: boolean is_private: boolean playlist_contents: { track_ids: PlaylistTrackId[] @@ -70,7 +71,7 @@ export type ComputedCollectionProperties = { _cover_art_sizes: CoverArtSizes _moved?: UID _temp?: boolean - artwork?: { file?: File; url?: string } + artwork?: { file?: File; url?: string; source?: string; error?: string } } export type Collection = CollectionMetadata & ComputedCollectionProperties diff --git a/packages/common/src/services/audius-api-client/AudiusAPIClient.ts b/packages/common/src/services/audius-api-client/AudiusAPIClient.ts index d4778c4b53..2bdd39d6de 100644 --- a/packages/common/src/services/audius-api-client/AudiusAPIClient.ts +++ b/packages/common/src/services/audius-api-client/AudiusAPIClient.ts @@ -1924,6 +1924,11 @@ export class AudiusAPIClient { // initialization state if needed before retrying if (this.initializationState.type === 'manual') { await this.waitForLibsInit() + this.initializationState = { + type: 'libs', + state: 'initialized', + endpoint: this.initializationState.endpoint + } } return this._getResponse(path, sanitizedParams, retry, pathType) } diff --git a/packages/common/src/services/audius-backend/AudiusBackend.ts b/packages/common/src/services/audius-backend/AudiusBackend.ts index f5300c4a94..61ad83a407 100644 --- a/packages/common/src/services/audius-backend/AudiusBackend.ts +++ b/packages/common/src/services/audius-backend/AudiusBackend.ts @@ -458,7 +458,6 @@ export const audiusBackend = ({ if (imagePreloader) { try { const preloaded = await imagePreloader(imageUrl) - console.log('we doing this actually?') if (preloaded) { return imageUrl } diff --git a/packages/common/src/store/cache/collections/types.ts b/packages/common/src/store/cache/collections/types.ts index 4565be9a4e..ee99eabfde 100644 --- a/packages/common/src/store/cache/collections/types.ts +++ b/packages/common/src/store/cache/collections/types.ts @@ -19,7 +19,7 @@ export type Image = { size?: number fileType?: string url: string - file?: string + file?: string | File } export type EditPlaylistValues = Collection & { diff --git a/packages/common/src/store/storeContext.ts b/packages/common/src/store/storeContext.ts index 7fb6302e3f..11aa9d365c 100644 --- a/packages/common/src/store/storeContext.ts +++ b/packages/common/src/store/storeContext.ts @@ -73,7 +73,7 @@ export type CommonStoreContext = { openSeaClient: OpenSeaClient audiusSdk: () => Promise imageUtils: { - createPlaylistArtwork: ( + generatePlaylistArtwork: ( urls: string[] ) => Promise<{ file: File; url: string }> } diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index 50063c976b..6b29ceef7e 100755 --- a/packages/mobile/android/app/build.gradle +++ b/packages/mobile/android/app/build.gradle @@ -117,7 +117,7 @@ android { pickFirst 'lib/armeabi-v7a/libc++_shared.so' pickFirst 'lib/arm64-v8a/libc++_shared.so' } - + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -132,13 +132,13 @@ android { // versionCode is automatically incremented in CI versionCode 1 // Make sure this is above the currently released Android version in the play store if your changes touch native code: - versionName "1.1.391" + versionName "1.1.393" resValue "string", "build_config_package", "co.audius.app" resValue 'string', "CODE_PUSH_APK_BUILD_TIME", String.format("\"%d\"", System.currentTimeMillis()) resConfigs "en" } - productFlavors { + productFlavors { prod { resValue "string", "CodePushDeploymentKey", '"09yN4-3LHKp_GVKFJEOoDDRqx2o3fJirP5nZf"' manifestPlaceholders = [ diff --git a/packages/mobile/ios/AudiusReactNative/Info.plist b/packages/mobile/ios/AudiusReactNative/Info.plist index 02bd5ace2f..49c2e3009d 100644 --- a/packages/mobile/ios/AudiusReactNative/Info.plist +++ b/packages/mobile/ios/AudiusReactNative/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.1.67 + 1.1.68 CFBundleSignature ???? CFBundleURLTypes diff --git a/packages/mobile/src/app/AppContextProvider.tsx b/packages/mobile/src/app/AppContextProvider.tsx index f05febb8f0..f0f74eb42b 100644 --- a/packages/mobile/src/app/AppContextProvider.tsx +++ b/packages/mobile/src/app/AppContextProvider.tsx @@ -4,7 +4,9 @@ import { AppContext } from '@audius/common' import { useAsync } from 'react-use' import * as analytics from 'app/services/analytics' +import { audiusBackendInstance } from 'app/services/audius-backend-instance' import { getStorageNodeSelector } from 'app/services/sdk/storageNodeSelector' +import { generatePlaylistArtwork } from 'app/utils/generatePlaylistArtwork' type AppContextProviderProps = { children: ReactNode @@ -18,7 +20,9 @@ export const AppContextProvider = (props: AppContextProviderProps) => { const value = useMemo( () => ({ analytics, - storageNodeSelector + storageNodeSelector, + imageUtils: { generatePlaylistArtwork }, + audiusBackend: audiusBackendInstance }), [storageNodeSelector] ) diff --git a/packages/mobile/src/components/fields/PickArtworkField.tsx b/packages/mobile/src/components/fields/PickArtworkField.tsx index bf30c79754..402d5b0bc4 100644 --- a/packages/mobile/src/components/fields/PickArtworkField.tsx +++ b/packages/mobile/src/components/fields/PickArtworkField.tsx @@ -1,6 +1,7 @@ -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useMemo } from 'react' -import { useField } from 'formik' +import { useGeneratePlaylistArtwork } from '@audius/common' +import { useField, useFormikContext } from 'formik' import { capitalize } from 'lodash' import { View } from 'react-native' import type { Asset } from 'react-native-image-picker' @@ -56,7 +57,10 @@ const useStyles = makeStyles(({ palette, spacing }) => ({ const messages = { addArtwork: 'Add Artwork', - changeArtwork: 'Change Artwork' + changeArtwork: 'Change Artwork', + removeArtwork: 'Remove Artwork', + removingArtwork: 'Removing Artwork', + updatingArtwork: 'Updating Artwork' } type PickArtworkFieldProps = { @@ -67,18 +71,21 @@ export const PickArtworkField = (props: PickArtworkFieldProps) => { const { name } = props const styles = useStyles() const { neutralLight8 } = useThemeColors() - // const name = 'artwork' - const [{ value }, { error, touched }, { setValue }] = useField(name) + const [{ value }, { error, touched }, { setValue: setArtwork }] = + useField(name) const [{ value: existingTrackArtwork }] = useField('trackArtwork') const trackArtworkUrl = value?.url ?? existingTrackArtwork + const [{ value: collectionId }] = useField('playlist_id') + const [{ value: isImageAutogenerated }, , { setValue: setIsAutogenerated }] = + useField('is_image_autogenerated') const { secondary } = useThemeColors() - const [isLoading, setIsLoading] = useState(false) + const { status, setStatus } = useFormikContext() - const handlePress = useCallback(() => { + const handleChangeArtwork = useCallback(() => { const handleImageSelected = (_image: Image, rawResponse: Asset) => { - setValue({ + setArtwork({ url: rawResponse.uri, file: { uri: rawResponse.uri, @@ -87,23 +94,35 @@ export const PickArtworkField = (props: PickArtworkFieldProps) => { }, source: 'original' }) - setIsLoading(true) + setIsAutogenerated(false) + setStatus({ imageLoading: true }) } launchSelectImageActionSheet(handleImageSelected, secondary) - }, [secondary, setValue]) + }, [secondary, setArtwork, setIsAutogenerated, setStatus]) const source = useMemo(() => ({ uri: trackArtworkUrl }), [trackArtworkUrl]) + const generatePlaylistArtwork = useGeneratePlaylistArtwork(collectionId) + + const handleRemoveArtwork = useCallback(async () => { + setStatus({ imageLoading: true, imageGenerating: true }) + const { file, url } = await generatePlaylistArtwork() + setArtwork({ url, file }) + setIsAutogenerated(true) + }, [setStatus, generatePlaylistArtwork, setArtwork, setIsAutogenerated]) + return ( setIsLoading(false)} + onLoad={() => + setStatus({ imageLoading: false, imageGenerating: false }) + } style={styles.image} noSkeleton > - {isLoading ? ( + {status.imageLoading || status.imageGenerating ? ( ) : trackArtworkUrl ? null : ( @@ -114,11 +133,23 @@ export const PickArtworkField = (props: PickArtworkFieldProps) => { variant='secondaryAlt' size='large' title={ - trackArtworkUrl ? messages.changeArtwork : messages.addArtwork + status.imageGenerating && isImageAutogenerated + ? messages.updatingArtwork + : trackArtworkUrl && status.imageGenerating + ? messages.removingArtwork + : trackArtworkUrl && !isImageAutogenerated + ? messages.removeArtwork + : trackArtworkUrl + ? messages.changeArtwork + : messages.addArtwork } icon={IconPencil} iconPosition='left' - onPress={handlePress} + onPress={ + trackArtworkUrl && !isImageAutogenerated + ? handleRemoveArtwork + : handleChangeArtwork + } /> diff --git a/packages/mobile/src/hooks/useIsOfflineModeEnabled.ts b/packages/mobile/src/hooks/useIsOfflineModeEnabled.ts index 5ce02d1340..b5eb9a50b0 100644 --- a/packages/mobile/src/hooks/useIsOfflineModeEnabled.ts +++ b/packages/mobile/src/hooks/useIsOfflineModeEnabled.ts @@ -1,37 +1,7 @@ import { FeatureFlags } from '@audius/common' -import AsyncStorage from '@react-native-async-storage/async-storage' -import { useAsync } from 'react-use' import { useFeatureFlag } from './useRemoteConfig' -const OFFLINE_OVERRIDE_ASYNC_STORAGE_KEY = 'offline_mode_release_local_override' - -// DO NOT CHECK IN VALUE: true -const hardCodeOverride = false -let asyncOverride = false - -export const toggleLocalOfflineModeOverride = () => { - asyncOverride = !asyncOverride - AsyncStorage.setItem( - OFFLINE_OVERRIDE_ASYNC_STORAGE_KEY, - asyncOverride.toString() - ) - alert(`Offline mode ${asyncOverride ? 'enabled' : 'disabled'}`) -} - -export const useReadOfflineOverride = () => - useAsync(async () => { - try { - asyncOverride = - (await AsyncStorage.getItem(OFFLINE_OVERRIDE_ASYNC_STORAGE_KEY)) === - 'true' - } catch (e) { - console.log('error reading local offline mode override') - } - }) - // TODO: remove helpers when feature is shipped export const useIsOfflineModeEnabled = () => - useFeatureFlag(FeatureFlags.OFFLINE_MODE_RELEASE).isEnabled || - asyncOverride || - hardCodeOverride + useFeatureFlag(FeatureFlags.OFFLINE_MODE_RELEASE).isEnabled diff --git a/packages/mobile/src/screens/chat-screen/ChatListScreen.tsx b/packages/mobile/src/screens/chat-screen/ChatListScreen.tsx index 722029d94f..53459695ad 100644 --- a/packages/mobile/src/screens/chat-screen/ChatListScreen.tsx +++ b/packages/mobile/src/screens/chat-screen/ChatListScreen.tsx @@ -1,13 +1,13 @@ import { useCallback, useEffect } from 'react' import { chatActions, chatSelectors, Status } from '@audius/common' -import { View, Text, TouchableOpacity } from 'react-native' +import { View, TouchableOpacity } from 'react-native' import { useDispatch, useSelector } from 'react-redux' import IconCompose from 'app/assets/images/iconCompose.svg' import IconMessage from 'app/assets/images/iconMessage.svg' import Button, { ButtonType } from 'app/components/button' -import { Screen, FlatList, ScreenContent } from 'app/components/core' +import { Text, Screen, FlatList, ScreenContent } from 'app/components/core' import { useNavigation } from 'app/hooks/useNavigation' import type { AppTabScreenParamList } from 'app/screens/app-screen' import { makeStyles } from 'app/styles' @@ -60,17 +60,12 @@ const useStyles = makeStyles(({ spacing, palette, typography }) => ({ padding: spacing(6) }, startConversationTitle: { - fontSize: typography.fontSize.xxl, - fontFamily: typography.fontByWeight.bold, textAlign: 'center', - lineHeight: typography.fontSize.xxl * 1.3, - color: palette.neutral + lineHeight: typography.fontSize.xxl * 1.3 }, connect: { - fontSize: typography.fontSize.medium, textAlign: 'center', lineHeight: typography.fontSize.medium * 1.3, - color: palette.neutral, marginTop: spacing(2) }, writeMessageButton: { @@ -83,10 +78,12 @@ const ChatsEmpty = ({ onPress }: { onPress: () => void }) => { const styles = useStyles() return ( - + {messages.startConversation} - {messages.connect} + + {messages.connect} +