Skip to content

Commit

Permalink
Add authenticated data context to video pages and realms
Browse files Browse the repository at this point in the history
Before this change, authenticating a video on video pages or in
video blocks would not rerender series blocks, i.e. the thumbnails
would not reflect the fact that the videos of these blocks were
unlocked.
This adds a shared state through context so these blocks are now
rerendered and correctly show the videos as unlocked.
  • Loading branch information
owi92 committed Nov 19, 2024
1 parent e511fd3 commit ae81723
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 38 deletions.
9 changes: 6 additions & 3 deletions frontend/src/routes/Embed.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReactNode, Suspense } from "react";
import { ReactNode, Suspense, useState } from "react";
import { LuFrown, LuAlertTriangle } from "react-icons/lu";
import { Translation, useTranslation } from "react-i18next";
import {
Expand All @@ -19,7 +19,7 @@ import { EmbedQuery } from "./__generated__/EmbedQuery.graphql";
import { EmbedDirectOpencastQuery } from "./__generated__/EmbedDirectOpencastQuery.graphql";
import { EmbedEventData$key } from "./__generated__/EmbedEventData.graphql";
import { PlayerContextProvider } from "../ui/player/PlayerContext";
import { PreviewPlaceholder } from "./Video";
import { AuthenticatedDataContext, AuthorizedData, PreviewPlaceholder } from "./Video";

export const EmbedVideoRoute = makeRoute({
url: ({ videoId }: { videoId: string }) => `/~embed/!v/${keyOfId(videoId)}`,
Expand Down Expand Up @@ -143,6 +143,7 @@ const Embed: React.FC<EmbedProps> = ({ query, queryRef }) => {
fragmentRef.event,
);
const { t } = useTranslation();
const [authenticatedData, setAuthenticatedData] = useState<AuthorizedData | null>(null);

if (!event) {
return <PlayerPlaceholder>
Expand Down Expand Up @@ -177,7 +178,9 @@ const Embed: React.FC<EmbedProps> = ({ query, queryRef }) => {

return authorizedData
? <Player event={{ ...event, authorizedData }} />
: <PreviewPlaceholder embedded {...{ event }}/>;
: <AuthenticatedDataContext.Provider value={{ authenticatedData, setAuthenticatedData }}>
<PreviewPlaceholder embedded {...{ event }}/>;
</AuthenticatedDataContext.Provider>;
};

export const BlockEmbedRoute = makeRoute({
Expand Down
10 changes: 7 additions & 3 deletions frontend/src/routes/Realm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { COLORS } from "../color";
import { useMenu } from "../layout/MenuState";
import { ManageNav } from "./manage";
import { BREAKPOINT as NAV_BREAKPOINT } from "../layout/Navigation";
import { AuthorizedData, AuthenticatedDataContext } from "./Video";


// eslint-disable-next-line @typescript-eslint/quotes
Expand Down Expand Up @@ -145,6 +146,7 @@ const RealmPage: React.FC<Props> = ({ realm }) => {
const { t } = useTranslation();
const siteTitle = useTranslatedConfig(CONFIG.siteTitle);
const breadcrumbs = realmBreadcrumbs(t, realm.ancestors);
const [authenticatedData, setAuthenticatedData] = useState<AuthorizedData | null>(null);

const title = realm.isMainRoot ? siteTitle : realm.name;
useTitle(title, realm.isMainRoot);
Expand All @@ -166,9 +168,11 @@ const RealmPage: React.FC<Props> = ({ realm }) => {
{realm.isUserRealm && <UserRealmNote realm={realm} />}
</div>
)}
{realm.blocks.length === 0 && realm.isMainRoot
? <WelcomeMessage />
: <Blocks realm={realm} />}
<AuthenticatedDataContext.Provider value={{ authenticatedData, setAuthenticatedData }}>
{realm.blocks.length === 0 && realm.isMainRoot
? <WelcomeMessage />
: <Blocks realm={realm} />}
</AuthenticatedDataContext.Provider>
</>;
};

Expand Down
91 changes: 59 additions & 32 deletions frontend/src/routes/Video.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import React, { ReactElement, ReactNode, useEffect, useRef, useState } from "react";
import React, {
createContext,
Dispatch,
ReactElement,
ReactNode,
SetStateAction,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { graphql, GraphQLTaggedNode, PreloadedQuery, useFragment } from "react-relay/hooks";
import { useTranslation } from "react-i18next";
import { fetchQuery, OperationType } from "relay-runtime";
Expand All @@ -9,6 +19,7 @@ import { QRCodeCanvas } from "qrcode.react";
import {
match, unreachable, screenWidthAtMost, screenWidthAbove, useColorScheme,
Floating, FloatingContainer, FloatingTrigger, WithTooltip, Card, Button, ProtoButton,
bug,
} from "@opencast/appkit";
import { VideoObject, WithContext } from "schema-dts";

Expand Down Expand Up @@ -453,6 +464,13 @@ export const authorizedDataQuery = graphql`
// ===== Components
// ===========================================================================================

export type AuthorizedData = VideoAuthorizedDataQuery$data["authorizedEvent"];
type AuthenticatedDataContext = {
authenticatedData: AuthorizedData;
setAuthenticatedData: Dispatch<SetStateAction<AuthorizedData>>;
}
export const AuthenticatedDataContext = createContext<AuthenticatedDataContext | null>(null);

type Props = {
eventRef: NonNullable<VideoPageEventData$key>;
realmRef: NonNullable<VideoPageRealmData$key>;
Expand All @@ -465,6 +483,7 @@ const VideoPage: React.FC<Props> = ({ eventRef, realmRef, playlistRef, basePath
const rerender = useForceRerender();
const event = useFragment(eventFragment, eventRef);
const realm = useFragment(realmFragment, realmRef);
const [authenticatedData, setAuthenticatedData] = useState<AuthorizedData | null>(null);

if (event.__typename === "NotAllowed") {
return <ErrorPage title={t("api-remote-errors.view.event")} />;
Expand Down Expand Up @@ -520,34 +539,36 @@ const VideoPage: React.FC<Props> = ({ eventRef, realmRef, playlistRef, basePath
return <>
<Breadcrumbs path={breadcrumbs} tail={event.title} />
<script type="application/ld+json">{JSON.stringify(structuredData)}</script>
<PlayerContextProvider>
{authorizedData
? <InlinePlayer
event={{ ...event, authorizedData }}
css={{ margin: "-4px auto 0" }}
onEventStateChange={rerender}
/>
: <PreviewPlaceholder {...{ event }}/>
}
<Metadata id={event.id} event={event} />
</PlayerContextProvider>
<AuthenticatedDataContext.Provider value={{ authenticatedData, setAuthenticatedData }}>
<PlayerContextProvider>
{authorizedData
? <InlinePlayer
event={{ ...event, authorizedData }}
css={{ margin: "-4px auto 0" }}
onEventStateChange={rerender}
/>
: <PreviewPlaceholder {...{ event }}/>
}
<Metadata id={event.id} event={event} />
</PlayerContextProvider>

<div css={{ height: 80 }} />
<div css={{ height: 80 }} />

{playlistRef
? <PlaylistBlockFromPlaylist
moreOfTitle
basePath={basePath}
fragRef={playlistRef}
activeEventId={event.id}
/>
: event.series && <SeriesBlockFromSeries
basePath={basePath}
fragRef={event.series}
title={t("video.more-from-series", { series: event.series.title })}
activeEventId={event.id}
/>
}
{playlistRef
? <PlaylistBlockFromPlaylist
moreOfTitle
basePath={basePath}
fragRef={playlistRef}
activeEventId={event.id}
/>
: event.series && <SeriesBlockFromSeries
basePath={basePath}
fragRef={event.series}
title={t("video.more-from-series", { series: event.series.title })}
activeEventId={event.id}
/>
}
</AuthenticatedDataContext.Provider>
</>;
};

Expand Down Expand Up @@ -575,7 +596,6 @@ export const PreviewPlaceholder: React.FC<ProtectedPlayerProps> = ({ event, embe
</div>;
};

export type AuthorizedData = VideoAuthorizedDataQuery$data["authorizedEvent"];
export const CREDENTIALS_STORAGE_KEY = "tobira-video-credentials-";

const ProtectedPlayer: React.FC<ProtectedPlayerProps> = ({ event, embedded }) => {
Expand All @@ -584,7 +604,7 @@ const ProtectedPlayer: React.FC<ProtectedPlayerProps> = ({ event, embedded }) =>
const user = useUser();
const [authState, setAuthState] = useState<AuthenticationFormState>("idle");
const [authError, setAuthError] = useState<string | null>(null);
const [authData, setAuthData] = useState<AuthorizedData | null>(null);
const authenticatedDataContext = useContext(AuthenticatedDataContext);

const embeddedStyles = {
height: "100%",
Expand All @@ -611,8 +631,15 @@ const ProtectedPlayer: React.FC<ProtectedPlayerProps> = ({ event, embedded }) =>
return;
}

if (authenticatedDataContext) {
authenticatedDataContext.setAuthenticatedData({
authorizedData: authorizedEvent.authorizedData,
});
} else {
bug("Authenticated data context is not initialized");
}

setAuthError(null);
setAuthData({ authorizedData: authorizedEvent.authorizedData });
setAuthState("success");

// To make the authentication "sticky", the credentials are stored in browser
Expand Down Expand Up @@ -646,10 +673,10 @@ const ProtectedPlayer: React.FC<ProtectedPlayerProps> = ({ event, embedded }) =>
});
};

return authData?.authorizedData && event.syncedData
return authenticatedDataContext?.authenticatedData?.authorizedData && event.syncedData
? <InlinePlayer event={{
...event,
authorizedData: authData.authorizedData,
authorizedData: authenticatedDataContext.authenticatedData.authorizedData,
syncedData: event.syncedData,
}} />
: (
Expand Down

0 comments on commit ae81723

Please sign in to comment.