mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2026-04-18 19:26:54 +00:00
feat: send email in share transcript and add email sending in room (#924)
* fix: add source language for file pipeline * feat: send email in share transcript and add email sending in room * fix: hide audio and video streaming for unauthenticated users * fix: security order
This commit is contained in:
committed by
GitHub
parent
74b9b97453
commit
e2ba502697
@@ -24,6 +24,8 @@ import {
|
||||
} from "@chakra-ui/react";
|
||||
import { useTranscriptGet } from "../../../lib/apiHooks";
|
||||
import { TranscriptStatus } from "../../../lib/transcript";
|
||||
import { useAuth } from "../../../lib/AuthProvider";
|
||||
import { featureEnabled } from "../../../lib/features";
|
||||
|
||||
type TranscriptDetails = {
|
||||
params: Promise<{
|
||||
@@ -57,7 +59,10 @@ export default function TranscriptDetails(details: TranscriptDetails) {
|
||||
const [finalSummaryElement, setFinalSummaryElement] =
|
||||
useState<HTMLDivElement | null>(null);
|
||||
|
||||
const hasCloudVideo = !!transcript.data?.has_cloud_video;
|
||||
const auth = useAuth();
|
||||
const isAuthenticated =
|
||||
auth.status === "authenticated" || !featureEnabled("requireLogin");
|
||||
const hasCloudVideo = !!transcript.data?.has_cloud_video && isAuthenticated;
|
||||
const [videoExpanded, setVideoExpanded] = useState(false);
|
||||
const [videoNewBadge, setVideoNewBadge] = useState(() => {
|
||||
if (typeof window === "undefined") return true;
|
||||
@@ -145,7 +150,7 @@ export default function TranscriptDetails(details: TranscriptDetails) {
|
||||
mt={4}
|
||||
mb={4}
|
||||
>
|
||||
{!mp3.audioDeleted && (
|
||||
{isAuthenticated && !mp3.audioDeleted && (
|
||||
<>
|
||||
{waveform.waveform && mp3.media && topics.topics ? (
|
||||
<Player
|
||||
|
||||
@@ -21,6 +21,10 @@ import { useAuth } from "../../../lib/AuthProvider";
|
||||
import { featureEnabled } from "../../../lib/features";
|
||||
import { SearchableLanguageSelect } from "../../../components/SearchableLanguageSelect";
|
||||
|
||||
const sourceLanguages = supportedLanguages.filter(
|
||||
(l) => l.value && l.value !== "NOTRANSLATION",
|
||||
);
|
||||
|
||||
const TranscriptCreate = () => {
|
||||
const router = useRouter();
|
||||
const auth = useAuth();
|
||||
@@ -33,8 +37,13 @@ const TranscriptCreate = () => {
|
||||
const nameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(event.target.value);
|
||||
};
|
||||
const [sourceLanguage, setSourceLanguage] = useState<string>("");
|
||||
const [targetLanguage, setTargetLanguage] = useState<string>("NOTRANSLATION");
|
||||
|
||||
const onSourceLanguageChange = (newval) => {
|
||||
(!newval || typeof newval === "string") &&
|
||||
setSourceLanguage(newval || "en");
|
||||
};
|
||||
const onLanguageChange = (newval) => {
|
||||
(!newval || typeof newval === "string") && setTargetLanguage(newval);
|
||||
};
|
||||
@@ -55,7 +64,7 @@ const TranscriptCreate = () => {
|
||||
const targetLang = getTargetLanguage();
|
||||
createTranscript.create({
|
||||
name,
|
||||
source_language: "en",
|
||||
source_language: sourceLanguage || "en",
|
||||
target_language: targetLang || "en",
|
||||
source_kind: "live",
|
||||
});
|
||||
@@ -67,7 +76,7 @@ const TranscriptCreate = () => {
|
||||
const targetLang = getTargetLanguage();
|
||||
createTranscript.create({
|
||||
name,
|
||||
source_language: "en",
|
||||
source_language: sourceLanguage || "en",
|
||||
target_language: targetLang || "en",
|
||||
source_kind: "file",
|
||||
});
|
||||
@@ -160,6 +169,15 @@ const TranscriptCreate = () => {
|
||||
placeholder="Optional"
|
||||
/>
|
||||
</Box>
|
||||
<Box mb={4}>
|
||||
<Text mb={1}>Audio language</Text>
|
||||
<SearchableLanguageSelect
|
||||
options={sourceLanguages}
|
||||
value={sourceLanguage}
|
||||
onChange={onSourceLanguageChange}
|
||||
placeholder="Select language"
|
||||
/>
|
||||
</Box>
|
||||
<Box mb={4}>
|
||||
<Text mb={1}>Do you want to enable live translation?</Text>
|
||||
<SearchableLanguageSelect
|
||||
|
||||
@@ -18,10 +18,11 @@ import {
|
||||
createListCollection,
|
||||
} from "@chakra-ui/react";
|
||||
import { LuShare2 } from "react-icons/lu";
|
||||
import { useTranscriptUpdate } from "../../lib/apiHooks";
|
||||
import { useTranscriptUpdate, useConfig } from "../../lib/apiHooks";
|
||||
import ShareLink from "./shareLink";
|
||||
import ShareCopy from "./shareCopy";
|
||||
import ShareZulip from "./shareZulip";
|
||||
import ShareEmail from "./shareEmail";
|
||||
import { useAuth } from "../../lib/AuthProvider";
|
||||
|
||||
import { featureEnabled } from "../../lib/features";
|
||||
@@ -55,6 +56,9 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) {
|
||||
const [shareLoading, setShareLoading] = useState(false);
|
||||
const requireLogin = featureEnabled("requireLogin");
|
||||
const updateTranscriptMutation = useTranscriptUpdate();
|
||||
const { data: config } = useConfig();
|
||||
const zulipEnabled = config?.zulip_enabled ?? false;
|
||||
const emailEnabled = config?.email_enabled ?? false;
|
||||
|
||||
const updateShareMode = async (selectedValue: string) => {
|
||||
const selectedOption = shareOptionsData.find(
|
||||
@@ -169,14 +173,20 @@ export default function ShareAndPrivacy(props: ShareAndPrivacyProps) {
|
||||
<Text fontSize="sm" mb="2" fontWeight={"bold"}>
|
||||
Share options
|
||||
</Text>
|
||||
<Flex gap={2} mb={2}>
|
||||
{requireLogin && (
|
||||
<Flex gap={2} mb={2} flexWrap="wrap">
|
||||
{requireLogin && zulipEnabled && (
|
||||
<ShareZulip
|
||||
transcript={props.transcript}
|
||||
topics={props.topics}
|
||||
disabled={toShareMode(shareMode.value) === "private"}
|
||||
/>
|
||||
)}
|
||||
{emailEnabled && (
|
||||
<ShareEmail
|
||||
transcript={props.transcript}
|
||||
disabled={toShareMode(shareMode.value) === "private"}
|
||||
/>
|
||||
)}
|
||||
<ShareCopy
|
||||
finalSummaryElement={props.finalSummaryElement}
|
||||
transcript={props.transcript}
|
||||
|
||||
110
www/app/(app)/transcripts/shareEmail.tsx
Normal file
110
www/app/(app)/transcripts/shareEmail.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useState } from "react";
|
||||
import type { components } from "../../reflector-api";
|
||||
|
||||
type GetTranscriptWithParticipants =
|
||||
components["schemas"]["GetTranscriptWithParticipants"];
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
CloseButton,
|
||||
Input,
|
||||
Box,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { LuMail } from "react-icons/lu";
|
||||
import { useTranscriptSendEmail } from "../../lib/apiHooks";
|
||||
|
||||
type ShareEmailProps = {
|
||||
transcript: GetTranscriptWithParticipants;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export default function ShareEmail(props: ShareEmailProps) {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [email, setEmail] = useState("");
|
||||
const [sent, setSent] = useState(false);
|
||||
const sendEmailMutation = useTranscriptSendEmail();
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!email) return;
|
||||
try {
|
||||
await sendEmailMutation.mutateAsync({
|
||||
params: {
|
||||
path: { transcript_id: props.transcript.id },
|
||||
},
|
||||
body: { email },
|
||||
});
|
||||
setSent(true);
|
||||
setTimeout(() => {
|
||||
setSent(false);
|
||||
setShowModal(false);
|
||||
setEmail("");
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error("Error sending email:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button disabled={props.disabled} onClick={() => setShowModal(true)}>
|
||||
<LuMail /> Send Email
|
||||
</Button>
|
||||
|
||||
<Dialog.Root
|
||||
open={showModal}
|
||||
onOpenChange={(e) => {
|
||||
setShowModal(e.open);
|
||||
if (!e.open) {
|
||||
setSent(false);
|
||||
setEmail("");
|
||||
}
|
||||
}}
|
||||
size="md"
|
||||
>
|
||||
<Dialog.Backdrop />
|
||||
<Dialog.Positioner>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Send Transcript via Email</Dialog.Title>
|
||||
<Dialog.CloseTrigger asChild>
|
||||
<CloseButton />
|
||||
</Dialog.CloseTrigger>
|
||||
</Dialog.Header>
|
||||
<Dialog.Body>
|
||||
{sent ? (
|
||||
<Text color="green.500">Email sent successfully!</Text>
|
||||
) : (
|
||||
<Box>
|
||||
<Text mb={2}>
|
||||
Enter the email address to send this transcript to:
|
||||
</Text>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="recipient@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSend()}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Dialog.Body>
|
||||
<Dialog.Footer>
|
||||
<Button variant="ghost" onClick={() => setShowModal(false)}>
|
||||
Close
|
||||
</Button>
|
||||
{!sent && (
|
||||
<Button
|
||||
disabled={!email || sendEmailMutation.isPending}
|
||||
onClick={handleSend}
|
||||
>
|
||||
{sendEmailMutation.isPending ? "Sending..." : "Send"}
|
||||
</Button>
|
||||
)}
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Positioner>
|
||||
</Dialog.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -56,7 +56,7 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => {
|
||||
}, [navigator.serviceWorker, !serviceWorker, accessTokenInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!transcriptId || later || !transcript) return;
|
||||
if (!transcriptId || later || !transcript || !accessTokenInfo) return;
|
||||
|
||||
let stopped = false;
|
||||
let audioElement: HTMLAudioElement | null = null;
|
||||
@@ -113,7 +113,7 @@ const useMp3 = (transcriptId: string, waiting?: boolean): Mp3Response => {
|
||||
if (handleError) audioElement.removeEventListener("error", handleError);
|
||||
}
|
||||
};
|
||||
}, [transcriptId, transcript, later]);
|
||||
}, [transcriptId, transcript, later, accessTokenInfo]);
|
||||
|
||||
const getNow = () => {
|
||||
setLater(false);
|
||||
|
||||
@@ -39,17 +39,16 @@ export default function VideoPlayer({
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (accessToken) {
|
||||
params.set("token", accessToken);
|
||||
}
|
||||
const url = `${API_URL}/v1/transcripts/${transcriptId}/video/url?${params}`;
|
||||
const url = `${API_URL}/v1/transcripts/${transcriptId}/video/url`;
|
||||
const headers: Record<string, string> = {};
|
||||
if (accessToken) {
|
||||
headers["Authorization"] = `Bearer ${accessToken}`;
|
||||
}
|
||||
const resp = await fetch(url, { headers });
|
||||
if (!resp.ok) {
|
||||
if (resp.status === 401) {
|
||||
throw new Error("Sign in to view the video recording");
|
||||
}
|
||||
throw new Error("Failed to load video");
|
||||
}
|
||||
const data = await resp.json();
|
||||
@@ -90,7 +89,7 @@ export default function VideoPlayer({
|
||||
w="fit-content"
|
||||
maxW="100%"
|
||||
>
|
||||
<Text fontSize="sm">Failed to load video recording</Text>
|
||||
<Text fontSize="sm">{error || "Failed to load video recording"}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -132,10 +131,14 @@ export default function VideoPlayer({
|
||||
</Flex>
|
||||
</Flex>
|
||||
{/* Video element with visible controls */}
|
||||
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
|
||||
<video
|
||||
src={videoUrl}
|
||||
controls
|
||||
autoPlay
|
||||
controlsList="nodownload"
|
||||
disablePictureInPicture
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
style={{
|
||||
display: "block",
|
||||
width: "100%",
|
||||
|
||||
Reference in New Issue
Block a user