Skip to content

Commit 0f186a0

Browse files
review comment fixed
1 parent 85c9aeb commit 0f186a0

2 files changed

Lines changed: 72 additions & 52 deletions

File tree

src/directLine.ts

Lines changed: 15 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import { objectExpression } from '@babel/types';
3737
import { DirectLineStreaming } from './directLineStreaming';
3838
export { DirectLineStreaming };
3939

40+
import { hasIframeMicrophonePermission, isInIframe } from './iframeMicrophone';
41+
4042
const DIRECT_LINE_VERSION = 'DirectLine/3.0';
4143

4244
declare var process: {
@@ -458,52 +460,6 @@ const konsole = {
458460
}
459461
}
460462

461-
/**
462-
* Checks if the current context is running inside an iframe.
463-
*/
464-
const isInIframe = (): boolean => {
465-
try {
466-
return typeof window !== 'undefined' && window.self !== window.top;
467-
} catch (e) {
468-
// If accessing window.top throws (cross-origin), we're definitely in an iframe
469-
return true;
470-
}
471-
}
472-
473-
/**
474-
* Checks if the iframe has microphone permission via the allow attribute.
475-
*/
476-
const hasIframeMicrophonePermission = async (): Promise<boolean> => {
477-
if (typeof window === 'undefined' || typeof document === 'undefined') {
478-
return false;
479-
}
480-
481-
try {
482-
// Try using the Permissions Policy API (Chrome 88+, Edge 88+)
483-
const doc = document as any;
484-
if (doc.permissionsPolicy && typeof doc.permissionsPolicy.allowsFeature === 'function') {
485-
return doc.permissionsPolicy.allowsFeature('microphone');
486-
}
487-
488-
// Fallback to deprecated Feature Policy API (Chrome 60-87, Edge 79-87)
489-
if (doc.featurePolicy && typeof doc.featurePolicy.allowsFeature === 'function') {
490-
return doc.featurePolicy.allowsFeature('microphone');
491-
}
492-
493-
// Fallback to Permissions API (broader support: Chrome 43+, Firefox 46+, Safari 16+)
494-
if (typeof navigator !== 'undefined' && navigator.permissions) {
495-
const result = await navigator.permissions.query({ name: 'microphone' as PermissionName });
496-
// 'granted' or 'prompt' means microphone is allowed by iframe policy
497-
// 'denied' means either user denied or iframe policy blocks it
498-
return result.state !== 'denied';
499-
}
500-
} catch (e) {
501-
// If permissions check fails, assume microphone is not allowed in iframe
502-
}
503-
504-
return false;
505-
}
506-
507463
export interface IBotConnection {
508464
connectionStatus$: BehaviorSubject<ConnectionStatus>,
509465
activity$: Observable<Activity>,
@@ -1281,19 +1237,26 @@ export class DirectLine implements IBotConnection {
12811237
}
12821238

12831239
/**
1284-
* Modifies stream URL for voice mode: replaces /stream with /stream/multimodal
1240+
* Modifies stream URL for voice mode: appends /multimodal to the /stream path
1241+
* while preserving query string, hash, and other URL parts.
12851242
*/
12861243
private getMultimodalStreamUrl(url: string): string {
12871244
if (!this.voiceModeEnabled || !url) {
12881245
return url;
12891246
}
12901247

1291-
// Replace /stream endpoint with /stream/multimodal (if not already multimodal)
1292-
if (!url.includes('/stream/multimodal')) {
1293-
return url.replace('/stream', '/stream/multimodal');
1294-
}
1248+
try {
1249+
const parsed = new URL(url);
1250+
1251+
if (parsed.pathname.endsWith('/stream')) {
1252+
parsed.pathname += '/multimodal';
1253+
}
12951254

1296-
return url;
1255+
return parsed.toString();
1256+
} catch {
1257+
// If URL parsing fails (malformed URL), return as-is
1258+
return url;
1259+
}
12971260
}
12981261

12991262
}

src/iframeMicrophone.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Utilities for detecting iframe context and microphone permission.
3+
*
4+
* Used by DirectLine to auto-detect whether voice mode should be enabled
5+
* when running inside an iframe with `allow="microphone"` attribute.
6+
*/
7+
8+
/**
9+
* Checks if the current context is running inside an iframe.
10+
*/
11+
export const isInIframe = (): boolean => {
12+
try {
13+
return typeof window !== 'undefined' && window.self !== window.top;
14+
} catch (e) {
15+
// If accessing window.top throws (cross-origin), we're definitely in an iframe
16+
return true;
17+
}
18+
};
19+
20+
/**
21+
* Checks if the iframe has microphone permission via the allow attribute.
22+
*
23+
* Tries (in order):
24+
* 1. Permissions Policy API (Chrome 88+, Edge 88+)
25+
* 2. Feature Policy API (Chrome 60-87, Edge 79-87) — deprecated
26+
* 3. Permissions API (Chrome 43+, Firefox 46+, Safari 16+)
27+
*/
28+
export const hasIframeMicrophonePermission = async (): Promise<boolean> => {
29+
if (typeof window === 'undefined' || typeof document === 'undefined') {
30+
return false;
31+
}
32+
33+
try {
34+
// Try using the Permissions Policy API (Chrome 88+, Edge 88+)
35+
const doc = document as any;
36+
if (doc.permissionsPolicy && typeof doc.permissionsPolicy.allowsFeature === 'function') {
37+
return doc.permissionsPolicy.allowsFeature('microphone');
38+
}
39+
40+
// Fallback to deprecated Feature Policy API (Chrome 60-87, Edge 79-87)
41+
if (doc.featurePolicy && typeof doc.featurePolicy.allowsFeature === 'function') {
42+
return doc.featurePolicy.allowsFeature('microphone');
43+
}
44+
45+
// Fallback to Permissions API (broader support: Chrome 43+, Firefox 46+, Safari 16+)
46+
if (typeof navigator !== 'undefined' && navigator.permissions) {
47+
const result = await navigator.permissions.query({ name: 'microphone' as PermissionName });
48+
// 'granted' or 'prompt' means microphone is allowed by iframe policy
49+
// 'denied' means either user denied or iframe policy blocks it
50+
return result.state !== 'denied';
51+
}
52+
} catch (e) {
53+
// If permissions check fails, assume microphone is not allowed in iframe
54+
}
55+
56+
return false;
57+
};

0 commit comments

Comments
 (0)