fix(meetings): show pending state on active-meeting Leave button (#4348)

This commit is contained in:
Cyrus Gray
2026-06-30 19:28:36 +05:30
committed by GitHub
parent 57bac2a07a
commit 1f015500b9
16 changed files with 150 additions and 8 deletions
@@ -6,7 +6,7 @@
* each component within the repo's ~500-line guideline. Behavior is identical
* to the original; it just lives in its own file now.
*/
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { type MascotFace, RiveMascot } from '../../features/human/Mascot';
import { useT } from '../../lib/i18n/I18nContext';
@@ -24,9 +24,20 @@ import {
} from '../../store/backendMeetSlice';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import Button from '../ui/Button';
import { Spinner } from '../ui/icons';
type Toast = { type: 'success' | 'error' | 'info'; title: string; message?: string };
/**
* Safety net for the pending "Leaving…" state. The leave RPC only confirms the
* `bot:leave` event was enqueued on the socket — it does not wait for the bot to
* actually leave. On the happy path `status` flips to ended/error (which unmounts
* this banner) well within this window. If no such transition ever arrives (e.g.
* the socket drops or the bot fails to emit its completion event), we re-enable
* the button after this timeout so the user can retry instead of being stuck.
*/
export const LEAVE_SAFETY_TIMEOUT_MS = 10_000;
export interface ActiveMeetingBannerProps {
onToast?: (toast: Toast) => void;
}
@@ -74,20 +85,38 @@ export function ActiveMeetingBanner({ onToast }: ActiveMeetingBannerProps) {
}, [meetUrl]);
const [leaving, setLeaving] = useState(false);
const leaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Clear the safety timer on unmount (the happy path: the banner unmounts when
// `status` flips to ended/error). No setState here — just timer cleanup.
useEffect(() => {
return () => {
if (leaveTimerRef.current) clearTimeout(leaveTimerRef.current);
};
}, []);
const handleLeave = async () => {
if (leaving) return;
setLeaving(true);
try {
await leaveBackendMeetBot('user-requested');
// Stay in the pending "Leaving…" state on success: when the bot actually
// leaves, `status` flips to ended/error and this banner unmounts (the
// parent only renders it while joining/active), so the flag never lingers.
// The RPC only confirms the leave was enqueued, not that the bot left, so
// arm a safety timeout that re-enables the button if no transition arrives.
if (leaveTimerRef.current) clearTimeout(leaveTimerRef.current);
leaveTimerRef.current = setTimeout(() => {
leaveTimerRef.current = null;
setLeaving(false);
}, LEAVE_SAFETY_TIMEOUT_MS);
} catch (err) {
setLeaving(false);
onToast?.({
type: 'error',
title: t('skills.meetingBots.couldNotStartTitle'),
title: t('skills.meetingBots.couldNotLeaveTitle'),
message: String(err),
});
} finally {
setLeaving(false);
}
};
@@ -118,8 +147,13 @@ export function ActiveMeetingBanner({ onToast }: ActiveMeetingBannerProps) {
{t('skills.meetingBots.liveBadge')}
</span>
{canLeave && (
<Button variant="secondary" size="sm" onClick={handleLeave} disabled={leaving}>
{t('skills.meetingBots.leaveButton')}
<Button
variant="secondary"
size="sm"
onClick={handleLeave}
disabled={leaving}
leadingIcon={leaving ? <Spinner className="h-3.5 w-3.5" /> : undefined}>
{leaving ? t('skills.meetingBots.leavingButton') : t('skills.meetingBots.leaveButton')}
</Button>
)}
{isDone && (
@@ -1,9 +1,9 @@
import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
import { act, cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { setBackendMeetJoined, setBackendMeetLeft } from '../../../store/backendMeetSlice';
import { renderWithProviders } from '../../../test/test-utils';
import { ActiveMeetingBanner } from '../ActiveMeetingBanner';
import { ActiveMeetingBanner, LEAVE_SAFETY_TIMEOUT_MS } from '../ActiveMeetingBanner';
const leaveMock = vi.fn();
@@ -81,6 +81,86 @@ describe('ActiveMeetingBanner', () => {
});
});
it('shows a pending "Leaving…" state while the leave request is in flight', async () => {
// Hold the leave request open so the pending state stays visible.
let resolveLeave: () => void = () => {};
leaveMock.mockImplementationOnce(
() =>
new Promise<void>(resolve => {
resolveLeave = resolve;
})
);
renderWithProviders(<ActiveMeetingBanner />, { preloadedState: activeState });
const leaveBtn = screen.getByRole('button', { name: /leave/i });
fireEvent.click(leaveBtn);
// While in flight the label switches to "Leaving…" and the button is disabled.
await waitFor(() => {
expect(screen.getByRole('button', { name: /leaving/i })).toBeDisabled();
});
expect(screen.queryByRole('button', { name: /^leave$/i })).not.toBeInTheDocument();
resolveLeave();
});
it('keeps the pending state after the request resolves until status changes', async () => {
leaveMock.mockResolvedValueOnce(undefined);
const { store } = renderWithProviders(<ActiveMeetingBanner />, { preloadedState: activeState });
fireEvent.click(screen.getByRole('button', { name: /leave/i }));
// Even after the request resolves, the pending state holds until the bot leaves.
await waitFor(() => {
expect(screen.getByRole('button', { name: /leaving/i })).toBeInTheDocument();
});
// Once the meeting ends, the pending Leave button is replaced by Close.
store.dispatch(setBackendMeetLeft({ reason: 'done' }));
await waitFor(() => {
expect(screen.queryByRole('button', { name: /leaving/i })).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
});
});
it('re-enables the Leave button via safety timeout if no left event arrives', async () => {
vi.useFakeTimers();
try {
leaveMock.mockResolvedValueOnce(undefined);
renderWithProviders(<ActiveMeetingBanner />, { preloadedState: activeState });
fireEvent.click(screen.getByRole('button', { name: /leave/i }));
// Flush the resolved leave promise → pending "Leaving…" + safety timer armed.
await act(async () => {
await vi.advanceTimersByTimeAsync(0);
});
expect(screen.getByRole('button', { name: /leaving/i })).toBeDisabled();
// No status transition arrives; once the safety timeout elapses the button
// returns to an enabled "Leave" so the user can retry.
await act(async () => {
await vi.advanceTimersByTimeAsync(LEAVE_SAFETY_TIMEOUT_MS + 100);
});
expect(screen.getByRole('button', { name: /^leave$/i })).toBeEnabled();
} finally {
vi.useRealTimers();
}
});
it('clears the pending state and toasts when leave fails', async () => {
leaveMock.mockRejectedValueOnce(new Error('Network error'));
const onToast = vi.fn();
renderWithProviders(<ActiveMeetingBanner onToast={onToast} />, { preloadedState: activeState });
fireEvent.click(screen.getByRole('button', { name: /leave/i }));
await waitFor(() => {
expect(onToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }));
});
// Button returns to the enabled "Leave" state so the user can retry.
expect(screen.getByRole('button', { name: /^leave$/i })).toBeEnabled();
});
it('renders ended state with Close button', () => {
const { store } = renderWithProviders(<ActiveMeetingBanner />, { preloadedState: activeState });
+2
View File
@@ -5030,6 +5030,7 @@ const messages: TranslationMap = {
'skills.meetingBots.bannerTitle': 'عنوان اللافتة',
'skills.meetingBots.busyTitle': 'OpenHuman مشغول',
'skills.meetingBots.comingSoon': 'قريبًا',
'skills.meetingBots.couldNotLeaveTitle': 'تعذّر مغادرة الاجتماع',
'skills.meetingBots.couldNotStartTitle': 'تعذّر بدء OpenHuman',
'skills.meetingBots.displayName': 'الاسم المعروض',
'skills.meetingBots.failedToStart': 'فشل تشغيل OpenHuman.',
@@ -5079,6 +5080,7 @@ const messages: TranslationMap = {
'skills.meetingBots.liveStatusEnded': 'انتهى الاجتماع',
'skills.meetingBots.liveStatusError': 'فشل الانضمام',
'skills.meetingBots.leaveButton': 'مغادرة',
'skills.meetingBots.leavingButton': 'جارٍ المغادرة…',
'skills.meetingBots.respondToParticipant': 'اسمك في هذا الاجتماع',
'skills.meetingBots.respondToParticipantHint': 'مثال: أحمد (اسمك في المكالمة)',
'skills.meetingBots.respondToParticipantDesc':
+2
View File
@@ -5134,6 +5134,7 @@ const messages: TranslationMap = {
'skills.meetingBots.bannerTitle': 'ব্যানার শিরোনাম',
'skills.meetingBots.busyTitle': 'OpenHuman ব্যস্ত',
'skills.meetingBots.comingSoon': 'শীঘ্রই আসছে',
'skills.meetingBots.couldNotLeaveTitle': 'মিটিং থেকে বেরোনো যায়নি',
'skills.meetingBots.couldNotStartTitle': 'OpenHuman শুরু করা যায়নি',
'skills.meetingBots.displayName': 'প্রদর্শন নাম',
'skills.meetingBots.failedToStart': 'OpenHuman শুরু করতে ব্যর্থ।',
@@ -5184,6 +5185,7 @@ const messages: TranslationMap = {
'skills.meetingBots.liveStatusEnded': 'মিটিং শেষ',
'skills.meetingBots.liveStatusError': 'যোগ দিতে ব্যর্থ',
'skills.meetingBots.leaveButton': 'ছেড়ে দিন',
'skills.meetingBots.leavingButton': 'বেরিয়ে যাচ্ছে…',
'skills.meetingBots.respondToParticipant': 'এই মিটিংয়ে আপনার নাম',
'skills.meetingBots.respondToParticipantHint': 'যেমন: রিয়া (কলে আপনার প্রদর্শনী নাম)',
'skills.meetingBots.respondToParticipantDesc':
+2
View File
@@ -5265,6 +5265,7 @@ const messages: TranslationMap = {
'skills.meetingBots.bannerTitle': 'Sende OpenHuman an eine Besprechung',
'skills.meetingBots.busyTitle': 'OpenHuman ist beschäftigt',
'skills.meetingBots.comingSoon': 'Kommt bald',
'skills.meetingBots.couldNotLeaveTitle': 'Meeting konnte nicht verlassen werden',
'skills.meetingBots.couldNotStartTitle': 'OpenHuman konnte nicht gestartet werden',
'skills.meetingBots.displayName': 'Anzeigename',
'skills.meetingBots.failedToStart': 'OpenHuman konnte nicht gestartet werden.',
@@ -5317,6 +5318,7 @@ const messages: TranslationMap = {
'skills.meetingBots.liveStatusEnded': 'Meeting beendet',
'skills.meetingBots.liveStatusError': 'Beitritt fehlgeschlagen',
'skills.meetingBots.leaveButton': 'Verlassen',
'skills.meetingBots.leavingButton': 'Wird verlassen…',
'skills.meetingBots.respondToParticipant': 'Ihr Name in diesem Meeting',
'skills.meetingBots.respondToParticipantHint': 'z. B. Max (Ihr Anzeigename im Anruf)',
'skills.meetingBots.respondToParticipantDesc':
+2
View File
@@ -5780,6 +5780,7 @@ const en: TranslationMap = {
'skills.meetingBots.bannerTitle': 'Send OpenHuman to your next meeting',
'skills.meetingBots.busyTitle': 'OpenHuman is busy',
'skills.meetingBots.comingSoon': 'coming soon',
'skills.meetingBots.couldNotLeaveTitle': 'Couldnt leave the meeting',
'skills.meetingBots.couldNotStartTitle': 'Could not start OpenHuman',
'skills.meetingBots.displayName': 'Display name',
'skills.meetingBots.failedToStart': 'Failed to start OpenHuman.',
@@ -5831,6 +5832,7 @@ const en: TranslationMap = {
'skills.meetingBots.liveStatusEnded': 'Meeting ended',
'skills.meetingBots.liveStatusError': 'Failed to join',
'skills.meetingBots.leaveButton': 'Leave',
'skills.meetingBots.leavingButton': 'Leaving…',
'skills.meetingBots.respondToParticipant': 'Your Name in This Meeting',
'skills.meetingBots.respondToParticipantHint': 'e.g. Alice (your display name in the call)',
'skills.meetingBots.respondToParticipantDesc':
+2
View File
@@ -5230,6 +5230,7 @@ const messages: TranslationMap = {
'skills.meetingBots.bannerTitle': 'Título del banner',
'skills.meetingBots.busyTitle': 'OpenHuman está ocupado',
'skills.meetingBots.comingSoon': 'Próximamente',
'skills.meetingBots.couldNotLeaveTitle': 'No se pudo salir de la reunión',
'skills.meetingBots.couldNotStartTitle': 'No se pudo iniciar OpenHuman',
'skills.meetingBots.displayName': 'Nombre de visualización',
'skills.meetingBots.failedToStart': 'No se pudo iniciar OpenHuman.',
@@ -5281,6 +5282,7 @@ const messages: TranslationMap = {
'skills.meetingBots.liveStatusEnded': 'Reunión finalizada',
'skills.meetingBots.liveStatusError': 'Error al unirse',
'skills.meetingBots.leaveButton': 'Salir',
'skills.meetingBots.leavingButton': 'Saliendo…',
'skills.meetingBots.respondToParticipant': 'Tu nombre en esta reunión',
'skills.meetingBots.respondToParticipantHint': 'p. ej. Ana (tu nombre visible en la llamada)',
'skills.meetingBots.respondToParticipantDesc':
+2
View File
@@ -5250,6 +5250,7 @@ const messages: TranslationMap = {
'skills.meetingBots.bannerTitle': 'Titre de la bannière',
'skills.meetingBots.busyTitle': 'OpenHuman est occupé',
'skills.meetingBots.comingSoon': 'Bientôt disponible',
'skills.meetingBots.couldNotLeaveTitle': 'Impossible de quitter la réunion',
'skills.meetingBots.couldNotStartTitle': 'Impossible de démarrer OpenHuman',
'skills.meetingBots.displayName': "Nom d'affichage",
'skills.meetingBots.failedToStart': "Échec du démarrage d'OpenHuman.",
@@ -5302,6 +5303,7 @@ const messages: TranslationMap = {
'skills.meetingBots.liveStatusEnded': 'Réunion terminée',
'skills.meetingBots.liveStatusError': 'Échec de connexion',
'skills.meetingBots.leaveButton': 'Quitter',
'skills.meetingBots.leavingButton': 'Sortie en cours…',
'skills.meetingBots.respondToParticipant': 'Votre nom dans cette réunion',
'skills.meetingBots.respondToParticipantHint': 'ex. Alice (votre nom affiché dans l\u2019appel)',
'skills.meetingBots.respondToParticipantDesc':
+2
View File
@@ -5137,6 +5137,7 @@ const messages: TranslationMap = {
'skills.meetingBots.bannerTitle': 'बैनर शीर्षक',
'skills.meetingBots.busyTitle': 'OpenHuman व्यस्त है',
'skills.meetingBots.comingSoon': 'जल्द आ रहा है',
'skills.meetingBots.couldNotLeaveTitle': 'मीटिंग से नहीं निकल सके',
'skills.meetingBots.couldNotStartTitle': 'OpenHuman प्रारंभ नहीं हो सका',
'skills.meetingBots.displayName': 'डिस्प्ले नाम',
'skills.meetingBots.failedToStart': 'OpenHuman शुरू नहीं हो पाया।',
@@ -5189,6 +5190,7 @@ const messages: TranslationMap = {
'skills.meetingBots.liveStatusEnded': 'मीटिंग समाप्त',
'skills.meetingBots.liveStatusError': 'शामिल होने में विफल',
'skills.meetingBots.leaveButton': 'छोड़ें',
'skills.meetingBots.leavingButton': 'छोड़ रहे हैं…',
'skills.meetingBots.respondToParticipant': 'इस मीटिंग में आपका नाम',
'skills.meetingBots.respondToParticipantHint': 'जैसे: अनीता (कॉल में आपका प्रदर्शन नाम)',
'skills.meetingBots.respondToParticipantDesc':
+2
View File
@@ -5152,6 +5152,7 @@ const messages: TranslationMap = {
'skills.meetingBots.bannerTitle': 'Judul banner',
'skills.meetingBots.busyTitle': 'OpenHuman sedang sibuk',
'skills.meetingBots.comingSoon': 'Segera hadir',
'skills.meetingBots.couldNotLeaveTitle': 'Tidak dapat keluar dari rapat',
'skills.meetingBots.couldNotStartTitle': 'Tidak bisa memulai OpenHuman',
'skills.meetingBots.displayName': 'Nama tampilan',
'skills.meetingBots.failedToStart': 'Gagal memulai OpenHuman.',
@@ -5203,6 +5204,7 @@ const messages: TranslationMap = {
'skills.meetingBots.liveStatusEnded': 'Rapat selesai',
'skills.meetingBots.liveStatusError': 'Gagal bergabung',
'skills.meetingBots.leaveButton': 'Keluar',
'skills.meetingBots.leavingButton': 'Keluar\u2026',
'skills.meetingBots.respondToParticipant': 'Nama Anda di Rapat Ini',
'skills.meetingBots.respondToParticipantHint': 'mis. Budi (nama tampilan Anda di panggilan)',
'skills.meetingBots.respondToParticipantDesc':
+2
View File
@@ -5219,6 +5219,7 @@ const messages: TranslationMap = {
'skills.meetingBots.bannerTitle': 'Titolo banner',
'skills.meetingBots.busyTitle': 'OpenHuman è occupato',
'skills.meetingBots.comingSoon': 'In arrivo',
'skills.meetingBots.couldNotLeaveTitle': 'Impossibile uscire dalla riunione',
'skills.meetingBots.couldNotStartTitle': 'Impossibile avviare OpenHuman',
'skills.meetingBots.displayName': 'Nome visualizzato',
'skills.meetingBots.failedToStart': 'Avvio di OpenHuman fallito.',
@@ -5270,6 +5271,7 @@ const messages: TranslationMap = {
'skills.meetingBots.liveStatusEnded': 'Riunione terminata',
'skills.meetingBots.liveStatusError': 'Partecipazione fallita',
'skills.meetingBots.leaveButton': 'Esci',
'skills.meetingBots.leavingButton': 'Uscita in corso…',
'skills.meetingBots.respondToParticipant': 'Il tuo nome in questa riunione',
'skills.meetingBots.respondToParticipantHint':
'es. Mario (il tuo nome visualizzato nella chiamata)',
+2
View File
@@ -5085,6 +5085,7 @@ const messages: TranslationMap = {
'skills.meetingBots.bannerTitle': 'OpenHuman을 회의에 보내기',
'skills.meetingBots.busyTitle': 'OpenHuman이 바쁩니다',
'skills.meetingBots.comingSoon': '곧 제공 예정',
'skills.meetingBots.couldNotLeaveTitle': '회의에서 나갈 수 없습니다',
'skills.meetingBots.couldNotStartTitle': 'OpenHuman을 시작할 수 없습니다',
'skills.meetingBots.displayName': '표시 이름',
'skills.meetingBots.failedToStart': 'OpenHuman 시작에 실패했습니다.',
@@ -5135,6 +5136,7 @@ const messages: TranslationMap = {
'skills.meetingBots.liveStatusEnded': '회의 종료',
'skills.meetingBots.liveStatusError': '참가 실패',
'skills.meetingBots.leaveButton': '나가기',
'skills.meetingBots.leavingButton': '나가는 중…',
'skills.meetingBots.respondToParticipant': '이 회의에서 내 이름',
'skills.meetingBots.respondToParticipantHint': '예: 김철수 (통화에서 표시되는 이름)',
'skills.meetingBots.respondToParticipantDesc':
+2
View File
@@ -5205,6 +5205,7 @@ const messages: TranslationMap = {
'skills.meetingBots.bannerTitle': 'Wyślij OpenHuman na spotkanie',
'skills.meetingBots.busyTitle': 'OpenHuman jest zajęty',
'skills.meetingBots.comingSoon': 'Wkrótce',
'skills.meetingBots.couldNotLeaveTitle': 'Nie można opuścić spotkania',
'skills.meetingBots.couldNotStartTitle': 'Nie udało się uruchomić OpenHuman',
'skills.meetingBots.displayName': 'Wyświetlana nazwa',
'skills.meetingBots.failedToStart': 'Nie udało się uruchomić OpenHuman.',
@@ -5257,6 +5258,7 @@ const messages: TranslationMap = {
'skills.meetingBots.liveStatusEnded': 'Spotkanie zakończone',
'skills.meetingBots.liveStatusError': 'Nie można dołączyć',
'skills.meetingBots.leaveButton': 'Wyjdź',
'skills.meetingBots.leavingButton': 'Opuszczanie…',
'skills.meetingBots.respondToParticipant': 'Twoje imię na tym spotkaniu',
'skills.meetingBots.respondToParticipantHint':
'np. Anna (Twoja nazwa wyświetlana podczas rozmowy)',
+2
View File
@@ -5222,6 +5222,7 @@ const messages: TranslationMap = {
'skills.meetingBots.bannerTitle': 'Título do banner',
'skills.meetingBots.busyTitle': 'OpenHuman está ocupado',
'skills.meetingBots.comingSoon': 'Em breve',
'skills.meetingBots.couldNotLeaveTitle': 'Não foi possível sair da reunião',
'skills.meetingBots.couldNotStartTitle': 'Não foi possível iniciar o OpenHuman',
'skills.meetingBots.displayName': 'Nome de exibição',
'skills.meetingBots.failedToStart': 'Falha ao iniciar o OpenHuman.',
@@ -5273,6 +5274,7 @@ const messages: TranslationMap = {
'skills.meetingBots.liveStatusEnded': 'Reunião encerrada',
'skills.meetingBots.liveStatusError': 'Falha ao entrar',
'skills.meetingBots.leaveButton': 'Sair',
'skills.meetingBots.leavingButton': 'Saindo…',
'skills.meetingBots.respondToParticipant': 'Seu nome nesta reunião',
'skills.meetingBots.respondToParticipantHint': 'ex. João (seu nome exibido na chamada)',
'skills.meetingBots.respondToParticipantDesc':
+2
View File
@@ -5181,6 +5181,7 @@ const messages: TranslationMap = {
'skills.meetingBots.bannerTitle': 'Заголовок баннера',
'skills.meetingBots.busyTitle': 'OpenHuman занят',
'skills.meetingBots.comingSoon': 'Скоро',
'skills.meetingBots.couldNotLeaveTitle': 'Не удалось покинуть встречу',
'skills.meetingBots.couldNotStartTitle': 'Не удалось запустить OpenHuman',
'skills.meetingBots.displayName': 'Отображаемое имя',
'skills.meetingBots.failedToStart': 'Не удалось запустить OpenHuman.',
@@ -5230,6 +5231,7 @@ const messages: TranslationMap = {
'skills.meetingBots.liveStatusEnded': 'Встреча завершена',
'skills.meetingBots.liveStatusError': 'Ошибка подключения',
'skills.meetingBots.leaveButton': 'Выйти',
'skills.meetingBots.leavingButton': 'Выход…',
'skills.meetingBots.respondToParticipant': 'Ваше имя на этой встрече',
'skills.meetingBots.respondToParticipantHint': 'напр. Иван (ваше отображаемое имя в звонке)',
'skills.meetingBots.respondToParticipantDesc':
+2
View File
@@ -4880,6 +4880,7 @@ const messages: TranslationMap = {
'skills.meetingBots.bannerTitle': '会议机器人',
'skills.meetingBots.busyTitle': 'OpenHuman 正忙',
'skills.meetingBots.comingSoon': '即将推出',
'skills.meetingBots.couldNotLeaveTitle': '无法离开会议',
'skills.meetingBots.couldNotStartTitle': '无法启动 OpenHuman',
'skills.meetingBots.displayName': '显示名称',
'skills.meetingBots.failedToStart': '启动 OpenHuman 失败。',
@@ -4928,6 +4929,7 @@ const messages: TranslationMap = {
'skills.meetingBots.liveStatusEnded': '会议已结束',
'skills.meetingBots.liveStatusError': '加入失败',
'skills.meetingBots.leaveButton': '离开',
'skills.meetingBots.leavingButton': '正在离开…',
'skills.meetingBots.respondToParticipant': '您在此会议中的姓名',
'skills.meetingBots.respondToParticipantHint': '例如:小明(通话中的显示名称)',
'skills.meetingBots.respondToParticipantDesc':