Lorem ipsum dolor sit amet, consectetuer adipiscing -elit. Aenean commodo ligula eget dolor. Aenean massa -strong. Cum sociis natoque penatibus -et magnis dis parturient montes, nascetur ridiculus -mus. Donec quam felis, ultricies nec, pellentesque -eu, pretium quis, sem. Nulla consequat massa quis -enim. Donec pede justo, fringilla vel, aliquet nec, -vulputate eget, arcu. In enim justo, rhoncus ut, -imperdiet a, venenatis vitae, justo. Nullam dictum -felis eu pede link -mollis pretium. Integer tincidunt. Cras dapibus. -Vivamus elementum semper nisi. Aenean vulputate -eleifend tellus. Aenean leo ligula, porttitor eu, -consequat vitae, eleifend ac, enim. Aliquam lorem ante, -dapibus in, viverra quis, feugiat a, tellus. Phasellus -viverra nulla ut metus varius laoreet. Quisque rutrum. -Aenean imperdiet. Etiam ultricies nisi vel augue. -Curabitur ullamcorper ultricies nisi.
-Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa strong. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede link mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi.
+Lorem ipsum dolor sit amet, consectetuer adipiscing -elit. Aenean commodo ligula eget dolor. Aenean massa. -Cum sociis natoque penatibus et magnis dis parturient -montes, nascetur ridiculus mus. Donec quam felis, -ultricies nec, pellentesque eu, pretium quis, sem.
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.
Lorem ipsum dolor sit amet, consectetuer adipiscing -elit. Aenean commodo ligula eget dolor. Aenean massa. -Cum sociis natoque penatibus et magnis dis parturient -montes, nascetur ridiculus mus. Donec quam felis, -ultricies nec, pellentesque eu, pretium quis, sem.
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.
Lorem ipsum dolor sit amet, consectetuer adipiscing -elit. Aenean commodo ligula eget dolor. Aenean massa. -Cum sociis natoque penatibus et magnis dis parturient -montes, nascetur ridiculus mus. Donec quam felis, -ultricies nec, pellentesque eu, pretium quis, sem.
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.
-Lorem ipsum dolor sit amet, consectetuer adipiscing -elit. Aenean commodo ligula eget dolor. Aenean massa. -Cum sociis natoque penatibus et magnis dis parturient -montes, nascetur ridiculus mus. Donec quam felis, -ultricies nec, pellentesque eu, pretium quis, sem.
-| Entry Header 1 | -Entry Header 2 | -Entry Header 3 | -Entry Header 4 | -
|---|---|---|---|
| Entry First Line 1 | -Entry First Line 2 | -Entry First Line 3 | -Entry First Line 4 | -
| Entry Line 1 | -Entry Line 2 | -Entry Line 3 | -Entry Line 4 | -
| Entry Last Line 1 | -Entry Last Line 2 | -Entry Last Line 3 | -Entry Last Line 4 | -
Lorem ipsum dolor sit amet, consectetuer adipiscing -elit. Aenean commodo ligula eget dolor. Aenean massa. -Cum sociis natoque penatibus et magnis dis parturient -montes, nascetur ridiculus mus. Donec quam felis, -ultricies nec, pellentesque eu, pretium quis, sem.
+Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.
+| Entry Header 1 | +Entry Header 2 | +Entry Header 3 | +Entry Header 4 | +
|---|---|---|---|
| Entry First Line 1 | +Entry First Line 2 | +Entry First Line 3 | +Entry First Line 4 | +
| Entry Line 1 | +Entry Line 2 | +Entry Line 3 | +Entry Line 4 | +
| Entry Last Line 1 | +Entry Last Line 2 | +Entry Last Line 3 | +Entry Last Line 4 | +
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem.
`; diff --git a/src/screens/updates/UpdatesScreen.tsx b/src/screens/updates/UpdatesScreen.tsx index 3987ee20a8..da6d05f259 100644 --- a/src/screens/updates/UpdatesScreen.tsx +++ b/src/screens/updates/UpdatesScreen.tsx @@ -94,11 +94,14 @@ const UpdatesScreen = ({ navigation }: UpdateScreenProps) => { acc: { data: UpdateOverview[]; date: string }[], cur: UpdateOverview, ) => { - if (acc.length === 0 || acc.at(-1)?.date !== cur.updateDate) { + if ( + acc.length === 0 || + acc[acc.length - 1]?.date !== cur.updateDate + ) { acc.push({ data: [cur], date: cur.updateDate }); return acc; } - acc.at(-1)?.data.push(cur); + acc[acc.length - 1]?.data.push(cur); return acc; }, [], diff --git a/src/services/ServiceManager.ts b/src/services/ServiceManager.ts index 9246715bf8..64776e14eb 100644 --- a/src/services/ServiceManager.ts +++ b/src/services/ServiceManager.ts @@ -149,8 +149,8 @@ export default class ServiceManager { taskList[0].task?.name !== 'DOWNLOAD_CHAPTER' ) { const now = Date.now(); - if (now - this.lastNotifUpdate > 1000) { - const delay = 1000 - now - this.lastNotifUpdate; + if (now - this.lastNotifUpdate < 1000) { + const delay = 1000 - (now - this.lastNotifUpdate); const id = ++this.currentPendingUpdate; setTimeout(() => { if (this.currentPendingUpdate !== id) { diff --git a/src/services/Trackers/__tests__/myAnimeList.test.ts b/src/services/Trackers/__tests__/myAnimeList.test.ts new file mode 100644 index 0000000000..4ca3e9ea7f --- /dev/null +++ b/src/services/Trackers/__tests__/myAnimeList.test.ts @@ -0,0 +1,76 @@ +jest.mock('expo-linking', () => ({ + createURL: jest.fn(() => 'lnreader://tracker/MAL'), +})); +jest.mock('expo-web-browser', () => ({ + openAuthSessionAsync: jest.fn(), +})); + +import { myAnimeListTracker } from '../myAnimeList'; + +const auth = { + accessToken: 'access-token', + expiresAt: new Date(), +}; + +describe('MyAnimeList tracker', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('creates a default list entry when the manga is not tracked yet', async () => { + const fetchMock = jest + .spyOn(global, 'fetch') + .mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ id: 123, num_chapters: 10 }), + } as unknown as Response) + .mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ + status: 'reading', + num_chapters_read: 0, + score: 0, + }), + } as unknown as Response); + + await expect( + myAnimeListTracker.getUserListEntry(123, auth), + ).resolves.toEqual({ + status: 'CURRENT', + progress: 0, + score: 0, + totalChapters: 10, + }); + + expect(fetchMock).toHaveBeenNthCalledWith( + 2, + 'https://api.myanimelist.net/v2/manga/123/my_list_status', + expect.objectContaining({ + method: 'PUT', + body: 'status=reading&is_rereading=false&num_chapters_read=0&score=0', + }), + ); + }); + + it('returns an existing list entry without updating it', async () => { + const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValueOnce({ + json: jest.fn().mockResolvedValue({ + num_chapters: 10, + my_list_status: { + status: 'plan_to_read', + num_chapters_read: 0, + score: 0, + }, + }), + } as unknown as Response); + + await expect( + myAnimeListTracker.getUserListEntry(123, auth), + ).resolves.toEqual({ + status: 'PLANNING', + progress: 0, + score: 0, + totalChapters: 10, + }); + + expect(fetchMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/services/Trackers/aniList.ts b/src/services/Trackers/aniList.ts index 50b72d43d8..30d392a4ae 100644 --- a/src/services/Trackers/aniList.ts +++ b/src/services/Trackers/aniList.ts @@ -1,6 +1,6 @@ import * as Linking from 'expo-linking'; import * as WebBrowser from 'expo-web-browser'; -import Config from 'react-native-config'; +import Config from '@env'; import { AuthenticationResult, Tracker } from './index'; const apiEndpoint = 'https://graphql.anilist.co'; diff --git a/src/services/Trackers/myAnimeList.ts b/src/services/Trackers/myAnimeList.ts index 0791db13fd..6746e613f4 100644 --- a/src/services/Trackers/myAnimeList.ts +++ b/src/services/Trackers/myAnimeList.ts @@ -1,7 +1,12 @@ import * as Linking from 'expo-linking'; import * as WebBrowser from 'expo-web-browser'; -import Config from 'react-native-config'; -import { Tracker, UserListStatus } from './index'; +import Config from '@env'; +import { + AuthenticationResult, + Tracker, + UserListEntry, + UserListStatus, +} from './index'; const clientId = Config.MYANIMELIST_CLIENT_ID; const baseOAuthUrl = 'https://myanimelist.net/v1/oauth2/authorize'; @@ -49,11 +54,11 @@ export const myAnimeListTracker: Tracker = { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', - }, + }, // @ts-expect-error - If no clientId is set, which will only be set in production builds, this will error out. body: new URLSearchParams({ client_id: clientId, grant_type: 'authorization_code', - code, + code: code, code_verifier: challenge, }).toString(), }); @@ -117,54 +122,73 @@ export const myAnimeListTracker: Tracker = { }); const data = await response.json(); - return { - status: malToNormalized[data.my_list_status?.status] || 'CURRENT', - score: data.my_list_status?.score || 0, - progress: data.my_list_status?.num_chapters_read || 0, - totalChapters: data.num_chapters, - }; - }, - updateUserListEntry: async (id, payload, auth) => { - let status = payload.status - ? normalizedToMal[payload.status] - : normalizedToMal.CURRENT; - let repeating = 'false'; - if (status.includes(';')) { - const split = status.split(';'); - status = split[0]; - repeating = split[1]; - } - - const url = `${baseApiUrl}/manga/${id}/my_list_status`; - const res = await fetch(url, { - method: 'PUT', - headers: { - Authorization: `Bearer ${auth.accessToken}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - status, - is_rereading: repeating, - num_chapters_read: '' + payload.progress!, - score: '' + payload.score!, - }).toString(), - }); - const data = await res.json(); - - let normalizedStatus = malToNormalized[data.status]; - if (!normalizedStatus && data.is_rereading) { - normalizedStatus = 'REPEATING'; + if (!data.my_list_status) { + const newListEntry = await updateMyAnimeListEntry( + id, + { status: 'CURRENT', progress: 0, score: 0 }, + auth, + ); + return { + ...newListEntry, + totalChapters: data.num_chapters, + }; } return { - status: normalizedStatus, - progress: data.num_chapters_read, - score: data.score, + status: malToNormalized[data.my_list_status.status] || 'CURRENT', + score: data.my_list_status.score || 0, + progress: data.my_list_status.num_chapters_read || 0, + totalChapters: data.num_chapters, }; }, + updateUserListEntry: updateMyAnimeListEntry, }; +async function updateMyAnimeListEntry( + id: number | string, + payload: Partial