diff --git a/config.js b/config.js
index bab14f84f..0de801d59 100644
--- a/config.js
+++ b/config.js
@@ -599,6 +599,7 @@ var config = {
// 'chat',
// 'closedcaptions',
// 'desktop',
+ // 'dock-iframe'
// 'download',
// 'embedmeeting',
// 'etherpad',
@@ -627,6 +628,7 @@ var config = {
// 'stats',
// 'tileview',
// 'toggle-camera',
+ // 'undock-iframe',
// 'videoquality',
// '__end'
// ],
diff --git a/lang/main.json b/lang/main.json
index eaa142e90..39d72a539 100644
--- a/lang/main.json
+++ b/lang/main.json
@@ -1013,6 +1013,7 @@
"chat": "Open / Close chat",
"clap": "Clap",
"collapse": "Collapse",
+ "dock": "Dock in main window",
"document": "Toggle shared document",
"download": "Download our apps",
"embedMeeting": "Embed meeting",
@@ -1063,6 +1064,7 @@
"tileView": "Toggle tile view",
"toggleCamera": "Toggle camera",
"toggleFilmstrip": "Toggle filmstrip",
+ "undock": "Undock into separate window",
"videoblur": "Toggle video blur",
"videomute": "Start / Stop camera"
},
@@ -1079,6 +1081,7 @@
"closeChat": "Close chat",
"closeReactionsMenu": "Close reactions menu",
"disableReactionSounds": "You can disable reaction sounds for this meeting",
+ "dock": "Dock in main window",
"documentClose": "Close shared document",
"documentOpen": "Open shared document",
"download": "Download our apps",
@@ -1147,6 +1150,7 @@
"talkWhileMutedPopup": "Trying to speak? You are muted.",
"tileViewToggle": "Toggle tile view",
"toggleCamera": "Toggle camera",
+ "undock": "Undock into separate window",
"videoSettings": "Video settings",
"videomute": "Start / Stop camera"
},
diff --git a/modules/API/API.js b/modules/API/API.js
index f3f9e6290..dd9adfc3d 100644
--- a/modules/API/API.js
+++ b/modules/API/API.js
@@ -1441,6 +1441,22 @@ class API {
});
}
+ /**
+ * Notify external application (if API is enabled) that the iframe
+ * docked state has been changed. The responsibility for implementing
+ * the dock / undock functionality lies with the external application.
+ *
+ * @param {boolean} docked - Whether or not the iframe has been set to
+ * be docked or undocked.
+ * @returns {void}
+ */
+ notifyIframeDockStateChanged(docked: boolean) {
+ this._sendEvent({
+ name: 'iframe-dock-state-changed',
+ docked
+ });
+ }
+
/**
* Notify external application of a participant, remote or local, being
* removed from the conference by another participant.
diff --git a/modules/API/external/external_api.js b/modules/API/external/external_api.js
index 3f6050749..d180d5bba 100644
--- a/modules/API/external/external_api.js
+++ b/modules/API/external/external_api.js
@@ -106,6 +106,7 @@ const events = {
'feedback-submitted': 'feedbackSubmitted',
'feedback-prompt-displayed': 'feedbackPromptDisplayed',
'filmstrip-display-changed': 'filmstripDisplayChanged',
+ 'iframe-dock-state-changed': 'iframeDockStateChanged',
'incoming-message': 'incomingMessage',
'knocking-participant': 'knockingParticipant',
'log': 'log',
diff --git a/react/features/base/icons/svg/dock.svg b/react/features/base/icons/svg/dock.svg
new file mode 100644
index 000000000..00ff2d0a3
--- /dev/null
+++ b/react/features/base/icons/svg/dock.svg
@@ -0,0 +1,3 @@
+
diff --git a/react/features/base/icons/svg/index.js b/react/features/base/icons/svg/index.js
index 9bf6feae0..a32a6ecf6 100644
--- a/react/features/base/icons/svg/index.js
+++ b/react/features/base/icons/svg/index.js
@@ -44,6 +44,7 @@ export { default as IconDeviceBluetooth } from './bluetooth.svg';
export { default as IconDeviceEarpiece } from './phone-talk.svg';
export { default as IconDeviceHeadphone } from './headset.svg';
export { default as IconDeviceSpeaker } from './volume.svg';
+export { default as IconDock } from './dock.svg';
export { default as IconDeviceDocument } from './document.svg';
export { default as IconDominantSpeaker } from './dominant-speaker.svg';
export { default as IconDownload } from './download.svg';
@@ -130,6 +131,7 @@ export { default as IconSwitchCamera } from './switch-camera.svg';
export { default as IconTileView } from './tiles-many.svg';
export { default as IconToggleRecording } from './camera-take-picture.svg';
export { default as IconTrash } from './trash.svg';
+export { default as IconUndock } from './undock.svg';
export { default as IconUnpin } from './unpin.svg';
export { default as IconVideoOff } from './video-off.svg';
export { default as IconVideoQualityAudioOnly } from './AUD.svg';
diff --git a/react/features/base/icons/svg/undock.svg b/react/features/base/icons/svg/undock.svg
new file mode 100644
index 000000000..7d0729f34
--- /dev/null
+++ b/react/features/base/icons/svg/undock.svg
@@ -0,0 +1,3 @@
+
diff --git a/react/features/toolbox/components/web/DockIframeButton.js b/react/features/toolbox/components/web/DockIframeButton.js
new file mode 100644
index 000000000..887abd892
--- /dev/null
+++ b/react/features/toolbox/components/web/DockIframeButton.js
@@ -0,0 +1,29 @@
+// @flow
+
+import { translate } from '../../../base/i18n';
+import { IconDock } from '../../../base/icons';
+import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
+
+declare var APP: Object;
+
+/**
+ * Implementation of a button for notifying integrators that iframe should be docked.
+ */
+class DockIframeButton extends AbstractButton {
+ accessibilityLabel = 'toolbar.accessibilityLabel.dock';
+ icon = IconDock;
+ label = 'toolbar.dock';
+ tooltip = 'toolbar.dock';
+
+ /**
+ * Handles clicking / pressing the button by triggering external api event.
+ *
+ * @protected
+ * @returns {void}
+ */
+ _handleClick() {
+ APP.API.notifyIframeDockStateChanged(true);
+ }
+}
+
+export default translate(DockIframeButton);
diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js
index 7e9d55ab1..0a7d74361 100644
--- a/react/features/toolbox/components/web/Toolbox.js
+++ b/react/features/toolbox/components/web/Toolbox.js
@@ -89,6 +89,7 @@ import MuteEveryoneButton from '../MuteEveryoneButton';
import MuteEveryonesVideoButton from '../MuteEveryonesVideoButton';
import AudioSettingsButton from './AudioSettingsButton';
+import DockIframeButton from './DockIframeButton';
import FullscreenButton from './FullscreenButton';
import LinkToSalesforceButton from './LinkToSalesforceButton';
import OverflowMenuButton from './OverflowMenuButton';
@@ -96,6 +97,7 @@ import ProfileButton from './ProfileButton';
import Separator from './Separator';
import ShareDesktopButton from './ShareDesktopButton';
import ToggleCameraButton from './ToggleCameraButton';
+import UndockIframeButton from './UndockIframeButton';
import VideoSettingsButton from './VideoSettingsButton';
/**
@@ -786,6 +788,18 @@ class Toolbox extends Component {
group: 3
};
+ const dockIframe = {
+ key: 'dock-iframe',
+ Content: DockIframeButton,
+ group: 3
+ };
+
+ const undockIframe = {
+ key: 'undock-iframe',
+ Content: UndockIframeButton,
+ group: 3
+ };
+
const speakerStats = {
key: 'stats',
Content: SpeakerStatsButton,
@@ -853,6 +867,8 @@ class Toolbox extends Component {
shareAudio,
etherpad,
virtualBackground,
+ dockIframe,
+ undockIframe,
speakerStats,
settings,
shortcuts,
diff --git a/react/features/toolbox/components/web/UndockIframeButton.js b/react/features/toolbox/components/web/UndockIframeButton.js
new file mode 100644
index 000000000..0e4c4a95c
--- /dev/null
+++ b/react/features/toolbox/components/web/UndockIframeButton.js
@@ -0,0 +1,29 @@
+// @flow
+
+import { translate } from '../../../base/i18n';
+import { IconUndock } from '../../../base/icons';
+import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
+
+declare var APP: Object;
+
+/**
+ * Implementation of a button for notifying integrators that iframe should be undocked.
+ */
+class UndockIframeButton extends AbstractButton {
+ accessibilityLabel = 'toolbar.accessibilityLabel.undock';
+ icon = IconUndock;
+ label = 'toolbar.undock';
+ tooltip = 'toolbar.undock';
+
+ /**
+ * Handles clicking / pressing the button by triggering external api event.
+ *
+ * @protected
+ * @returns {void}
+ */
+ _handleClick() {
+ APP.API.notifyIframeDockStateChanged(false);
+ }
+}
+
+export default translate(UndockIframeButton);