feat: 添加会议访问密码功能和相关UI组件

- 在 `MeetingCommandServiceImpl` 中添加 `normalizeAccessPassword` 方法,处理访问密码
- 在 `MeetingVO` 和 `UpdateMeetingBasicCommand` 中添加 `accessPassword` 字段
- 在前端 `MeetingPreview.tsx` 和 `MeetingDetail.tsx` 中添加访问密码输入和预览链接生成逻辑
- 在 `MeetingPublicPreviewController` 中移除预览响应中的访问密码
- 在 `LegacyAuthController` 中添加异常处理,返回错误信息
- 更新相关前端API和组件,支持访问密码功能
dev_na
chenhao 2026-04-16 10:55:10 +08:00
parent caf2e22df0
commit 712d31d911
9 changed files with 418 additions and 24 deletions

View File

@ -30,8 +30,13 @@ public class LegacyAuthController {
@PostMapping("/login") @PostMapping("/login")
public LegacyApiResponse<LegacyLoginResponse> login(@Valid @RequestBody LoginRequest request) { public LegacyApiResponse<LegacyLoginResponse> login(@Valid @RequestBody LoginRequest request) {
TokenResponse tokenResponse = authService.login(request, true); TokenResponse tokenResponse = null;
return LegacyApiResponse.ok(new LegacyLoginResponse( try {
tokenResponse = authService.login(request, true);
} catch (Exception e) {
return LegacyApiResponse.error("400",e.getMessage());
}
return LegacyApiResponse.ok(new LegacyLoginResponse(
tokenResponse.getAccessToken(), tokenResponse.getAccessToken(),
tokenResponse.getRefreshToken(), tokenResponse.getRefreshToken(),
toLegacyUser(tokenResponse.getUser()) toLegacyUser(tokenResponse.getUser())
@ -42,8 +47,13 @@ public class LegacyAuthController {
public LegacyApiResponse<LegacyRefreshTokenResponse> refresh(@RequestBody(required = false) RefreshRequest request, public LegacyApiResponse<LegacyRefreshTokenResponse> refresh(@RequestBody(required = false) RefreshRequest request,
@RequestHeader(value = "Authorization", required = false) String authorization, @RequestHeader(value = "Authorization", required = false) String authorization,
@RequestHeader(value = "X-Android-Access-Token", required = false) String androidAccessToken) { @RequestHeader(value = "X-Android-Access-Token", required = false) String androidAccessToken) {
TokenResponse tokenResponse = authService.refresh(resolveRefreshToken(request, authorization, androidAccessToken)); TokenResponse tokenResponse = null;
return LegacyApiResponse.ok(new LegacyRefreshTokenResponse(tokenResponse.getAccessToken(),tokenResponse.getRefreshToken())); try {
tokenResponse = authService.refresh(resolveRefreshToken(request, authorization, androidAccessToken));
} catch (Exception e) {
return LegacyApiResponse.error("400",e.getMessage());
}
return LegacyApiResponse.ok(new LegacyRefreshTokenResponse(tokenResponse.getAccessToken(),tokenResponse.getRefreshToken()));
} }
private LegacyLoginUserResponse toLegacyUser(SysUserDTO user) { private LegacyLoginUserResponse toLegacyUser(SysUserDTO user) {

View File

@ -44,6 +44,9 @@ public class MeetingPublicPreviewController {
PublicMeetingPreviewVO data = new PublicMeetingPreviewVO(); PublicMeetingPreviewVO data = new PublicMeetingPreviewVO();
data.setMeeting(meetingQueryService.getDetail(id)); data.setMeeting(meetingQueryService.getDetail(id));
if (data.getMeeting() != null) {
data.getMeeting().setAccessPassword(null);
}
data.setTranscripts(meetingQueryService.getTranscripts(id)); data.setTranscripts(meetingQueryService.getTranscripts(id));
return ApiResponse.ok(data); return ApiResponse.ok(data);
} catch (RuntimeException ex) { } catch (RuntimeException ex) {

View File

@ -25,6 +25,7 @@ public class MeetingVO {
private String audioUrl; private String audioUrl;
private String audioSaveStatus; private String audioSaveStatus;
private String audioSaveMessage; private String audioSaveMessage;
private String accessPassword;
private Integer duration; private Integer duration;
private String summaryContent; private String summaryContent;
private Map<String, Object> analysis; private Map<String, Object> analysis;

View File

@ -14,4 +14,6 @@ public class UpdateMeetingBasicCommand {
private LocalDateTime meetingTime; private LocalDateTime meetingTime;
private String tags; private String tags;
private String accessPassword;
} }

View File

@ -423,7 +423,16 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
.eq(Meeting::getId, command.getMeetingId()) .eq(Meeting::getId, command.getMeetingId())
.set(command.getTitle() != null, Meeting::getTitle, command.getTitle()) .set(command.getTitle() != null, Meeting::getTitle, command.getTitle())
.set(command.getMeetingTime() != null, Meeting::getMeetingTime, command.getMeetingTime()) .set(command.getMeetingTime() != null, Meeting::getMeetingTime, command.getMeetingTime())
.set(command.getTags() != null, Meeting::getTags, command.getTags())); .set(command.getTags() != null, Meeting::getTags, command.getTags())
.set(command.getAccessPassword() != null, Meeting::getAccessPassword, normalizeAccessPassword(command.getAccessPassword())));
}
private String normalizeAccessPassword(String accessPassword) {
if (accessPassword == null) {
return null;
}
String normalized = accessPassword.trim();
return normalized.isEmpty() ? null : normalized;
} }
@Override @Override

View File

@ -241,6 +241,7 @@ public class MeetingDomainSupport {
vo.setAudioUrl(meeting.getAudioUrl()); vo.setAudioUrl(meeting.getAudioUrl());
vo.setAudioSaveStatus(meeting.getAudioSaveStatus()); vo.setAudioSaveStatus(meeting.getAudioSaveStatus());
vo.setAudioSaveMessage(meeting.getAudioSaveMessage()); vo.setAudioSaveMessage(meeting.getAudioSaveMessage());
vo.setAccessPassword(meeting.getAccessPassword());
vo.setDuration(resolveMeetingDuration(meeting.getId())); vo.setDuration(resolveMeetingDuration(meeting.getId()));
vo.setStatus(meeting.getStatus()); vo.setStatus(meeting.getStatus());
vo.setCreatedAt(meeting.getCreatedAt()); vo.setCreatedAt(meeting.getCreatedAt());

View File

@ -16,6 +16,7 @@ export interface MeetingVO {
audioUrl: string; audioUrl: string;
audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED"; audioSaveStatus?: "NONE" | "SUCCESS" | "FAILED";
audioSaveMessage?: string; audioSaveMessage?: string;
accessPassword?: string;
summaryContent: string; summaryContent: string;
analysis?: { analysis?: {
overview?: string; overview?: string;
@ -75,6 +76,7 @@ export interface UpdateMeetingBasicCommand {
title?: string; title?: string;
meetingTime?: string; meetingTime?: string;
tags?: string; tags?: string;
accessPassword?: string | null;
} }
export type MeetingUpdateBasicDTO = UpdateMeetingBasicCommand; export type MeetingUpdateBasicDTO = UpdateMeetingBasicCommand;

View File

@ -1,16 +1,19 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { Alert, Avatar, Breadcrumb, Button, Card, Checkbox, Col, Divider, Drawer, Empty, Form, Input, List, Modal, Popover, Progress, Row, Select, Skeleton, Space, Tag, Typography, App } from 'antd'; import { Alert, Avatar, Breadcrumb, Button, Card, Checkbox, Col, Divider, Drawer, Empty, Form, Input, List, Modal, Popover, Progress, QRCode, Row, Select, Skeleton, Space, Switch, Tag, Typography, App } from 'antd';
import { import {
AudioOutlined, AudioOutlined,
CaretRightFilled, CaretRightFilled,
ClockCircleOutlined, ClockCircleOutlined,
CopyOutlined,
DownloadOutlined, DownloadOutlined,
EditOutlined, EditOutlined,
FastForwardOutlined, FastForwardOutlined,
LeftOutlined, LeftOutlined,
LinkOutlined,
LoadingOutlined, LoadingOutlined,
PauseOutlined, PauseOutlined,
QrcodeOutlined,
RobotOutlined, RobotOutlined,
SyncOutlined, SyncOutlined,
UserOutlined, UserOutlined,
@ -78,6 +81,43 @@ const ANALYSIS_EMPTY: MeetingAnalysis = {
todos: [], todos: [],
}; };
const ACCESS_PASSWORD_PATTERN = /^[A-Za-z0-9]{4}$/;
const ACCESS_PASSWORD_SOURCE = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const copyMeetingLink = async (text: string) => {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', 'true');
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
};
const generateAccessPassword = () =>
Array.from({ length: 4 }, () => ACCESS_PASSWORD_SOURCE[Math.floor(Math.random() * ACCESS_PASSWORD_SOURCE.length)]).join('');
const normalizeAccessPasswordInput = (value?: string | null) => (value || '').replace(/[^A-Za-z0-9]/g, '').slice(0, 4);
const buildMeetingPreviewUrl = (meetingId?: number, accessPassword?: string) => {
if (!meetingId || Number.isNaN(meetingId) || typeof window === 'undefined') {
return '';
}
const url = new URL(`/meetings/${meetingId}/preview`, window.location.origin);
const normalizedPassword = (accessPassword || '').trim();
if (normalizedPassword) {
url.searchParams.set('accessPassword', normalizedPassword);
}
return url.toString();
};
const splitLines = (value?: string | null) => const splitLines = (value?: string | null) =>
(value || '') (value || '')
@ -356,6 +396,7 @@ const SpeakerEditor: React.FC<{
const { message } = App.useApp(); const { message } = App.useApp();
const { items: speakerLabels } = useDict('biz_speaker_label'); const { items: speakerLabels } = useDict('biz_speaker_label');
const handleSave = async (event: React.MouseEvent) => { const handleSave = async (event: React.MouseEvent) => {
event.stopPropagation(); event.stopPropagation();
setLoading(true); setLoading(true);
@ -534,6 +575,11 @@ const MeetingDetail: React.FC = () => {
const [prompts, setPrompts] = useState<PromptTemplateVO[]>([]); const [prompts, setPrompts] = useState<PromptTemplateVO[]>([]);
const [, setUserList] = useState<SysUser[]>([]); const [, setUserList] = useState<SysUser[]>([]);
const { items: speakerLabels } = useDict('biz_speaker_label'); const { items: speakerLabels } = useDict('biz_speaker_label');
const [sharePopoverOpen, setSharePopoverOpen] = useState(false);
const [shareSaving, setShareSaving] = useState(false);
const [sharePasswordEnabled, setSharePasswordEnabled] = useState(false);
const [sharePasswordDraft, setSharePasswordDraft] = useState('');
const audioRef = useRef<HTMLAudioElement>(null); const audioRef = useRef<HTMLAudioElement>(null);
const summaryPdfRef = useRef<HTMLDivElement>(null); const summaryPdfRef = useRef<HTMLDivElement>(null);
@ -555,6 +601,15 @@ const MeetingDetail: React.FC = () => {
() => new Map(speakerLabels.map((item) => [item.itemValue, item.itemLabel])), () => new Map(speakerLabels.map((item) => [item.itemValue, item.itemLabel])),
[speakerLabels], [speakerLabels],
); );
const previewAccessPassword = useMemo(() => (meeting?.accessPassword || '').trim(), [meeting?.accessPassword]);
const sharePreviewUrl = useMemo(() => {
const meetingId = meeting?.id ?? (id ? Number(id) : NaN);
return buildMeetingPreviewUrl(meetingId);
}, [meeting?.id, id]);
const meetingPreviewUrl = useMemo(() => {
const meetingId = meeting?.id ?? (id ? Number(id) : NaN);
return buildMeetingPreviewUrl(meetingId, previewAccessPassword);
}, [meeting?.id, id, previewAccessPassword]);
const isOwner = useMemo(() => { const isOwner = useMemo(() => {
if (!meeting) return false; if (!meeting) return false;
@ -586,6 +641,16 @@ const MeetingDetail: React.FC = () => {
} }
}, [meeting?.id, meeting?.audioSaveStatus, meeting?.audioSaveMessage]); }, [meeting?.id, meeting?.audioSaveStatus, meeting?.audioSaveMessage]);
useEffect(() => {
if (!sharePopoverOpen) {
return;
}
const normalizedPassword = normalizeAccessPasswordInput(meeting?.accessPassword);
setSharePasswordEnabled(!!normalizedPassword);
setSharePasswordDraft(normalizedPassword);
}, [sharePopoverOpen, meeting?.accessPassword]);
useEffect(() => { useEffect(() => {
const audio = audioRef.current; const audio = audioRef.current;
if (!audio) return undefined; if (!audio) return undefined;
@ -937,6 +1002,146 @@ const MeetingDetail: React.FC = () => {
} }
}; };
const handleCopyPreviewLink = async () => {
if (!sharePreviewUrl) {
message.error('预览链接暂不可用');
return;
}
try {
await copyMeetingLink(sharePreviewUrl);
message.success('预览链接已复制');
} catch (error) {
console.error(error);
message.error('复制预览链接失败');
}
};
const handleOpenPreview = () => {
if (!sharePreviewUrl) {
message.error('预览链接暂不可用');
return;
}
window.open(sharePreviewUrl, '_blank', 'noopener,noreferrer');
};
const handleSharePopoverOpenChange = (open: boolean) => {
setSharePopoverOpen(open);
};
const handleSharePasswordToggle = (checked: boolean) => {
setSharePasswordEnabled(checked);
setSharePasswordDraft((current) => {
const normalizedCurrent = normalizeAccessPasswordInput(current);
if (checked) {
return normalizedCurrent || generateAccessPassword();
}
return '';
});
};
const handleRegenerateSharePassword = () => {
setSharePasswordEnabled(true);
setSharePasswordDraft(generateAccessPassword());
};
const handleSaveShareAccess = async () => {
if (!meeting) {
return;
}
const normalizedPassword = normalizeAccessPasswordInput(sharePasswordDraft);
if (sharePasswordEnabled && !ACCESS_PASSWORD_PATTERN.test(normalizedPassword)) {
message.error('\u8bbf\u95ee\u5bc6\u7801\u9700\u4e3a 4 \u4f4d\u82f1\u6587\u6216\u6570\u5b57');
return;
}
setShareSaving(true);
try {
await updateMeetingBasic({
meetingId: meeting.id,
accessPassword: sharePasswordEnabled ? normalizedPassword : '',
});
const nextPassword = sharePasswordEnabled ? normalizedPassword : '';
setMeeting((current) => (current ? { ...current, accessPassword: nextPassword } : current));
message.success(
sharePasswordEnabled
? '\u9884\u89c8\u5bc6\u7801\u5df2\u66f4\u65b0'
: '\u9884\u89c8\u5bc6\u7801\u5df2\u5173\u95ed',
);
setSharePopoverOpen(false);
} catch (error) {
console.error(error);
message.error('\u4fdd\u5b58\u9884\u89c8\u5bc6\u7801\u5931\u8d25');
} finally {
setShareSaving(false);
}
};
const shareQrContent = sharePreviewUrl ? (
<div className="meeting-share-card">
{isOwner ? (
<div className="meeting-share-settings">
<div className="meeting-share-settings-row">
<div className="meeting-share-settings-copy">
<strong>{'\u9884\u89c8\u5bc6\u7801'}</strong>
<span>
{sharePasswordEnabled && previewAccessPassword
? `访问密码${previewAccessPassword}`
: '关闭后将不需要访问密码'}
</span>
</div>
<Switch
checked={sharePasswordEnabled}
checkedChildren={'\u5f00\u542f'}
unCheckedChildren={'\u5173\u95ed'}
onChange={handleSharePasswordToggle}
/>
</div>
{sharePasswordEnabled ? (
<div className="meeting-share-settings-actions">
<Input
value={sharePasswordDraft}
maxLength={4}
placeholder={'\u4f8b\u5982 A7K2'}
onChange={(event) => setSharePasswordDraft(normalizeAccessPasswordInput(event.target.value))}
/>
<Button onClick={handleRegenerateSharePassword}>{'\u91cd\u7f6e\u9ed8\u8ba4'}</Button>
</div>
) : null}
<Button type="primary" loading={shareSaving} onClick={handleSaveShareAccess}>
{'\u4fdd\u5b58\u5bc6\u7801\u8bbe\u7f6e'}
</Button>
</div>
) : null}
<div className="meeting-share-qr-wrap">
<QRCode
value={sharePreviewUrl}
type="svg"
size={172}
color="#315f8b"
bgColor="#fffaf4"
errorLevel="H"
bordered={false}
/>
</div>
<div className="meeting-share-caption">
{'\u4f7f\u7528\u624b\u673a\u626b\u7801\u540e\u5c06\u8df3\u8f6c\u5230\u4f1a\u8bae\u9884\u89c8\u9875\uff0c\u82e5\u5df2\u5f00\u542f\u5bc6\u7801\u9700\u624b\u52a8\u8f93\u5165\u3002'}
</div>
<div className="meeting-share-link-box">
<LinkOutlined />
<span title={sharePreviewUrl}>{sharePreviewUrl}</span>
</div>
<div className="meeting-share-actions">
<Button size="small" icon={<CopyOutlined />} onClick={handleCopyPreviewLink}>
{'\u590d\u5236\u94fe\u63a5'}
</Button>
<Button size="small" type="primary" ghost onClick={handleOpenPreview}>
{'\u6253\u5f00\u9884\u89c8'}
</Button>
</div>
</div>
) : null;
if (loading) { if (loading) {
return ( return (
<div style={{ padding: 24 }}> <div style={{ padding: 24 }}>
@ -1015,6 +1220,20 @@ const MeetingDetail: React.FC = () => {
</Button> </Button>
</> </>
)} )}
{shareQrContent ? (
<Popover
content={shareQrContent}
trigger="click"
open={sharePopoverOpen}
onOpenChange={handleSharePopoverOpenChange}
placement="bottomRight"
overlayClassName="meeting-share-popover"
>
<Button size="small" icon={<QrcodeOutlined />}>
</Button>
</Popover>
) : null}
<Button icon={<LeftOutlined />} onClick={() => navigate('/meetings')}> <Button icon={<LeftOutlined />} onClick={() => navigate('/meetings')}>
</Button> </Button>
@ -1401,6 +1620,127 @@ const MeetingDetail: React.FC = () => {
background: rgba(250, 173, 20, 0.14); background: rgba(250, 173, 20, 0.14);
color: #d48806; color: #d48806;
} }
.meeting-share-popover .ant-popover-inner {
padding: 0;
overflow: hidden;
border-radius: 24px;
box-shadow: 0 24px 56px rgba(76, 61, 39, 0.18);
background:
radial-gradient(circle at top right, rgba(82, 164, 255, 0.12), transparent 32%),
radial-gradient(circle at top left, rgba(252, 208, 157, 0.18), transparent 26%),
linear-gradient(180deg, rgba(255, 252, 247, 0.98), rgba(248, 241, 231, 0.98));
}
.meeting-share-popover .ant-popover-inner-content {
padding: 0;
}
.meeting-share-card {
width: min(82vw, 300px);
padding: 18px;
display: flex;
flex-direction: column;
gap: 14px;
color: #33261c;
}
.meeting-share-card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.meeting-share-kicker {
color: rgba(72, 53, 39, 0.64);
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
}
.meeting-share-title {
margin-top: 6px;
font-family: Georgia, 'Times New Roman', 'Songti SC', serif;
font-size: 22px;
line-height: 1.08;
color: #33261c;
}
.meeting-share-pill {
display: inline-flex;
align-items: center;
padding: 6px 10px;
border-radius: 999px;
background: rgba(49, 95, 139, 0.08);
color: #315f8b;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.meeting-share-qr-wrap {
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
border-radius: 22px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(84, 57, 31, 0.08);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
.meeting-share-caption {
color: rgba(72, 53, 39, 0.76);
font-size: 13px;
line-height: 1.7;
}
.meeting-share-settings {
display: grid;
gap: 12px;
padding: 14px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.62);
border: 1px solid rgba(84, 57, 31, 0.08);
}
.meeting-share-settings-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.meeting-share-settings-copy {
display: grid;
gap: 4px;
}
.meeting-share-settings-copy strong {
color: #33261c;
font-size: 14px;
}
.meeting-share-settings-copy span {
color: rgba(72, 53, 39, 0.72);
font-size: 12px;
line-height: 1.6;
}
.meeting-share-settings-actions {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
}
.meeting-share-link-box {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(84, 57, 31, 0.08);
color: #315f8b;
font-size: 12px;
}
.meeting-share-link-box span {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.meeting-share-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.summary-block { .summary-block {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -1928,6 +2268,9 @@ const MeetingDetail: React.FC = () => {
} }
} }
@media (max-width: 992px) { @media (max-width: 992px) {
.meeting-share-card {
width: min(86vw, 292px);
}
.detail-side-column { .detail-side-column {
height: auto; height: auto;
} }
@ -1954,6 +2297,7 @@ const MeetingDetail: React.FC = () => {
<Select mode="tags" placeholder="输入标签后回车" /> <Select mode="tags" placeholder="输入标签后回车" />
</Form.Item> </Form.Item>
<Text type="warning"> ID </Text> <Text type="warning"> ID </Text>
</Form> </Form>
</Modal> </Modal>
)} )}

View File

@ -1,6 +1,6 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { Alert, Button, Empty, Input, Result, Segmented, Skeleton, Tabs, Tag, message } from "antd"; import { Alert, Button, Empty, Input, Result, Segmented, Skeleton, Tabs, Tag, message } from "antd";
import { useParams } from "react-router-dom"; import { useParams, useSearchParams } from "react-router-dom";
import { import {
AudioOutlined, AudioOutlined,
CalendarOutlined, CalendarOutlined,
@ -156,6 +156,7 @@ async function copyText(text: string) {
export default function MeetingPreview() { export default function MeetingPreview() {
const { id } = useParams(); const { id } = useParams();
const [searchParams] = useSearchParams();
const audioRef = useRef<HTMLAudioElement | null>(null); const audioRef = useRef<HTMLAudioElement | null>(null);
const transcriptItemRefs = useRef<Record<number, HTMLDivElement | null>>({}); const transcriptItemRefs = useRef<Record<number, HTMLDivElement | null>>({});
const [meeting, setMeeting] = useState<MeetingVO | null>(null); const [meeting, setMeeting] = useState<MeetingVO | null>(null);
@ -172,6 +173,7 @@ export default function MeetingPreview() {
const [isMobile, setIsMobile] = useState(() => const [isMobile, setIsMobile] = useState(() =>
typeof window !== "undefined" ? window.matchMedia("(max-width: 767px)").matches : false, typeof window !== "undefined" ? window.matchMedia("(max-width: 767px)").matches : false,
); );
const presetAccessPassword = useMemo(() => (searchParams.get("accessPassword") || "").trim(), [searchParams]);
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
@ -185,28 +187,48 @@ export default function MeetingPreview() {
setLoading(true); setLoading(true);
setError(""); setError("");
setMeeting(null); setMeeting(null);
setTranscripts([]); setTranscripts([]);
setPasswordRequired(false); setPasswordRequired(false);
setPasswordVerified(false); setPasswordVerified(false);
setAccessPassword(""); setAccessPassword(presetAccessPassword);
setPasswordError(""); setPasswordError("");
try { try {
const meetingId = Number(id); const meetingId = Number(id);
const accessRes = await getMeetingPreviewAccess(meetingId); const accessRes = await getMeetingPreviewAccess(meetingId);
if (!mounted) { if (!mounted) {
return; return;
} }
const requiresPassword = !!accessRes.data.data.passwordRequired; const requiresPassword = !!accessRes.data.data.passwordRequired;
setPasswordRequired(requiresPassword); setPasswordRequired(requiresPassword);
if (requiresPassword) { if (requiresPassword) {
setLoading(false); if (!presetAccessPassword) {
return; setLoading(false);
} return;
}
try {
const previewRes = await getPublicMeetingPreview(meetingId, presetAccessPassword);
if (!mounted) {
return;
}
setMeeting(previewRes.data.data.meeting);
setTranscripts(previewRes.data.data.transcripts || []);
setPasswordVerified(true);
return;
} catch (requestError: any) {
if (!mounted) {
return;
}
setPasswordError(requestError?.response?.data?.msg || requestError?.msg || TEXT.invalidPassword);
setPasswordVerified(false);
setLoading(false);
return;
}
}
const previewRes = await getPublicMeetingPreview(meetingId); const previewRes = await getPublicMeetingPreview(meetingId);
if (!mounted) { if (!mounted) {
return; return;
} }
@ -232,7 +254,7 @@ export default function MeetingPreview() {
return () => { return () => {
mounted = false; mounted = false;
}; };
}, [id]); }, [id, presetAccessPassword]);
useEffect(() => { useEffect(() => {
if (typeof window === "undefined") { if (typeof window === "undefined") {