From a6975c5ef2c8b46d05b42c530f4d057db3b6f79b Mon Sep 17 00:00:00 2001 From: nicoback2 <36916764+nicoback2@users.noreply.github.com> Date: Fri, 15 Sep 2023 11:52:47 -0700 Subject: [PATCH] Bring in library work from protocol repo to deploy to RC (#4069) Co-authored-by: Nikki Kang --- packages/common/src/api/library.ts | 2 +- packages/common/src/models/Analytics.ts | 8 +- .../src/store/cache/collections/selectors.ts | 2 +- .../store/cache/collections/utils/index.ts | 3 +- packages/common/src/store/pages/index.ts | 1 + .../src/store/pages/saved-page/actions.ts | 112 ++++++- .../src/store/pages/saved-page/reducer.ts | 188 +++++++++--- .../src/store/pages/saved-page/selectors.ts | 163 +++++++++- .../src/store/pages/saved-page/types.ts | 57 +++- .../src/store/pages/saved-page/utils.ts | 39 +++ packages/common/src/utils/collectionUtils.ts | 30 +- packages/common/src/utils/typeUtils.ts | 2 + packages/mobile/android/app/build.gradle | 2 +- .../app/src/main/res/raw/library_dark.riv | Bin 0 -> 4905 bytes .../app/src/main/res/raw/library_default.riv | Bin 0 -> 4905 bytes .../app/src/main/res/raw/library_matrix.riv | Bin 0 -> 4905 bytes packages/mobile/ios/Assets/library_dark.riv | Bin 0 -> 4905 bytes .../mobile/ios/Assets/library_default.riv | Bin 0 -> 4905 bytes packages/mobile/ios/Assets/library_matrix.riv | Bin 0 -> 4905 bytes .../project.pbxproj | 13 + .../mobile/ios/AudiusReactNative/Info.plist | 2 +- packages/mobile/src/app/Drawers.tsx | 4 +- .../mobile/src/assets/images/iconLibrary.svg | 3 + .../FavoritesButton.tsx | 8 - .../bottom-tab-bar-buttons/LibraryButton.tsx | 8 + .../bottom-tab-bar-buttons/index.ts | 6 +- .../collection-list/AddCollectionCard.tsx | 2 +- .../collection-list/CollectionList.tsx | 4 +- .../components/core/HarmonyModalHeader.tsx | 55 ++++ .../drawers/OfflineListeningDrawer.tsx | 185 +++++++++++ .../mobile/src/components/drawers/index.ts | 1 + .../NavigationContainer.tsx | 4 +- .../share-drawer/useShareToStory.tsx | 46 +-- .../src/screens/app-screen/AppTabsScreen.tsx | 2 +- .../screens/app-screen/FavoritesTabScreen.tsx | 2 +- .../screens/favorites-screen/AlbumsTab.tsx | 33 +- .../FavoritesDownloadSection.tsx | 40 ++- .../favorites-screen/FavoritesScreen.tsx | 20 +- .../LibraryCategorySelectionMenu.tsx | 125 ++++++++ .../screens/favorites-screen/PlaylistsTab.tsx | 29 +- .../screens/favorites-screen/TracksTab.tsx | 62 +++- .../useCollectionsScreenData.ts | 120 +++++--- packages/mobile/src/store/drawers/slice.ts | 6 + .../sagas/requestDownloadAllFavoritesSaga.ts | 4 +- packages/mobile/src/utils/challenges.tsx | 4 +- .../stems/src/assets/icons/iconLibrary.svg | 3 + packages/stems/src/components/Icons/index.ts | 1 + .../src/components/SelectablePill/types.ts | 1 - .../store/cache/collections/commonSagas.js | 32 +- .../cache/collections/createPlaylistSaga.ts | 13 +- .../common/store/pages/saved/lineups/sagas.js | 216 ++++++++----- .../web/src/common/store/pages/saved/sagas.js | 142 --------- .../web/src/common/store/pages/saved/sagas.ts | 290 ++++++++++++++++++ .../common/store/social/collections/sagas.ts | 38 ++- packages/web/src/common/store/upload/sagas.js | 12 +- .../src/components/bottom-bar/BottomBar.tsx | 19 +- ...{FavoritesButton.tsx => LibraryButton.tsx} | 6 +- .../header/desktop/Header.module.css | 1 + .../src/components/nav/desktop/LeftNav.tsx | 8 +- .../nav/mobile/ConnectedBottomBar.tsx | 12 +- packages/web/src/pages/App.js | 7 +- .../src/pages/audio-rewards-page/config.tsx | 6 +- .../pages/saved-page/SavedPageProvider.tsx | 78 +++-- .../components/desktop/AlbumsTabPage.tsx | 81 ++--- .../LibraryCategorySelectionMenu.module.css | 4 + .../desktop/LibraryCategorySelectionMenu.tsx | 83 +++++ .../components/desktop/PlaylistsTabPage.tsx | 97 +++--- .../components/desktop/SavedPage.module.css | 17 + .../components/desktop/SavedPage.tsx | 72 +++-- .../components/emptyStateMessages.ts | 18 ++ .../components/mobile/NewPlaylistButton.tsx | 4 +- .../components/mobile/SavedPage.tsx | 50 ++- .../saved-page/hooks/useCollectionsData.tsx | 79 +++++ .../web/src/store/lineup/lineupForRoute.js | 5 +- packages/web/src/utils/route.ts | 6 + 75 files changed, 2192 insertions(+), 606 deletions(-) create mode 100644 packages/common/src/store/pages/saved-page/utils.ts create mode 100755 packages/mobile/android/app/src/main/res/raw/library_dark.riv create mode 100755 packages/mobile/android/app/src/main/res/raw/library_default.riv create mode 100755 packages/mobile/android/app/src/main/res/raw/library_matrix.riv create mode 100755 packages/mobile/ios/Assets/library_dark.riv create mode 100755 packages/mobile/ios/Assets/library_default.riv create mode 100755 packages/mobile/ios/Assets/library_matrix.riv create mode 100644 packages/mobile/src/assets/images/iconLibrary.svg delete mode 100644 packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/FavoritesButton.tsx create mode 100644 packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/LibraryButton.tsx create mode 100644 packages/mobile/src/components/core/HarmonyModalHeader.tsx create mode 100644 packages/mobile/src/components/drawers/OfflineListeningDrawer.tsx create mode 100644 packages/mobile/src/screens/favorites-screen/LibraryCategorySelectionMenu.tsx create mode 100644 packages/stems/src/assets/icons/iconLibrary.svg delete mode 100644 packages/web/src/common/store/pages/saved/sagas.js create mode 100644 packages/web/src/common/store/pages/saved/sagas.ts rename packages/web/src/components/bottom-bar/buttons/{FavoritesButton.tsx => LibraryButton.tsx} (84%) create mode 100644 packages/web/src/pages/saved-page/components/desktop/LibraryCategorySelectionMenu.module.css create mode 100644 packages/web/src/pages/saved-page/components/desktop/LibraryCategorySelectionMenu.tsx create mode 100644 packages/web/src/pages/saved-page/components/emptyStateMessages.ts create mode 100644 packages/web/src/pages/saved-page/hooks/useCollectionsData.tsx diff --git a/packages/common/src/api/library.ts b/packages/common/src/api/library.ts index 99000a9d0a..d6fc097add 100644 --- a/packages/common/src/api/library.ts +++ b/packages/common/src/api/library.ts @@ -37,7 +37,7 @@ const fetchLibraryCollections = async ({ offset, limit, category, - query = '', + query, sortMethod = 'added_date', sortDirection = 'desc' } = args diff --git a/packages/common/src/models/Analytics.ts b/packages/common/src/models/Analytics.ts index d40e88b8aa..66cfa99e4b 100644 --- a/packages/common/src/models/Analytics.ts +++ b/packages/common/src/models/Analytics.ts @@ -586,7 +586,7 @@ export enum RepostSource { TRACK_PAGE = 'page', COLLECTION_PAGE = 'collection page', HISTORY_PAGE = 'history page', - FAVORITES_PAGE = 'favorites page', + LIBRARY_PAGE = 'library page', OVERFLOW = 'overflow', TRACK_LIST = 'track list' } @@ -597,7 +597,7 @@ export enum FavoriteSource { TRACK_PAGE = 'page', COLLECTION_PAGE = 'collection page', HISTORY_PAGE = 'history page', - FAVORITES_PAGE = 'favorites page', + LIBRARY_PAGE = 'library page', OVERFLOW = 'overflow', TRACK_LIST = 'track list', SIGN_UP = 'sign up', @@ -685,7 +685,7 @@ export enum CreatePlaylistSource { NAV = 'nav', CREATE_PAGE = 'create page', FROM_TRACK = 'from track', - FAVORITES_PAGE = 'favorites page', + LIBRARY_PAGE = 'library page', PROFILE_PAGE = 'profile page' } @@ -1025,7 +1025,7 @@ export enum PlaybackSource { PLAYLIST_TRACK = 'playlist page track list', PLAYLIST_TILE_TRACK = 'playlist track tile', HISTORY_PAGE = 'history page', - FAVORITES_PAGE = 'favorites page', + LIBRARY_PAGE = 'library page', PASSIVE = 'passive', EMBED_PLAYER = 'embed player', CHAT_TRACK = 'chat_track', diff --git a/packages/common/src/store/cache/collections/selectors.ts b/packages/common/src/store/cache/collections/selectors.ts index 217e5fe9a3..df835914e9 100644 --- a/packages/common/src/store/cache/collections/selectors.ts +++ b/packages/common/src/store/cache/collections/selectors.ts @@ -195,7 +195,7 @@ export const getTracksFromCollection = ( .filter(Boolean) as EnhancedCollectionTrack[] } -type EnhancedCollection = Collection & { user: User } +export type EnhancedCollection = Collection & { user: User } export const getCollectionWithUser = ( state: CommonState, props: { id?: ID } diff --git a/packages/common/src/store/cache/collections/utils/index.ts b/packages/common/src/store/cache/collections/utils/index.ts index cf6599bab6..36c65f4890 100644 --- a/packages/common/src/store/cache/collections/utils/index.ts +++ b/packages/common/src/store/cache/collections/utils/index.ts @@ -1,3 +1,4 @@ -// TODO: Migrate utils from packages/web/common/store/cache/collections/utils to here +// TODO: Finish common migration - migrate utils from packages/web/common/store/cache/collections/utils to here: +// https://linear.app/audius/issue/C-1280/migrate-remaining-webcommon-to-common export * from './reformatCollection' diff --git a/packages/common/src/store/pages/index.ts b/packages/common/src/store/pages/index.ts index f26f5360fa..473e9870c2 100644 --- a/packages/common/src/store/pages/index.ts +++ b/packages/common/src/store/pages/index.ts @@ -61,6 +61,7 @@ export { tracksActions as savedPageTracksLineupActions } from './saved-page/line export * as savedPageActions from './saved-page/actions' export * as savedPageSelectors from './saved-page/selectors' export * from './saved-page/types' +export * from './saved-page/utils' export { default as savedPageReducer } from './saved-page/reducer' export { diff --git a/packages/common/src/store/pages/saved-page/actions.ts b/packages/common/src/store/pages/saved-page/actions.ts index 13c303faf8..57fc4e738a 100644 --- a/packages/common/src/store/pages/saved-page/actions.ts +++ b/packages/common/src/store/pages/saved-page/actions.ts @@ -1,5 +1,7 @@ -// @ts-nocheck -// TODO(nkang) - convert to TS +import { Favorite } from 'models/Favorite' + +import { LibraryCategory, LibraryCategoryType, SavedPageTabs } from './types' + export const FETCH_SAVES = 'SAVED/FETCH_SAVES' export const FETCH_SAVES_REQUESTED = 'SAVED/FETCH_SAVES_REQUESTED' export const FETCH_SAVES_SUCCEEDED = 'SAVED/FETCH_SAVES_SUCCEEDED' @@ -13,12 +15,21 @@ export const FETCH_MORE_SAVES_FAILED = 'SAVED/FETCH_MORE_SAVES_FAILED' // Usually when filtering export const END_FETCHING = 'SAVED/END_FETCHING' -export const ADD_LOCAL_SAVE = 'SAVED/ADD_LOCAL_SAVE' -export const REMOVE_LOCAL_SAVE = 'SAVED/REMOVE_LOCAL_SAVE' +export const ADD_LOCAL_TRACK = 'SAVED/ADD_LOCAL_TRACK' +export const REMOVE_LOCAL_TRACK = 'SAVED/REMOVE_LOCAL_TRACK' +export const ADD_LOCAL_COLLECTION = 'SAVED/ADD_LOCAL_COLLECTION' +export const REMOVE_LOCAL_COLLECTION = 'SAVED/REMOVE_LOCAL_COLLECTION' + +export const SET_SELECTED_CATEGORY = 'SAVED/SET_SELECTED_CATEGORY' +export const INIT_COLLECTIONS_CATEGORY_FROM_LOCAL_STORAGE = + 'SAVED/INIT_COLLECTIONS_CATEGORY_FROM_LOCAL_STORAGE' +export const INIT_TRACKS_CATEGORY_FROM_LOCAL_STORAGE = + 'SAVED/INIT_TRACKS_CATEGORY_FROM_LOCAL_STORAGE' export const fetchSaves = ( // the filter query for the "get tracks" query query = '', + category: LibraryCategoryType = LibraryCategory.Favorite, // the sort method for the "get tracks" query sortMethod = '', // the sort direction for the "get tracks" query @@ -29,6 +40,7 @@ export const fetchSaves = ( limit = 50 ) => ({ type: FETCH_SAVES, + category, offset, limit, query, @@ -39,6 +51,7 @@ export const fetchSaves = ( export const fetchMoreSaves = ( // the filter query for the "get tracks" query query = '', + category: LibraryCategoryType = LibraryCategory.Favorite, // the sort method for the "get tracks" query sortMethod = '', // the sort direction for the "get tracks" query @@ -49,6 +62,7 @@ export const fetchMoreSaves = ( limit = 50 ) => ({ type: FETCH_MORE_SAVES, + category, offset, limit, query, @@ -60,7 +74,7 @@ export const fetchSavesRequested = () => ({ type: FETCH_SAVES_REQUESTED }) -export const fetchSavesSucceeded = (saves) => ({ +export const fetchSavesSucceeded = (saves: Favorite[]) => ({ type: FETCH_SAVES_SUCCEEDED, saves }) @@ -69,7 +83,7 @@ export const fetchSavesFailed = () => ({ type: FETCH_SAVES_FAILED }) -export const fetchMoreSavesSucceeded = (saves, offset) => ({ +export const fetchMoreSavesSucceeded = (saves: Favorite[], offset: number) => ({ type: FETCH_MORE_SAVES_SUCCEEDED, saves, offset @@ -79,18 +93,90 @@ export const fetchMoreSavesFailed = () => ({ type: FETCH_MORE_SAVES_FAILED }) -export const endFetching = (endIndex) => ({ +export const endFetching = (endIndex: number) => ({ type: END_FETCHING, endIndex }) -export const addLocalSave = (trackId, uid) => ({ - type: ADD_LOCAL_SAVE, +export const addLocalTrack = ({ trackId, - uid + uid, + category +}: { + trackId: number + uid: string + category: LibraryCategoryType +}) => ({ + type: ADD_LOCAL_TRACK, + trackId, + uid, + category +}) + +export const removeLocalTrack = ({ + trackId, + category +}: { + trackId: number + category: LibraryCategoryType +}) => ({ + type: REMOVE_LOCAL_TRACK, + trackId, + category +}) + +export const addLocalCollection = ({ + collectionId, + isAlbum, + category +}: { + collectionId: number + isAlbum: boolean + category: LibraryCategoryType +}) => ({ + type: ADD_LOCAL_COLLECTION, + collectionId, + isAlbum, + category +}) + +export const removeLocalCollection = ({ + collectionId, + isAlbum, + category +}: { + collectionId: number + isAlbum: boolean + category: LibraryCategoryType +}) => ({ + type: REMOVE_LOCAL_COLLECTION, + collectionId, + isAlbum, + category +}) + +export const initializeTracksCategoryFromLocalStorage = ( + category: LibraryCategoryType +) => ({ + type: INIT_TRACKS_CATEGORY_FROM_LOCAL_STORAGE, + category +}) + +export const initializeCollectionsCategoryFromLocalStorage = ( + category: LibraryCategoryType +) => ({ + type: INIT_COLLECTIONS_CATEGORY_FROM_LOCAL_STORAGE, + category }) -export const removeLocalSave = (trackId) => ({ - type: REMOVE_LOCAL_SAVE, - trackId +export const setSelectedCategory = ({ + category, + currentTab +}: { + category: LibraryCategoryType + currentTab: SavedPageTabs +}) => ({ + type: SET_SELECTED_CATEGORY, + category, + currentTab }) diff --git a/packages/common/src/store/pages/saved-page/reducer.ts b/packages/common/src/store/pages/saved-page/reducer.ts index a6f430f588..9b55f42c47 100644 --- a/packages/common/src/store/pages/saved-page/reducer.ts +++ b/packages/common/src/store/pages/saved-page/reducer.ts @@ -1,42 +1,102 @@ -// @ts-nocheck -// TODO(nkang) - convert to TS +import { ID } from 'models/Identifiers' import { asLineup } from 'store/lineup/reducer' import { + ADD_LOCAL_COLLECTION, + ADD_LOCAL_TRACK, + END_FETCHING, + FETCH_MORE_SAVES, + FETCH_MORE_SAVES_FAILED, + FETCH_MORE_SAVES_SUCCEEDED, FETCH_SAVES, + FETCH_SAVES_FAILED, FETCH_SAVES_REQUESTED, FETCH_SAVES_SUCCEEDED, - FETCH_SAVES_FAILED, - FETCH_MORE_SAVES, - FETCH_MORE_SAVES_SUCCEEDED, - FETCH_MORE_SAVES_FAILED, - ADD_LOCAL_SAVE, - REMOVE_LOCAL_SAVE, - END_FETCHING + INIT_COLLECTIONS_CATEGORY_FROM_LOCAL_STORAGE, + INIT_TRACKS_CATEGORY_FROM_LOCAL_STORAGE, + REMOVE_LOCAL_COLLECTION, + REMOVE_LOCAL_TRACK, + SET_SELECTED_CATEGORY } from 'store/pages/saved-page/actions' import tracksReducer, { initialState as initialLineupState } from 'store/pages/saved-page/lineups/tracks/reducer' import { signOut } from 'store/sign-out/slice' +import { ActionsMap } from 'utils/reducer' import { PREFIX as tracksPrefix } from './lineups/tracks/actions' +import { LibraryCategory, LibraryCategoryType, SavedPageState } from './types' +import { calculateNewLibraryCategories } from './utils' const initialState = { - // id => uid - localSaves: {}, - saves: [], + trackSaves: [], initialFetch: false, hasReachedEnd: false, fetchingMore: false, - tracks: initialLineupState + tracks: initialLineupState, + tracksCategory: LibraryCategory.Favorite, + collectionsCategory: LibraryCategory.Favorite, + local: { + track: { + favorites: { + added: {}, + removed: {} + }, + reposts: { + added: {}, + removed: {} + }, + purchased: { + added: {} + } + }, + album: { + favorites: { + added: [], + removed: [] + }, + reposts: { + added: [], + removed: [] + }, + purchased: { + added: [] + } + }, + playlist: { + favorites: { + added: [], + removed: [] + }, + reposts: { + added: [], + removed: [] + } + } + } +} as SavedPageState + +const getCategoryLocalStateKey = ( + category: Omit +) => { + switch (category) { + case LibraryCategory.Favorite: + return 'favorites' + case LibraryCategory.Purchase: + return 'purchased' + case LibraryCategory.Repost: + return 'reposts' + default: + return 'favorites' + } } -const actionsMap = { - [FETCH_SAVES](state, action) { +const actionsMap: ActionsMap = { + [FETCH_SAVES](state) { return { ...state } }, - [FETCH_SAVES_REQUESTED](state, action) { + [FETCH_SAVES_REQUESTED](state) { return { ...state, initialFetch: true, @@ -46,70 +106,122 @@ const actionsMap = { [FETCH_SAVES_SUCCEEDED](state, action) { return { ...state, - saves: action.saves, + trackSaves: action.saves, initialFetch: false } }, - [FETCH_MORE_SAVES](state, action) { + [FETCH_MORE_SAVES](state) { return { ...state, fetchingMore: true } }, - [FETCH_SAVES_FAILED](state, action) { + [FETCH_SAVES_FAILED](state) { return { ...state, fetchingMore: false, - saves: [] + trackSaves: [] } }, [FETCH_MORE_SAVES_SUCCEEDED](state, action) { - const savesCopy = state.saves.slice() + const savesCopy = state.trackSaves.slice() savesCopy.splice(action.offset, action.saves.length, ...action.saves) return { ...state, fetchingMore: false, - saves: savesCopy + trackSaves: savesCopy } }, - [FETCH_MORE_SAVES_FAILED](state, action) { + [FETCH_MORE_SAVES_FAILED](state) { return { ...state } }, [END_FETCHING](state, action) { - const savesCopy = state.saves.slice(0, action.endIndex) + const savesCopy = state.trackSaves.slice(0, action.endIndex) return { ...state, - saves: savesCopy, + trackSaves: savesCopy, hasReachedEnd: true } }, - [ADD_LOCAL_SAVE](state, action) { - return { - ...state, - localSaves: { - ...state.localSaves, - [action.trackId]: action.uid - } + [ADD_LOCAL_TRACK](state, action) { + const categoryKey = getCategoryLocalStateKey(action.category) + const newState = { ...state } + newState.local.track[categoryKey].added = { + ...newState.local.track[categoryKey].added, + [action.trackId]: action.uid } + return newState }, - [REMOVE_LOCAL_SAVE](state, action) { + [REMOVE_LOCAL_TRACK](state, action) { + const categoryKey = getCategoryLocalStateKey(action.category) const newState = { ...state } - delete newState.localSaves[action.trackId] - newState.saves = newState.saves.filter( + delete newState.local.track[categoryKey].added[action.trackId] + + newState.trackSaves = newState.trackSaves.filter( ({ save_item_id: id }) => id !== action.trackId ) return newState }, - [signOut.type](state) { + [ADD_LOCAL_COLLECTION](state, action) { + const kindKey = action.isAlbum ? 'album' : 'playlist' + const categoryKey = getCategoryLocalStateKey(action.category) + const newState = { ...state } + newState.local[kindKey][categoryKey].added = [ + action.collectionId, + ...newState.local[kindKey][categoryKey].added + ] + newState.local[kindKey][categoryKey].removed = newState.local[kindKey][ + categoryKey + ].removed.filter((id: ID) => id !== action.collectionId) + + return newState + }, + [REMOVE_LOCAL_COLLECTION](state, action) { + const kindKey = action.isAlbum ? 'album' : 'playlist' + const categoryKey = getCategoryLocalStateKey(action.category) + const newState = { ...state } + newState.local[kindKey][categoryKey].removed = [ + action.collectionId, + ...newState.local[kindKey][categoryKey].removed + ] + newState.local[kindKey][categoryKey].added = newState.local[kindKey][ + categoryKey + ].added.filter((id: ID) => id !== action.collectionId) + + return newState + }, + [SET_SELECTED_CATEGORY](state, action) { + return { + ...state, + ...calculateNewLibraryCategories({ + currentTab: action.currentTab, + chosenCategory: action.category, + prevTracksCategory: state.tracksCategory + }) + } + }, + [INIT_TRACKS_CATEGORY_FROM_LOCAL_STORAGE](state, action) { + return { + ...state, + tracksCategory: action.category + } + }, + [INIT_COLLECTIONS_CATEGORY_FROM_LOCAL_STORAGE](state, action) { + return { + ...state, + collectionsCategory: action.category + } + }, + [signOut.type]() { return initialState } } const tracksLineupReducer = asLineup(tracksPrefix, tracksReducer) -const reducer = (state = initialState, action) => { - const tracks = tracksLineupReducer(state.tracks, action) +const reducer = (state = initialState, action: any) => { + const tracks = tracksLineupReducer(state.tracks as any, action) if (tracks !== state.tracks) return { ...state, tracks } const matchingReduceFunction = actionsMap[action.type] diff --git a/packages/common/src/store/pages/saved-page/selectors.ts b/packages/common/src/store/pages/saved-page/selectors.ts index 98358fbb2d..fed17b8c01 100644 --- a/packages/common/src/store/pages/saved-page/selectors.ts +++ b/packages/common/src/store/pages/saved-page/selectors.ts @@ -1,13 +1,166 @@ +import { uniq } from 'lodash' + import { CommonState } from 'store/commonStore' import { ID } from '../../../models/Identifiers' +import { LibraryCategory, SavedPageTabs } from './types' + export const getSaved = (state: CommonState) => state.pages.savedPage -export const getSaves = (state: CommonState) => state.pages.savedPage.saves -export const getLocalSaves = (state: CommonState) => - state.pages.savedPage.localSaves -export const getLocalSave = (state: CommonState, props: { id: ID }) => - state.pages.savedPage.localSaves[props.id] +export const getTrackSaves = (state: CommonState) => + state.pages.savedPage.trackSaves + +export const getCollectionsCategory = (state: CommonState) => { + return state.pages.savedPage.collectionsCategory +} + +export const getTracksCategory = (state: CommonState) => { + return state.pages.savedPage.tracksCategory +} + +export const getCategory = ( + state: CommonState, + props: { currentTab: SavedPageTabs } +) => { + if (props.currentTab === SavedPageTabs.TRACKS) { + return getTracksCategory(state) + } else { + return getCollectionsCategory(state) + } +} + +export const getLocalTrackFavorites = (state: CommonState) => + state.pages.savedPage.local.track.favorites.added +export const getLocalTrackFavorite = (state: CommonState, props: { id: ID }) => + state.pages.savedPage.local.track.favorites.added[props.id] +export const getLocalTrackReposts = (state: CommonState) => + state.pages.savedPage.local.track.reposts.added +export const getLocalTrackRepost = (state: CommonState, props: { id: ID }) => + state.pages.savedPage.local.track.reposts.added[props.id] +export const getLocalTrackPurchases = (state: CommonState) => + state.pages.savedPage.local.track.purchased.added +export const getLocalTrackPurchase = (state: CommonState, props: { id: ID }) => + state.pages.savedPage.local.track.purchased.added[props.id] + +export const getLocalAlbumFavorites = (state: CommonState) => + state.pages.savedPage.local.album.favorites.added +export const getLocalAlbumReposts = (state: CommonState) => + state.pages.savedPage.local.album.reposts.added +export const getLocalAlbumPurchases = (state: CommonState) => + state.pages.savedPage.local.album.purchased.added +export const getLocalRemovedAlbumFavorites = (state: CommonState) => + state.pages.savedPage.local.album.favorites.removed +export const getLocalRemovedAlbumReposts = (state: CommonState) => + state.pages.savedPage.local.album.reposts.removed + +export const getLocalPlaylistFavorites = (state: CommonState) => + state.pages.savedPage.local.playlist.favorites.added +export const getLocalPlaylistReposts = (state: CommonState) => + state.pages.savedPage.local.playlist.reposts.added +export const getLocalRemovedPlaylistFavorites = (state: CommonState) => + state.pages.savedPage.local.playlist.favorites.removed +export const getLocalRemovedPlaylistReposts = (state: CommonState) => + state.pages.savedPage.local.playlist.favorites.removed + +/** Get the tracks in currently selected category that have been added to the library in current session */ +export const getSelectedCategoryLocalTrackAdds = (state: CommonState) => { + const selectedCategory = getCategory(state, { + currentTab: SavedPageTabs.TRACKS + }) + const localFavorites = getLocalTrackFavorites(state) + const localPurchases = getLocalTrackPurchases(state) + const localReposts = getLocalTrackReposts(state) + let localLibraryAdditions + if (selectedCategory === LibraryCategory.Favorite) { + localLibraryAdditions = localFavorites + } else if (selectedCategory === LibraryCategory.Purchase) { + localLibraryAdditions = localPurchases + } else if (selectedCategory === LibraryCategory.Repost) { + localLibraryAdditions = localReposts + } else { + // Category = ALL + localLibraryAdditions = { + ...localReposts, + ...localFavorites, + ...localPurchases + } + } + + return localLibraryAdditions +} + +const getSelectedCategoryLocalCollectionUpdates = ( + state: CommonState, + props: { collectionType: 'album' | 'playlist'; updateType: 'add' | 'remove' } +) => { + const { collectionType, updateType } = props + const currentTab = + collectionType === 'album' ? SavedPageTabs.ALBUMS : SavedPageTabs.PLAYLISTS + const selectedCategory = getCategory(state, { currentTab }) + let localFavorites: ID[], localPurchases: ID[], localReposts: ID[] + if (updateType === 'add') { + localFavorites = + collectionType === 'album' + ? getLocalAlbumFavorites(state) + : getLocalPlaylistFavorites(state) + localPurchases = + collectionType === 'album' ? getLocalAlbumPurchases(state) : [] // Can't buy playlists + localReposts = + collectionType === 'album' + ? getLocalAlbumReposts(state) + : getLocalPlaylistReposts(state) + } else { + localFavorites = + collectionType === 'album' + ? getLocalRemovedAlbumFavorites(state) + : getLocalRemovedPlaylistFavorites(state) + localPurchases = [] // Can't remove purchases + localReposts = + collectionType === 'album' + ? getLocalRemovedAlbumReposts(state) + : getLocalRemovedPlaylistReposts(state) + } + + switch (selectedCategory) { + case LibraryCategory.Favorite: + return localFavorites + case LibraryCategory.Purchase: + return localPurchases + case LibraryCategory.Repost: + return localReposts + default: + // Category = ALL + return uniq([...localReposts, ...localFavorites, ...localPurchases]) + } +} + +export const getSelectedCategoryLocalAlbumAdds = (state: CommonState) => { + return getSelectedCategoryLocalCollectionUpdates(state, { + collectionType: 'album', + updateType: 'add' + }) +} +export const getSelectedCategoryLocalAlbumRemovals = (state: CommonState) => { + return getSelectedCategoryLocalCollectionUpdates(state, { + collectionType: 'album', + updateType: 'remove' + }) +} +export const getSelectedCategoryLocalPlaylistRemovals = ( + state: CommonState +) => { + return getSelectedCategoryLocalCollectionUpdates(state, { + collectionType: 'playlist', + updateType: 'remove' + }) +} +export const getSelectedCategoryLocalPlaylistAdds = (state: CommonState) => { + return getSelectedCategoryLocalCollectionUpdates(state, { + collectionType: 'playlist', + updateType: 'add' + }) +} + export const getInitialFetchStatus = (state: CommonState) => state.pages.savedPage.initialFetch export const getIsFetchingMore = (state: CommonState) => diff --git a/packages/common/src/store/pages/saved-page/types.ts b/packages/common/src/store/pages/saved-page/types.ts index 8d4ceae3dc..de22d7d3e0 100644 --- a/packages/common/src/store/pages/saved-page/types.ts +++ b/packages/common/src/store/pages/saved-page/types.ts @@ -1,5 +1,8 @@ +import { full } from '@audius/sdk' import { Moment } from 'moment' +import { ValueOf } from 'utils/typeUtils' + import { UID, ID, @@ -9,13 +12,63 @@ import { LineupTrack } from '../../../models' +export const LIBRARY_TRACKS_CATEGORY_LS_KEY = 'libraryTracksCategory' + +export const LIBRARY_COLLECTIONS_CATEGORY_LS_KEY = 'libraryCollectionsCategory' + +export const LibraryCategory = full.GetUserLibraryTracksTypeEnum +export type LibraryCategoryType = ValueOf + +export function isLibraryCategory(value: string): value is LibraryCategoryType { + return Object.values(LibraryCategory).includes(value as LibraryCategoryType) +} export interface SavedPageState { - localSaves: { [id: number]: UID } + local: { + track: { + favorites: { + added: { [id: number]: UID } + removed: { [id: number]: UID } + } + reposts: { + added: { [id: number]: UID } + removed: { [id: number]: UID } + } + purchased: { + added: { [id: number]: UID } + } + } + album: { + favorites: { + added: ID[] + removed: ID[] + } + reposts: { + added: ID[] + removed: ID[] + } + purchased: { + added: ID[] + } + } + playlist: { + favorites: { + added: ID[] + removed: ID[] + } + reposts: { + added: ID[] + removed: ID[] + } + } + } tracks: LineupState - saves: Favorite[] + trackSaves: Favorite[] hasReachedEnd: boolean initialFetch: boolean fetchingMore: boolean + + tracksCategory: LibraryCategoryType + collectionsCategory: LibraryCategoryType } export enum SavedPageTabs { diff --git a/packages/common/src/store/pages/saved-page/utils.ts b/packages/common/src/store/pages/saved-page/utils.ts new file mode 100644 index 0000000000..6be4938463 --- /dev/null +++ b/packages/common/src/store/pages/saved-page/utils.ts @@ -0,0 +1,39 @@ +import { LibraryCategory, LibraryCategoryType, SavedPageTabs } from './types' + +export const calculateNewLibraryCategories = ({ + currentTab, + chosenCategory, + prevTracksCategory +}: { + currentTab: SavedPageTabs + chosenCategory: LibraryCategoryType + prevTracksCategory: unknown +}) => { + if ( + currentTab === SavedPageTabs.TRACKS && + chosenCategory === LibraryCategory.Purchase + ) { + // If the category is changed to "Purchased" on the tracks tab, change the collections tabs category to "All" because collections tabs don't have "Purchased". + return { + tracksCategory: chosenCategory, + collectionsCategory: LibraryCategory.All + } + } + if ( + (currentTab === SavedPageTabs.ALBUMS || + currentTab === SavedPageTabs.PLAYLISTS) && + prevTracksCategory === LibraryCategory.Purchase + ) { + // If tracks tab is on "Purchased", we want it to stay on "Purchased" until the user goes back to it. + return { + tracksCategory: prevTracksCategory, + collectionsCategory: chosenCategory + } + } + + // Default behavior: change category for all tabs. + return { + collectionsCategory: chosenCategory, + tracksCategory: chosenCategory + } +} diff --git a/packages/common/src/utils/collectionUtils.ts b/packages/common/src/utils/collectionUtils.ts index de47479459..207acb897e 100644 --- a/packages/common/src/utils/collectionUtils.ts +++ b/packages/common/src/utils/collectionUtils.ts @@ -1,22 +1,26 @@ import { AccountCollection } from 'store/account' +import { EnhancedCollection } from 'store/cache/collections/selectors' type FilterCollectionsOptions = { filterText?: string } -export function filterCollections( - collections: AccountCollection[], - { filterText = '' }: FilterCollectionsOptions -): AccountCollection[] { - return collections.filter((item: AccountCollection) => { - if (filterText) { - const matchesPlaylistName = - item.name.toLowerCase().indexOf(filterText.toLowerCase()) > -1 - const matchesOwnerName = - item.user.handle.toLowerCase().indexOf(filterText.toLowerCase()) > -1 +export const isAccountCollection = ( + collection: AccountCollection | EnhancedCollection +): collection is AccountCollection => { + return (collection as AccountCollection).name !== undefined +} + +export function filterCollections< + T extends AccountCollection | EnhancedCollection +>(collections: T[], { filterText = '' }: FilterCollectionsOptions): T[] { + return collections.filter((item: AccountCollection | EnhancedCollection) => { + const name = isAccountCollection(item) ? item.name : item.playlist_name + const matchesPlaylistName = + name.toLowerCase().indexOf(filterText.toLowerCase()) > -1 + const matchesOwnerName = + item.user.handle.toLowerCase().indexOf(filterText.toLowerCase()) > -1 - return matchesPlaylistName || matchesOwnerName - } - return true + return matchesPlaylistName || matchesOwnerName }) } diff --git a/packages/common/src/utils/typeUtils.ts b/packages/common/src/utils/typeUtils.ts index 6898a9b20b..31a8ced79b 100644 --- a/packages/common/src/utils/typeUtils.ts +++ b/packages/common/src/utils/typeUtils.ts @@ -29,3 +29,5 @@ export type Maybe = T | undefined * */ export type Brand = T & { _brand: U } + +export type ValueOf = T[keyof T] diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index 501572d998..7cfa644289 100755 --- a/packages/mobile/android/app/build.gradle +++ b/packages/mobile/android/app/build.gradle @@ -132,7 +132,7 @@ 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.397" + versionName "1.1.398" resValue "string", "build_config_package", "co.audius.app" resValue 'string', "CODE_PUSH_APK_BUILD_TIME", String.format("\"%d\"", System.currentTimeMillis()) resConfigs "en" diff --git a/packages/mobile/android/app/src/main/res/raw/library_dark.riv b/packages/mobile/android/app/src/main/res/raw/library_dark.riv new file mode 100755 index 0000000000000000000000000000000000000000..b252efd15c3bf9c27738e2ae3fad9b84c6714ac1 GIT binary patch literal 4905 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 APPL CFBundleShortVersionString - 1.1.72 + 1.1.73 CFBundleSignature ???? CFBundleURLTypes diff --git a/packages/mobile/src/app/Drawers.tsx b/packages/mobile/src/app/Drawers.tsx index 43824bd6db..2c4e6a28e2 100644 --- a/packages/mobile/src/app/Drawers.tsx +++ b/packages/mobile/src/app/Drawers.tsx @@ -45,7 +45,8 @@ import { RemoveDownloadedCollectionDrawer, RemoveDownloadedFavoritesDrawer, UnfavoriteDownloadedCollectionDrawer, - DeleteTrackConfirmationDrawer + DeleteTrackConfirmationDrawer, + OfflineListeningDrawer } from '../components/drawers' import { ShareToStoryProgressDrawer } from '../components/share-drawer/useShareToStory' import { VipDiscordDrawer } from '../components/vip-discord-drawer' @@ -114,6 +115,7 @@ const commonDrawersMap: { [Modal in Modals]?: ComponentType } = { const nativeDrawersMap: { [DrawerName in Drawer]?: ComponentType } = { EnablePushNotifications: EnablePushNotificationsDrawer, + OfflineListening: OfflineListeningDrawer, DownloadTrackProgress: DownloadTrackProgressDrawer, ForgotPassword: ForgotPasswordDrawer, DeleteTrackConfirmation: DeleteTrackConfirmationDrawer, diff --git a/packages/mobile/src/assets/images/iconLibrary.svg b/packages/mobile/src/assets/images/iconLibrary.svg new file mode 100644 index 0000000000..e178b6edbb --- /dev/null +++ b/packages/mobile/src/assets/images/iconLibrary.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/FavoritesButton.tsx b/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/FavoritesButton.tsx deleted file mode 100644 index ba4f80efda..0000000000 --- a/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/FavoritesButton.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import type { BaseBottomTabBarButtonProps } from './BottomTabBarButton' -import { BottomTabBarButton } from './BottomTabBarButton' - -type FavoritesButtonProps = BaseBottomTabBarButtonProps - -export const FavoritesButton = (props: FavoritesButtonProps) => { - return -} diff --git a/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/LibraryButton.tsx b/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/LibraryButton.tsx new file mode 100644 index 0000000000..b97bfcd69e --- /dev/null +++ b/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/LibraryButton.tsx @@ -0,0 +1,8 @@ +import type { BaseBottomTabBarButtonProps } from './BottomTabBarButton' +import { BottomTabBarButton } from './BottomTabBarButton' + +type LibraryButtonProps = BaseBottomTabBarButtonProps + +export const LibraryButton = (props: LibraryButtonProps) => { + return +} diff --git a/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/index.ts b/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/index.ts index 72c8cdea1c..97588d6162 100644 --- a/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/index.ts +++ b/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/index.ts @@ -1,6 +1,6 @@ import { ExploreButton } from './ExploreButton' -import { FavoritesButton } from './FavoritesButton' import { FeedButton } from './FeedButton' +import { LibraryButton } from './LibraryButton' import { NotificationsButton } from './NotificationsButton' import { TrendingButton } from './TrendingButton' @@ -8,12 +8,12 @@ export const bottomTabBarButtons = { feed: FeedButton, trending: TrendingButton, explore: ExploreButton, - favorites: FavoritesButton, + library: LibraryButton, notifications: NotificationsButton } export * from './ExploreButton' -export * from './FavoritesButton' +export * from './LibraryButton' export * from './FeedButton' export * from './TrendingButton' export * from './NotificationsButton' diff --git a/packages/mobile/src/components/collection-list/AddCollectionCard.tsx b/packages/mobile/src/components/collection-list/AddCollectionCard.tsx index 0a2dc53ac1..6aba004914 100644 --- a/packages/mobile/src/components/collection-list/AddCollectionCard.tsx +++ b/packages/mobile/src/components/collection-list/AddCollectionCard.tsx @@ -41,7 +41,7 @@ const useStyles = makeStyles(({ spacing }) => ({ export const AddCollectionCard = ({ onCreate, - source = CreatePlaylistSource.FAVORITES_PAGE, + source = CreatePlaylistSource.LIBRARY_PAGE, sourceTrackId = null }: AddCollectionCardProps) => { const styles = useStyles() diff --git a/packages/mobile/src/components/collection-list/CollectionList.tsx b/packages/mobile/src/components/collection-list/CollectionList.tsx index 131a756a11..e2f1fe9f9d 100644 --- a/packages/mobile/src/components/collection-list/CollectionList.tsx +++ b/packages/mobile/src/components/collection-list/CollectionList.tsx @@ -46,7 +46,7 @@ const FullCollectionList = (props: FullCollectionListProps) => { collection, collectionIdsToNumTracks, showCreatePlaylistTile = false, - createPlaylistSource = CreatePlaylistSource.FAVORITES_PAGE, + createPlaylistSource = CreatePlaylistSource.LIBRARY_PAGE, createPlaylistTrackId, createPlaylistCallback, renderItem, @@ -99,7 +99,7 @@ const CollectionIDList = (props: CollectionIdListProps) => { const { collectionIds, showCreatePlaylistTile = false, - createPlaylistSource = CreatePlaylistSource.FAVORITES_PAGE, + createPlaylistSource = CreatePlaylistSource.LIBRARY_PAGE, createPlaylistTrackId, createPlaylistCallback, ...other diff --git a/packages/mobile/src/components/core/HarmonyModalHeader.tsx b/packages/mobile/src/components/core/HarmonyModalHeader.tsx new file mode 100644 index 0000000000..d62fdab5f7 --- /dev/null +++ b/packages/mobile/src/components/core/HarmonyModalHeader.tsx @@ -0,0 +1,55 @@ +import type { ComponentType } from 'react' + +import { View } from 'react-native' +import type { SvgProps } from 'react-native-svg' + +import { Text } from 'app/components/core' +import { makeStyles } from 'app/styles' +import { useThemeColors } from 'app/utils/theme' + +const useStyles = makeStyles(({ spacing, palette }) => ({ + container: { + width: '100%', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + paddingBottom: spacing(4), + borderBottomColor: palette.neutralLight8, + borderBottomWidth: 1 + }, + titleIcon: { + marginRight: spacing(2) + } +})) + +type HarmonyModalHeaderProps = { + icon: ComponentType + title: string +} + +export const HarmonyModalHeader = ({ + icon: Icon, + title +}: HarmonyModalHeaderProps) => { + const styles = useStyles() + const { neutralLight2 } = useThemeColors() + + return ( + + + + {title} + + + ) +} diff --git a/packages/mobile/src/components/drawers/OfflineListeningDrawer.tsx b/packages/mobile/src/components/drawers/OfflineListeningDrawer.tsx new file mode 100644 index 0000000000..a8446fd8b8 --- /dev/null +++ b/packages/mobile/src/components/drawers/OfflineListeningDrawer.tsx @@ -0,0 +1,185 @@ +import type { ComponentType } from 'react' +import { useCallback, useState } from 'react' + +import { View } from 'react-native' +import type { SvgProps } from 'react-native-svg' +import { useDispatch } from 'react-redux' + +import IconCart from 'app/assets/images/iconCart.svg' +import IconDownload from 'app/assets/images/iconDownloadQueued.svg' +import IconFavorite from 'app/assets/images/iconFavorite.svg' +import IconRepost from 'app/assets/images/iconRepost.svg' +import { Button, Switch, Text } from 'app/components/core' +import { useDrawer } from 'app/hooks/useDrawer' +import { useIsUSDCEnabled } from 'app/hooks/useIsUSDCEnabled' +import { setVisibility } from 'app/store/drawers/slice' +import { requestDownloadAllFavorites } from 'app/store/offline-downloads/slice' +import { makeStyles } from 'app/styles' +import { useThemeColors } from 'app/utils/theme' + +import { HarmonyModalHeader } from '../core/HarmonyModalHeader' +import { NativeDrawer } from '../drawer' + +const useDrawerStyles = makeStyles(({ spacing, palette, typography }) => ({ + container: { + paddingVertical: spacing(6), + flexDirection: 'column', + paddingHorizontal: spacing(4), + rowGap: spacing(6), + alignItems: 'center' + }, + descriptionText: { + textAlign: 'center', + lineHeight: typography.fontSize.large * 1.3 + }, + titleIcon: { + position: 'relative', + top: 7, + color: palette.neutral, + marginRight: spacing(3) + } +})) + +const useToggleStyles = makeStyles(({ spacing, palette }) => ({ + toggleContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + width: '100%', + alignItems: 'center' + }, + titleContainer: { + columnGap: spacing(2), + flexDirection: 'row' + } +})) + +const messages = { + offlineListeningTitle: 'Offline Listening', + offlineListeningDescription: + 'Use the toggles to select what you’d like synced for offline streaming.', + comingSoonToggleSuffix: '(coming soon...)', + favorites: 'Favorites', + reposts: 'Reposts', + purchased: 'Purchased', + saveChanges: 'Save Changes' +} + +type OfflineListeningOptionToggleProps = { + title: string + icon: ComponentType + value: boolean + onValueChange?: (value: boolean) => void | Promise + disabled?: boolean +} + +const OfflineListeningOptionToggle = ({ + title, + icon: Icon, + value, + onValueChange, + disabled +}: OfflineListeningOptionToggleProps) => { + const styles = useToggleStyles() + const { neutral, neutralLight4 } = useThemeColors() + + return ( + + + + + {title} + + + + + ) +} + +export const OfflineListeningDrawer = () => { + const styles = useDrawerStyles() + const dispatch = useDispatch() + const { data, onClose } = useDrawer('OfflineListening') + const { isFavoritesMarkedForDownload, onSaveChanges } = data + + const [isFavoritesOn, setIsFavoritesOn] = useState( + isFavoritesMarkedForDownload + ) + + const handleSaveChanges = useCallback(() => { + if (isFavoritesMarkedForDownload && !isFavoritesOn) { + dispatch( + setVisibility({ + drawer: 'RemoveDownloadedFavorites', + visible: true + }) + ) + onSaveChanges(isFavoritesOn) + } else if (!isFavoritesMarkedForDownload && isFavoritesOn) { + dispatch(requestDownloadAllFavorites()) + onSaveChanges(isFavoritesOn) + } + + onClose() + }, [ + dispatch, + isFavoritesMarkedForDownload, + isFavoritesOn, + onClose, + onSaveChanges + ]) + + const handleToggleFavorites = useCallback((value: boolean) => { + setIsFavoritesOn(value) + }, []) + + const isUSDCPurchasesEnabled = useIsUSDCEnabled() + + return ( + + + + + {messages.offlineListeningDescription} + + + + {isUSDCPurchasesEnabled ? ( + + ) : null} +