From 857158c6cf928c5f99a6e6a280dadeab437899c8 Mon Sep 17 00:00:00 2001 From: Arthur Jamet <60505370+Arthi-chaud@users.noreply.github.com> Date: Fri, 23 Jun 2023 15:16:22 +0100 Subject: [PATCH] Front: Standardise queries (#233) --- front/.eslintrc.json | 10 +- front/API.ts | 475 ++++++++++--------- front/App.tsx | 9 +- front/Navigation.tsx | 11 +- front/Queries.ts | 80 ++++ front/components/SearchResult.tsx | 44 +- front/components/utils/api.tsx | 15 - front/hooks/userSettings.ts | 5 +- front/models/AuthToken.ts | 3 - front/views/ArtistDetailsView.tsx | 8 +- front/views/HomeView.tsx | 22 +- front/views/PlayView.tsx | 7 +- front/views/ScoreView.tsx | 29 +- front/views/SearchView.tsx | 11 +- front/views/SongLobbyView.tsx | 15 +- front/views/settings/SettingsProfileView.tsx | 4 +- front/views/settings/SettingsView.tsx | 4 +- 17 files changed, 414 insertions(+), 338 deletions(-) create mode 100644 front/Queries.ts delete mode 100644 front/components/utils/api.tsx delete mode 100644 front/models/AuthToken.ts diff --git a/front/.eslintrc.json b/front/.eslintrc.json index c49a8ef..501a0c8 100644 --- a/front/.eslintrc.json +++ b/front/.eslintrc.json @@ -19,6 +19,14 @@ "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-empty-function": "off" + "@typescript-eslint/no-empty-function": "off", + "no-restricted-imports": [ + "error", + { + "name": "react-query", + "importNames": ["useQuery", "useInfiniteQuery", "useQueries"], + "message": "Use wrapper functions provided by Queries.ts" + } + ] } } diff --git a/front/API.ts b/front/API.ts index 42e6e04..3f0b407 100644 --- a/front/API.ts +++ b/front/API.ts @@ -1,6 +1,5 @@ import Artist from './models/Artist'; import Album from './models/Album'; -import AuthToken from './models/AuthToken'; import Chapter from './models/Chapter'; import Lesson from './models/Lesson'; import Genre from './models/Genre'; @@ -12,10 +11,11 @@ import Constants from 'expo-constants'; import store from './state/Store'; import { Platform } from 'react-native'; import { en } from './i18n/Translations'; -import { QueryClient } from 'react-query'; import UserSettings from './models/UserSettings'; import { PartialDeep } from 'type-fest'; import SearchHistory from './models/SearchHistory'; +import { Query } from './Queries'; +import CompetenciesTable from './components/CompetenciesTable'; type AuthenticationInput = { username: string; password: string }; type RegistrationInput = AuthenticationInput & { email: string }; @@ -137,43 +137,53 @@ export default class API { /*** * Retrieve information of the currently authentified user */ - public static async getUserInfo(): Promise { - const user = await API.fetch({ - route: '/auth/me', - }); - - // this a dummy settings object, we will need to fetch the real one from the API + public static getUserInfo(): Query { return { - id: user.id as number, - name: (user.username ?? user.name) as string, - email: user.email as string, - premium: false, - isGuest: user.isGuest as boolean, - data: { - gamesPlayed: user.partyPlayed as number, - xp: 0, - createdAt: new Date('2023-04-09T00:00:00.000Z'), - avatar: 'https://imgs.search.brave.com/RnQpFhmAFvuQsN_xTw7V-CN61VeHDBg2tkEXnKRYHAE/rs:fit:768:512:1/g:ce/aHR0cHM6Ly96b29h/c3Ryby5jb20vd3At/Y29udGVudC91cGxv/YWRzLzIwMjEvMDIv/Q2FzdG9yLTc2OHg1/MTIuanBn', + key: 'user', + exec: async () => { + const user = await API.fetch({ + route: '/auth/me', + }); + + // this a dummy settings object, we will need to fetch the real one from the API + return { + id: user.id as number, + name: (user.username ?? user.name) as string, + email: user.email as string, + premium: false, + isGuest: user.isGuest as boolean, + data: { + gamesPlayed: user.partyPlayed as number, + xp: 0, + createdAt: new Date('2023-04-09T00:00:00.000Z'), + avatar: 'https://imgs.search.brave.com/RnQpFhmAFvuQsN_xTw7V-CN61VeHDBg2tkEXnKRYHAE/rs:fit:768:512:1/g:ce/aHR0cHM6Ly96b29h/c3Ryby5jb20vd3At/Y29udGVudC91cGxv/YWRzLzIwMjEvMDIv/Q2FzdG9yLTc2OHg1/MTIuanBn', + }, + } as User; }, - } as User; + }; } - public static async getUserSettings(): Promise { - const settings = await API.fetch({ - route: '/auth/me/settings', - }); - + public static getUserSettings(): Query { return { - notifications: { - pushNotif: settings.pushNotification, - emailNotif: settings.emailNotification, - trainNotif: settings.trainingNotification, - newSongNotif: settings.newSongNotification, + key: 'settings', + exec: async () => { + const settings = await API.fetch({ + route: '/auth/me/settings', + }); + + return { + notifications: { + pushNotif: settings.pushNotification, + emailNotif: settings.emailNotification, + trainNotif: settings.trainingNotification, + newSongNotif: settings.newSongNotification, + }, + recommendations: settings.recommendations, + weeklyReport: settings.weeklyReport, + leaderBoard: settings.leaderBoard, + showActivity: settings.showActivity, + }; }, - recommendations: settings.recommendations, - weeklyReport: settings.weeklyReport, - leaderBoard: settings.leaderBoard, - showActivity: settings.showActivity, }; } @@ -195,36 +205,62 @@ export default class API { }); } - public static async getUserSkills() { + public static getUserSkills(): Query[0]> { return { - pedalsCompetency: Math.random() * 100, - rightHandCompetency: Math.random() * 100, - leftHandCompetency: Math.random() * 100, - accuracyCompetency: Math.random() * 100, - arpegeCompetency: Math.random() * 100, - chordsCompetency: Math.random() * 100, + key: 'skills', + exec: async () => ({ + pedalsCompetency: Math.random() * 100, + rightHandCompetency: Math.random() * 100, + leftHandCompetency: Math.random() * 100, + accuracyCompetency: Math.random() * 100, + arpegeCompetency: Math.random() * 100, + chordsCompetency: Math.random() * 100, + }), + }; + } + + public static getAllSongs(): Query { + return { + key: 'songs', + exec: async () => { + const songs = await API.fetch({ + route: '/song', + }); + + // this is a dummy illustration, we will need to fetch the real one from the API + return songs.data.map( + // To be fixed with #168 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (song: any) => + ({ + id: song.id as number, + name: song.name as string, + artistId: song.artistId as number, + albumId: song.albumId as number, + genreId: song.genreId as number, + details: song.difficulties, + cover: `${baseAPIUrl}/song/${song.id}/illustration`, + metrics: {}, + } as Song) + ); + }, }; } /** - * Authentify a new user through Google + * Retrieve a song + * @param songId the id to find the song */ - public static async authWithGoogle(): Promise { - //TODO - return '11111'; - } + public static getSong(songId: number): Query { + return { + key: ['song', songId], + exec: async () => { + const song = await API.fetch({ + route: `/song/${songId}`, + }); - public static async getAllSongs(): Promise { - const songs = await API.fetch({ - route: '/song', - }); - - // this is a dummy illustration, we will need to fetch the real one from the API - return songs.data.map( - // To be fixed with #168 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (song: any) => - ({ + // this is a dummy illustration, we will need to fetch the real one from the API + return { id: song.id as number, name: song.name as string, artistId: song.artistId as number, @@ -232,40 +268,23 @@ export default class API { genreId: song.genreId as number, details: song.difficulties, cover: `${baseAPIUrl}/song/${song.id}/illustration`, - metrics: {}, - } as Song) - ); - } - - /** - * Retrieve a song - * @param songId the id to find the song - */ - public static async getSong(songId: number): Promise { - const song = await API.fetch({ - route: `/song/${songId}`, - }); - - // this is a dummy illustration, we will need to fetch the real one from the API - return { - id: song.id as number, - name: song.name as string, - artistId: song.artistId as number, - albumId: song.albumId as number, - genreId: song.genreId as number, - details: song.difficulties, - cover: `${baseAPIUrl}/song/${song.id}/illustration`, - } as Song; + } as Song; + }, + }; } /** * Retrive a song's midi partition * @param songId the id to find the song */ - public static async getSongMidi(songId: number): Promise { - return API.fetch({ - route: `/song/${songId}/midi`, - raw: true, - }); + public static getSongMidi(songId: number): Query { + return { + key: ['midi', songId], + exec: () => + API.fetch({ + route: `/song/${songId}/midi`, + raw: true, + }), + }; } /** @@ -288,119 +307,152 @@ export default class API { * Retrive a song's musicXML partition * @param songId the id to find the song */ - public static async getSongMusicXML(songId: number): Promise { - return API.fetch({ - route: `/song/${songId}/musicXml`, - raw: true, - }); + public static getSongMusicXML(songId: number): Query { + return { + key: ['musixml', songId], + exec: () => + API.fetch({ + route: `/song/${songId}/musicXml`, + raw: true, + }), + }; } /** * Retrive an artist */ - public static async getArtist(artistId: number): Promise { - return API.fetch({ - route: `/artist/${artistId}`, - }); + public static getArtist(artistId: number): Query { + return { + key: ['artist', artistId], + exec: () => + API.fetch({ + route: `/artist/${artistId}`, + }), + }; } /** * Retrive a song's chapters * @param songId the id to find the song */ - public static async getSongChapters(songId: number): Promise { - return [1, 2, 3, 4, 5].map((value) => ({ - start: 100 * (value - 1), - end: 100 * value, - songId: songId, - name: `Chapter ${value}`, - type: 'chorus', - key_aspect: 'rhythm', - difficulty: value, - id: value * 10, - })); + public static getSongChapters(songId: number): Query { + return { + key: ['chapters', songId], + exec: async () => + [1, 2, 3, 4, 5].map((value) => ({ + start: 100 * (value - 1), + end: 100 * value, + songId: songId, + name: `Chapter ${value}`, + type: 'chorus', + key_aspect: 'rhythm', + difficulty: value, + id: value * 10, + })), + }; } /** * Retrieve a song's play history * @param songId the id to find the song */ - public static async getSongHistory( - songId: number - ): Promise<{ best: number; history: SongHistory[] }> { - return API.fetch({ - route: `/song/${songId}/history`, - }); + public static getSongHistory(songId: number): Query<{ best: number; history: SongHistory[] }> { + return { + key: ['song', 'history', songId], + exec: () => + API.fetch({ + route: `/song/${songId}/history`, + }), + }; } /** * Search a song by its name * @param query the string used to find the songs */ - public static async searchSongs(query: string): Promise { - return API.fetch({ - route: `/search/songs/${query}`, - }); + public static searchSongs(query: string): Query { + return { + key: ['search', 'song', query], + exec: () => + API.fetch({ + route: `/search/songs/${query}`, + }), + }; } /** * Search artists by name * @param query the string used to find the artists */ - public static async searchArtists(query?: string): Promise { - return API.fetch({ - route: `/search/artists/${query}`, - }); + public static searchArtists(query: string): Query { + return { + key: ['search', 'artist', query], + exec: () => + API.fetch({ + route: `/search/artists/${query}`, + }), + }; } /** * Search Album by name * @param query the string used to find the album */ - public static async searchAlbum( + public static searchAlbum( // eslint-disable-next-line @typescript-eslint/no-unused-vars - query?: string - ): Promise { - return [ - { - id: 1, - name: 'Super Trooper', - }, - { - id: 2, - name: 'Kingdom Heart 365/2 OST', - }, - { - id: 3, - name: 'The Legend Of Zelda Ocarina Of Time OST', - }, - { - id: 4, - name: 'Random Access Memories', - }, - ] as Album[]; + query: string + ): Query { + return { + key: ['search', 'album', query], + exec: async () => + [ + { + id: 1, + name: 'Super Trooper', + }, + { + id: 2, + name: 'Kingdom Heart 365/2 OST', + }, + { + id: 3, + name: 'The Legend Of Zelda Ocarina Of Time OST', + }, + { + id: 4, + name: 'Random Access Memories', + }, + ] as Album[], + }; } /** * Retrieve music genres */ - public static async searchGenres(query?: string): Promise { - return API.fetch({ - route: `/search/genres/${query}`, - }); + public static searchGenres(query: string): Query { + return { + key: ['search', 'genre', query], + exec: () => + API.fetch({ + route: `/search/genres/${query}`, + }), + }; } /** * Retrieve a lesson * @param lessonId the id to find the lesson */ - public static async getLesson(lessonId: number): Promise { + public static getLesson(lessonId: number): Query { return { - title: 'Song', - description: 'A song', - requiredLevel: 1, - mainSkill: 'lead-head-change', - id: lessonId, + key: ['lesson', lessonId], + exec: async () => ({ + title: 'Song', + description: 'A song', + requiredLevel: 1, + mainSkill: 'lead-head-change', + id: lessonId, + }), }; } @@ -410,26 +462,28 @@ export default class API { * @param take how much do we take to return * @returns Returns an array of history entries (temporary type any) */ - public static async getSearchHistory(skip?: number, take?: number): Promise { - return ( - ( - await API.fetch({ + public static getSearchHistory(skip?: number, take?: number): Query { + return { + key: ['search', 'history', 'skip', skip, 'take', take], + exec: () => + API.fetch({ route: `/history/search?skip=${skip ?? 0}&take=${take ?? 5}`, method: 'GET', - }) - ) - // To be fixed with #168 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .map((e: any) => { - return { - id: e.id, - query: e.query, - type: e.type, - userId: e.userId, - timestamp: new Date(e.searchDate), - } as SearchHistory; - }) - ); + }).then((value) => + value.map( + // To be fixed with #168 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e: any) => + ({ + id: e.id, + query: e.query, + type: e.type, + userId: e.userId, + timestamp: new Date(e.searchDate), + } as SearchHistory) + ) + ), + }; } /** @@ -454,85 +508,38 @@ export default class API { * Retrieve the authenticated user's recommendations * @returns an array of songs */ - public static async getSongSuggestions(): Promise { - const queryClient = new QueryClient(); - return await queryClient.fetchQuery(['API', 'allsongs'], API.getAllSongs); + public static getSongSuggestions(): Query { + return API.getAllSongs(); } /** * Retrieve the authenticated user's play history * * @returns an array of songs */ - public static async getUserPlayHistory(): Promise { - return this.fetch({ - route: '/history', - }); + public static getUserPlayHistory(): Query { + return { + key: ['history'], + exec: () => + API.fetch({ + route: '/history', + }), + }; } /** * Retrieve a lesson's history * @param lessonId the id to find the lesson */ - public static async getLessonHistory(lessonId: number): Promise { - return [ - { - lessonId, - userId: 1, - }, - ]; - } - - /** - * Retrieve a partition images - * @param _songId the id of the song - * This API may be merged with the fetch song in the future - */ - public static async getPartitionRessources( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - songId: number - ): Promise<[string, number, number][]> { - return [ - [ - 'https://media.discordapp.net/attachments/717080637038788731/1067469560426545222/vivaldi_split_1.png', - 1868, - 400, + public static getLessonHistory(lessonId: number): Query { + return { + key: ['lesson', 'history', lessonId], + exec: async () => [ + { + lessonId, + userId: 1, + }, ], - [ - 'https://media.discordapp.net/attachments/717080637038788731/1067469560900505660/vivaldi_split_2.png', - 1868, - 400, - ], - [ - 'https://media.discordapp.net/attachments/717080637038788731/1067469561261203506/vivaldi_split_3.png', - 1868, - 400, - ], - [ - 'https://media.discordapp.net/attachments/717080637038788731/1067469561546424381/vivaldi_split_4.png', - 1868, - 400, - ], - [ - 'https://media.discordapp.net/attachments/717080637038788731/1067469562058133564/vivaldi_split_5.png', - 1868, - 400, - ], - [ - 'https://media.discordapp.net/attachments/717080637038788731/1067469562347528202/vivaldi_split_6.png', - 1868, - 400, - ], - [ - 'https://media.discordapp.net/attachments/717080637038788731/1067469562792136815/vivaldi_split_7.png', - 1868, - 400, - ], - [ - 'https://media.discordapp.net/attachments/717080637038788731/1067469563073142804/vivaldi_split_8.png', - 1868, - 400, - ], - ]; + }; } public static async updateUserEmail(newEmail: string): Promise { diff --git a/front/App.tsx b/front/App.tsx index e66dade..243702a 100644 --- a/front/App.tsx +++ b/front/App.tsx @@ -9,14 +9,9 @@ import { PersistGate } from 'redux-persist/integration/react'; import LanguageGate from './i18n/LanguageGate'; import ThemeProvider, { ColorSchemeProvider } from './Theme'; import 'react-native-url-polyfill/auto'; +import { QueryRules } from './Queries'; -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - refetchOnWindowFocus: false, - }, - }, -}); +const queryClient = new QueryClient(QueryRules); export default function App() { SplashScreen.preventAutoHideAsync(); diff --git a/front/Navigation.tsx b/front/Navigation.tsx index deb21b2..011967b 100644 --- a/front/Navigation.tsx +++ b/front/Navigation.tsx @@ -16,7 +16,7 @@ import StartPageView from './views/StartPageView'; import HomeView from './views/HomeView'; import SearchView from './views/SearchView'; import SetttingsNavigator from './views/settings/SettingsView'; -import { useQuery } from 'react-query'; +import { useQuery } from './Queries'; import API, { APIError } from './API'; import PlayView from './views/PlayView'; import ScoreView from './views/ScoreView'; @@ -29,6 +29,9 @@ import { unsetAccessToken } from './state/UserSlice'; import TextButton from './components/TextButton'; import ErrorView from './views/ErrorView'; +// Util function to hide route props in URL +const removeMe = () => ''; + const protectedRoutes = () => ({ Home: { @@ -36,13 +39,13 @@ const protectedRoutes = () => options: { title: translate('welcome'), headerLeft: null }, link: '/', }, - Play: { component: PlayView, options: { title: translate('play') }, link: '/play' }, + Play: { component: PlayView, options: { title: translate('play') }, link: '/play/:songId' }, Settings: { component: SetttingsNavigator, options: { title: 'Settings' }, link: '/settings/:screen?', stringify: { - screen: () => '', + screen: removeMe, }, }, Song: { @@ -188,7 +191,7 @@ const ProfileErrorView = (props: { onTryAgain: () => void }) => { export const Router = () => { const dispatch = useDispatch(); const accessToken = useSelector((state: RootState) => state.user.accessToken); - const userProfile = useQuery(['user', 'me', accessToken], () => API.getUserInfo(), { + const userProfile = useQuery(API.getUserInfo, { retry: 1, refetchOnWindowFocus: false, onError: (err) => { diff --git a/front/Queries.ts b/front/Queries.ts new file mode 100644 index 0000000..d72b777 --- /dev/null +++ b/front/Queries.ts @@ -0,0 +1,80 @@ +/* eslint-disable no-restricted-imports */ +// Disabled for obvious reasons +import * as RQ from 'react-query'; + +const QueryRules: RQ.QueryClientConfig = { + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + // This is needed explicitly, otherwise will refetch **all** the time + staleTime: Infinity, + }, + }, +}; + +// The options of the query. +// E.g. enabled. +type QueryOptions = RQ.UseQueryOptions; + +// What a query *is* +export type Query = { + key: RQ.QueryKey; + exec: () => Promise; +}; + +// We want `useQuery`/`ies` to accept either a function or a `Query` directly +type QueryOrQueryFn = Query | (() => Query); +// A simple util function to avoid conditions everywhere +const queryToFn = (q: QueryOrQueryFn) => { + if (typeof q === 'function') { + return q; + } + return () => q; +}; + +// This also allows lazy laoding of query function. +// I.e. not call the function before it is enabled; +const buildRQuery = >(q: QueryOrQueryFn, opts?: Opts) => { + const laziedQuery = queryToFn(q); + if (opts?.enabled === false) { + return { + queryKey: [], + // This will not be called because the query is disabled. + // However, this is done for type-safety + queryFn: () => laziedQuery().exec(), + ...opts, + }; + } + const resolvedQuery = laziedQuery(); + return { + queryKey: resolvedQuery.key, + queryFn: resolvedQuery.exec, + ...opts, + }; +}; + +const useQuery = >( + query: QueryOrQueryFn, + options?: Opts +) => { + return RQ.useQuery(buildRQuery(query, options)); +}; + +const transformQuery = ( + query: Query, + fn: (res: OldReturnType) => NewReturnType +) => { + return { + key: query.key, + exec: () => query.exec().then(fn), + }; +}; + +const useQueries = ( + queries: readonly QueryOrQueryFn[], + options?: QueryOptions +) => { + return RQ.useQueries(queries.map((q) => buildRQuery(q, options))); +}; + +export { useQuery, useQueries, QueryRules, transformQuery }; diff --git a/front/components/SearchResult.tsx b/front/components/SearchResult.tsx index 59e8035..274cf54 100644 --- a/front/components/SearchResult.tsx +++ b/front/components/SearchResult.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { HStack, VStack, @@ -16,7 +16,7 @@ import { import { SafeAreaView, useColorScheme } from 'react-native'; import { RootState, useSelector } from '../state/Store'; import { SearchContext } from '../views/SearchView'; -import { useQuery } from 'react-query'; +import { useQueries, useQuery } from '../Queries'; import { translate } from '../i18n/i18n'; import API from '../API'; import LoadingComponent from './Loading'; @@ -27,8 +27,8 @@ import CardGridCustom from './CardGridCustom'; import TextButton from './TextButton'; import SearchHistoryCard from './HistoryCard'; import Song, { SongWithArtist } from '../models/Song'; -import { getSongWArtistSuggestions } from './utils/api'; import { useNavigation } from '../Navigation'; +import Artist from '../models/Artist'; const swaToSongCardProps = (song: SongWithArtist) => ({ songId: song.id, @@ -135,18 +135,38 @@ SongRow.defaultProps = { const HomeSearchComponent = () => { const { updateStringQuery } = React.useContext(SearchContext); const { isLoading: isLoadingHistory, data: historyData = [] } = useQuery( - 'history', - () => API.getSearchHistory(0, 12), + API.getSearchHistory(0, 12), { enabled: true } ); - - const { isLoading: isLoadingSuggestions, data: suggestionsData = [] } = useQuery( - 'suggestions', - () => getSongWArtistSuggestions(), - { - enabled: true, - } + const songSuggestions = useQuery(API.getSongSuggestions); + const songArtistSuggestions = useQueries( + songSuggestions.data + ?.filter((song) => song.artistId !== null) + .map(({ artistId }) => API.getArtist(artistId)) ?? [] ); + const isLoadingSuggestions = useMemo( + () => songSuggestions.isLoading || songArtistSuggestions.some((q) => q.isLoading), + [songSuggestions, songArtistSuggestions] + ); + const suggestionsData = useMemo(() => { + if (isLoadingSuggestions) { + return []; + } + return ( + songSuggestions.data + ?.map((song): [Song, Artist | undefined] => [ + song, + songArtistSuggestions + .map((q) => q.data) + .filter((d) => d !== undefined) + .find((data) => data?.id === song.artistId), + ]) + // We do not need the song + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .filter(([song, artist]) => artist !== undefined) + .map(([song, artist]) => ({ ...song, artist: artist! })) ?? [] + ); + }, [songSuggestions, songArtistSuggestions]); return ( diff --git a/front/components/utils/api.tsx b/front/components/utils/api.tsx deleted file mode 100644 index 12d98e5..0000000 --- a/front/components/utils/api.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import API from '../../API'; -import { SongWithArtist } from '../../models/Song'; - -export const getSongWArtistSuggestions = async () => { - const nextStepQuery = await API.getSongSuggestions(); - - const songWartist = await Promise.all( - nextStepQuery.map(async (song) => { - if (!song.artistId) throw new Error('Song has no artistId'); - const artist = await API.getArtist(song.artistId); - return { ...song, artist } as SongWithArtist; - }) - ); - return songWartist; -}; diff --git a/front/hooks/userSettings.ts b/front/hooks/userSettings.ts index 12fb577..cc3c325 100644 --- a/front/hooks/userSettings.ts +++ b/front/hooks/userSettings.ts @@ -1,9 +1,8 @@ -import { useQuery } from 'react-query'; +import { useQuery } from '../Queries'; import API from '../API'; const useUserSettings = () => { - const queryKey = ['settings']; - const settings = useQuery(queryKey, () => API.getUserSettings()); + const settings = useQuery(API.getUserSettings); const updateSettings = (...params: Parameters) => API.updateUserSettings(...params).then(() => settings.refetch()); return { settings, updateSettings }; diff --git a/front/models/AuthToken.ts b/front/models/AuthToken.ts deleted file mode 100644 index 2f3c3b4..0000000 --- a/front/models/AuthToken.ts +++ /dev/null @@ -1,3 +0,0 @@ -type AuthToken = string; - -export default AuthToken; diff --git a/front/views/ArtistDetailsView.tsx b/front/views/ArtistDetailsView.tsx index ea59bb0..cc567bd 100644 --- a/front/views/ArtistDetailsView.tsx +++ b/front/views/ArtistDetailsView.tsx @@ -1,7 +1,7 @@ import { VStack, Image, Heading, IconButton, Icon, Container } from 'native-base'; import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native'; -import { useQuery } from 'react-query'; +import { useQuery } from '../Queries'; import { LoadingView } from '../components/Loading'; import API from '../API'; import { useNavigation } from '../Navigation'; @@ -14,11 +14,7 @@ type ArtistDetailsViewProps = { const ArtistDetailsView = ({ artistId }: ArtistDetailsViewProps) => { const navigation = useNavigation(); - const { - isLoading, - data: artistData, - isError, - } = useQuery(['artist', artistId], () => API.getArtist(artistId)); + const { isLoading, data: artistData, isError } = useQuery(API.getArtist(artistId)); if (isLoading) { return ; diff --git a/front/views/HomeView.tsx b/front/views/HomeView.tsx index 22c6c44..131b74f 100644 --- a/front/views/HomeView.tsx +++ b/front/views/HomeView.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useQueries, useQuery } from 'react-query'; +import { useQueries, useQuery } from '../Queries'; import API from '../API'; import { LoadingView } from '../components/Loading'; import { Box, ScrollView, Flex, Stack, Heading, VStack, HStack } from 'native-base'; @@ -14,26 +14,20 @@ import { FontAwesome5 } from '@expo/vector-icons'; const HomeView = () => { const navigation = useNavigation(); - const userQuery = useQuery(['user'], () => API.getUserInfo()); - const playHistoryQuery = useQuery(['history', 'play'], () => API.getUserPlayHistory()); - const searchHistoryQuery = useQuery(['history', 'search'], () => API.getSearchHistory(0, 10)); - const skillsQuery = useQuery(['skills'], () => API.getUserSkills()); - const nextStepQuery = useQuery(['user', 'recommendations'], () => API.getSongSuggestions()); + const userQuery = useQuery(API.getUserInfo); + const playHistoryQuery = useQuery(API.getUserPlayHistory); + const searchHistoryQuery = useQuery(API.getSearchHistory(0, 10)); + const skillsQuery = useQuery(API.getUserSkills); + const nextStepQuery = useQuery(API.getSongSuggestions); const songHistory = useQueries( - playHistoryQuery.data?.map(({ songID }) => ({ - queryKey: ['song', songID], - queryFn: () => API.getSong(songID), - })) ?? [] + playHistoryQuery.data?.map(({ songID }) => API.getSong(songID)) ?? [] ); const artistsQueries = useQueries( songHistory .map((entry) => entry.data) .concat(nextStepQuery.data ?? []) .filter((s): s is Song => s !== undefined) - .map((song) => ({ - queryKey: ['artist', song.id], - queryFn: () => API.getArtist(song.artistId), - })) + .map((song) => API.getArtist(song.artistId)) ); if ( diff --git a/front/views/PlayView.tsx b/front/views/PlayView.tsx index 1c8a6f8..117462d 100644 --- a/front/views/PlayView.tsx +++ b/front/views/PlayView.tsx @@ -17,7 +17,7 @@ import { import IconButton from '../components/IconButton'; import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons'; import { RouteProps, useNavigation } from '../Navigation'; -import { useQuery } from 'react-query'; +import { transformQuery, useQuery } from '../Queries'; import API from '../API'; import LoadingComponent, { LoadingView } from '../components/Loading'; import Constants from 'expo-constants'; @@ -72,7 +72,7 @@ function parseMidiMessage(message: MIDIMessageEvent) { const PlayView = ({ songId, type, route }: RouteProps) => { const accessToken = useSelector((state: RootState) => state.user.accessToken); const navigation = useNavigation(); - const song = useQuery(['song', songId], () => API.getSong(songId), { staleTime: Infinity }); + const song = useQuery(API.getSong(songId), { staleTime: Infinity }); const toast = useToast(); const [lastScoreMessage, setLastScoreMessage] = useState(); const webSocket = useRef(); @@ -84,8 +84,7 @@ const PlayView = ({ songId, type, route }: RouteProps) => { const [score, setScore] = useState(0); // Between 0 and 100 const fadeAnim = useRef(new Animated.Value(0)).current; const musixml = useQuery( - ['musixml', songId], - () => API.getSongMusicXML(songId).then((data) => new TextDecoder().decode(data)), + transformQuery(API.getSongMusicXML(songId), (data) => new TextDecoder().decode(data)), { staleTime: Infinity } ); const getElapsedTime = () => stopwatch.getElapsedRunningTime() - 3000; diff --git a/front/views/ScoreView.tsx b/front/views/ScoreView.tsx index 1f6c0c0..359c179 100644 --- a/front/views/ScoreView.tsx +++ b/front/views/ScoreView.tsx @@ -6,7 +6,7 @@ import TextButton from '../components/TextButton'; import API from '../API'; import CardGridCustom from '../components/CardGridCustom'; import SongCard from '../components/SongCard'; -import { useQueries, useQuery } from 'react-query'; +import { useQueries, useQuery } from '../Queries'; import { LoadingView } from '../components/Loading'; type ScoreViewProps = { @@ -25,25 +25,18 @@ type ScoreViewProps = { }; }; -const ScoreView = ({ songId, overallScore, precision, score }: RouteProps) => { +const ScoreView = (props: RouteProps) => { + const { songId, overallScore, precision, score } = props; const navigation = useNavigation(); - const songQuery = useQuery(['song', songId], () => API.getSong(songId)); - const artistQuery = useQuery( - ['song', songId, 'artist'], - () => API.getArtist(songQuery.data!.artistId!), - { - enabled: songQuery.data != undefined, - } - ); - // const perfoamnceRecommandationsQuery = useQuery(['song', props.songId, 'score', 'latest', 'recommendations'], () => API.getLastSongPerformanceScore(props.songId)); - const recommendations = useQuery(['song', 'recommendations'], () => API.getSongSuggestions()); + const songQuery = useQuery(API.getSong(songId)); + const artistQuery = useQuery(() => API.getArtist(songQuery.data!.artistId!), { + enabled: songQuery.data !== undefined, + }); + const recommendations = useQuery(API.getSongSuggestions); const artistRecommendations = useQueries( recommendations.data ?.filter(({ artistId }) => artistId !== null) - .map((song) => ({ - queryKey: ['artist', song.artistId], - queryFn: () => API.getArtist(song.artistId!), - })) ?? [] + .map((song) => API.getArtist(song.artistId)) ?? [] ); if ( @@ -54,6 +47,10 @@ const ScoreView = ({ songId, overallScore, precision, score }: RouteProps; } + if (songQuery.isError) { + navigation.navigate('Error'); + return <>; + } return ( diff --git a/front/views/SearchView.tsx b/front/views/SearchView.tsx index c9de230..108ff54 100644 --- a/front/views/SearchView.tsx +++ b/front/views/SearchView.tsx @@ -4,7 +4,7 @@ import Artist from '../models/Artist'; import Song from '../models/Song'; import Genre from '../models/Genre'; import API from '../API'; -import { useQuery } from 'react-query'; +import { useQuery } from '../Queries'; import { SearchResultComponent } from '../components/SearchResult'; import { SafeAreaView } from 'react-native'; import { Filter } from '../components/SearchBar'; @@ -46,20 +46,17 @@ const SearchView = (props: RouteProps) => { const [stringQuery, setStringQuery] = useState(props?.query ?? ''); const { isLoading: isLoadingSong, data: songData = [] } = useQuery( - ['song', stringQuery], - () => API.searchSongs(stringQuery), + API.searchSongs(stringQuery), { enabled: !!stringQuery } ); const { isLoading: isLoadingArtist, data: artistData = [] } = useQuery( - ['artist', stringQuery], - () => API.searchArtists(stringQuery), + API.searchArtists(stringQuery), { enabled: !!stringQuery } ); const { isLoading: isLoadingGenre, data: genreData = [] } = useQuery( - ['genre', stringQuery], - () => API.searchGenres(stringQuery), + API.searchGenres(stringQuery), { enabled: !!stringQuery } ); diff --git a/front/views/SongLobbyView.tsx b/front/views/SongLobbyView.tsx index a9736a9..7f278a9 100644 --- a/front/views/SongLobbyView.tsx +++ b/front/views/SongLobbyView.tsx @@ -1,5 +1,5 @@ import { Divider, Box, Image, Text, VStack, PresenceTransition, Icon, Stack } from 'native-base'; -import { useQuery } from 'react-query'; +import { useQuery } from '../Queries'; import LoadingComponent, { LoadingView } from '../components/Loading'; import React, { useEffect, useState } from 'react'; import { Translate, translate } from '../i18n/i18n'; @@ -16,13 +16,12 @@ interface SongLobbyProps { const SongLobbyView = (props: RouteProps) => { const navigation = useNavigation(); - const songQuery = useQuery(['song', props.songId], () => API.getSong(props.songId)); - const chaptersQuery = useQuery(['song', props.songId, 'chapters'], () => - API.getSongChapters(props.songId) - ); - const scoresQuery = useQuery(['song', props.songId, 'scores'], () => - API.getSongHistory(props.songId) - ); + // Refetch to update score when coming back from score view + const songQuery = useQuery(API.getSong(props.songId), { refetchOnWindowFocus: true }); + const chaptersQuery = useQuery(API.getSongChapters(props.songId), { + refetchOnWindowFocus: true, + }); + const scoresQuery = useQuery(API.getSongHistory(props.songId), { refetchOnWindowFocus: true }); const [chaptersOpen, setChaptersOpen] = useState(false); useEffect(() => { if (chaptersOpen && !chaptersQuery.data) chaptersQuery.refetch(); diff --git a/front/views/settings/SettingsProfileView.tsx b/front/views/settings/SettingsProfileView.tsx index cbb289f..4a0b40c 100644 --- a/front/views/settings/SettingsProfileView.tsx +++ b/front/views/settings/SettingsProfileView.tsx @@ -7,7 +7,7 @@ import TextButton from '../../components/TextButton'; import { LoadingView } from '../../components/Loading'; import ElementList from '../../components/GtkUI/ElementList'; import { translate } from '../../i18n/i18n'; -import { useQuery } from 'react-query'; +import { useQuery } from '../../Queries'; const getInitials = (name: string) => { return name @@ -19,7 +19,7 @@ const getInitials = (name: string) => { // Too painful to infer the settings-only, typed navigator. Gave up // eslint-disable-next-line @typescript-eslint/no-explicit-any const ProfileSettings = ({ navigation }: { navigation: any }) => { - const userQuery = useQuery(['user'], () => API.getUserInfo()); + const userQuery = useQuery(API.getUserInfo); const dispatch = useDispatch(); if (!userQuery.data || userQuery.isLoading) { diff --git a/front/views/settings/SettingsView.tsx b/front/views/settings/SettingsView.tsx index 84f609d..a0350e1 100644 --- a/front/views/settings/SettingsView.tsx +++ b/front/views/settings/SettingsView.tsx @@ -10,7 +10,7 @@ import NotificationsView from './NotificationView'; import PrivacyView from './PrivacyView'; import PreferencesView from './PreferencesView'; import GuestToUserView from './GuestToUserView'; -import { useQuery } from 'react-query'; +import { useQuery } from '../../Queries'; import API from '../../API'; import { RouteProps } from '../../Navigation'; @@ -77,7 +77,7 @@ type SetttingsNavigatorProps = { }; const SetttingsNavigator = (props?: RouteProps) => { - const userQuery = useQuery(['user'], () => API.getUserInfo()); + const userQuery = useQuery(API.getUserInfo); const user = useMemo(() => userQuery.data, [userQuery]); if (userQuery.isLoading) {