mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 04:59:41 +02:00
fix(dreams): macOS-aware mic deny message + force-retry escape hatch
The previous "click the lock icon" advice is wrong on macOS, where
"sticky deny" usually originates from the system-level Privacy &
Security setting, not a per-site browser setting. There is no lock
icon to click and the user has no obvious next step.
Now:
- The denied message detects macOS/iOS via navigator.platform and
walks through the actual fix path: System Settings → Privacy &
Security → Microphone → enable browser → fully quit and restart it
(Cmd+Q, not just close the tab — the permission only re-reads on
cold start). Also points to chrome://settings/content/microphone
as the second-most-likely culprit
- Non-mac path lists the same two causes in the right order
- Recorder.start now accepts { force: true } that bypasses the
Permissions API pre-check and actually calls getUserMedia, so the
raw browser error (NotAllowedError, SecurityError, etc) surfaces.
Useful when the Permissions API is wrong or stale
- ListView shows a "Trotzdem versuchen" button next to the error
text. Clicking it routes through the force path
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a6828a16c2
commit
f7a5bb841e
2 changed files with 72 additions and 12 deletions
|
|
@ -208,6 +208,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function forceRetryMic() {
|
||||
recError = null;
|
||||
await dreamRecorder.start({ force: true });
|
||||
if (dreamRecorder.error) {
|
||||
recError = dreamRecorder.error;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelRecording() {
|
||||
dreamRecorder.cancel();
|
||||
}
|
||||
|
|
@ -265,7 +273,10 @@
|
|||
{/if}
|
||||
</div>
|
||||
{#if recError}
|
||||
<p class="rec-error">{recError}</p>
|
||||
<div class="rec-error">
|
||||
<p>{recError}</p>
|
||||
<button class="rec-retry" onclick={forceRetryMic}>Trotzdem versuchen</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Quick create -->
|
||||
|
|
@ -629,10 +640,37 @@
|
|||
}
|
||||
|
||||
.rec-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
.rec-error p {
|
||||
font-size: 0.6875rem;
|
||||
color: #ef4444;
|
||||
color: #b91c1c;
|
||||
margin: 0;
|
||||
padding: 0 0.25rem;
|
||||
white-space: pre-line;
|
||||
line-height: 1.5;
|
||||
}
|
||||
:global(.dark) .rec-error p {
|
||||
color: #fca5a5;
|
||||
}
|
||||
.rec-retry {
|
||||
align-self: flex-start;
|
||||
padding: 0.25rem 0.625rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
background: transparent;
|
||||
color: #ef4444;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
.rec-retry:hover {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
|
||||
/* ── View Tabs ─────────────────────────────── */
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class DreamRecorder {
|
|||
return typeof window !== 'undefined' && window.isSecureContext === true;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
async start(options: { force?: boolean } = {}): Promise<void> {
|
||||
if (this.status !== 'idle') return;
|
||||
|
||||
// 1. Secure context check — getUserMedia is silently unavailable
|
||||
|
|
@ -55,14 +55,17 @@ class DreamRecorder {
|
|||
return;
|
||||
}
|
||||
|
||||
// 3. Sticky deny check — Permissions API tells us if the user
|
||||
// previously denied access. The browser will silently reject
|
||||
// getUserMedia without showing a prompt in that case.
|
||||
const stickyDenied = await this.#checkStickyDeny();
|
||||
if (stickyDenied) {
|
||||
this.error =
|
||||
'Mikrofon-Zugriff wurde für diese Seite blockiert. Klicke in der Adressleiste auf das Schloss-Symbol → Mikrofon → Erlauben, dann lade die Seite neu.';
|
||||
return;
|
||||
// 3. Sticky deny check — Permissions API tells us if the browser
|
||||
// will silently reject getUserMedia without showing a prompt.
|
||||
// On macOS this is most often a SYSTEM-level block, not a per-site
|
||||
// setting, which is why no lock icon helps. Skip the check if the
|
||||
// caller explicitly forces a retry to surface the real error.
|
||||
if (!options.force) {
|
||||
const stickyDenied = await this.#checkStickyDeny();
|
||||
if (stickyDenied) {
|
||||
this.error = this.#stickyDenyMessage();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.error = null;
|
||||
|
|
@ -162,6 +165,25 @@ class DreamRecorder {
|
|||
reject?.(err);
|
||||
}
|
||||
|
||||
#stickyDenyMessage(): string {
|
||||
const isMac =
|
||||
typeof navigator !== 'undefined' && /Mac|iPhone|iPad/i.test(navigator.platform || '');
|
||||
if (isMac) {
|
||||
return [
|
||||
'Mikrofon-Zugriff blockiert. Auf macOS hat das fast immer eine von zwei Ursachen:',
|
||||
'1) System-Einstellungen → Datenschutz & Sicherheit → Mikrofon: dein Browser muss in der Liste aktiviert sein. Wenn er fehlt oder deaktiviert ist, schalte ihn ein und starte den Browser komplett neu (Cmd+Q, nicht nur Tab schließen).',
|
||||
'2) Browser-Einstellung: chrome://settings/content/microphone (Chrome) oder about:preferences#privacy (Firefox) → "localhost" darf nicht in der Block-Liste stehen.',
|
||||
'Tipp: Klicke auf "Trotzdem versuchen" um den exakten Browser-Fehler zu sehen.',
|
||||
].join('\n');
|
||||
}
|
||||
return [
|
||||
'Mikrofon-Zugriff blockiert. Mögliche Ursachen:',
|
||||
'1) Browser-Einstellungen → Mikrofon → "localhost" darf nicht blockiert sein.',
|
||||
'2) System-Einstellungen → Datenschutz → Mikrofon → Browser muss erlaubt sein.',
|
||||
'Tipp: Klicke auf "Trotzdem versuchen" um den exakten Browser-Fehler zu sehen.',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async #checkStickyDeny(): Promise<boolean> {
|
||||
try {
|
||||
// Permissions API may not be available everywhere; treat as unknown.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue