diff --git a/src/components/CourseListing.tsx b/src/components/CourseListing.tsx index c8655c044ec26ecb6b57915c67ecd932de5ef43f..5729465e7b09d393b40a74783c999b0695be2840 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 d633aca3dfe08ee3e332f1eaf4fa85eaf08d674d..5a0032106eaf782aff66f3dc516258eab010ac30 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 0000000000000000000000000000000000000000..a44e3678d3b6ba3d4b774f1c4be684544771f8d6 --- /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 685a2e8b7868e35e8e2f2ec75729642d07c501c2..975e8ca2c9bbbcc07741127e0469aba526dceae6 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 185e1ac2c058317d1221a912d1a6f5a614fa872d..90d449ec8757e41522e015d5f799573c4ebb0c7e 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 50ebbdabc8627d3c75e4bf6b60f9c9bb7830f2d4..f23ba366819d7757c3334e55daf6ba4dbe647208 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 a19de2585230426db1ebab2f7789f44adf046a69..ac7fc5efa5e3ce75ebb81bd09d60a72289d88a72 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 {