From 0da7b334db41d415e55d9f65d6c13065e4d20892 Mon Sep 17 00:00:00 2001 From: Dorian Koch <doriank@fsmpi.rwth-aachen.de> Date: Sat, 14 Dec 2024 12:05:34 +0100 Subject: [PATCH] Do not hide page while reloading data, Closes #72 --- src/components/CourseListing.tsx | 10 ++++----- src/components/Player.tsx | 22 +++++++++++++++++-- src/components/UpdateOverlay.tsx | 19 ++++++++++++++++ src/pages/courses.tsx | 26 ++++++++++++++++++++-- src/pages/dynamic_course_listing.tsx | 19 +++++++++++----- src/pages/dynamic_player.tsx | 33 +++++++++++++++++++++++----- src/styles/globals.scss | 2 +- 7 files changed, 109 insertions(+), 22 deletions(-) create mode 100644 src/components/UpdateOverlay.tsx diff --git a/src/components/CourseListing.tsx b/src/components/CourseListing.tsx index c8655c0..5729465 100644 --- a/src/components/CourseListing.tsx +++ b/src/components/CourseListing.tsx @@ -15,6 +15,7 @@ import type { GetCourseResponse, lecture } from "@/api/api_v1_types"; import { ResourceType } from "@/misc/PromiseHelpers"; import { AuthenticationMethodIcons } from "@/misc/Util"; import { useLanguage } from "./LanguageProvider"; +import { UpdateOverlay } from "./UpdateOverlay"; function ListingHeader({ course }: { course: GetCourseResponse }) { const { editMode } = useEditMode(); @@ -368,11 +369,10 @@ export default function CourseListing({ return ( <> <Title title={course.full_name} /> - <div> - <ListingHeader course={course} /> - <ListingBody course={course} /> - <div className={"edits-disabled-overlay " + (disabled ? "visible" : "")} /> - </div> + + <ListingHeader course={course} /> + <ListingBody course={course} /> + <UpdateOverlay show={disabled} /> </> ); } diff --git a/src/components/Player.tsx b/src/components/Player.tsx index d633aca..5a00321 100644 --- a/src/components/Player.tsx +++ b/src/components/Player.tsx @@ -36,6 +36,7 @@ import Head from "next/head"; import { VideoCard } from "./VideoCard"; import { urlForLecture } from "@/misc/Util"; import VideoJsMarkers from "@/misc/videojs-markers"; +import { UpdateOverlay } from "./UpdateOverlay"; function initPlayer( player: VideoJsPlayerType, @@ -803,7 +804,13 @@ export function DownloadSources({ ); } -export function EmbeddedPlayer({ playerData }: { playerData: ResourceType<PlayerData> }) { +export function EmbeddedPlayer({ + playerData, + disabled, +}: { + playerData: ResourceType<PlayerData>; + disabled: boolean; +}) { const { course, perms, lectureId } = playerData.read()!; const lecture = course.lectures!.find((l) => l.id === lectureId)!; @@ -841,10 +848,20 @@ export function EmbeddedPlayer({ playerData }: { playerData: ResourceType<Player ); } + if (disabled) { + return <UpdateOverlay show={true} />; + } + return <VideoPlayer lecture={lecture} className="h-100" />; } -export default function Player({ playerData }: { playerData: ResourceType<PlayerData> }) { +export default function Player({ + playerData, + disabled, +}: { + playerData: ResourceType<PlayerData>; + disabled: boolean; +}) { const { course, lectureId, perms, loaderHadUserInfo } = playerData.read()!; const api = useBackendContext(); const userContext = useUserContext(); @@ -1026,6 +1043,7 @@ export default function Player({ playerData }: { playerData: ResourceType<Player /> </Head> <Title title={`${course.short_name} - ${lecture.title}`} /> + <UpdateOverlay show={disabled} /> <div className={`card ${lecture.is_visible === false ? "bg-danger-subtle" : ""}`}> <div className="card-header d-flex"> <div className="flex-fill align-self-center"> diff --git a/src/components/UpdateOverlay.tsx b/src/components/UpdateOverlay.tsx new file mode 100644 index 0000000..a44e367 --- /dev/null +++ b/src/components/UpdateOverlay.tsx @@ -0,0 +1,19 @@ +export function UpdateOverlay({ show }: { show: boolean }) { + return ( + <div + className={ + "edits-disabled-overlay d-flex flex-column justify-content-center align-items-center " + + (show ? "visible" : "") + } + > + <div className="d-flex flex-column align-items-center"> + <div + className="spinner-grow mb-2" + role="status" + style={{ width: "3rem", height: "3rem" }} + /> + Updating... + </div> + </div> + ); +} diff --git a/src/pages/courses.tsx b/src/pages/courses.tsx index 685a2e8..975e8ca 100644 --- a/src/pages/courses.tsx +++ b/src/pages/courses.tsx @@ -8,12 +8,13 @@ import { semesterToHuman } from "@/misc/Formatting"; import { ResourceType, fetchDataWrapper } from "@/misc/PromiseHelpers"; import { ErrorPage } from "@/misc/ErrorHandlers"; import Link from "next/link"; -import { Suspense, useEffect, useState } from "react"; +import { Suspense, useEffect, useRef, useState } from "react"; import DropdownButton from "react-bootstrap/DropdownButton"; import DropdownItem from "react-bootstrap/DropdownItem"; import type { GetCoursesResponse, course } from "@/api/api_v1_types"; import { useLanguage } from "@/components/LanguageProvider"; +import { UpdateOverlay } from "@/components/UpdateOverlay"; type GroupByTypes = "semester" | "full_name" | "organizer" | "topic"; @@ -181,13 +182,33 @@ export default function Courses() { const hasUserInfo = userContext.hasUserInfo(); const { editMode } = useEditMode(); const [courseData, setCourseData] = useState<ResourceType<GetCoursesResponse>>(); + const nextCoursesDataPromise = useRef<Promise<GetCoursesResponse> | null>(null); + const [isReloading, setIsReloading] = useState(false); const { language } = useLanguage(); const [onlyShowPublic, setOnlyShowPublic] = useState(false); const reloadData = () => { - setCourseData(fetchDataWrapper(api.getCourses())); + if (isReloading) { + console.log("Already reloading, overwriting old request..."); + } + const prom = api.getCourses(); + nextCoursesDataPromise.current = prom; + if (!courseData) { + setCourseData(fetchDataWrapper(prom)); + } else { + setIsReloading(true); + prom.finally(() => { + if (prom !== nextCoursesDataPromise.current) { + console.log("Ignoring stale promise"); + return; + } + setCourseData(fetchDataWrapper(prom)); + setIsReloading(false); + }); + } }; + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(reloadData, [api, hasUserInfo]); const [groupedBy, setGroupedBy] = useState<GroupByTypes>("semester"); @@ -266,6 +287,7 @@ export default function Courses() { groupBy={groupedBy} onlyShowPublicCourses={onlyShowPublic} /> + <UpdateOverlay show={isReloading} /> </Suspense> </FallbackErrorBoundary> </ReloadBoundary> diff --git a/src/pages/dynamic_course_listing.tsx b/src/pages/dynamic_course_listing.tsx index 185e1ac..90d449e 100644 --- a/src/pages/dynamic_course_listing.tsx +++ b/src/pages/dynamic_course_listing.tsx @@ -1,4 +1,4 @@ -import { Suspense, useEffect, useState } from "react"; +import { Suspense, useEffect, useRef, useState } from "react"; import Four04 from "./404"; import CourseListing from "@/components/CourseListing"; import { useBackendContext } from "@/components/BackendProvider"; @@ -17,22 +17,29 @@ function ActualRedirector() { const pathname = usePathname(); const matchedParams = pathname.match(/^\/([a-z0-9_-]+)\/?$/); const [courseData, setCourseData] = useState<ResourceType<GetCourseResponse>>(); + const nextCourseDataPromise = useRef<Promise<GetCourseResponse> | null>(null); const [isReloading, setIsReloading] = useState(false); const reload = () => { if (!matchedParams) return; - if (isReloading) return; + if (isReloading) { + console.log("Already reloading, overwriting old request..."); + } const prom = api.getCourse(matchedParams[1], true); - if (!courseData) setCourseData(fetchDataWrapper(prom)); - else { - console.log("Reloading course data"); + nextCourseDataPromise.current = prom; + if (!courseData) { + setCourseData(fetchDataWrapper(prom)); + } else { setIsReloading(true); // already showing course data, disable old one and load new one in the background prom.finally(() => { + if (prom !== nextCourseDataPromise.current) { + console.log("Ignoring stale promise"); + return; + } setCourseData(fetchDataWrapper(prom)); setIsReloading(false); - console.log("reload done"); }); } }; diff --git a/src/pages/dynamic_player.tsx b/src/pages/dynamic_player.tsx index 50ebbda..f23ba36 100644 --- a/src/pages/dynamic_player.tsx +++ b/src/pages/dynamic_player.tsx @@ -1,4 +1,4 @@ -import { Suspense, useEffect, useState } from "react"; +import { Suspense, useEffect, useRef, useState } from "react"; import Four04 from "./404"; import { useBackendContext } from "@/components/BackendProvider"; @@ -53,15 +53,32 @@ export function ActualRedirector() { const matchedParamsPlayer = pathname.match(/^\/([a-z0-9_-]+)\/([a-z0-9_-]+)$/); const matchedParamsEmbed = pathname.match(/^\/([a-z0-9_-]+)\/([a-z0-9_-]+)\/embed$/); const [playerData, setPlayerData] = useState<ResourceType<PlayerData>>(); + const nextPlayerDataPromise = useRef<Promise<PlayerData> | null>(null); + const [isReloading, setIsReloading] = useState(false); const reloadData = () => { if (!matchedParamsPlayer && !matchedParamsEmbed) return; + if (isReloading) { + console.log("Already reloading, overwriting old request..."); + } const matched = (matchedParamsPlayer ?? matchedParamsEmbed)!; const courseId = matched[1]; const lectureId = matched[2]; - setPlayerData( - fetchDataWrapper(dataLoader(api, courseId, lectureId, userContext.hasUserInfo())), - ); + const prom = dataLoader(api, courseId, lectureId, userContext.hasUserInfo()); + nextPlayerDataPromise.current = prom; + if (!playerData) { + setPlayerData(fetchDataWrapper(prom)); + } else { + setIsReloading(true); + prom.finally(() => { + if (prom !== nextPlayerDataPromise.current) { + console.log("Ignoring stale promise"); + return; + } + setPlayerData(fetchDataWrapper(prom)); + setIsReloading(false); + }); + } }; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -90,8 +107,12 @@ export function ActualRedirector() { )} > <Suspense fallback={<div className="spinner-border" />}> - {matchedParamsEmbed && <EmbeddedPlayer playerData={playerData} />} - {matchedParamsPlayer && <Player playerData={playerData} />} + {matchedParamsEmbed && ( + <EmbeddedPlayer playerData={playerData} disabled={isReloading} /> + )} + {matchedParamsPlayer && ( + <Player playerData={playerData} disabled={isReloading} /> + )} </Suspense> </FallbackErrorBoundary> </ReloadBoundary> diff --git a/src/styles/globals.scss b/src/styles/globals.scss index a19de25..ac7fc5e 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -286,7 +286,7 @@ $link-decoration: none; background-color: rgba(0, 0, 0, 0.5); z-index: 1000; pointer-events: none; - animation: fadeOut 0.1s ease-in-out forwards; + animation: fadeOut 0.3s ease-in-out forwards; } .edits-disabled-overlay.visible { -- GitLab