Front: Standardise queries (#233)

This commit is contained in:
Arthur Jamet
2023-06-23 15:16:22 +01:00
committed by GitHub
parent 80b06f15fe
commit 857158c6cf
17 changed files with 414 additions and 338 deletions

View File

@@ -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"
}
]
}
}

View File

@@ -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<User> {
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<User> {
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<UserSettings> {
const settings = await API.fetch({
route: '/auth/me/settings',
});
public static getUserSettings(): Query<UserSettings> {
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<Parameters<typeof CompetenciesTable>[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<Song[]> {
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<AuthToken> {
//TODO
return '11111';
}
public static getSong(songId: number): Query<Song> {
return {
key: ['song', songId],
exec: async () => {
const song = await API.fetch({
route: `/song/${songId}`,
});
public static async getAllSongs(): Promise<Song[]> {
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<Song> {
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<ArrayBuffer> {
return API.fetch({
route: `/song/${songId}/midi`,
raw: true,
});
public static getSongMidi(songId: number): Query<ArrayBuffer> {
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<ArrayBuffer> {
return API.fetch({
route: `/song/${songId}/musicXml`,
raw: true,
});
public static getSongMusicXML(songId: number): Query<ArrayBuffer> {
return {
key: ['musixml', songId],
exec: () =>
API.fetch({
route: `/song/${songId}/musicXml`,
raw: true,
}),
};
}
/**
* Retrive an artist
*/
public static async getArtist(artistId: number): Promise<Artist> {
return API.fetch({
route: `/artist/${artistId}`,
});
public static getArtist(artistId: number): Query<Artist> {
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<Chapter[]> {
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<Chapter[]> {
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<Song[]> {
return API.fetch({
route: `/search/songs/${query}`,
});
public static searchSongs(query: string): Query<Song[]> {
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<Artist[]> {
return API.fetch({
route: `/search/artists/${query}`,
});
public static searchArtists(query: string): Query<Artist[]> {
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<Album[]> {
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<Album[]> {
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<Genre[]> {
return API.fetch({
route: `/search/genres/${query}`,
});
public static searchGenres(query: string): Query<Genre[]> {
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<Lesson> {
public static getLesson(lessonId: number): Query<Lesson> {
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<SearchHistory[]> {
return (
(
await API.fetch({
public static getSearchHistory(skip?: number, take?: number): Query<SearchHistory[]> {
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<Song[]> {
const queryClient = new QueryClient();
return await queryClient.fetchQuery(['API', 'allsongs'], API.getAllSongs);
public static getSongSuggestions(): Query<Song[]> {
return API.getAllSongs();
}
/**
* Retrieve the authenticated user's play history
* * @returns an array of songs
*/
public static async getUserPlayHistory(): Promise<SongHistory[]> {
return this.fetch({
route: '/history',
});
public static getUserPlayHistory(): Query<SongHistory[]> {
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<LessonHistory[]> {
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<LessonHistory[]> {
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<User> {

View File

@@ -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();

View File

@@ -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) => {

80
front/Queries.ts Normal file
View File

@@ -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<T> = RQ.UseQueryOptions<T>;
// What a query *is*
export type Query<ReturnType> = {
key: RQ.QueryKey;
exec: () => Promise<ReturnType>;
};
// We want `useQuery`/`ies` to accept either a function or a `Query` directly
type QueryOrQueryFn<T> = Query<T> | (() => Query<T>);
// A simple util function to avoid conditions everywhere
const queryToFn = <T>(q: QueryOrQueryFn<T>) => {
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 = <T, Opts extends QueryOptions<T>>(q: QueryOrQueryFn<T>, 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 = <ReturnType, Opts extends QueryOptions<ReturnType>>(
query: QueryOrQueryFn<ReturnType>,
options?: Opts
) => {
return RQ.useQuery<ReturnType>(buildRQuery(query, options));
};
const transformQuery = <OldReturnType, NewReturnType>(
query: Query<OldReturnType>,
fn: (res: OldReturnType) => NewReturnType
) => {
return {
key: query.key,
exec: () => query.exec().then(fn),
};
};
const useQueries = <ReturnTypes>(
queries: readonly QueryOrQueryFn<ReturnTypes>[],
options?: QueryOptions<ReturnTypes>
) => {
return RQ.useQueries(queries.map((q) => buildRQuery(q, options)));
};
export { useQuery, useQueries, QueryRules, transformQuery };

View File

@@ -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 (
<VStack mt="5" style={{ overflow: 'hidden' }}>

View File

@@ -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;
};

View File

@@ -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<typeof API.updateUserSettings>) =>
API.updateUserSettings(...params).then(() => settings.refetch());
return { settings, updateSettings };

View File

@@ -1,3 +0,0 @@
type AuthToken = string;
export default AuthToken;

View File

@@ -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 <LoadingView />;

View File

@@ -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 (

View File

@@ -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<PlayViewProps>) => {
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<ScoreMessage>();
const webSocket = useRef<WebSocket>();
@@ -84,8 +84,7 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
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;

View File

@@ -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<ScoreViewProps>) => {
const ScoreView = (props: RouteProps<ScoreViewProps>) => {
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<ScoreV
) {
return <LoadingView />;
}
if (songQuery.isError) {
navigation.navigate('Error');
return <></>;
}
return (
<ScrollView p={8} contentContainerStyle={{ alignItems: 'center' }}>

View File

@@ -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<SearchViewProps>) => {
const [stringQuery, setStringQuery] = useState<string>(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 }
);

View File

@@ -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<SongLobbyProps>) => {
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();

View File

@@ -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) {

View File

@@ -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<SetttingsNavigatorProps>) => {
const userQuery = useQuery(['user'], () => API.getUserInfo());
const userQuery = useQuery(API.getUserInfo);
const user = useMemo(() => userQuery.data, [userQuery]);
if (userQuery.isLoading) {