fix: cpu usage + email improvements (#944)

* fix: cpu usage on server ws manager, 100% to 0% on idle

* fix:  change email icon to white and prefill email in daily room for authenticated users

* fix: improve email sending with full ts transcript
This commit is contained in:
Juan Diego García
2026-03-31 16:34:10 -05:00
committed by GitHub
parent ec8b49738e
commit 8c4f5e9c0f
17 changed files with 459 additions and 58 deletions

View File

@@ -120,7 +120,8 @@ class Meeting(BaseModel):
daily_composed_video_s3_key: str | None = None
daily_composed_video_duration: int | None = None
# Email recipients for transcript notification
email_recipients: list[str] | None = None
# Each entry is {"email": str, "include_link": bool} or a legacy plain str
email_recipients: list[dict | str] | None = None
class MeetingController:
@@ -399,15 +400,27 @@ class MeetingController:
async with get_database().transaction(isolation="serializable"):
yield
async def add_email_recipient(self, meeting_id: str, email: str) -> list[str]:
"""Add an email to the meeting's email_recipients list (no duplicates)."""
async def add_email_recipient(
self, meeting_id: str, email: str, *, include_link: bool = True
) -> list[dict]:
"""Add an email to the meeting's email_recipients list (no duplicates).
Each entry is stored as {"email": str, "include_link": bool}.
Legacy plain-string entries are normalised on read.
"""
async with self.transaction():
meeting = await self.get_by_id(meeting_id)
if not meeting:
raise ValueError(f"Meeting {meeting_id} not found")
current = meeting.email_recipients or []
if email not in current:
current.append(email)
# Normalise legacy string entries
current: list[dict] = [
entry
if isinstance(entry, dict)
else {"email": entry, "include_link": True}
for entry in (meeting.email_recipients or [])
]
if not any(r["email"] == email for r in current):
current.append({"email": email, "include_link": include_link})
await self.update_meeting(meeting_id, email_recipients=current)
return current

View File

@@ -1,11 +1,13 @@
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from html import escape
import aiosmtplib
import structlog
from reflector.db.transcripts import Transcript
from reflector.db.transcripts import SourceKind, Transcript
from reflector.settings import settings
from reflector.utils.transcript_formats import transcript_to_text_timestamped
logger = structlog.get_logger(__name__)
@@ -18,35 +20,111 @@ def get_transcript_url(transcript: Transcript) -> str:
return f"{settings.UI_BASE_URL}/transcripts/{transcript.id}"
def _build_plain_text(transcript: Transcript, url: str) -> str:
def _get_timestamped_text(transcript: Transcript) -> str:
"""Build the full timestamped transcript text using existing utility."""
if not transcript.topics:
return ""
is_multitrack = transcript.source_kind == SourceKind.ROOM
return transcript_to_text_timestamped(
transcript.topics, transcript.participants, is_multitrack=is_multitrack
)
def _build_plain_text(transcript: Transcript, url: str, include_link: bool) -> str:
title = transcript.title or "Unnamed recording"
lines = [
f"Your transcript is ready: {title}",
"",
f"View it here: {url}",
]
lines = [f"Reflector: {title}", ""]
if transcript.short_summary:
lines.extend(["", "Summary:", transcript.short_summary])
lines.extend(["Summary:", transcript.short_summary, ""])
timestamped = _get_timestamped_text(transcript)
if timestamped:
lines.extend(["Transcript:", timestamped, ""])
if include_link:
lines.append(f"View transcript: {url}")
lines.append("")
lines.append(
"This email was sent because you requested to receive "
"the transcript from a meeting."
)
return "\n".join(lines)
def _build_html(transcript: Transcript, url: str) -> str:
title = transcript.title or "Unnamed recording"
def _build_html(transcript: Transcript, url: str, include_link: bool) -> str:
title = escape(transcript.title or "Unnamed recording")
summary_html = ""
if transcript.short_summary:
summary_html = f"<p style='color:#555;'>{transcript.short_summary}</p>"
summary_html = (
f'<p style="color:#555;margin-bottom:16px;">'
f"{escape(transcript.short_summary)}</p>"
)
transcript_html = ""
timestamped = _get_timestamped_text(transcript)
if timestamped:
# Build styled transcript lines
styled_lines = []
for line in timestamped.split("\n"):
if not line.strip():
continue
# Lines are formatted as "[MM:SS] Speaker: text"
if line.startswith("[") and "] " in line:
bracket_end = line.index("] ")
timestamp = escape(line[: bracket_end + 1])
rest = line[bracket_end + 2 :]
if ": " in rest:
colon_pos = rest.index(": ")
speaker = escape(rest[:colon_pos])
text = escape(rest[colon_pos + 2 :])
styled_lines.append(
f'<div style="margin-bottom:4px;">'
f'<span style="color:#888;font-size:12px;">{timestamp}</span> '
f"<strong>{speaker}:</strong> {text}</div>"
)
else:
styled_lines.append(
f'<div style="margin-bottom:4px;">{escape(line)}</div>'
)
else:
styled_lines.append(
f'<div style="margin-bottom:4px;">{escape(line)}</div>'
)
transcript_html = (
'<h3 style="margin-top:20px;margin-bottom:8px;">Transcript</h3>'
'<div style="background:#f7f7f7;padding:16px;border-radius:6px;'
'font-size:13px;line-height:1.6;max-height:600px;overflow-y:auto;">'
f"{''.join(styled_lines)}</div>"
)
link_html = ""
if include_link:
link_html = (
'<p style="margin-top:20px;">'
f'<a href="{url}" style="display:inline-block;padding:10px 20px;'
"background:#4A90D9;color:#fff;text-decoration:none;"
'border-radius:4px;">View Transcript</a></p>'
)
return f"""\
<div style="font-family:sans-serif;max-width:600px;margin:0 auto;">
<h2>Your transcript is ready</h2>
<p><strong>{title}</strong></p>
<h2 style="margin-bottom:4px;">{title}</h2>
{summary_html}
<p><a href="{url}" style="display:inline-block;padding:10px 20px;background:#4A90D9;color:#fff;text-decoration:none;border-radius:4px;">View Transcript</a></p>
<p style="color:#999;font-size:12px;">This email was sent because you requested to receive the transcript from a meeting.</p>
{transcript_html}
{link_html}
<p style="color:#999;font-size:12px;margin-top:20px;">This email was sent because you requested to receive the transcript from a meeting.</p>
</div>"""
async def send_transcript_email(to_emails: list[str], transcript: Transcript) -> int:
async def send_transcript_email(
to_emails: list[str],
transcript: Transcript,
*,
include_link: bool = True,
) -> int:
"""Send transcript notification to all emails. Returns count sent."""
if not is_email_configured() or not to_emails:
return 0
@@ -57,12 +135,12 @@ async def send_transcript_email(to_emails: list[str], transcript: Transcript) ->
for email_addr in to_emails:
msg = MIMEMultipart("alternative")
msg["Subject"] = f"Transcript Ready: {title}"
msg["Subject"] = f"Reflector: {title}"
msg["From"] = settings.SMTP_FROM_EMAIL
msg["To"] = email_addr
msg.attach(MIMEText(_build_plain_text(transcript, url), "plain"))
msg.attach(MIMEText(_build_html(transcript, url), "html"))
msg.attach(MIMEText(_build_plain_text(transcript, url, include_link), "plain"))
msg.attach(MIMEText(_build_html(transcript, url, include_link), "html"))
try:
await aiosmtplib.send(

View File

@@ -64,3 +64,9 @@ TIMEOUT_HEAVY = 1200 # Transcription, fan-out LLM tasks (Hatchet execution_time
TIMEOUT_HEAVY_HTTP = (
1150 # httpx timeout for transcribe_track — below 1200 so Hatchet doesn't race
)
TIMEOUT_EXTRA_HEAVY = (
3600 # Detect Topics, fan-out LLM tasks (Hatchet execution_timeout)
)
TIMEOUT_EXTRA_HEAVY_HTTP = (
3400 # httpx timeout for detect_topics — below 3600 so Hatchet doesn't race
)

View File

@@ -41,6 +41,7 @@ from reflector.hatchet.broadcast import (
from reflector.hatchet.client import HatchetClientManager
from reflector.hatchet.constants import (
TIMEOUT_AUDIO,
TIMEOUT_EXTRA_HEAVY,
TIMEOUT_HEAVY,
TIMEOUT_LONG,
TIMEOUT_MEDIUM,
@@ -693,7 +694,7 @@ async def generate_waveform(input: PipelineInput, ctx: Context) -> WaveformResul
@daily_multitrack_pipeline.task(
parents=[process_tracks],
execution_timeout=timedelta(seconds=TIMEOUT_HEAVY),
execution_timeout=timedelta(seconds=TIMEOUT_EXTRA_HEAVY),
retries=3,
backoff_factor=2.0,
backoff_max_seconds=30,
@@ -1510,22 +1511,41 @@ async def send_email(input: PipelineInput, ctx: Context) -> EmailResult:
if recording and recording.meeting_id:
meeting = await meetings_controller.get_by_id(recording.meeting_id)
recipients = (
list(meeting.email_recipients)
# Normalise meeting recipients (legacy strings → dicts)
meeting_recipients: list[dict] = (
[
entry
if isinstance(entry, dict)
else {"email": entry, "include_link": True}
for entry in (meeting.email_recipients or [])
]
if meeting and meeting.email_recipients
else []
)
# Also check room-level email
# Room-level email always gets a link (room owner)
from reflector.db.rooms import rooms_controller # noqa: PLC0415
room_email = None
if transcript.room_id:
room = await rooms_controller.get_by_id(transcript.room_id)
if room and room.email_transcript_to:
if room.email_transcript_to not in recipients:
recipients.append(room.email_transcript_to)
room_email = room.email_transcript_to
if not recipients:
# Build two groups: with link and without link
with_link = [
r["email"] for r in meeting_recipients if r.get("include_link", True)
]
without_link = [
r["email"] for r in meeting_recipients if not r.get("include_link", True)
]
if room_email:
if room_email not in with_link:
with_link.append(room_email)
without_link = [e for e in without_link if e != room_email]
if not with_link and not without_link:
ctx.log("send_email skipped (no email recipients)")
return EmailResult(skipped=True)
@@ -1533,7 +1553,15 @@ async def send_email(input: PipelineInput, ctx: Context) -> EmailResult:
if meeting and meeting.email_recipients:
await transcripts_controller.update(transcript, {"share_mode": "public"})
count = await send_transcript_email(recipients, transcript)
count = 0
if with_link:
count += await send_transcript_email(
with_link, transcript, include_link=True
)
if without_link:
count += await send_transcript_email(
without_link, transcript, include_link=False
)
ctx.log(f"send_email complete: sent {count} emails")
return EmailResult(emails_sent=count)

View File

@@ -916,22 +916,41 @@ async def send_email(input: FilePipelineInput, ctx: Context) -> EmailResult:
if recording and recording.meeting_id:
meeting = await meetings_controller.get_by_id(recording.meeting_id)
recipients = (
list(meeting.email_recipients)
# Normalise meeting recipients (legacy strings → dicts)
meeting_recipients: list[dict] = (
[
entry
if isinstance(entry, dict)
else {"email": entry, "include_link": True}
for entry in (meeting.email_recipients or [])
]
if meeting and meeting.email_recipients
else []
)
# Also check room-level email
# Room-level email always gets a link (room owner)
from reflector.db.rooms import rooms_controller # noqa: PLC0415
room_email = None
if transcript.room_id:
room = await rooms_controller.get_by_id(transcript.room_id)
if room and room.email_transcript_to:
if room.email_transcript_to not in recipients:
recipients.append(room.email_transcript_to)
room_email = room.email_transcript_to
if not recipients:
# Build two groups: with link and without link
with_link = [
r["email"] for r in meeting_recipients if r.get("include_link", True)
]
without_link = [
r["email"] for r in meeting_recipients if not r.get("include_link", True)
]
if room_email:
if room_email not in with_link:
with_link.append(room_email)
without_link = [e for e in without_link if e != room_email]
if not with_link and not without_link:
ctx.log("send_email skipped (no email recipients)")
return EmailResult(skipped=True)
@@ -939,7 +958,15 @@ async def send_email(input: FilePipelineInput, ctx: Context) -> EmailResult:
if meeting and meeting.email_recipients:
await transcripts_controller.update(transcript, {"share_mode": "public"})
count = await send_transcript_email(recipients, transcript)
count = 0
if with_link:
count += await send_transcript_email(
with_link, transcript, include_link=True
)
if without_link:
count += await send_transcript_email(
without_link, transcript, include_link=False
)
ctx.log(f"send_email complete: sent {count} emails")
return EmailResult(emails_sent=count)

View File

@@ -397,22 +397,41 @@ async def send_email(input: LivePostPipelineInput, ctx: Context) -> EmailResult:
if recording and recording.meeting_id:
meeting = await meetings_controller.get_by_id(recording.meeting_id)
recipients = (
list(meeting.email_recipients)
# Normalise meeting recipients (legacy strings → dicts)
meeting_recipients: list[dict] = (
[
entry
if isinstance(entry, dict)
else {"email": entry, "include_link": True}
for entry in (meeting.email_recipients or [])
]
if meeting and meeting.email_recipients
else []
)
# Also check room-level email
# Room-level email always gets a link (room owner)
from reflector.db.rooms import rooms_controller # noqa: PLC0415
room_email = None
if transcript.room_id:
room = await rooms_controller.get_by_id(transcript.room_id)
if room and room.email_transcript_to:
if room.email_transcript_to not in recipients:
recipients.append(room.email_transcript_to)
room_email = room.email_transcript_to
if not recipients:
# Build two groups: with link and without link
with_link = [
r["email"] for r in meeting_recipients if r.get("include_link", True)
]
without_link = [
r["email"] for r in meeting_recipients if not r.get("include_link", True)
]
if room_email:
if room_email not in with_link:
with_link.append(room_email)
without_link = [e for e in without_link if e != room_email]
if not with_link and not without_link:
ctx.log("send_email skipped (no email recipients)")
return EmailResult(skipped=True)
@@ -420,7 +439,15 @@ async def send_email(input: LivePostPipelineInput, ctx: Context) -> EmailResult:
if meeting and meeting.email_recipients:
await transcripts_controller.update(transcript, {"share_mode": "public"})
count = await send_transcript_email(recipients, transcript)
count = 0
if with_link:
count += await send_transcript_email(
with_link, transcript, include_link=True
)
if without_link:
count += await send_transcript_email(
without_link, transcript, include_link=False
)
ctx.log(f"send_email complete: sent {count} emails")
return EmailResult(emails_sent=count)

View File

@@ -168,8 +168,9 @@ async def add_email_recipient(
if not meeting:
raise HTTPException(status_code=404, detail="Meeting not found")
include_link = user is not None
recipients = await meetings_controller.add_email_recipient(
meeting_id, request.email
meeting_id, request.email, include_link=include_link
)
return {"status": "success", "email_recipients": recipients}

View File

@@ -797,5 +797,7 @@ async def transcript_send_email(
)
if not transcript:
raise HTTPException(status_code=404, detail="Transcript not found")
sent = await send_transcript_email([request.email], transcript)
sent = await send_transcript_email(
[request.email], transcript, include_link=(transcript.share_mode == "public")
)
return SendEmailResponse(sent=sent)

View File

@@ -146,7 +146,6 @@ else:
app.conf.broker_connection_retry_on_startup = True
app.autodiscover_tasks(
[
"reflector.pipelines.main_live_pipeline",
"reflector.worker.healthcheck",
"reflector.worker.process",
"reflector.worker.cleanup",

View File

@@ -12,6 +12,7 @@ from celery import shared_task
from celery.utils.log import get_task_logger
from pydantic import ValidationError
from reflector.asynctask import asynctask
from reflector.dailyco_api import FinishedRecordingResponse, RecordingResponse
from reflector.db.daily_participant_sessions import (
DailyParticipantSession,
@@ -25,9 +26,6 @@ from reflector.db.transcripts import (
transcripts_controller,
)
from reflector.hatchet.client import HatchetClientManager
from reflector.pipelines.main_live_pipeline import asynctask
from reflector.pipelines.topic_processing import EmptyPipeline
from reflector.processors import AudioFileWriterProcessor
from reflector.processors.audio_waveform_processor import AudioWaveformProcessor
from reflector.redis_cache import RedisAsyncLock
from reflector.settings import settings
@@ -908,6 +906,11 @@ async def convert_audio_and_waveform(transcript) -> None:
transcript_id=transcript.id,
)
from reflector.pipelines.topic_processing import EmptyPipeline # noqa: PLC0415
from reflector.processors.audio_file_writer import (
AudioFileWriterProcessor, # noqa: PLC0415
)
upload_path = transcript.data_path / "upload.webm"
mp3_path = transcript.audio_mp3_filename

View File

@@ -8,8 +8,8 @@ import structlog
from celery import shared_task
from celery.utils.log import get_task_logger
from reflector.asynctask import asynctask
from reflector.db.rooms import rooms_controller
from reflector.pipelines.main_live_pipeline import asynctask
from reflector.utils.webhook import (
WebhookRoomPayload,
WebhookTestPayload,

View File

@@ -107,7 +107,8 @@ class WebsocketManager:
while True:
# timeout=1.0 prevents tight CPU loop when no messages available
message = await pubsub_subscriber.get_message(
ignore_subscribe_messages=True
ignore_subscribe_messages=True,
timeout=1.0,
)
if message is not None:
room_id = message["channel"].decode("utf-8")

206
server/tests/test_email.py Normal file
View File

@@ -0,0 +1,206 @@
"""Tests for reflector.email — transcript email composition and sending."""
from unittest.mock import AsyncMock, patch
import pytest
from reflector.db.transcripts import (
SourceKind,
Transcript,
TranscriptParticipant,
TranscriptTopic,
)
from reflector.email import (
_build_html,
_build_plain_text,
get_transcript_url,
send_transcript_email,
)
from reflector.processors.types import Word
def _make_transcript(
*,
title: str | None = "Weekly Standup",
short_summary: str | None = "Team discussed sprint progress.",
with_topics: bool = True,
share_mode: str = "private",
source_kind: SourceKind = SourceKind.FILE,
) -> Transcript:
topics = []
participants = []
if with_topics:
participants = [
TranscriptParticipant(id="p1", speaker=0, name="Alice"),
TranscriptParticipant(id="p2", speaker=1, name="Bob"),
]
topics = [
TranscriptTopic(
title="Intro",
summary="Greetings",
timestamp=0.0,
duration=10.0,
words=[
Word(text="Hello", start=0.0, end=0.5, speaker=0),
Word(text="everyone", start=0.5, end=1.0, speaker=0),
Word(text="Thanks", start=5.0, end=5.5, speaker=1),
Word(text="for", start=5.5, end=5.8, speaker=1),
Word(text="joining", start=5.8, end=6.2, speaker=1),
],
),
]
return Transcript(
id="tx-123",
title=title,
short_summary=short_summary,
topics=topics,
participants=participants,
share_mode=share_mode,
source_kind=source_kind,
)
URL = "http://localhost:3000/transcripts/tx-123"
class TestBuildPlainText:
def test_full_content_with_link(self):
t = _make_transcript()
text = _build_plain_text(t, URL, include_link=True)
assert text.startswith("Reflector: Weekly Standup")
assert "Team discussed sprint progress." in text
assert "[00:00] Alice:" in text
assert "[00:05] Bob:" in text
assert URL in text
def test_full_content_without_link(self):
t = _make_transcript()
text = _build_plain_text(t, URL, include_link=False)
assert "Reflector: Weekly Standup" in text
assert "Team discussed sprint progress." in text
assert "[00:00] Alice:" in text
assert URL not in text
def test_no_summary(self):
t = _make_transcript(short_summary=None)
text = _build_plain_text(t, URL, include_link=True)
assert "Summary:" not in text
assert "[00:00] Alice:" in text
def test_no_topics(self):
t = _make_transcript(with_topics=False)
text = _build_plain_text(t, URL, include_link=True)
assert "Transcript:" not in text
assert "Reflector: Weekly Standup" in text
def test_unnamed_recording(self):
t = _make_transcript(title=None)
text = _build_plain_text(t, URL, include_link=True)
assert "Reflector: Unnamed recording" in text
class TestBuildHtml:
def test_full_content_with_link(self):
t = _make_transcript()
html = _build_html(t, URL, include_link=True)
assert "Weekly Standup" in html
assert "Team discussed sprint progress." in html
assert "Alice" in html
assert "Bob" in html
assert URL in html
assert "View Transcript" in html
def test_full_content_without_link(self):
t = _make_transcript()
html = _build_html(t, URL, include_link=False)
assert "Weekly Standup" in html
assert "Alice" in html
assert URL not in html
assert "View Transcript" not in html
def test_no_summary(self):
t = _make_transcript(short_summary=None)
html = _build_html(t, URL, include_link=True)
assert "sprint progress" not in html
assert "Alice" in html
def test_no_topics(self):
t = _make_transcript(with_topics=False)
html = _build_html(t, URL, include_link=True)
assert "Transcript" not in html or "View Transcript" in html
def test_html_escapes_title(self):
t = _make_transcript(title='<script>alert("xss")</script>')
html = _build_html(t, URL, include_link=True)
assert "<script>" not in html
assert "&lt;script&gt;" in html
class TestGetTranscriptUrl:
def test_url_format(self):
t = _make_transcript()
url = get_transcript_url(t)
assert url.endswith("/transcripts/tx-123")
class TestSendTranscriptEmail:
@pytest.mark.asyncio
async def test_include_link_default_true(self):
t = _make_transcript()
with (
patch("reflector.email.is_email_configured", return_value=True),
patch(
"reflector.email.aiosmtplib.send", new_callable=AsyncMock
) as mock_send,
):
count = await send_transcript_email(["a@test.com"], t)
assert count == 1
call_args = mock_send.call_args
msg = call_args[0][0]
assert msg["Subject"] == "Reflector: Weekly Standup"
# Default include_link=True, so HTML part should contain the URL
html_part = msg.get_payload()[1].get_payload()
assert "/transcripts/tx-123" in html_part
@pytest.mark.asyncio
async def test_include_link_false(self):
t = _make_transcript()
with (
patch("reflector.email.is_email_configured", return_value=True),
patch(
"reflector.email.aiosmtplib.send", new_callable=AsyncMock
) as mock_send,
):
count = await send_transcript_email(["a@test.com"], t, include_link=False)
assert count == 1
msg = mock_send.call_args[0][0]
html_part = msg.get_payload()[1].get_payload()
assert "/transcripts/tx-123" not in html_part
plain_part = msg.get_payload()[0].get_payload()
assert "/transcripts/tx-123" not in plain_part
@pytest.mark.asyncio
async def test_skips_when_not_configured(self):
t = _make_transcript()
with patch("reflector.email.is_email_configured", return_value=False):
count = await send_transcript_email(["a@test.com"], t)
assert count == 0
@pytest.mark.asyncio
async def test_skips_empty_recipients(self):
t = _make_transcript()
with patch("reflector.email.is_email_configured", return_value=True):
count = await send_transcript_email([], t)
assert count == 0

View File

@@ -212,8 +212,13 @@ export default function DailyRoom({ meeting, room }: DailyRoomProps) {
const showConsentModalRef = useRef(showConsentModal);
showConsentModalRef.current = showConsentModal;
const userEmail =
auth.status === "authenticated" || auth.status === "refreshing"
? auth.user.email
: null;
const { showEmailModal } = useEmailTranscriptDialog({
meetingId: assertMeetingId(meeting.id),
userEmail,
});
const showEmailModalRef = useRef(showEmailModal);
showEmailModalRef.current = showEmailModal;

View File

@@ -6,13 +6,15 @@ import { Box, Button, Input, Text, VStack, HStack } from "@chakra-ui/react";
interface EmailTranscriptDialogProps {
onSubmit: (email: string) => void;
onDismiss: () => void;
initialEmail?: string;
}
export function EmailTranscriptDialog({
onSubmit,
onDismiss,
initialEmail,
}: EmailTranscriptDialogProps) {
const [email, setEmail] = useState("");
const [email, setEmail] = useState(initialEmail ?? "");
const [inputEl, setInputEl] = useState<HTMLInputElement | null>(null);
useEffect(() => {

View File

@@ -11,10 +11,12 @@ const TOAST_CHECK_INTERVAL_MS = 100;
type UseEmailTranscriptDialogParams = {
meetingId: MeetingId;
userEmail?: string | null;
};
export function useEmailTranscriptDialog({
meetingId,
userEmail,
}: UseEmailTranscriptDialogParams) {
const [modalOpen, setModalOpen] = useState(false);
const addEmailMutation = useMeetingAddEmailRecipient();
@@ -83,6 +85,7 @@ export function useEmailTranscriptDialog({
duration: null,
render: ({ dismiss }) => (
<EmailTranscriptDialog
initialEmail={userEmail ?? undefined}
onSubmit={(email) => {
handleSubmitEmail(email);
dismiss();
@@ -120,7 +123,7 @@ export function useEmailTranscriptDialog({
}
}, TOAST_CHECK_INTERVAL_MS);
});
}, [handleSubmitEmail, modalOpen]);
}, [handleSubmitEmail, modalOpen, userEmail]);
return {
showEmailModal,

View File

@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="4" width="20" height="16" rx="2"/>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
</svg>

Before

Width:  |  Height:  |  Size: 274 B

After

Width:  |  Height:  |  Size: 267 B