feat: implement app content readiness handling in page and components
- Added `appContentReady` state to manage visibility of app content once loading is complete. - Updated `useEffect` hooks in `CurrentDutyView` and `CalendarPage` to signal when content is ready, enhancing user experience by hiding native loading indicators. - Refactored `Home` component to conditionally render content based on `appContentReady`, ensuring a smoother transition for users. - Enhanced app store to include `setAppContentReady` method for state management.
This commit is contained in:
@@ -5,12 +5,13 @@
|
|||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import { useAppStore } from "@/store/app-store";
|
import { useAppStore } from "@/store/app-store";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { useTelegramTheme } from "@/hooks/use-telegram-theme";
|
import { useTelegramTheme } from "@/hooks/use-telegram-theme";
|
||||||
import { useTelegramAuth } from "@/hooks/use-telegram-auth";
|
import { useTelegramAuth } from "@/hooks/use-telegram-auth";
|
||||||
import { useAppInit } from "@/hooks/use-app-init";
|
import { useAppInit } from "@/hooks/use-app-init";
|
||||||
|
import { callMiniAppReadyOnce } from "@/lib/telegram-ready";
|
||||||
import { CurrentDutyView } from "@/components/current-duty/CurrentDutyView";
|
import { CurrentDutyView } from "@/components/current-duty/CurrentDutyView";
|
||||||
import { CalendarPage } from "@/components/CalendarPage";
|
import { CalendarPage } from "@/components/CalendarPage";
|
||||||
|
|
||||||
@@ -22,29 +23,48 @@ export default function Home() {
|
|||||||
|
|
||||||
useAppInit({ isAllowed, startParam });
|
useAppInit({ isAllowed, startParam });
|
||||||
|
|
||||||
const { currentView, setCurrentView, setSelectedDay } = useAppStore(
|
const { currentView, setCurrentView, setSelectedDay, appContentReady } =
|
||||||
|
useAppStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
currentView: s.currentView,
|
currentView: s.currentView,
|
||||||
setCurrentView: s.setCurrentView,
|
setCurrentView: s.setCurrentView,
|
||||||
setSelectedDay: s.setSelectedDay,
|
setSelectedDay: s.setSelectedDay,
|
||||||
|
appContentReady: s.appContentReady,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// When content is ready, tell Telegram to hide native loading and show our app.
|
||||||
|
useEffect(() => {
|
||||||
|
if (appContentReady) {
|
||||||
|
callMiniAppReadyOnce();
|
||||||
|
}
|
||||||
|
}, [appContentReady]);
|
||||||
|
|
||||||
const handleBackFromCurrentDuty = useCallback(() => {
|
const handleBackFromCurrentDuty = useCallback(() => {
|
||||||
setCurrentView("calendar");
|
setCurrentView("calendar");
|
||||||
setSelectedDay(null);
|
setSelectedDay(null);
|
||||||
}, [setCurrentView, setSelectedDay]);
|
}, [setCurrentView, setSelectedDay]);
|
||||||
|
|
||||||
if (currentView === "currentDuty") {
|
const content =
|
||||||
return (
|
currentView === "currentDuty" ? (
|
||||||
<div className="mx-auto flex min-h-screen w-full max-w-[var(--max-width-app)] flex-col bg-background px-3 pb-6 pt-safe">
|
<div className="mx-auto flex min-h-screen w-full max-w-[var(--max-width-app)] flex-col bg-background px-3 pb-6 pt-safe">
|
||||||
<CurrentDutyView
|
<CurrentDutyView
|
||||||
onBack={handleBackFromCurrentDuty}
|
onBack={handleBackFromCurrentDuty}
|
||||||
openedFromPin={startParam === "duty"}
|
openedFromPin={startParam === "duty"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<CalendarPage isAllowed={isAllowed} initDataRaw={initDataRaw} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
visibility: appContentReady ? "visible" : "hidden",
|
||||||
|
minHeight: "100vh",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <CalendarPage isAllowed={isAllowed} initDataRaw={initDataRaw} />;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { CalendarHeader } from "@/components/calendar/CalendarHeader";
|
|||||||
import { CalendarGrid } from "@/components/calendar/CalendarGrid";
|
import { CalendarGrid } from "@/components/calendar/CalendarGrid";
|
||||||
import { DutyList } from "@/components/duty/DutyList";
|
import { DutyList } from "@/components/duty/DutyList";
|
||||||
import { DayDetail, type DayDetailHandle } from "@/components/day-detail";
|
import { DayDetail, type DayDetailHandle } from "@/components/day-detail";
|
||||||
import { callMiniAppReadyOnce } from "@/lib/telegram-ready";
|
|
||||||
import { ErrorState } from "@/components/states/ErrorState";
|
import { ErrorState } from "@/components/states/ErrorState";
|
||||||
import { AccessDenied } from "@/components/states/AccessDenied";
|
import { AccessDenied } from "@/components/states/AccessDenied";
|
||||||
|
|
||||||
@@ -60,6 +59,7 @@ export function CalendarPage({ isAllowed, initDataRaw }: CalendarPageProps) {
|
|||||||
prevMonth,
|
prevMonth,
|
||||||
setCurrentMonth,
|
setCurrentMonth,
|
||||||
setSelectedDay,
|
setSelectedDay,
|
||||||
|
setAppContentReady,
|
||||||
} = useAppStore(
|
} = useAppStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
currentMonth: s.currentMonth,
|
currentMonth: s.currentMonth,
|
||||||
@@ -75,6 +75,7 @@ export function CalendarPage({ isAllowed, initDataRaw }: CalendarPageProps) {
|
|||||||
prevMonth: s.prevMonth,
|
prevMonth: s.prevMonth,
|
||||||
setCurrentMonth: s.setCurrentMonth,
|
setCurrentMonth: s.setCurrentMonth,
|
||||||
setSelectedDay: s.setSelectedDay,
|
setSelectedDay: s.setSelectedDay,
|
||||||
|
setAppContentReady: s.setAppContentReady,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -126,13 +127,13 @@ export function CalendarPage({ isAllowed, initDataRaw }: CalendarPageProps) {
|
|||||||
}, [setSelectedDay]);
|
}, [setSelectedDay]);
|
||||||
|
|
||||||
const readyCalledRef = useRef(false);
|
const readyCalledRef = useRef(false);
|
||||||
// Signal Telegram to hide loading when the first load finishes (loading goes true -> false).
|
// Mark content ready when first load finishes or access denied, so page can call ready() and show content.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && !readyCalledRef.current) {
|
if ((!loading || accessDenied) && !readyCalledRef.current) {
|
||||||
readyCalledRef.current = true;
|
readyCalledRef.current = true;
|
||||||
callMiniAppReadyOnce();
|
setAppContentReady(true);
|
||||||
}
|
}
|
||||||
}, [loading]);
|
}, [loading, accessDenied, setAppContentReady]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto flex min-h-screen w-full max-w-[var(--max-width-app)] flex-col bg-background px-3 pb-6 pt-safe">
|
<div className="mx-auto flex min-h-screen w-full max-w-[var(--max-width-app)] flex-col bg-background px-3 pb-6 pt-safe">
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
formatHHMM,
|
formatHHMM,
|
||||||
} from "@/lib/date-utils";
|
} from "@/lib/date-utils";
|
||||||
import { getRemainingTime, findCurrentDuty } from "@/lib/current-duty";
|
import { getRemainingTime, findCurrentDuty } from "@/lib/current-duty";
|
||||||
import { callMiniAppReadyOnce } from "@/lib/telegram-ready";
|
|
||||||
import { ContactLinks } from "@/components/contact/ContactLinks";
|
import { ContactLinks } from "@/components/contact/ContactLinks";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -47,6 +46,7 @@ type ViewState = "loading" | "error" | "ready";
|
|||||||
export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyViewProps) {
|
export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyViewProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const lang = useAppStore((s) => s.lang);
|
const lang = useAppStore((s) => s.lang);
|
||||||
|
const setAppContentReady = useAppStore((s) => s.setAppContentReady);
|
||||||
const { initDataRaw } = useTelegramAuth();
|
const { initDataRaw } = useTelegramAuth();
|
||||||
|
|
||||||
const [state, setState] = useState<ViewState>("loading");
|
const [state, setState] = useState<ViewState>("loading");
|
||||||
@@ -93,12 +93,12 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
|
|||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [loadTodayDuties]);
|
}, [loadTodayDuties]);
|
||||||
|
|
||||||
// Signal Telegram to hide loading when this view is ready (or error).
|
// Mark content ready when data is loaded or error, so page can call ready() and show content.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state !== "loading") {
|
if (state !== "loading") {
|
||||||
callMiniAppReadyOnce();
|
setAppContentReady(true);
|
||||||
}
|
}
|
||||||
}, [state]);
|
}, [state, setAppContentReady]);
|
||||||
|
|
||||||
// Auto-update remaining time every second when there is an active duty.
|
// Auto-update remaining time every second when there is an active duty.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export interface AppState {
|
|||||||
accessDeniedDetail: string | null;
|
accessDeniedDetail: string | null;
|
||||||
currentView: CurrentView;
|
currentView: CurrentView;
|
||||||
selectedDay: string | null;
|
selectedDay: string | null;
|
||||||
|
/** True when the first visible screen has finished loading; used to hide content until ready(). */
|
||||||
|
appContentReady: boolean;
|
||||||
|
|
||||||
setCurrentMonth: (d: Date) => void;
|
setCurrentMonth: (d: Date) => void;
|
||||||
nextMonth: () => void;
|
nextMonth: () => void;
|
||||||
@@ -41,8 +43,9 @@ export interface AppState {
|
|||||||
setLang: (v: "ru" | "en") => void;
|
setLang: (v: "ru" | "en") => void;
|
||||||
setCurrentView: (v: CurrentView) => void;
|
setCurrentView: (v: CurrentView) => void;
|
||||||
setSelectedDay: (key: string | null) => void;
|
setSelectedDay: (key: string | null) => void;
|
||||||
|
setAppContentReady: (v: boolean) => void;
|
||||||
/** Batch multiple state updates into a single re-render. */
|
/** Batch multiple state updates into a single re-render. */
|
||||||
batchUpdate: (partial: Partial<Pick<AppState, "currentMonth" | "pendingMonth" | "lang" | "duties" | "calendarEvents" | "dataForMonthKey" | "loading" | "error" | "accessDenied" | "accessDeniedDetail" | "currentView" | "selectedDay">>) => void;
|
batchUpdate: (partial: Partial<Pick<AppState, "currentMonth" | "pendingMonth" | "lang" | "duties" | "calendarEvents" | "dataForMonthKey" | "loading" | "error" | "accessDenied" | "accessDeniedDetail" | "currentView" | "selectedDay" | "appContentReady">>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -67,6 +70,7 @@ export const useAppStore = create<AppState>((set) => ({
|
|||||||
accessDeniedDetail: null,
|
accessDeniedDetail: null,
|
||||||
currentView: getInitialView(),
|
currentView: getInitialView(),
|
||||||
selectedDay: null,
|
selectedDay: null,
|
||||||
|
appContentReady: false,
|
||||||
|
|
||||||
setCurrentMonth: (d) => set({ currentMonth: d }),
|
setCurrentMonth: (d) => set({ currentMonth: d }),
|
||||||
nextMonth: () =>
|
nextMonth: () =>
|
||||||
@@ -86,5 +90,6 @@ export const useAppStore = create<AppState>((set) => ({
|
|||||||
setLang: (v) => set({ lang: v }),
|
setLang: (v) => set({ lang: v }),
|
||||||
setCurrentView: (v) => set({ currentView: v }),
|
setCurrentView: (v) => set({ currentView: v }),
|
||||||
setSelectedDay: (key) => set({ selectedDay: key }),
|
setSelectedDay: (key) => set({ selectedDay: key }),
|
||||||
|
setAppContentReady: (v) => set({ appContentReady: v }),
|
||||||
batchUpdate: (partial) => set(partial),
|
batchUpdate: (partial) => set(partial),
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user