mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-20 20:29:06 +00:00
consent skip feature
This commit is contained in:
@@ -26,6 +26,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { formatDateTime, formatStartedAgo } from "../lib/timeUtils";
|
import { formatDateTime, formatStartedAgo } from "../lib/timeUtils";
|
||||||
import MeetingMinimalHeader from "../components/MeetingMinimalHeader";
|
import MeetingMinimalHeader from "../components/MeetingMinimalHeader";
|
||||||
import { NonEmptyString } from "../lib/utils";
|
import { NonEmptyString } from "../lib/utils";
|
||||||
|
import { MeetingId } from "../lib/types";
|
||||||
|
|
||||||
type Meeting = components["schemas"]["Meeting"];
|
type Meeting = components["schemas"]["Meeting"];
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ export default function MeetingSelection({
|
|||||||
onMeetingSelect(meeting);
|
onMeetingSelect(meeting);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEndMeeting = async (meetingId: string) => {
|
const handleEndMeeting = async (meetingId: MeetingId) => {
|
||||||
try {
|
try {
|
||||||
await deactivateMeetingMutation.mutateAsync({
|
await deactivateMeetingMutation.mutateAsync({
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@@ -21,13 +21,11 @@ import DailyIframe, {
|
|||||||
} from "@daily-co/daily-js";
|
} from "@daily-co/daily-js";
|
||||||
import type { components } from "../../reflector-api";
|
import type { components } from "../../reflector-api";
|
||||||
import { useAuth } from "../../lib/AuthProvider";
|
import { useAuth } from "../../lib/AuthProvider";
|
||||||
import {
|
import { useConsentDialog } from "../../lib/consent";
|
||||||
recordingTypeRequiresConsent,
|
|
||||||
useConsentDialog,
|
|
||||||
} from "../../lib/consent";
|
|
||||||
import { useRoomJoinMeeting } from "../../lib/apiHooks";
|
import { useRoomJoinMeeting } from "../../lib/apiHooks";
|
||||||
import { omit } from "remeda";
|
import { omit } from "remeda";
|
||||||
import { assertExists } from "../../lib/utils";
|
import { assertExists } from "../../lib/utils";
|
||||||
|
import { assertMeetingId } from "../../lib/types";
|
||||||
|
|
||||||
const CONSENT_BUTTON_ID = "recording-consent";
|
const CONSENT_BUTTON_ID = "recording-consent";
|
||||||
const RECORDING_INDICATOR_ID = "recording-indicator";
|
const RECORDING_INDICATOR_ID = "recording-indicator";
|
||||||
@@ -179,21 +177,15 @@ export default function DailyRoom({ meeting, room }: DailyRoomProps) {
|
|||||||
|
|
||||||
const roomName = params?.roomName as string;
|
const roomName = params?.roomName as string;
|
||||||
|
|
||||||
const needsConsent =
|
const {
|
||||||
meeting.recording_type &&
|
showConsentModal,
|
||||||
recordingTypeRequiresConsent(meeting.recording_type) &&
|
showRecordingIndicator: showRecordingInTray,
|
||||||
!room.skip_consent;
|
showConsentButton,
|
||||||
const { showConsentModal, consentState, hasAnswered, hasAccepted } =
|
} = useConsentDialog({
|
||||||
useConsentDialog(meeting.id);
|
meetingId: assertMeetingId(meeting.id),
|
||||||
|
recordingType: meeting.recording_type,
|
||||||
// Show recording indicator when:
|
skipConsent: room.skip_consent,
|
||||||
// - skip_consent=true, OR
|
});
|
||||||
// - user has accepted consent
|
|
||||||
// If user rejects, recording still happens but we don't show indicator
|
|
||||||
const showRecordingInTray =
|
|
||||||
meeting.recording_type &&
|
|
||||||
recordingTypeRequiresConsent(meeting.recording_type) &&
|
|
||||||
(room.skip_consent || hasAccepted(meeting.id));
|
|
||||||
const showConsentModalRef = useRef(showConsentModal);
|
const showConsentModalRef = useRef(showConsentModal);
|
||||||
showConsentModalRef.current = showConsentModal;
|
showConsentModalRef.current = showConsentModal;
|
||||||
|
|
||||||
@@ -293,8 +285,6 @@ export default function DailyRoom({ meeting, room }: DailyRoomProps) {
|
|||||||
);
|
);
|
||||||
}, [showRecordingInTray, recordingIconUrl, setCustomTrayButton]);
|
}, [showRecordingInTray, recordingIconUrl, setCustomTrayButton]);
|
||||||
|
|
||||||
const showConsentButton = needsConsent && !hasAnswered(meeting.id);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCustomTrayButton(
|
setCustomTrayButton(
|
||||||
CONSENT_BUTTON_ID,
|
CONSENT_BUTTON_ID,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { useAuth } from "../../lib/AuthProvider";
|
|||||||
import { useError } from "../../(errors)/errorContext";
|
import { useError } from "../../(errors)/errorContext";
|
||||||
import { parseNonEmptyString } from "../../lib/utils";
|
import { parseNonEmptyString } from "../../lib/utils";
|
||||||
import { printApiError } from "../../api/_error";
|
import { printApiError } from "../../api/_error";
|
||||||
|
import { assertMeetingId } from "../../lib/types";
|
||||||
|
|
||||||
type Meeting = components["schemas"]["Meeting"];
|
type Meeting = components["schemas"]["Meeting"];
|
||||||
|
|
||||||
@@ -67,7 +68,10 @@ export default function RoomContainer(details: RoomDetails) {
|
|||||||
room && !room.ics_enabled && !pageMeetingId ? roomName : null,
|
room && !room.ics_enabled && !pageMeetingId ? roomName : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const explicitMeeting = useRoomGetMeeting(roomName, pageMeetingId || null);
|
const explicitMeeting = useRoomGetMeeting(
|
||||||
|
roomName,
|
||||||
|
pageMeetingId ? assertMeetingId(pageMeetingId) : null,
|
||||||
|
);
|
||||||
|
|
||||||
const meeting = explicitMeeting.data || defaultMeeting.response;
|
const meeting = explicitMeeting.data || defaultMeeting.response;
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import { useRouter } from "next/navigation";
|
|||||||
import type { components } from "../../reflector-api";
|
import type { components } from "../../reflector-api";
|
||||||
import { useAuth } from "../../lib/AuthProvider";
|
import { useAuth } from "../../lib/AuthProvider";
|
||||||
import { getWherebyUrl, useWhereby } from "../../lib/wherebyClient";
|
import { getWherebyUrl, useWhereby } from "../../lib/wherebyClient";
|
||||||
import { assertExistsAndNonEmptyString, NonEmptyString } from "../../lib/utils";
|
|
||||||
import {
|
import {
|
||||||
ConsentDialogButton as BaseConsentDialogButton,
|
ConsentDialogButton as BaseConsentDialogButton,
|
||||||
RecordingIndicator,
|
RecordingIndicator,
|
||||||
useConsentDialog,
|
useConsentDialog,
|
||||||
recordingTypeRequiresConsent,
|
|
||||||
} from "../../lib/consent";
|
} from "../../lib/consent";
|
||||||
|
import { assertMeetingId, MeetingId } from "../../lib/types";
|
||||||
|
|
||||||
type Meeting = components["schemas"]["Meeting"];
|
type Meeting = components["schemas"]["Meeting"];
|
||||||
type Room = components["schemas"]["RoomDetails"];
|
type Room = components["schemas"]["RoomDetails"];
|
||||||
@@ -23,9 +22,13 @@ interface WherebyRoomProps {
|
|||||||
|
|
||||||
function WherebyConsentDialogButton({
|
function WherebyConsentDialogButton({
|
||||||
meetingId,
|
meetingId,
|
||||||
|
recordingType,
|
||||||
|
skipConsent,
|
||||||
wherebyRef,
|
wherebyRef,
|
||||||
}: {
|
}: {
|
||||||
meetingId: NonEmptyString;
|
meetingId: MeetingId;
|
||||||
|
recordingType: Meeting["recording_type"];
|
||||||
|
skipConsent: boolean;
|
||||||
wherebyRef: React.RefObject<HTMLElement>;
|
wherebyRef: React.RefObject<HTMLElement>;
|
||||||
}) {
|
}) {
|
||||||
const previousFocusRef = useRef<HTMLElement | null>(null);
|
const previousFocusRef = useRef<HTMLElement | null>(null);
|
||||||
@@ -48,7 +51,13 @@ function WherebyConsentDialogButton({
|
|||||||
};
|
};
|
||||||
}, [wherebyRef]);
|
}, [wherebyRef]);
|
||||||
|
|
||||||
return <BaseConsentDialogButton meetingId={meetingId} />;
|
return (
|
||||||
|
<BaseConsentDialogButton
|
||||||
|
meetingId={meetingId}
|
||||||
|
recordingType={recordingType}
|
||||||
|
skipConsent={skipConsent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WherebyRoom({ meeting, room }: WherebyRoomProps) {
|
export default function WherebyRoom({ meeting, room }: WherebyRoomProps) {
|
||||||
@@ -60,9 +69,14 @@ export default function WherebyRoom({ meeting, room }: WherebyRoomProps) {
|
|||||||
const isAuthenticated = status === "authenticated";
|
const isAuthenticated = status === "authenticated";
|
||||||
|
|
||||||
const wherebyRoomUrl = getWherebyUrl(meeting);
|
const wherebyRoomUrl = getWherebyUrl(meeting);
|
||||||
const recordingType = meeting.recording_type;
|
|
||||||
const meetingId = meeting.id;
|
const meetingId = meeting.id;
|
||||||
|
|
||||||
|
const { showRecordingIndicator, showConsentButton } = useConsentDialog({
|
||||||
|
meetingId: assertMeetingId(meetingId),
|
||||||
|
recordingType: meeting.recording_type,
|
||||||
|
skipConsent: room.skip_consent,
|
||||||
|
});
|
||||||
|
|
||||||
const isLoading = status === "loading";
|
const isLoading = status === "loading";
|
||||||
|
|
||||||
const handleLeave = useCallback(() => {
|
const handleLeave = useCallback(() => {
|
||||||
@@ -91,17 +105,15 @@ export default function WherebyRoom({ meeting, room }: WherebyRoomProps) {
|
|||||||
room={wherebyRoomUrl}
|
room={wherebyRoomUrl}
|
||||||
style={{ width: "100vw", height: "100vh" }}
|
style={{ width: "100vw", height: "100vh" }}
|
||||||
/>
|
/>
|
||||||
{recordingType &&
|
{showRecordingIndicator && <RecordingIndicator />}
|
||||||
recordingTypeRequiresConsent(recordingType) &&
|
{showConsentButton && (
|
||||||
meetingId &&
|
<WherebyConsentDialogButton
|
||||||
(room.skip_consent ? (
|
meetingId={assertMeetingId(meetingId)}
|
||||||
<RecordingIndicator />
|
recordingType={meeting.recording_type}
|
||||||
) : (
|
skipConsent={room.skip_consent}
|
||||||
<WherebyConsentDialogButton
|
wherebyRef={wherebyRef}
|
||||||
meetingId={assertExistsAndNonEmptyString(meetingId)}
|
/>
|
||||||
wherebyRef={wherebyRef}
|
)}
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
useContext,
|
|
||||||
RefObject,
|
RefObject,
|
||||||
use,
|
use,
|
||||||
} from "react";
|
} from "react";
|
||||||
@@ -25,8 +24,6 @@ import { useRecordingConsent } from "../recordingConsentContext";
|
|||||||
import {
|
import {
|
||||||
useMeetingAudioConsent,
|
useMeetingAudioConsent,
|
||||||
useRoomGetByName,
|
useRoomGetByName,
|
||||||
useRoomActiveMeetings,
|
|
||||||
useRoomUpcomingMeetings,
|
|
||||||
useRoomsCreateMeeting,
|
useRoomsCreateMeeting,
|
||||||
useRoomGetMeeting,
|
useRoomGetMeeting,
|
||||||
} from "../lib/apiHooks";
|
} from "../lib/apiHooks";
|
||||||
@@ -39,12 +36,9 @@ import { FaBars } from "react-icons/fa6";
|
|||||||
import { useAuth } from "../lib/AuthProvider";
|
import { useAuth } from "../lib/AuthProvider";
|
||||||
import { getWherebyUrl, useWhereby } from "../lib/wherebyClient";
|
import { getWherebyUrl, useWhereby } from "../lib/wherebyClient";
|
||||||
import { useError } from "../(errors)/errorContext";
|
import { useError } from "../(errors)/errorContext";
|
||||||
import {
|
import { parseNonEmptyString } from "../lib/utils";
|
||||||
assertExistsAndNonEmptyString,
|
|
||||||
NonEmptyString,
|
|
||||||
parseNonEmptyString,
|
|
||||||
} from "../lib/utils";
|
|
||||||
import { printApiError } from "../api/_error";
|
import { printApiError } from "../api/_error";
|
||||||
|
import { assertMeetingId, MeetingId } from "../lib/types";
|
||||||
|
|
||||||
export type RoomDetails = {
|
export type RoomDetails = {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@@ -92,7 +86,7 @@ const useConsentWherebyFocusManagement = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const useConsentDialog = (
|
const useConsentDialog = (
|
||||||
meetingId: string,
|
meetingId: MeetingId,
|
||||||
wherebyRef: RefObject<HTMLElement> /*accessibility*/,
|
wherebyRef: RefObject<HTMLElement> /*accessibility*/,
|
||||||
) => {
|
) => {
|
||||||
const { state: consentState, touch, hasAnswered } = useRecordingConsent();
|
const { state: consentState, touch, hasAnswered } = useRecordingConsent();
|
||||||
@@ -101,7 +95,7 @@ const useConsentDialog = (
|
|||||||
const audioConsentMutation = useMeetingAudioConsent();
|
const audioConsentMutation = useMeetingAudioConsent();
|
||||||
|
|
||||||
const handleConsent = useCallback(
|
const handleConsent = useCallback(
|
||||||
async (meetingId: string, given: boolean) => {
|
async (meetingId: MeetingId, given: boolean) => {
|
||||||
try {
|
try {
|
||||||
await audioConsentMutation.mutateAsync({
|
await audioConsentMutation.mutateAsync({
|
||||||
params: {
|
params: {
|
||||||
@@ -225,7 +219,7 @@ function ConsentDialogButton({
|
|||||||
meetingId,
|
meetingId,
|
||||||
wherebyRef,
|
wherebyRef,
|
||||||
}: {
|
}: {
|
||||||
meetingId: NonEmptyString;
|
meetingId: MeetingId;
|
||||||
wherebyRef: React.RefObject<HTMLElement>;
|
wherebyRef: React.RefObject<HTMLElement>;
|
||||||
}) {
|
}) {
|
||||||
const { showConsentModal, consentState, hasAnswered, consentLoading } =
|
const { showConsentModal, consentState, hasAnswered, consentLoading } =
|
||||||
@@ -284,7 +278,10 @@ export default function Room(details: RoomDetails) {
|
|||||||
room && !room.ics_enabled && !pageMeetingId ? roomName : null,
|
room && !room.ics_enabled && !pageMeetingId ? roomName : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const explicitMeeting = useRoomGetMeeting(roomName, pageMeetingId || null);
|
const explicitMeeting = useRoomGetMeeting(
|
||||||
|
roomName,
|
||||||
|
pageMeetingId ? assertMeetingId(pageMeetingId) : null,
|
||||||
|
);
|
||||||
const wherebyRoomUrl = explicitMeeting.data
|
const wherebyRoomUrl = explicitMeeting.data
|
||||||
? getWherebyUrl(explicitMeeting.data)
|
? getWherebyUrl(explicitMeeting.data)
|
||||||
: defaultMeeting.response
|
: defaultMeeting.response
|
||||||
@@ -437,7 +434,7 @@ export default function Room(details: RoomDetails) {
|
|||||||
recordingTypeRequiresConsent(recordingType) &&
|
recordingTypeRequiresConsent(recordingType) &&
|
||||||
meetingId && (
|
meetingId && (
|
||||||
<ConsentDialogButton
|
<ConsentDialogButton
|
||||||
meetingId={assertExistsAndNonEmptyString(meetingId)}
|
meetingId={assertMeetingId(meetingId)}
|
||||||
wherebyRef={wherebyRef}
|
wherebyRef={wherebyRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useError } from "../(errors)/errorContext";
|
|||||||
import { QueryClient, useQueryClient } from "@tanstack/react-query";
|
import { QueryClient, useQueryClient } from "@tanstack/react-query";
|
||||||
import type { components } from "../reflector-api";
|
import type { components } from "../reflector-api";
|
||||||
import { useAuth } from "./AuthProvider";
|
import { useAuth } from "./AuthProvider";
|
||||||
|
import { MeetingId } from "./types";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* XXX error types returned from the hooks are not always correct; declared types are ValidationError but real type could be string or any other
|
* XXX error types returned from the hooks are not always correct; declared types are ValidationError but real type could be string or any other
|
||||||
@@ -718,7 +719,7 @@ export function useRoomActiveMeetings(roomName: string | null) {
|
|||||||
|
|
||||||
export function useRoomGetMeeting(
|
export function useRoomGetMeeting(
|
||||||
roomName: string | null,
|
roomName: string | null,
|
||||||
meetingId: string | null,
|
meetingId: MeetingId | null,
|
||||||
) {
|
) {
|
||||||
return $api.useQuery(
|
return $api.useQuery(
|
||||||
"get",
|
"get",
|
||||||
|
|||||||
@@ -9,16 +9,26 @@ import {
|
|||||||
CONSENT_BUTTON_Z_INDEX,
|
CONSENT_BUTTON_Z_INDEX,
|
||||||
CONSENT_DIALOG_TEXT,
|
CONSENT_DIALOG_TEXT,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
|
import { MeetingId } from "../types";
|
||||||
|
import type { components } from "../../reflector-api";
|
||||||
|
|
||||||
interface ConsentDialogButtonProps {
|
type Meeting = components["schemas"]["Meeting"];
|
||||||
meetingId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ConsentDialogButton({ meetingId }: ConsentDialogButtonProps) {
|
type ConsentDialogButtonProps = {
|
||||||
const { showConsentModal, consentState, hasAnswered, consentLoading } =
|
meetingId: MeetingId;
|
||||||
useConsentDialog(meetingId);
|
recordingType: Meeting["recording_type"];
|
||||||
|
skipConsent: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
if (!consentState.ready || hasAnswered(meetingId) || consentLoading) {
|
export function ConsentDialogButton({
|
||||||
|
meetingId,
|
||||||
|
recordingType,
|
||||||
|
skipConsent,
|
||||||
|
}: ConsentDialogButtonProps) {
|
||||||
|
const { showConsentModal, consentState, showConsentButton, consentLoading } =
|
||||||
|
useConsentDialog({ meetingId, recordingType, skipConsent });
|
||||||
|
|
||||||
|
if (!consentState.ready || !showConsentButton || consentLoading) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
export interface ConsentDialogResult {
|
import { MeetingId } from "../types";
|
||||||
|
|
||||||
|
export type ConsentDialogResult = {
|
||||||
showConsentModal: () => void;
|
showConsentModal: () => void;
|
||||||
consentState: {
|
consentState: {
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
consentForMeetings?: Map<string, boolean>;
|
consentForMeetings?: Map<MeetingId, boolean>;
|
||||||
};
|
};
|
||||||
hasAnswered: (meetingId: string) => boolean;
|
hasAnswered: (meetingId: MeetingId) => boolean;
|
||||||
hasAccepted: (meetingId: string) => boolean;
|
hasAccepted: (meetingId: MeetingId) => boolean;
|
||||||
consentLoading: boolean;
|
consentLoading: boolean;
|
||||||
}
|
showRecordingIndicator: boolean;
|
||||||
|
showConsentButton: boolean;
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,8 +7,23 @@ import { useMeetingAudioConsent } from "../apiHooks";
|
|||||||
import { ConsentDialog } from "./ConsentDialog";
|
import { ConsentDialog } from "./ConsentDialog";
|
||||||
import { TOAST_CHECK_INTERVAL_MS } from "./constants";
|
import { TOAST_CHECK_INTERVAL_MS } from "./constants";
|
||||||
import type { ConsentDialogResult } from "./types";
|
import type { ConsentDialogResult } from "./types";
|
||||||
|
import { MeetingId } from "../types";
|
||||||
|
import { recordingTypeRequiresConsent } from "./utils";
|
||||||
|
import type { components } from "../../reflector-api";
|
||||||
|
|
||||||
export function useConsentDialog(meetingId: string): ConsentDialogResult {
|
type Meeting = components["schemas"]["Meeting"];
|
||||||
|
|
||||||
|
type UseConsentDialogParams = {
|
||||||
|
meetingId: MeetingId;
|
||||||
|
recordingType: Meeting["recording_type"];
|
||||||
|
skipConsent: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useConsentDialog({
|
||||||
|
meetingId,
|
||||||
|
recordingType,
|
||||||
|
skipConsent,
|
||||||
|
}: UseConsentDialogParams): ConsentDialogResult {
|
||||||
const {
|
const {
|
||||||
state: consentState,
|
state: consentState,
|
||||||
touch,
|
touch,
|
||||||
@@ -105,11 +120,23 @@ export function useConsentDialog(meetingId: string): ConsentDialogResult {
|
|||||||
});
|
});
|
||||||
}, [handleConsent, modalOpen]);
|
}, [handleConsent, modalOpen]);
|
||||||
|
|
||||||
|
const requiresConsent = Boolean(
|
||||||
|
recordingType && recordingTypeRequiresConsent(recordingType),
|
||||||
|
);
|
||||||
|
|
||||||
|
const showRecordingIndicator =
|
||||||
|
requiresConsent && (skipConsent || hasAccepted(meetingId));
|
||||||
|
|
||||||
|
const showConsentButton =
|
||||||
|
requiresConsent && !skipConsent && !hasAnswered(meetingId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showConsentModal,
|
showConsentModal,
|
||||||
consentState,
|
consentState,
|
||||||
hasAnswered,
|
hasAnswered,
|
||||||
hasAccepted,
|
hasAccepted,
|
||||||
consentLoading: audioConsentMutation.isPending,
|
consentLoading: audioConsentMutation.isPending,
|
||||||
|
showRecordingIndicator,
|
||||||
|
showConsentButton,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { Session } from "next-auth";
|
import type { Session } from "next-auth";
|
||||||
import type { JWT } from "next-auth/jwt";
|
import type { JWT } from "next-auth/jwt";
|
||||||
import { parseMaybeNonEmptyString } from "./utils";
|
import {
|
||||||
|
assertExistsAndNonEmptyString,
|
||||||
|
NonEmptyString,
|
||||||
|
parseMaybeNonEmptyString,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
export interface JWTWithAccessToken extends JWT {
|
export interface JWTWithAccessToken extends JWT {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
@@ -78,3 +82,10 @@ export const assertCustomSession = <T extends Session>(
|
|||||||
export type Mutable<T> = {
|
export type Mutable<T> = {
|
||||||
-readonly [P in keyof T]: T[P];
|
-readonly [P in keyof T]: T[P];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MeetingId = NonEmptyString & { __type: "MeetingId" };
|
||||||
|
export const assertMeetingId = (s: string): MeetingId => {
|
||||||
|
const nes = assertExistsAndNonEmptyString(s);
|
||||||
|
// just cast for now
|
||||||
|
return nes as MeetingId;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
import { MeetingId } from "./lib/types";
|
||||||
|
|
||||||
// Map of meetingId -> accepted (true/false)
|
type ConsentMap = Map<MeetingId, boolean>;
|
||||||
type ConsentMap = Map<string, boolean>;
|
|
||||||
|
|
||||||
type ConsentContextState =
|
type ConsentContextState =
|
||||||
| { ready: false }
|
| { ready: false }
|
||||||
@@ -14,9 +14,9 @@ type ConsentContextState =
|
|||||||
|
|
||||||
interface RecordingConsentContextValue {
|
interface RecordingConsentContextValue {
|
||||||
state: ConsentContextState;
|
state: ConsentContextState;
|
||||||
touch: (meetingId: string, accepted: boolean) => void;
|
touch: (meetingId: MeetingId, accepted: boolean) => void;
|
||||||
hasAnswered: (meetingId: string) => boolean;
|
hasAnswered: (meetingId: MeetingId) => boolean;
|
||||||
hasAccepted: (meetingId: string) => boolean;
|
hasAccepted: (meetingId: MeetingId) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RecordingConsentContext = createContext<
|
const RecordingConsentContext = createContext<
|
||||||
@@ -39,24 +39,33 @@ interface RecordingConsentProviderProps {
|
|||||||
|
|
||||||
const LOCAL_STORAGE_KEY = "recording_consent_meetings";
|
const LOCAL_STORAGE_KEY = "recording_consent_meetings";
|
||||||
|
|
||||||
|
const ACCEPTED = "T" as const;
|
||||||
|
type Accepted = typeof ACCEPTED;
|
||||||
|
const REJECTED = "F" as const;
|
||||||
|
type Rejected = typeof REJECTED;
|
||||||
|
type Consent = Accepted | Rejected;
|
||||||
|
const SEPARATOR = "|" as const;
|
||||||
|
type Separator = typeof SEPARATOR;
|
||||||
|
const DEFAULT_CONSENT = ACCEPTED;
|
||||||
|
type Entry = `${MeetingId}${Separator}${Consent}`;
|
||||||
|
type EntryAndDefault = Entry | MeetingId;
|
||||||
|
|
||||||
// Format: "meetingId|T" or "meetingId|F", legacy format "meetingId" is treated as accepted
|
// Format: "meetingId|T" or "meetingId|F", legacy format "meetingId" is treated as accepted
|
||||||
const encodeEntry = (meetingId: string, accepted: boolean): string =>
|
const encodeEntry = (meetingId: MeetingId, accepted: boolean): Entry =>
|
||||||
`${meetingId}|${accepted ? "T" : "F"}`;
|
`${meetingId}|${accepted ? ACCEPTED : REJECTED}`;
|
||||||
|
|
||||||
const decodeEntry = (
|
const decodeEntry = (
|
||||||
entry: string,
|
entry: EntryAndDefault,
|
||||||
): { meetingId: string; accepted: boolean } | null => {
|
): { meetingId: MeetingId; accepted: boolean } | null => {
|
||||||
if (!entry || typeof entry !== "string") return null;
|
const pipeIndex = entry.lastIndexOf(SEPARATOR);
|
||||||
const pipeIndex = entry.lastIndexOf("|");
|
|
||||||
if (pipeIndex === -1) {
|
if (pipeIndex === -1) {
|
||||||
// Legacy format: no pipe means accepted (backward compat)
|
// Legacy format: no pipe means accepted (backward compat)
|
||||||
return { meetingId: entry, accepted: true };
|
return { meetingId: entry as MeetingId, accepted: true };
|
||||||
}
|
}
|
||||||
const suffix = entry.slice(pipeIndex + 1);
|
const suffix = entry.slice(pipeIndex + 1);
|
||||||
const meetingId = entry.slice(0, pipeIndex);
|
const meetingId = entry.slice(0, pipeIndex) as MeetingId;
|
||||||
if (!meetingId) return null;
|
|
||||||
// T = accepted, F = rejected, anything else = accepted (safe default)
|
// T = accepted, F = rejected, anything else = accepted (safe default)
|
||||||
const accepted = suffix !== "F";
|
const accepted = suffix !== REJECTED;
|
||||||
return { meetingId, accepted };
|
return { meetingId, accepted };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,7 +87,7 @@ export const RecordingConsentProvider: React.FC<
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const touch = (meetingId: string, accepted: boolean): void => {
|
const touch = (meetingId: MeetingId, accepted: boolean): void => {
|
||||||
if (!state.ready) {
|
if (!state.ready) {
|
||||||
console.warn("Attempted to touch consent before context is ready");
|
console.warn("Attempted to touch consent before context is ready");
|
||||||
return;
|
return;
|
||||||
@@ -90,12 +99,12 @@ export const RecordingConsentProvider: React.FC<
|
|||||||
setState({ ready: true, consentForMeetings: newMap });
|
setState({ ready: true, consentForMeetings: newMap });
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasAnswered = (meetingId: string): boolean => {
|
const hasAnswered = (meetingId: MeetingId): boolean => {
|
||||||
if (!state.ready) return false;
|
if (!state.ready) return false;
|
||||||
return state.consentForMeetings.has(meetingId);
|
return state.consentForMeetings.has(meetingId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasAccepted = (meetingId: string): boolean => {
|
const hasAccepted = (meetingId: MeetingId): boolean => {
|
||||||
if (!state.ready) return false;
|
if (!state.ready) return false;
|
||||||
return state.consentForMeetings.get(meetingId) === true;
|
return state.consentForMeetings.get(meetingId) === true;
|
||||||
};
|
};
|
||||||
@@ -121,7 +130,7 @@ export const RecordingConsentProvider: React.FC<
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const consentForMeetings = new Map<string, boolean>();
|
const consentForMeetings = new Map<MeetingId, boolean>();
|
||||||
for (const entry of parsed) {
|
for (const entry of parsed) {
|
||||||
const decoded = decodeEntry(entry);
|
const decoded = decodeEntry(entry);
|
||||||
if (decoded) {
|
if (decoded) {
|
||||||
|
|||||||
Reference in New Issue
Block a user