diff --git a/src/lib/telegram/prevent-telegram-swipe-down-closing.tsx b/src/lib/telegram/prevent-telegram-swipe-down-closing.tsx index 6fb587bc..f9401c13 100644 --- a/src/lib/telegram/prevent-telegram-swipe-down-closing.tsx +++ b/src/lib/telegram/prevent-telegram-swipe-down-closing.tsx @@ -85,7 +85,7 @@ export const PreventTelegramSwipeDownClosingIos = (props: { // A hacky way to force expand app back whenever user pull app down export const useRestoreFullScreenExpand = () => { useEffect(() => { - if (WebApp.platform !== "android" && WebApp.platform !== 'ios') { + if (WebApp.platform !== "android" && WebApp.platform !== "ios") { return; } const onViewPortChanged = () => { diff --git a/src/lib/throttle/throttle.ts b/src/lib/throttle/throttle.ts index 244b5dfd..45bc5adc 100644 --- a/src/lib/throttle/throttle.ts +++ b/src/lib/throttle/throttle.ts @@ -2,7 +2,7 @@ export function throttle(func, limit) { let inThrottle; - return function() { + return function () { // eslint-disable-next-line const args = arguments; // eslint-disable-next-line diff --git a/src/screens/app.tsx b/src/screens/app.tsx index f3435c89..60f5064b 100644 --- a/src/screens/app.tsx +++ b/src/screens/app.tsx @@ -14,13 +14,18 @@ import { deckListStore } from "../store/deck-list-store.ts"; import { FullScreenLoader } from "./deck-list/full-screen-loader.tsx"; import { PreventTelegramSwipeDownClosingIos, - useRestoreFullScreenExpand + useRestoreFullScreenExpand, } from "../lib/telegram/prevent-telegram-swipe-down-closing.tsx"; +import { RepeatAllScreen } from "./deck-review/repeat-all-screen.tsx"; export const App = observer(() => { useRestoreFullScreenExpand(); - if (deckListStore.isSharedDeckLoading || deckListStore.isDeckRemoving) { + if ( + deckListStore.isSharedDeckLoading || + deckListStore.isDeckRemoving || + deckListStore.isReviewAllLoading + ) { return ; } @@ -39,6 +44,13 @@ export const App = observer(() => { )} + {screenStore.screen.type === "reviewAll" && ( + + + + + + )} {screenStore.screen.type === "deckForm" && ( diff --git a/src/screens/deck-review/deck-finished.tsx b/src/screens/deck-review/deck-finished.tsx index 02deb332..de89fab7 100644 --- a/src/screens/deck-review/deck-finished.tsx +++ b/src/screens/deck-review/deck-finished.tsx @@ -26,7 +26,12 @@ const encouragingMessages = [ "Just think of the compounded knowledge you're getting with every review. Your future self thanks you!", ]; -export const DeckFinished = observer(() => { +type Props = { + type: "deck" | "repeat_all"; +}; + +export const DeckFinished = observer((props: Props) => { + const { type } = props; const reviewStore = useReviewStore(); useMount(() => { @@ -46,7 +51,11 @@ export const DeckFinished = observer(() => { alignItems: "center", })} > -

You have finished this deck for now 🎉

+

+ {type === "deck" + ? `You have finished this deck for now 🎉` + : `You have repeated all the cards for today 🎉`} +

{random(encouragingMessages)} 😊

diff --git a/src/screens/deck-review/deck-preview.tsx b/src/screens/deck-review/deck-preview.tsx index a29c7642..fd629460 100644 --- a/src/screens/deck-review/deck-preview.tsx +++ b/src/screens/deck-review/deck-preview.tsx @@ -22,7 +22,7 @@ export const DeckPreview = observer(() => { useMainButton( "Review deck", () => { - deckListStore.startReview(reviewStore); + deckListStore.startDeckReview(reviewStore); }, () => deckListStore.canReview, ); diff --git a/src/screens/deck-review/deck-screen.tsx b/src/screens/deck-review/deck-screen.tsx index 55d21b58..9739227e 100644 --- a/src/screens/deck-review/deck-screen.tsx +++ b/src/screens/deck-review/deck-screen.tsx @@ -9,7 +9,7 @@ export const DeckScreen = observer(() => { const reviewStore = useReviewStore(); if (reviewStore.isFinished) { - return ; + return ; } else if (reviewStore.currentCardId) { return ; } diff --git a/src/screens/deck-review/repeat-all-screen.tsx b/src/screens/deck-review/repeat-all-screen.tsx new file mode 100644 index 00000000..70c7b0c8 --- /dev/null +++ b/src/screens/deck-review/repeat-all-screen.tsx @@ -0,0 +1,23 @@ +import { observer } from "mobx-react-lite"; +import { useReviewStore } from "../../store/review-store-context.tsx"; +import { useMount } from "../../lib/react/use-mount.ts"; +import { deckListStore } from "../../store/deck-list-store.ts"; +import { DeckFinished } from "./deck-finished.tsx"; +import { Review } from "./review.tsx"; +import React from "react"; + +export const RepeatAllScreen = observer(() => { + const reviewStore = useReviewStore(); + + useMount(() => { + reviewStore.startAllRepeatReview(deckListStore.myDecks); + }); + + if (reviewStore.isFinished) { + return ; + } else if (reviewStore.currentCardId) { + return ; + } + + return null; +}); diff --git a/src/store/deck-list-store.ts b/src/store/deck-list-store.ts index 8a36c484..64f8cb9e 100644 --- a/src/store/deck-list-store.ts +++ b/src/store/deck-list-store.ts @@ -1,4 +1,4 @@ -import { action, makeAutoObservable, runInAction, when } from "mobx"; +import { action, makeAutoObservable, when } from "mobx"; import { fromPromise, IPromiseBasedObservable } from "mobx-utils"; import { addDeckToMineRequest, @@ -18,14 +18,23 @@ import { ReviewStore } from "./review-store.ts"; import { reportHandledError } from "../lib/rollbar/rollbar.tsx"; import { UserDbType } from "../../functions/db/user/upsert-user-db.ts"; +export enum StartParamType { + RepeatAll = "repeat_all", +} + export type DeckWithCardsWithReviewType = DeckWithCardsDbType & { cardsToReview: Array; }; export class DeckListStore { myInfo?: IPromiseBasedObservable; + isSharedDeckLoading = false; isSharedDeckLoaded = false; + + isReviewAllLoading = false; + isReviewAllLoaded = false; + skeletonLoaderData = { publicCount: 3, myDecksCount: 3 }; isDeckRemoving = false; @@ -34,9 +43,9 @@ export class DeckListStore { makeAutoObservable(this, {}, { autoBind: true }); } - loadFirstTime(shareId?: string) { + loadFirstTime(startParam?: string) { this.load(); - this.loadSharedDeck(shareId); + this.handleStartParam(startParam); } load() { @@ -52,51 +61,76 @@ export class DeckListStore { } } - async loadSharedDeck(shareId?: string) { - if (!shareId || this.isSharedDeckLoaded) { + async handleStartParam(startParam?: string) { + if (!startParam) { return; } - this.isSharedDeckLoading = true; - await when(() => this.myInfo?.state === "fulfilled"); - - getSharedDeckRequest(shareId) - .then( - action((sharedDeck) => { - assert(this.myInfo?.state === "fulfilled"); - if ( - this.myInfo.value.myDecks.find( - (myDeck) => myDeck.id === sharedDeck.deck.id, - ) - ) { - screenStore.go({ type: "deckMine", deckId: sharedDeck.deck.id }); - return; - } - - if ( - this.publicDecks.find( - (publicDeck) => publicDeck.id === sharedDeck.deck.id, - ) - ) { + if (startParam === StartParamType.RepeatAll) { + if (this.isReviewAllLoaded) { + return; + } + + this.isReviewAllLoading = true; + when(() => this.myInfo?.state === "fulfilled") + .then(() => { + screenStore.go({ type: "reviewAll" }); + }) + .finally( + action(() => { + this.isReviewAllLoading = false; + this.isReviewAllLoaded = true; + }), + ); + } else { + if (this.isSharedDeckLoaded) { + return; + } + + this.isSharedDeckLoading = true; + await when(() => this.myInfo?.state === "fulfilled"); + + getSharedDeckRequest(startParam) + .then( + action((sharedDeck) => { + assert(this.myInfo?.state === "fulfilled"); + if ( + this.myInfo.value.myDecks.find( + (myDeck) => myDeck.id === sharedDeck.deck.id, + ) + ) { + screenStore.go({ type: "deckMine", deckId: sharedDeck.deck.id }); + return; + } + + if ( + this.publicDecks.find( + (publicDeck) => publicDeck.id === sharedDeck.deck.id, + ) + ) { + screenStore.go({ + type: "deckPublic", + deckId: sharedDeck.deck.id, + }); + return; + } + + this.myInfo.value.publicDecks.push(sharedDeck.deck); screenStore.go({ type: "deckPublic", deckId: sharedDeck.deck.id }); - return; - } - - this.myInfo.value.publicDecks.push(sharedDeck.deck); - screenStore.go({ type: "deckPublic", deckId: sharedDeck.deck.id }); - }), - ) - .catch((e) => { - reportHandledError("Error while retrieving shared deck", e, { - shareId, - }); - }) - .finally( - action(() => { - this.isSharedDeckLoading = false; - this.isSharedDeckLoaded = true; - }), - ); + }), + ) + .catch((e) => { + reportHandledError("Error while retrieving shared deck", e, { + shareId: startParam, + }); + }) + .finally( + action(() => { + this.isSharedDeckLoading = false; + this.isSharedDeckLoaded = true; + }), + ); + } } get canReview() { @@ -108,7 +142,7 @@ export class DeckListStore { ); } - startReview(reviewStore: ReviewStore) { + startDeckReview(reviewStore: ReviewStore) { if (!this.canReview) { return; } @@ -198,7 +232,7 @@ export class DeckListStore { ); } - async removeDeck() { + removeDeck() { const deck = this.selectedDeck; if (!deck) { return; @@ -206,17 +240,21 @@ export class DeckListStore { this.isDeckRemoving = true; - try { - await removeDeckFromMine({ deckId: deck.id }); - screenStore.go({ type: "main" }); - this.myInfo = fromPromise(myInfoRequest()); - } catch (e) { - reportHandledError(`Unable to remove deck ${deck.id}`, e); - } finally { - runInAction(() => { - this.isDeckRemoving = false; - }); - } + removeDeckFromMine({ deckId: deck.id }) + .then( + action(() => { + screenStore.go({ type: "main" }); + this.myInfo = fromPromise(myInfoRequest()); + }), + ) + .catch((e) => { + reportHandledError(`Unable to remove deck ${deck.id}`, e); + }) + .finally( + action(() => { + this.isDeckRemoving = false; + }), + ); } optimisticUpdateSettings( diff --git a/src/store/review-store.ts b/src/store/review-store.ts index 2ee2c56d..51935c8d 100644 --- a/src/store/review-store.ts +++ b/src/store/review-store.ts @@ -5,7 +5,10 @@ import { assert } from "../lib/typescript/assert.ts"; import { reviewCardsRequest } from "../api/api.ts"; import { ReviewOutcome } from "../../functions/services/review-card.ts"; import { screenStore } from "./screen-store.ts"; -import { deckListStore } from "./deck-list-store.ts"; +import { + deckListStore, + DeckWithCardsWithReviewType, +} from "./deck-list-store.ts"; type ReviewResult = { forgotIds: number[]; @@ -49,6 +52,34 @@ export class ReviewStore { } } + startAllRepeatReview(myDecks: DeckWithCardsWithReviewType[]) { + if (!myDecks.length) { + return; + } + + myDecks.forEach((deck) => { + deck.cardsToReview + .filter((card) => card.type === "repeat") + .forEach((card) => { + this.cardsToReview.push( + new CardFormStore( + card.id, + card.front, + card.back, + card.example, + deck.name, + ), + ); + }); + }); + + this.initialCardCount = this.cardsToReview.length; + this.currentCardId = this.cardsToReview[0].id; + if (this.cardsToReview.length > 1) { + this.nextCardId = this.cardsToReview[1].id; + } + } + get currentCard() { if (!this.currentCardId) { return null; diff --git a/src/store/screen-store.ts b/src/store/screen-store.ts index 28898c91..c6e7407f 100644 --- a/src/store/screen-store.ts +++ b/src/store/screen-store.ts @@ -5,6 +5,7 @@ type Route = | { type: "deckMine"; deckId: number } | { type: "deckPublic"; deckId: number } | { type: "deckForm"; deckId?: number } + | { type: "reviewAll" } | { type: "cardQuickAddForm"; deckId: number } | { type: "userSettings" };