Compare commits

..

25 Commits

Author SHA1 Message Date
hristoterezov
9fa1b210f5 feat(loadScript): Use the function from js-utils 2018-03-22 13:15:14 -05:00
Paweł Domas
15e1633d86 fix(config): no legacy over non-legacy override (#2644)
Do not take legacy property into account if there's non-legacy value.
2018-03-22 10:57:08 -05:00
Daniel Ornelas
de0a7bfcd3 Some improvements for handling completion of transitions. Fixed a wrong conferenceEnded value when user left the conversation. 2018-03-22 00:04:25 -04:00
Daniel Ornelas
5858859838 Addressing feedback from PR 2018-03-22 00:04:25 -04:00
Daniel Ornelas
8428dd95c2 Implement Exit PiP mode in PiPWindow 2018-03-21 22:00:43 -04:00
Daniel Ornelas
a26ccf195e Added initialization of JitsiMeetManager components 2018-03-21 22:00:43 -04:00
Daniel Ornelas
0a5f60c637 Refactor PiP code into its own components 2018-03-21 22:00:43 -04:00
Daniel Ornelas
f8163de765 Implement Jitsi meet presentation interface that supports custom PiP solution 2018-03-21 22:00:43 -04:00
Daniel Ornelas
181580871f Initial work for testing PiP mode 2018-03-21 22:00:43 -04:00
damencho
ea431ca90c Adds new text for Firefox screen-sharing permission denied error. 2018-03-21 18:06:17 -05:00
damencho
a902540167 Removes Firefox extension handle (removed in FF newer than latest ESR). 2018-03-21 18:06:17 -05:00
virtuacoplenny
823481dc1d feat(recording): use google api to get stream key (#2481)
* feat(recording): use google api to get stream key

* squash: renaming pass

* squash: return full load promise

* sqush: use google api state enum

* squash: workaround for lib not loading

* another new design...

* increase timeout workaround for gapi load issue

* styling pass

* tweak copy

* squash: auto select first broadcast
2018-03-21 11:26:52 -07:00
paweldomas
b5b99301ca fix: synchronize global 'config' var with the reducer's state 2018-03-20 21:07:45 -04:00
paweldomas
ef52f09910 ref(config): override config from URL on mobile
Moves the things around to be able to override the config with the URL
params specified in the hash part of the location URI to which the app
is navigating to.
2018-03-20 21:07:45 -04:00
Leonard Kim
6a066c3079 squash: update package-lock again for integrity shas 2018-03-20 16:06:50 -05:00
Leonard Kim
71e368f70e squash: override atlaskit styles for dropdowns to display clearer 2018-03-20 16:06:50 -05:00
Leonard Kim
134ff71c78 feat(modals): use dark theme 2018-03-20 16:06:50 -05:00
bbaldino
fef1d8b520 add a prosody module to insert identity information (when available) … (#2627)
* add a prosody module to insert identity information (when available) into
presence

prosody will check for jitsi_meet_context_user and
jitsi_meet_context_group in the session and, if they are present, insert
them into presence (we do this in prosody so they cannot be spoofed).

* remove unused 'presence' variable

* refactor to modify presence message in place

* make object member access consistent

* make the group information optional
2018-03-20 15:27:39 -05:00
virtuacoplenny
309fcf9672 fix(filmstrip): fix local video alignment with no invite button (#2629)
Hardcoding an offset from the bottom of 32px causes issues in
horizontal filmstrip when there is no invite button, because
then the local video just displays 32px from the bottom as there
is no button to take up space above it. Instead leverage flex
alignments to align the bottom of the video to the bototm of
the filmstrip.
2018-03-20 12:28:09 -07:00
virtuacoplenny
2eafbacdd4 chore(deps): update lib-jitsi-meet to 0503ec4d for edge p2p progress (#2631) 2018-03-20 11:34:57 -07:00
virtuacoplenny
26ea667170 Merge pull request #2628 from jitsi/welcome_page_analytics
fix(analytics): Init analytics for the web welcome page.
2018-03-20 10:02:07 -07:00
jitsi-pootle
5f43e8f8c7 New files added from translate.jitsi.org based on templates 2018-03-20 15:52:21 +00:00
Shikhar Agnihotri
9ef83702cf Update CONTRIBUTING.md (#2601) 2018-03-20 09:49:35 -05:00
hristoterezov
fda2548a38 fix(analytics): Init analytics for the web welcome page. 2018-03-19 17:51:22 -05:00
Дамян Минков
eb53944a4d Adds poltergeist support for locked rooms. (#2626) 2018-03-19 16:20:44 -05:00
65 changed files with 3891 additions and 462 deletions

View File

@@ -18,6 +18,7 @@
; Ignore packages in node_modules which we (i.e. the jitsi-meet project) have
; seen to cause errors and we have chosen not to fix.
.*/node_modules/@atlaskit/.*/*.js.flow
.*/node_modules/@atlassian
.*/node_modules/bower/lib/node_modules/bower-json/test/.*
.*/node_modules/immutable/dist/immutable.js.flow

View File

@@ -21,7 +21,7 @@ license agreement as either a [corporation](https://jitsi.org/ccla) or an
in the agreement, unfortunately, we cannot accept your contribution.
## Creating Pull Requests
- Make sure your code passes the linter rules beforehand. The linter is exeuted
- Make sure your code passes the linter rules beforehand. The linter is executed
automatically when committing code.
- Perform **one** logical change per pull request.
- Maintain a clean list of commits, squash them if necessary.

View File

@@ -754,7 +754,7 @@ export default {
track.mute();
}
});
logger.log(`initialized with ${tracks.length} local tracks`);
logger.log('initialized with %s local tracks', tracks.length);
this._localTracksInitialized = true;
con.addEventListener(
JitsiConnectionEvents.CONNECTION_FAILED,
@@ -1709,12 +1709,6 @@ export default {
}
);
return;
} else if (error.name === JitsiTrackErrors.FIREFOX_EXTENSION_NEEDED) {
APP.UI.showExtensionRequiredDialog(
config.desktopSharingFirefoxExtensionURL
);
return;
}
@@ -1727,8 +1721,23 @@ export default {
let titleKey;
if (error.name === JitsiTrackErrors.PERMISSION_DENIED) {
descriptionKey = 'dialog.screenSharingPermissionDeniedError';
titleKey = 'dialog.screenSharingFailedToInstallTitle';
// in FF the only option for user is to deny access temporary or
// permanently and we only receive permission_denied
// we always show some info cause in case of permanently, no info
// shown will be bad experience
//
// TODO: detect interval between requesting permissions and received
// error, this way we can detect user interaction which will have
// longer delay
if (JitsiMeetJS.util.browser.isFirefox()) {
descriptionKey
= 'dialog.screenSharingFirefoxPermissionDeniedError';
titleKey = 'dialog.screenSharingFirefoxPermissionDeniedTitle';
} else {
descriptionKey = 'dialog.screenSharingPermissionDeniedError';
titleKey = 'dialog.screenSharingFailedToInstallTitle';
}
} else {
descriptionKey = 'dialog.screenSharingFailedToInstall';
titleKey = 'dialog.screenSharingFailedToInstallTitle';

View File

@@ -150,23 +150,9 @@ var config = {
// Required version of Chrome extension
desktopSharingChromeMinExtVersion: '0.1',
// The ID of the jidesha extension for Firefox. If null, we assume that no
// extension is required.
desktopSharingFirefoxExtId: null,
// Whether desktop sharing should be disabled on Firefox.
desktopSharingFirefoxDisabled: false,
// The maximum version of Firefox which requires a jidesha extension.
// Example: if set to 41, we will require the extension for Firefox versions
// up to and including 41. On Firefox 42 and higher, we will run without the
// extension.
// If set to -1, an extension will be required for all versions of Firefox.
desktopSharingFirefoxMaxVersionExtRequired: 51,
// The URL to the Firefox extension for desktop sharing.
desktopSharingFirefoxExtensionURL: null,
// Optional desktop sharing frame rate options. Default value: min:5, max:5.
// desktopSharingFrameRate: {
// min: 5,
@@ -333,7 +319,6 @@ var config = {
// userRegion: "asia"
}
// List of undocumented settings used in jitsi-meet
/**
alwaysVisibleToolbar
@@ -353,6 +338,7 @@ var config = {
etherpad_base
externalConnectUrl
firefox_fake_device
googleApiApplicationClientID
iAmRecorder
iAmSipGateway
peopleSearchQueryTypes

View File

@@ -135,7 +135,6 @@ h3 {
margin: 30px 0 0 0;
}
h4 {
color: #333;
font-size: 16px;
font-weight: bold;
line-height: 1.25;

View File

@@ -64,7 +64,7 @@
* The local video identifier.
*/
&#filmstripLocalVideo {
bottom: 32px;
align-self: flex-end;
display: block;
/**

View File

@@ -1,3 +1,81 @@
.recordingSpinner {
vertical-align: top;
}
.live-stream-dialog {
/**
* Set font-size to be consistent with Atlaskit FieldText.
*/
font-size: 14px;
.broadcast-dropdown,
.broadcast-dropdown-trigger {
text-align: left;
}
.form-footer {
text-align: right;
}
.live-stream-cta {
a {
cursor: pointer;
}
}
.google-api {
margin-top: 10px;
min-height: 36px;
text-align: center;
width: 100%;
}
/**
* The Google sign in button must follow Google's design guidelines.
* See: https://developers.google.com/identity/branding-guidelines
*/
.google-sign-in {
background-color: #4285f4;
border-radius: 2px;
cursor: pointer;
display: inline-flex;
font-family: Roboto, arial, sans-serif;
font-size: 14px;
padding: 1px;
.google-cta {
color: white;
display: inline-block;
/**
* Hack the line height for vertical centering of text.
*/
line-height: 32px;
margin: 0 15px;
}
.google-logo {
background-color: white;
border-radius: 2px;
display: inline-block;
padding: 8px;
height: 18px;
width: 18px;
}
}
.google-panel {
align-items: center;
border-bottom: 2px solid rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
padding-bottom: 10px;
}
.stream-key-form {
.helper-link {
display: inline-block;
cursor: pointer;
margin-top: 5px;
}
}
}

View File

@@ -72,6 +72,7 @@
* vertical filmstrip layout.
*/
#filmstripLocalVideo {
align-self: initial;
bottom: 5px;
display: flex;
flex-direction: column-reverse;

View File

@@ -101,7 +101,7 @@
}
&-content {
background-color: $auiDialogContentBg;
background-color: $auiDialogBg;
box-sizing: border-box;
color: $auiDialogColor;
font-size: em(14, 12);
@@ -126,6 +126,11 @@
}
}
.input-control {
background-color: $auiDialogContentBg;
color: $auiDialogColor;
}
.form-control:not(:last-child) {
border-bottom: 1px solid $auiBorderColor;
}
@@ -138,7 +143,6 @@
}
.modal-dialog-form {
color: $modalTextColor;
margin-top: 5px !important;
.input-control {
@@ -150,6 +154,14 @@
&-error {
margin-bottom: 8px;
}
/**
* Override Atlaskit dropdown styling when in a modal because the dropdown
* backgrounds clash with the modal backgrounds.
*/
.htclLc[data-role=droplistContent] {
border: 1px solid #455166;
}
}
.modal-dialog-footer {
font-size: $modalButtonFontSize;

View File

@@ -36,13 +36,12 @@
}
.desktop-picker-source {
color: $defaultDarkFontColor;
margin-top: 10px;
text-align: center;
&.is-selected {
.desktop-source-preview-image-container {
background: rgba(0, 0, 0, 0.1);
background: rgba(255,255,255,0.3);
border-radius: $borderRadius;
}
}

View File

@@ -1,6 +1,4 @@
.device-selection {
color: $feedbackInputTextColor;
.device-selectors {
font-size: 14px;
@@ -22,19 +20,15 @@
/* device-selector-trigger stylings attempt to mimic AtlasKit button */
.device-selector-trigger {
background-color: rgba(9, 30, 66, 0.04);
border-radius: 3px;
color: #505f79;
background-color: #0E1624;
border: 1px solid #455166;
border-radius: 5px;
display: flex;
height: 2.3em;
justify-content: space-between;
line-height: 2.3em;
overflow: hidden;
padding: 0 8px;
&:hover {
background-color: rgba(9,30,66,.08);
}
}
.device-selector-trigger-disabled {
.device-selector-trigger {
@@ -108,19 +102,21 @@
.audio-output-preview {
font-size: 14px;
margin-top: 10px;
a {
color: 4C9AFF;
cursor: pointer;
text-decoration: none;
}
}
.audio-input-preview {
background: #f4f5f7;
background: #7b7b7b;
border-radius: 5px;
height: 6px;
.audio-input-preview-level {
background: #0052cc;
background: #4C9AFF;
border-radius: 5px;
height: 100%;
-webkit-transition: width .1s ease-in-out;

View File

@@ -42,10 +42,6 @@
width: 55%;
}
.speaker-stats-item:nth-child(even) {
background: whitesmoke;
}
.speaker-stats-item__name,
.speaker-stats-item__time {
overflow: hidden;

View File

@@ -14,11 +14,11 @@ $sliderThumbBackground: #3572b0;
/**
* Buttons
*/
$buttonBackground: #f5f5f5;
$buttonHoverBackground: #e9e9e9;
$buttonBorder: #ccc;
$buttonHoverBorder: #999;
$buttonColor: #333;
$buttonBackground: #44A5FF;
$buttonHoverBackground: #2c4062;
$buttonBorder: transparent;
$buttonHoverBorder: transparent;
$buttonColor: #eceef1;
$buttonLightBackground: #f5f5f5;
$buttonLightHoverBackground: #e9e9e9;
@@ -47,10 +47,10 @@ $reloadProgressBarBg: #0074E0;
/**
* Dialog colors
**/
$auiDialogColor: #333;
$auiDialogBg: #f5f5f5;
$auiDialogContentBg: $baseLight;
$auiBorderColor: #ccc;
$auiDialogColor: #eceef1;
$auiDialogBg: #253858;
$auiDialogContentBg: #344563;
$auiBorderColor: #253858;
$dialogTitleFontWeight: 400;
$dialogErrorText: #344563;
@@ -58,7 +58,7 @@ $dialogErrorText: #344563;
* Inlay colors
**/
$inlayColorBg: lighten($defaultBackground, 20%);
$inlayBorderColor: lighten($auiDialogContentBg, 10%);
$inlayBorderColor: lighten($baseLight, 10%);
$inlayIconBg: #000;
$inlayIconColor: #fff;
$inlayFilmstripOnlyColor: #474747;

View File

@@ -5,7 +5,7 @@
width: auto;
&__title {
border-bottom: 1px solid $auiBorderColor;
border-bottom: 1px solid $inlayBorderColor;
color: $unsupportedBrowserTitleColor;
font-weight: 400;
letter-spacing: 0.5px;

11
images/googleLogo.svg Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="18px" height="18px" viewBox="0 0 48 48" class="abcRioButtonSvg">
<g>
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"></path>
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"></path>
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"></path>
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"></path>
<path fill="none" d="M0 0h48v48H0z"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 909 B

View File

@@ -55,14 +55,14 @@
</dict>
</dict>
</dict>
<key>NSCalendarsUsageDescription</key>
<string>Displays the user's meetings in the app.</string>
<key>NSCalendarsUsageDescription</key>
<string>See your scheduled conferences in the app.</string>
<key>NSCameraUsageDescription</key>
<string>Participate in conferences with video.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>NSMicrophoneUsageDescription</key>
<string>Participate in conferences with audio.</string>
<string>Participate in conferences with voice.</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>

View File

@@ -0,0 +1,416 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 48;
objects = {
/* Begin PBXBuildFile section */
C6245F57205044120040BE68 /* JitsiMeet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6F99C4F204DE79F0001F710 /* JitsiMeet.framework */; };
C6245F58205044150040BE68 /* WebRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6A34247204DF18000E062DD /* WebRTC.framework */; };
C6A34249204DF18000E062DD /* WebRTC.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C6A34247204DF18000E062DD /* WebRTC.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
C6F99C3B204DE6BE0001F710 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6F99C3A204DE6BE0001F710 /* AppDelegate.swift */; };
C6F99C3D204DE6BE0001F710 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6F99C3C204DE6BE0001F710 /* ViewController.swift */; };
C6F99C40204DE6BE0001F710 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C6F99C3E204DE6BE0001F710 /* Main.storyboard */; };
C6F99C42204DE6BE0001F710 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C6F99C41204DE6BE0001F710 /* Assets.xcassets */; };
C6F99C45204DE6BE0001F710 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C6F99C43204DE6BE0001F710 /* LaunchScreen.storyboard */; };
C6F99C60204DEDC10001F710 /* JitsiMeet.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C6F99C4F204DE79F0001F710 /* JitsiMeet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
C6F99C61204DEDC20001F710 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
C6F99C60204DEDC10001F710 /* JitsiMeet.framework in Embed Frameworks */,
C6A34249204DF18000E062DD /* WebRTC.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
C6A34247204DF18000E062DD /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = "../../node_modules/react-native-webrtc/ios/WebRTC.framework"; sourceTree = "<group>"; };
C6F99C37204DE6BE0001F710 /* example-pip-app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example-pip-app.app; sourceTree = BUILT_PRODUCTS_DIR; };
C6F99C3A204DE6BE0001F710 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
C6F99C3C204DE6BE0001F710 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
C6F99C3F204DE6BE0001F710 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
C6F99C41204DE6BE0001F710 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
C6F99C44204DE6BE0001F710 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
C6F99C46204DE6BE0001F710 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C6F99C4F204DE79F0001F710 /* JitsiMeet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = JitsiMeet.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
C6F99C34204DE6BE0001F710 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
C6245F57205044120040BE68 /* JitsiMeet.framework in Frameworks */,
C6245F58205044150040BE68 /* WebRTC.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
C6F99C2E204DE6BE0001F710 = {
isa = PBXGroup;
children = (
C6F99C4D204DE79F0001F710 /* Frameworks */,
C6F99C38204DE6BE0001F710 /* Products */,
C6F99C4C204DE7230001F710 /* src */,
);
sourceTree = "<group>";
};
C6F99C38204DE6BE0001F710 /* Products */ = {
isa = PBXGroup;
children = (
C6F99C37204DE6BE0001F710 /* example-pip-app.app */,
);
name = Products;
sourceTree = "<group>";
};
C6F99C4C204DE7230001F710 /* src */ = {
isa = PBXGroup;
children = (
C6F99C3A204DE6BE0001F710 /* AppDelegate.swift */,
C6F99C3C204DE6BE0001F710 /* ViewController.swift */,
C6F99C3E204DE6BE0001F710 /* Main.storyboard */,
C6F99C41204DE6BE0001F710 /* Assets.xcassets */,
C6F99C43204DE6BE0001F710 /* LaunchScreen.storyboard */,
C6F99C46204DE6BE0001F710 /* Info.plist */,
);
path = src;
sourceTree = "<group>";
};
C6F99C4D204DE79F0001F710 /* Frameworks */ = {
isa = PBXGroup;
children = (
C6F99C4F204DE79F0001F710 /* JitsiMeet.framework */,
C6A34247204DF18000E062DD /* WebRTC.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
C6F99C36204DE6BE0001F710 /* example-pip-app */ = {
isa = PBXNativeTarget;
buildConfigurationList = C6F99C49204DE6BE0001F710 /* Build configuration list for PBXNativeTarget "example-pip-app" */;
buildPhases = (
C6A3424A204DF91D00E062DD /* Run Adjust ATS for loading JS bundle */,
C6F99C62204DEFFE0001F710 /* Run React Packager */,
C6F99C33204DE6BE0001F710 /* Sources */,
C6F99C34204DE6BE0001F710 /* Frameworks */,
C6F99C35204DE6BE0001F710 /* Resources */,
C6F99C61204DEDC20001F710 /* Embed Frameworks */,
C6A3426E20503ECC00E062DD /* Adjust embedded framework architectures */,
);
buildRules = (
);
dependencies = (
);
name = example-pip-app;
productName = example-pip-app;
productReference = C6F99C37204DE6BE0001F710 /* example-pip-app.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
C6F99C2F204DE6BE0001F710 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 0920;
ORGANIZATIONNAME = "Atlassian Inc";
TargetAttributes = {
C6F99C36204DE6BE0001F710 = {
CreatedOnToolsVersion = 9.2;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.BackgroundModes = {
enabled = 1;
};
};
};
};
};
buildConfigurationList = C6F99C32204DE6BE0001F710 /* Build configuration list for PBXProject "example-pip-app" */;
compatibilityVersion = "Xcode 8.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = C6F99C2E204DE6BE0001F710;
productRefGroup = C6F99C38204DE6BE0001F710 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
C6F99C36204DE6BE0001F710 /* example-pip-app */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
C6F99C35204DE6BE0001F710 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C6F99C45204DE6BE0001F710 /* LaunchScreen.storyboard in Resources */,
C6F99C42204DE6BE0001F710 /* Assets.xcassets in Resources */,
C6F99C40204DE6BE0001F710 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
C6A3424A204DF91D00E062DD /* Run Adjust ATS for loading JS bundle */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Adjust ATS for loading JS bundle";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "../scripts/fixup-ats.sh";
};
C6A3426E20503ECC00E062DD /* Adjust embedded framework architectures */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Adjust embedded framework architectures";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "../scripts/fixup-frameworks.sh";
};
C6F99C62204DEFFE0001F710 /* Run React Packager */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run React Packager";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "../scripts/run-packager.sh";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
C6F99C33204DE6BE0001F710 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C6F99C3D204DE6BE0001F710 /* ViewController.swift in Sources */,
C6F99C3B204DE6BE0001F710 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
C6F99C3E204DE6BE0001F710 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
C6F99C3F204DE6BE0001F710 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
C6F99C43204DE6BE0001F710 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
C6F99C44204DE6BE0001F710 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
C6F99C47204DE6BE0001F710 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.2;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
C6F99C48204DE6BE0001F710 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 11.2;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
C6F99C4A204DE6BE0001F710 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
FRAMEWORK_SEARCH_PATHS = "../../node_modules/react-native-webrtc/ios";
INFOPLIST_FILE = src/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.jitsi.example-pip-app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
C6F99C4B204DE6BE0001F710 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
FRAMEWORK_SEARCH_PATHS = "../../node_modules/react-native-webrtc/ios";
INFOPLIST_FILE = src/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.jitsi.example-pip-app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
C6F99C32204DE6BE0001F710 /* Build configuration list for PBXProject "example-pip-app" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C6F99C47204DE6BE0001F710 /* Debug */,
C6F99C48204DE6BE0001F710 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
C6F99C49204DE6BE0001F710 /* Build configuration list for PBXNativeTarget "example-pip-app" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C6F99C4A204DE6BE0001F710 /* Debug */,
C6F99C4B204DE6BE0001F710 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = C6F99C2F204DE6BE0001F710 /* Project object */;
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import JitsiMeet
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
guard let launchOptions = launchOptions else { return false }
return JitsiMeetView.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// MARK: - Linking delegate methods
func application(_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
return JitsiMeetView.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool {
return JitsiMeetView.application(application, open: url, sourceApplication: sourceApplication, annotation: annotation)
}
}

View File

@@ -0,0 +1,98 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" systemVersion="17A277" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="example-pip-app" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="QxY-C8-fwD">
<rect key="frame" x="116" y="324.5" width="143" height="38"/>
<fontDescription key="fontDescription" type="system" pointSize="21"/>
<state key="normal" title="Open Jitsi Meet"/>
<connections>
<action selector="openJitsiMeetWithSender:" destination="BYZ-38-t0r" eventType="touchUpInside" id="79C-XR-05w"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="QxY-C8-fwD" firstAttribute="centerX" secondItem="6Tk-OE-BBY" secondAttribute="centerX" id="6a6-l1-7Ct"/>
<constraint firstItem="QxY-C8-fwD" firstAttribute="centerY" secondItem="6Tk-OE-BBY" secondAttribute="centerY" id="Hfg-TH-0g2"/>
</constraints>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
<connections>
<outlet property="videoButton" destination="QxY-C8-fwD" id="Zwa-Hx-Zub"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="32.799999999999997" y="658.92053973013503"/>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,39 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import UIKit
import JitsiMeet
class ViewController: UIViewController {
@IBOutlet weak var videoButton: UIButton?
private var jitsiMeetCoordinator: JitsiMeetPresentationCoordinator?
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Actions
@IBAction func openJitsiMeet(sender: Any?) {
let jitsiMeetCoordinator = JitsiMeetPresentationCoordinator()
self.jitsiMeetCoordinator = jitsiMeetCoordinator
jitsiMeetCoordinator.jitsiMeetView().welcomePageEnabled = true
jitsiMeetCoordinator.jitsiMeetView().load(nil)
jitsiMeetCoordinator.show()
}
}

View File

@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:example-pip-app/example-pip-app.xcodeproj">
</FileRef>
<FileRef
location = "group:app/app.xcodeproj">
</FileRef>

View File

@@ -27,6 +27,13 @@
0BCA496C1EC4BBF900B793EE /* jitsi.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0BCA496B1EC4BBF900B793EE /* jitsi.ttf */; };
0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */ = {isa = PBXBuildFile; fileRef = 0BD906E81EC0C00300C8C18E /* JitsiMeet.h */; settings = {ATTRIBUTES = (Public, ); }; };
0F65EECE1D95DA94561BB47E /* libPods-JitsiMeet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 03F2ADC957FF109849B7FCA1 /* libPods-JitsiMeet.a */; };
C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5B2053091D0040BE68 /* image-resize@2x.png */; };
C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5C2053091D0040BE68 /* image-resize@3x.png */; };
C6A3425F204EF76800E062DD /* PiPWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425C204EF76800E062DD /* PiPWindow.swift */; };
C6A34260204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */; };
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; };
C6A3426D204F1C3300E062DD /* JitsiMeetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */; };
C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -55,6 +62,13 @@
0BD906E91EC0C00300C8C18E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
98E09B5C73D9036B4ED252FC /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = "<group>"; };
9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.release.xcconfig"; sourceTree = "<group>"; };
C6245F5B2053091D0040BE68 /* image-resize@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@2x.png"; path = "src/picture-in-picture/image-resize@2x.png"; sourceTree = "<group>"; };
C6245F5C2053091D0040BE68 /* image-resize@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@3x.png"; path = "src/picture-in-picture/image-resize@3x.png"; sourceTree = "<group>"; };
C6A3425C204EF76800E062DD /* PiPWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PiPWindow.swift; sourceTree = "<group>"; };
C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JitsiMeetPresentationCoordinator.swift; sourceTree = "<group>"; };
C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = "<group>"; };
C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JitsiMeetViewController.swift; sourceTree = "<group>"; };
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -76,6 +90,8 @@
isa = PBXGroup;
children = (
0BC4B8681F8C01E100CE8B21 /* CallKitIcon.png */,
C6245F5B2053091D0040BE68 /* image-resize@2x.png */,
C6245F5C2053091D0040BE68 /* image-resize@3x.png */,
0BCA496B1EC4BBF900B793EE /* jitsi.ttf */,
);
name = Resources;
@@ -103,6 +119,7 @@
0BD906E71EC0C00300C8C18E /* src */ = {
isa = PBXGroup;
children = (
C6A3426B204F127900E062DD /* picture-in-picture */,
0BCA495C1EC4B6C600B793EE /* AudioMode.m */,
0BB9AD7C1F60356D001C08DB /* AppInfo.m */,
0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */,
@@ -112,6 +129,7 @@
0BD906E81EC0C00300C8C18E /* JitsiMeet.h */,
0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */,
0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */,
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */,
0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */,
0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */,
0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */,
@@ -143,6 +161,17 @@
name = Pods;
sourceTree = "<group>";
};
C6A3426B204F127900E062DD /* picture-in-picture */ = {
isa = PBXGroup;
children = (
C6A3425E204EF76800E062DD /* DragGestureController.swift */,
C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */,
C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */,
C6A3425C204EF76800E062DD /* PiPWindow.swift */,
);
path = "picture-in-picture";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -150,6 +179,7 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */,
0B412F181EDEC65D00B1A0A6 /* JitsiMeetView.h in Headers */,
0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */,
0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */,
@@ -192,6 +222,7 @@
TargetAttributes = {
0BD906E41EC0C00300C8C18E = {
CreatedOnToolsVersion = 8.3.2;
LastSwiftMigration = 0920;
ProvisioningStyle = Automatic;
};
};
@@ -219,7 +250,9 @@
buildActionMask = 2147483647;
files = (
0BCA496C1EC4BBF900B793EE /* jitsi.ttf in Resources */,
C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */,
0BC4B8691F8C03A700CE8B21 /* CallKitIcon.png in Resources */,
C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -304,14 +337,18 @@
buildActionMask = 2147483647;
files = (
0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */,
C6A34260204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift in Sources */,
0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */,
0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */,
0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */,
0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */,
0B7C2CFD200F51D60060D076 /* LaunchOptions.m in Sources */,
C6A3426D204F1C3300E062DD /* JitsiMeetViewController.swift in Sources */,
C6A3425F204EF76800E062DD /* PiPWindow.swift in Sources */,
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */,
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */,
0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -435,6 +472,8 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 98E09B5C73D9036B4ED252FC /* Pods-JitsiMeet.debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
DEFINES_MODULE = YES;
@@ -450,6 +489,9 @@
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 3.0;
};
name = Debug;
};
@@ -457,6 +499,8 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
DEFINES_MODULE = YES;
@@ -472,6 +516,8 @@
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_VERSION = 3.0;
};
name = Release;
};

View File

@@ -0,0 +1,125 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class DragGestureController {
var insets: UIEdgeInsets = UIEdgeInsets.zero
private var frameBeforeDragging: CGRect = CGRect.zero
private weak var view: UIView?
private lazy var panGesture: UIPanGestureRecognizer = {
return UIPanGestureRecognizer(target: self,
action: #selector(handlePan(gesture:)))
}()
func startDragListener(inView view: UIView) {
self.view = view
view.addGestureRecognizer(panGesture)
panGesture.isEnabled = true
}
func stopDragListener() {
panGesture.isEnabled = false
view?.removeGestureRecognizer(panGesture)
view = nil
}
@objc private func handlePan(gesture: UIPanGestureRecognizer) {
guard let view = self.view else { return }
let translation = gesture.translation(in: view.superview)
let velocity = gesture.velocity(in: view.superview)
var frame = frameBeforeDragging
switch gesture.state {
case .began:
frameBeforeDragging = view.frame
case .changed:
frame.origin.x = floor(frame.origin.x + translation.x)
frame.origin.y = floor(frame.origin.y + translation.y)
view.frame = frame
case .ended:
let currentPos = view.frame.origin
let finalPos = calculateFinalPosition()
let distance = CGPoint(x: currentPos.x - finalPos.x,
y: currentPos.y - finalPos.y)
let distanceMagnitude = magnitude(vector: distance)
let velocityMagnitude = magnitude(vector: velocity)
let animationDuration = 0.5
let initialSpringVelocity =
velocityMagnitude / distanceMagnitude / CGFloat(animationDuration)
frame.origin = CGPoint(x: finalPos.x, y: finalPos.y)
UIView.animate(withDuration: animationDuration,
delay: 0,
usingSpringWithDamping: 0.9,
initialSpringVelocity: initialSpringVelocity,
options: .curveLinear,
animations: {
view.frame = frame
}, completion: nil)
default:
break
}
}
private func calculateFinalPosition() -> CGPoint {
guard
let view = self.view,
let bounds = view.superview?.frame
else { return CGPoint.zero }
let currentSize = view.frame.size
let adjustedBounds = UIEdgeInsetsInsetRect(bounds, insets)
let threshold: CGFloat = 20.0
let velocity = panGesture.velocity(in: view.superview)
let location = panGesture.location(in: view.superview)
let goLeft: Bool
if fabs(velocity.x) > threshold {
goLeft = velocity.x < -threshold
} else {
goLeft = location.x < bounds.midX
}
let goUp: Bool
if fabs(velocity.y) > threshold {
goUp = velocity.y < -threshold
} else {
goUp = location.y < bounds.midY
}
let finalPosX: CGFloat =
goLeft
? adjustedBounds.origin.x
: bounds.size.width - insets.right - currentSize.width
let finalPosY: CGFloat =
goUp
? adjustedBounds.origin.y
: bounds.size.height - insets.bottom - currentSize.height
return CGPoint(x: finalPosX, y: finalPosY)
}
private func magnitude(vector: CGPoint) -> CGFloat {
return sqrt(pow(vector.x, 2) + pow(vector.y, 2))
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
/// Coordinates the presentation of JitsiMeetViewController inside of
/// an external window that can be resized and dragged with custom PiP mode
open class JitsiMeetPresentationCoordinator: NSObject {
fileprivate let meetViewController: JitsiMeetViewController
fileprivate let meetWindow: PiPWindow
public init(meetViewController: JitsiMeetViewController? = nil,
meetWindow: PiPWindow? = nil) {
self.meetViewController = meetViewController ?? JitsiMeetViewController()
self.meetWindow = meetWindow ?? PiPWindow(frame: UIScreen.main.bounds)
super.init()
configureMeetWindow()
configureMeetViewController()
}
public func jitsiMeetView() -> JitsiMeetView {
return meetViewController.jitsiMeetView
}
open func show(completion: CompletionAction? = nil) {
meetWindow.show(completion: completion)
}
open func hide(completion: CompletionAction? = nil) {
meetWindow.hide(completion: completion)
}
open func cleanUp() {
// TODO: more clean up work on this
meetWindow.isHidden = true
meetWindow.stopDragGesture()
}
deinit {
cleanUp()
}
// MARK: - helpers
private func configureMeetViewController() {
meetViewController.jitsiMeetView.pictureInPictureEnabled = true
meetViewController.delegate = self
}
private func configureMeetWindow() {
meetWindow.backgroundColor = .clear
meetWindow.windowLevel = UIWindowLevelStatusBar + 100
meetWindow.rootViewController = self.meetViewController
}
}
extension JitsiMeetPresentationCoordinator: JitsiMeetViewControllerDelegate {
open func performPresentationUpdate(to: JitsiMeetPresentationUpdate) {
switch to {
case .enterPictureInPicture:
meetWindow.enterPictureInPicture()
case .traitChange:
// resize to full screen if rotation happens
if meetWindow.isInPiP {
meetWindow.exitPictureInPicture()
}
}
}
open func conferenceStarted() {
if meetWindow.isHidden {
meetWindow.show()
}
}
open func conferenceEnded(didFail: Bool) {
cleanUp()
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
public enum JitsiMeetPresentationUpdate {
/// The conference wants to enter Picture-in-Picture
case enterPictureInPicture
/// A system traitCollectionChange (usually screen rotation)
case traitChange
}
public protocol JitsiMeetViewControllerDelegate: class {
/// Notifies a change of the conference presentation style.
///
/// - Parameter to: The presentation state that will be changed to
func performPresentationUpdate(to: JitsiMeetPresentationUpdate)
/// The conference started
func conferenceStarted()
/// The conference ended
///
/// - Parameter didFail: The reason of ending the conference
func conferenceEnded(didFail: Bool)
}
/// Wrapper ViewController of a JitsiMeetView
///
/// Is suggested to override this class and implement some customization
/// on how to handle the JitsiMeetView delegate events
open class JitsiMeetViewController: UIViewController {
open weak var delegate: JitsiMeetViewControllerDelegate?
private(set) var jitsiMeetView: JitsiMeetView = JitsiMeetView()
override open func loadView() {
super.loadView()
self.view = jitsiMeetView
}
open override func viewDidLoad() {
super.viewDidLoad()
jitsiMeetView.delegate = self
}
open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
delegate?.performPresentationUpdate(to: .traitChange)
}
}
extension JitsiMeetViewController: JitsiMeetViewDelegate {
open func conferenceWillJoin(_ data: [AnyHashable : Any]!) {
// do something
}
open func conferenceJoined(_ data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.delegate?.conferenceStarted()
}
}
open func conferenceWillLeave(_ data: [AnyHashable : Any]!) {
// do something
}
open func conferenceLeft(_ data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.delegate?.conferenceEnded(didFail: false)
}
}
open func conferenceFailed(_ data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.delegate?.conferenceEnded(didFail: true)
}
}
open func loadConfigError(_ data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.delegate?.conferenceEnded(didFail: true)
}
}
open func enterPicture(inPicture data: [AnyHashable : Any]!) {
DispatchQueue.main.async {
self.delegate?.performPresentationUpdate(to: .enterPictureInPicture)
}
}
}

View File

@@ -0,0 +1,189 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// Alias defining a completion closure that returns a Bool
public typealias CompletionAction = (Bool) -> Void
/// A window that allows its root view controller to be presented
/// in full screen or in a custom Picture in Picture mode
open class PiPWindow: UIWindow {
/// Limits the boundries of root view position on screen when minimized
public var dragBoundInsets: UIEdgeInsets = UIEdgeInsets(top: 25,
left: 5,
bottom: 5,
right: 5) {
didSet {
dragController.insets = dragBoundInsets
}
}
/// The size ratio for root view controller view when in PiP mode
public var pipSizeRatio: CGFloat = 0.333
/// The PiP state of this contents of the window
private(set) var isInPiP: Bool = false
private let dragController: DragGestureController = DragGestureController()
/// Used when in PiP mode to enable/disable exit PiP UI
private var tapGestureRecognizer: UITapGestureRecognizer?
private var exitPiPButton: UIButton?
/// Help out to bubble up the gesture detection outside of the rootVC frame
open override func point(inside point: CGPoint,
with event: UIEvent?) -> Bool {
guard let vc = rootViewController else {
return super.point(inside: point, with: event)
}
return vc.view.frame.contains(point)
}
/// animate in the window
open func show(completion: CompletionAction? = nil) {
if self.isHidden || self.alpha < 1 {
self.isHidden = false
self.alpha = 0
animateTransition(animations: {
self.alpha = 1
}, completion: completion)
}
}
/// animate out the window
open func hide(completion: CompletionAction? = nil) {
if !self.isHidden || self.alpha > 0 {
animateTransition(animations: {
self.alpha = 1
}, completion: completion)
}
}
/// Resize the root view to PiP mode
open func enterPictureInPicture() {
guard let view = rootViewController?.view else { return }
isInPiP = true
animateRootViewChange()
dragController.startDragListener(inView: view)
dragController.insets = dragBoundInsets
// add single tap gesture recognition for displaying exit PiP UI
let exitSelector = #selector(toggleExitPiP)
let tapGestureRecognizer = UITapGestureRecognizer(target: self,
action: exitSelector)
self.tapGestureRecognizer = tapGestureRecognizer
view.addGestureRecognizer(tapGestureRecognizer)
}
/// Resize the root view to full screen
open func exitPictureInPicture() {
isInPiP = false
animateRootViewChange()
dragController.stopDragListener()
// hide PiP UI
exitPiPButton?.removeFromSuperview()
exitPiPButton = nil
// remove gesture
let exitSelector = #selector(toggleExitPiP)
tapGestureRecognizer?.removeTarget(self, action: exitSelector)
tapGestureRecognizer = nil
}
/// Stop the dragging gesture of the root view
public func stopDragGesture() {
dragController.stopDragListener()
}
/// Customize the presentation of exit pip button
open func configureExitPiPButton(target: Any,
action: Selector) -> UIButton {
let buttonImage = UIImage.init(named: "image-resize",
in: Bundle(for: type(of: self)),
compatibleWith: nil)
let button = UIButton(type: .custom)
let size: CGSize = CGSize(width: 44, height: 44)
button.setImage(buttonImage, for: .normal)
button.backgroundColor = .gray
button.layer.cornerRadius = size.width / 2
button.frame = CGRect(origin: CGPoint.zero, size: size)
if let view = rootViewController?.view {
button.center = view.convert(view.center, from:view.superview)
}
button.addTarget(target, action: action, for: .touchUpInside)
return button
}
// MARK: - Manage presentation switching
private func animateRootViewChange() {
UIView.animate(withDuration: 0.25) {
self.rootViewController?.view.frame = self.changeRootViewRect()
self.rootViewController?.view.setNeedsLayout()
}
}
private func changeRootViewRect() -> CGRect {
guard isInPiP else {
return self.bounds
}
// resize to suggested ratio and position to the bottom right
let adjustedBounds = UIEdgeInsetsInsetRect(self.bounds, dragBoundInsets)
let size = CGSize(width: bounds.size.width * pipSizeRatio,
height: bounds.size.height * pipSizeRatio)
let x: CGFloat = adjustedBounds.maxX - size.width
let y: CGFloat = adjustedBounds.maxY - size.height
return CGRect(x: x, y: y, width: size.width, height: size.height)
}
// MARK: - Exit PiP
@objc private func toggleExitPiP() {
guard let view = rootViewController?.view else { return }
if exitPiPButton == nil {
// show button
let exitSelector = #selector(exitPictureInPicture)
let button = configureExitPiPButton(target: self,
action: exitSelector)
view.addSubview(button)
exitPiPButton = button
} else {
// hide button
exitPiPButton?.removeFromSuperview()
exitPiPButton = nil
}
}
@objc private func exitPiP() {
exitPictureInPicture()
}
// MARK: - Animation transition
private func animateTransition(animations: @escaping () -> Void,
completion: CompletionAction?) {
UIView.animate(withDuration: 0.1,
delay: 0,
options: .beginFromCurrentState,
animations: animations,
completion: completion)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

20
lang/languages-hu.json Normal file
View File

@@ -0,0 +1,20 @@
{
"en": "",
"bg": "",
"de": "",
"es": "",
"fr": "",
"hy": "",
"it": "",
"oc": "",
"pl": "",
"ptBR": "",
"ru": "",
"sk": "",
"sl": "",
"sv": "",
"tr": "",
"zhCN": "",
"nb": "",
"eo": ""
}

502
lang/main-hu.json Normal file
View File

@@ -0,0 +1,502 @@
{
"contactlist": "",
"contactlist_plural": "",
"passwordSetRemotely": "",
"poweredby": "",
"inviteUrlDefaultMsg": "",
"me": "",
"speaker": "",
"raisedHand": "",
"defaultNickname": "",
"defaultLink": "",
"audioDevices": {
"bluetooth": "",
"headphones": "",
"phone": "",
"speaker": ""
},
"audioOnly": {
"audioOnly": "",
"featureToggleDisabled": ""
},
"userMedia": {
"react-nativeGrantPermissions": "",
"chromeGrantPermissions": "",
"androidGrantPermissions": "",
"firefoxGrantPermissions": "",
"operaGrantPermissions": "",
"iexplorerGrantPermissions": "",
"safariGrantPermissions": "",
"nwjsGrantPermissions": "",
"edgeGrantPermissions": ""
},
"keyboardShortcuts": {
"keyboardShortcuts": "",
"raiseHand": "",
"pushToTalk": "",
"toggleScreensharing": "",
"toggleFilmstrip": "",
"toggleShortcuts": "",
"focusLocal": "",
"focusRemote": "",
"toggleChat": "",
"mute": "",
"fullScreen": "",
"videoMute": "",
"showSpeakerStats": ""
},
"welcomepage": {
"disable": "",
"feature1": {
"content": "",
"title": ""
},
"feature2": {
"content": "",
"title": ""
},
"feature3": {
"content": "",
"title": ""
},
"feature4": {
"content": "",
"title": ""
},
"feature5": {
"content": "",
"title": ""
},
"feature6": {
"content": "",
"title": ""
},
"feature7": {
"content": "",
"title": ""
},
"feature8": {
"content": "",
"title": ""
},
"go": "",
"join": "",
"privacy": "",
"roomname": "",
"roomnamePlaceHolder": "",
"sendFeedback": "",
"terms": ""
},
"startupoverlay": {
"policyText": "",
"title": ""
},
"suspendedoverlay": {
"title": "",
"text": "",
"rejoinKeyTitle": ""
},
"toolbar": {
"addPeople": "",
"audioonly": "",
"mute": "",
"videomute": "",
"authenticate": "",
"lock": "",
"invite": "",
"chat": "",
"etherpad": "",
"sharedvideo": "",
"sharescreen": "",
"fullscreen": "",
"sip": "",
"Settings": "",
"hangup": "",
"login": "",
"logout": "",
"dialpad": "",
"sharedVideoMutedPopup": "",
"micMutedPopup": "",
"talkWhileMutedPopup": "",
"unableToUnmutePopup": "",
"cameraDisabled": "",
"micDisabled": "",
"filmstrip": "",
"profile": "",
"raiseHand": ""
},
"unsupportedBrowser": {
"appNotInstalled": "",
"downloadApp": "",
"openApp": ""
},
"bottomtoolbar": {
"chat": "",
"filmstrip": "",
"contactlist": ""
},
"chat": {
"nickname": {
"title": "",
"popover": ""
},
"messagebox": ""
},
"settings": {
"title": "",
"update": "",
"name": "",
"startAudioMuted": "",
"startVideoMuted": "",
"selectCamera": "",
"selectMic": "",
"selectAudioOutput": "",
"followMe": "",
"noDevice": "",
"cameraAndMic": "",
"moderator": "",
"password": "",
"audioVideo": ""
},
"profile": {
"title": "",
"setDisplayNameLabel": "",
"setEmailLabel": "",
"setEmailInput": ""
},
"videothumbnail": {
"moderator": "",
"videomute": "",
"mute": "",
"kick": "",
"muted": "",
"domute": "",
"flip": "",
"remoteControl": ""
},
"connectionindicator": {
"header": "",
"bitrate": "",
"packetloss": "",
"resolution": "",
"framerate": "",
"less": "",
"more": "",
"address": "",
"remoteport": "",
"remoteport_plural": "",
"localport": "",
"localport_plural": "",
"localaddress": "",
"localaddress_plural": "",
"remoteaddress": "",
"remoteaddress_plural": "",
"transport": "",
"transport_plural": "",
"bandwidth": "",
"na": "",
"turn": "",
"quality": {
"good": "",
"inactive": "",
"lost": "",
"nonoptimal": "",
"poor": ""
},
"status": ""
},
"notify": {
"disconnected": "",
"moderator": "",
"connectedOneMember": "",
"connectedTwoMembers": "",
"connectedThreePlusMembers": "",
"somebody": "",
"me": "",
"focus": "",
"focusFail": "",
"grantedTo": "",
"grantedToUnknown": "",
"muted": "",
"mutedTitle": "",
"raisedHand": ""
},
"dialog": {
"add": "",
"allow": "",
"kickMessage": "",
"popupErrorTitle": "",
"popupError": "",
"passwordErrorTitle": "",
"passwordError": "",
"passwordError2": "",
"connectError": "",
"connectErrorWithMsg": "",
"incorrectPassword": "",
"connecting": "",
"copy": "",
"contactSupport": "",
"error": "",
"createPassword": "",
"detectext": "",
"failedpermissions": "",
"conferenceReloadTitle": "",
"conferenceReloadMsg": "",
"conferenceDisconnectTitle": "",
"conferenceDisconnectMsg": "",
"dismiss": "",
"rejoinNow": "",
"maxUsersLimitReachedTitle": "",
"maxUsersLimitReached": "",
"lockTitle": "",
"lockMessage": "",
"warning": "",
"passwordNotSupportedTitle": "",
"passwordNotSupported": "",
"internalErrorTitle": "",
"internalError": "",
"unableToSwitch": "",
"SLDFailure": "",
"SRDFailure": "",
"oops": "",
"currentPassword": "",
"passwordLabel": "",
"defaultError": "",
"passwordRequired": "",
"Ok": "",
"done": "",
"Remove": "",
"removePassword": "",
"shareVideoTitle": "",
"shareVideoLinkError": "",
"removeSharedVideoTitle": "",
"removeSharedVideoMsg": "",
"alreadySharedVideoMsg": "",
"alreadySharedVideoTitle": "",
"WaitingForHost": "",
"WaitForHostMsg": "",
"IamHost": "",
"Cancel": "",
"Submit": "",
"retry": "",
"logoutTitle": "",
"logoutQuestion": "",
"sessTerminated": "",
"hungUp": "",
"joinAgain": "",
"Share": "",
"Save": "",
"recording": "",
"recordingToken": "",
"passwordCheck": "",
"passwordMsg": "",
"shareLink": "",
"yourPassword": "",
"Back": "",
"serviceUnavailable": "",
"gracefulShutdown": "",
"Yes": "",
"reservationError": "",
"reservationErrorMsg": "",
"password": "",
"userPassword": "",
"token": "",
"tokenAuthFailedTitle": "",
"tokenAuthFailed": "",
"displayNameRequired": "",
"enterDisplayName": "",
"extensionRequired": "",
"firefoxExtensionPrompt": "",
"feedbackHelp": "",
"feedbackQuestion": "",
"thankYou": "",
"sorryFeedback": "",
"liveStreaming": "",
"streamKey": "",
"startLiveStreaming": "",
"stopStreamingWarning": "",
"stopRecordingWarning": "",
"stopLiveStreaming": "",
"stopRecording": "",
"doNotShowMessageAgain": "",
"permissionDenied": "",
"screenSharingFailedToInstall": "",
"screenSharingFailedToInstallTitle": "",
"screenSharingPermissionDeniedError": "",
"cameraUnsupportedResolutionError": "",
"cameraUnknownError": "",
"cameraPermissionDeniedError": "",
"cameraNotFoundError": "",
"cameraConstraintFailedError": "",
"micUnknownError": "",
"micPermissionDeniedError": "",
"micNotFoundError": "",
"micConstraintFailedError": "",
"micNotSendingDataTitle": "",
"micNotSendingData": "",
"cameraNotSendingDataTitle": "",
"cameraNotSendingData": "",
"goToStore": "",
"externalInstallationTitle": "",
"externalInstallationMsg": "",
"inlineInstallationMsg": "",
"inlineInstallExtension": "",
"muteParticipantTitle": "",
"muteParticipantBody": "",
"muteParticipantButton": "",
"remoteControlTitle": "",
"remoteControlRequestMessage": "",
"remoteControlShareScreenWarning": "",
"remoteControlDeniedMessage": "",
"remoteControlAllowedMessage": "",
"remoteControlErrorMessage": "",
"startRemoteControlErrorMessage": "",
"remoteControlStopMessage": "",
"close": "",
"shareYourScreen": "",
"yourEntireScreen": "",
"applicationWindow": ""
},
"email": {
"sharedKey": "",
"subject": "",
"body": "",
"and": ""
},
"connection": {
"ERROR": "",
"CONNECTING": "",
"RECONNECTING": "",
"CONNFAIL": "",
"AUTHENTICATING": "",
"AUTHFAIL": "",
"CONNECTED": "",
"DISCONNECTED": "",
"DISCONNECTING": "",
"ATTACHED": ""
},
"recording": {
"busy": "",
"busyTitle": "",
"buttonTooltip": "",
"error": "",
"failedToStart": "",
"off": "",
"on": "",
"pending": "",
"serviceName": "",
"unavailable": "",
"unavailableTitle": ""
},
"liveStreaming": {
"busy": "",
"busyTitle": "",
"buttonTooltip": "",
"error": "",
"failedToStart": "",
"off": "",
"on": "",
"pending": "",
"serviceName": "",
"streamIdRequired": "",
"streamIdHelp": "",
"unavailableTitle": ""
},
"videoSIPGW": {
"busy": "",
"busyTitle": "",
"errorInvite": "",
"errorInviteTitle": "",
"errorAlreadyInvited": "",
"errorInviteFailedTitle": "",
"errorInviteFailed": "",
"pending": "",
"serviceName": "",
"unavailableTitle": ""
},
"speakerStats": {
"hours": "",
"minutes": "",
"name": "",
"seconds": "",
"speakerStats": "",
"speakerTime": ""
},
"deviceSelection": {
"deviceSettings": "",
"noPermission": "",
"previewUnavailable": "",
"selectADevice": "",
"testAudio": ""
},
"invite": {
"addPassword": "",
"callNumber": "",
"enterID": "",
"howToDialIn": "",
"hidePassword": "",
"inviteTo": "",
"invitedYouTo": "",
"invitePeople": "",
"locked": "",
"showPassword": "",
"unlocked": ""
},
"videoStatus": {
"callQuality": "",
"hd": "",
"highDefinition": "",
"labelTooltipVideo": "",
"labelTooltipAudioOnly": "",
"ld": "",
"lowDefinition": "",
"onlyAudioAvailable": "",
"onlyAudioSupported": "",
"p2pEnabled": "",
"p2pVideoQualityDescription": "",
"recHighDefinitionOnly": "",
"sd": "",
"standardDefinition": "",
"qualityButtonTip": ""
},
"dialOut": {
"dial": "",
"dialOut": "",
"statusMessage": "",
"enterPhone": "",
"phoneNotAllowed": ""
},
"addPeople": {
"add": "",
"noResults": "",
"searchPlaceholder": "",
"title": "",
"failedToAdd": ""
},
"inlineDialogFailure": {
"msg": "",
"retry": "",
"support": "",
"supportMsg": ""
},
"deviceError": {
"cameraError": "",
"microphoneError": "",
"cameraPermission": "",
"microphonePermission": ""
},
"feedback": {
"average": "",
"bad": "",
"good": "",
"rateExperience": "",
"veryBad": "",
"veryGood": ""
},
"info": {
"copy": "",
"invite": "",
"title": "",
"tooltip": ""
}
}

View File

@@ -278,15 +278,13 @@
"tokenAuthFailed": "Sorry, you're not allowed to join this call.",
"displayNameRequired": "Display name is required",
"enterDisplayName": "Please enter your display name",
"extensionRequired": "Extension required:",
"firefoxExtensionPrompt": "You need to install a Firefox extension in order to use screen sharing. Please try again after you <a href='__url__'>get it from here</a>!",
"feedbackHelp": "Your feedback will help us to improve our video experience.",
"feedbackQuestion": "Tell us about your call!",
"thankYou": "Thank you for using __appName__!",
"sorryFeedback": "We're sorry to hear that. Would you like to tell us more?",
"liveStreaming": "Live Streaming",
"streamKey": "Stream name/key",
"startLiveStreaming": "Start live streaming",
"streamKey": "Live stream key",
"startLiveStreaming": "Go live now",
"stopStreamingWarning": "Are you sure you would like to stop the live streaming?",
"stopRecordingWarning": "Are you sure you would like to stop the recording?",
"stopLiveStreaming": "Stop live streaming",
@@ -295,6 +293,8 @@
"permissionDenied": "Permission Denied",
"screenSharingFailedToInstall": "Oops! Your screen sharing extension failed to install.",
"screenSharingFailedToInstallTitle": "Screen sharing extension failed to install",
"screenSharingFirefoxPermissionDeniedError": "Something went wrong while we were trying to share your screen. Please make sure that you have given us permission to do so. ",
"screenSharingFirefoxPermissionDeniedTitle": "Oops! We werent able to start screen sharing!",
"screenSharingPermissionDeniedError": "Oops! Something went wrong with your screen sharing extension permissions. Please reload and try again.",
"cameraUnsupportedResolutionError": "Your camera does not support required video resolution.",
"cameraUnknownError": "Cannot use camera for an unknown reason.",
@@ -396,14 +396,21 @@
"busy": "We're working on freeing streaming resources. Please try again in a few minutes.",
"busyTitle": "All streamers are currently busy",
"buttonTooltip": "Start / Stop Live Stream",
"changeSignIn": "Switch accounts.",
"choose": "Choose a live stream",
"chooseCTA": "Choose a streaming option. You're currently logged in as __email__.",
"enterStreamKey": "Enter your YouTube live stream key here.",
"error": "Live Streaming failed. Please try again.",
"errorAPI": "An error occurred while accessing your YouTube broadcasts. Please try logging in again.",
"failedToStart": "Live Streaming failed to start",
"off": "Live Streaming stopped",
"on": "Live Streaming",
"pending": "Starting Live Stream...",
"serviceName": "Live Streaming service",
"streamIdRequired": "Please fill in the stream id in order to launch the Live Streaming.",
"streamIdHelp": "Where do I find this?",
"signIn": "Sign in with Google",
"signInCTA": "Sign in or enter your live stream key from YouTube.",
"start": "Start a livestream",
"streamIdHelp": "What's this?",
"unavailableTitle": "Live Streaming unavailable"
},
"videoSIPGW":

View File

@@ -1039,16 +1039,6 @@ UI.getLargeVideo = function() {
*/
UI.isPinned = userId => VideoLayout.getPinnedId() === userId;
/**
* Shows dialog with a link to FF extension.
*/
UI.showExtensionRequiredDialog = function(url) {
messageHandler.openMessageDialog(
'dialog.extensionRequired',
'[html]dialog.firefoxExtensionPrompt',
{ url });
};
/**
* Shows "Please go to chrome webstore to install the desktop sharing extension"
* 2 button dialog with buttons - cancel and go to web store.

View File

@@ -20,6 +20,7 @@ import UIEvents from '../../../service/UI/UIEvents';
import UIUtil from '../util/UIUtil';
import VideoLayout from '../videolayout/VideoLayout';
import { openDialog } from '../../../react/features/base/dialog';
import {
JitsiRecordingStatus
} from '../../../react/features/base/lib-jitsi-meet';
@@ -31,6 +32,8 @@ import {
import { setToolboxEnabled } from '../../../react/features/toolbox';
import { setNotificationsEnabled } from '../../../react/features/notifications';
import {
StartLiveStreamDialog,
StopLiveStreamDialog,
hideRecordingLabel,
updateRecordingState
} from '../../../react/features/recording';
@@ -102,91 +105,11 @@ function _isRecordingButtonEnabled() {
* @returns {Promise}
*/
function _requestLiveStreamId() {
const cancelButton
= APP.translation.generateTranslationHTML('dialog.Cancel');
const backButton = APP.translation.generateTranslationHTML('dialog.Back');
const startStreamingButton
= APP.translation.generateTranslationHTML('dialog.startLiveStreaming');
const streamIdRequired
= APP.translation.generateTranslationHTML(
'liveStreaming.streamIdRequired');
const streamIdHelp
= APP.translation.generateTranslationHTML(
'liveStreaming.streamIdHelp');
return new Promise((resolve, reject) => {
dialog = APP.UI.messageHandler.openDialogWithStates({
state0: {
titleKey: 'dialog.liveStreaming',
html:
`<input class="input-control"
name="streamId" type="text"
data-i18n="[placeholder]dialog.streamKey"
autofocus><div style="text-align: right">
<a class="helper-link" target="_new"
href="${interfaceConfig.LIVE_STREAMING_HELP_LINK}">${
streamIdHelp
}</a></div>`,
persistent: false,
buttons: [
{ title: cancelButton,
value: false },
{ title: startStreamingButton,
value: true }
],
focus: ':input:first',
defaultButton: 1,
submit(e, v, m, f) { // eslint-disable-line max-params
e.preventDefault();
if (v) {
if (f.streamId && f.streamId.length > 0) {
resolve(UIUtil.escapeHtml(f.streamId));
dialog.close();
return;
}
dialog.goToState('state1');
return false;
}
reject(APP.UI.messageHandler.CANCEL);
dialog.close();
return false;
}
},
state1: {
titleKey: 'dialog.liveStreaming',
html: streamIdRequired,
persistent: false,
buttons: [
{ title: cancelButton,
value: false },
{ title: backButton,
value: true }
],
focus: ':input:first',
defaultButton: 1,
submit(e, v) {
e.preventDefault();
if (v === 0) {
reject(APP.UI.messageHandler.CANCEL);
dialog.close();
} else {
dialog.goToState('state0');
}
}
}
}, {
close() {
dialog = null;
}
});
});
return new Promise((resolve, reject) =>
APP.store.dispatch(openDialog(StartLiveStreamDialog, {
onCancel: reject,
onSubmit: resolve
})));
}
/**
@@ -232,25 +155,20 @@ function _requestRecordingToken() {
* @private
*/
function _showStopRecordingPrompt(recordingType) {
let title;
let message;
let buttonKey;
if (recordingType === 'jibri') {
title = 'dialog.liveStreaming';
message = 'dialog.stopStreamingWarning';
buttonKey = 'dialog.stopLiveStreaming';
} else {
title = 'dialog.recording';
message = 'dialog.stopRecordingWarning';
buttonKey = 'dialog.stopRecording';
return new Promise((resolve, reject) => {
APP.store.dispatch(openDialog(StopLiveStreamDialog, {
onCancel: reject,
onSubmit: resolve
}));
});
}
return new Promise((resolve, reject) => {
dialog = APP.UI.messageHandler.openTwoButtonDialog({
titleKey: title,
msgKey: message,
leftButtonKey: buttonKey,
titleKey: 'dialog.recording',
msgKey: 'dialog.stopRecordingWarning',
leftButtonKey: 'dialog.stopRecording',
submitFunction: (e, v) => (v ? resolve : reject)(),
closeFunction: () => {
dialog = null;

525
package-lock.json generated
View File

@@ -4,28 +4,25 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@atlaskit/analytics-next": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@atlaskit/analytics-next/-/analytics-next-1.1.3.tgz",
"integrity": "sha512-1r9kU2ZbleLCEpJhoPoor/IBbyRue22sutT/+5b8PY43xRgN4HzYT+lWBJRI9fWpG9yJJXLnr9uAjO8LJUKrCg==",
"requires": {
"prop-types": "15.6.0"
}
},
"@atlaskit/avatar": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/@atlaskit/avatar/-/avatar-8.0.5.tgz",
"integrity": "sha1-mJ4NkoR7N4yphKI+oHWY9jUhP/I=",
"requires": {
"@atlaskit/dropdown-menu": "3.10.2",
"@atlaskit/theme": "2.2.0",
"@atlaskit/theme": "2.4.0",
"@atlaskit/util-shared-styles": "2.10.6",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.3.0"
}
},
"@atlaskit/blanket": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/@atlaskit/blanket/-/blanket-2.4.3.tgz",
"integrity": "sha1-5kJGkzk+FxcZ1z8MDiw4OnqkzYA=",
"requires": {
"@atlaskit/util-shared-styles": "2.10.6",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
}
},
"@atlaskit/button": {
@@ -33,11 +30,11 @@
"resolved": "https://registry.npmjs.org/@atlaskit/button/-/button-5.4.2.tgz",
"integrity": "sha1-CSu+/KR+fPJJBQzRu9/E18o0KYQ=",
"requires": {
"@atlaskit/theme": "2.2.0",
"@atlaskit/theme": "2.4.0",
"babel-runtime": "6.26.0",
"classnames": "2.2.5",
"prop-types": "15.6.0",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
}
},
"@atlaskit/dropdown-menu": {
@@ -93,7 +90,7 @@
"@atlaskit/item": "4.2.11",
"@atlaskit/layer": "2.7.2",
"@atlaskit/spinner": "4.0.0",
"@atlaskit/theme": "2.2.0",
"@atlaskit/theme": "2.4.0",
"@atlaskit/tooltip": "6.0.0",
"babel-runtime": "6.26.0",
"classnames": "2.2.5",
@@ -219,10 +216,159 @@
"integrity": "sha1-pasqvkxDkjQXiTb2JYNHmaiZRQ0=",
"requires": {
"@atlaskit/field-base": "8.1.13",
"@atlaskit/theme": "2.2.0",
"@atlaskit/theme": "2.4.0",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
}
},
"@atlaskit/field-text-area": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@atlaskit/field-text-area/-/field-text-area-1.2.0.tgz",
"integrity": "sha512-dLZk+KX8F8/nslTYAQEpf3iZnlhmey5uWhtoytBRax2aeBAHAt102E26C86B7Rw5r9JX81c7MuSZWW2pjbdY/Q==",
"requires": {
"@atlaskit/button": "6.6.3",
"@atlaskit/field-base": "8.2.1",
"@atlaskit/icon": "10.12.2",
"@atlaskit/theme": "2.4.0",
"@atlaskit/util-readme": "3.6.5",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/button": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/@atlaskit/button/-/button-6.6.3.tgz",
"integrity": "sha512-+NLQrWv6HeJPC32Q6o+3w59F3secYWPT+9AM9nblPOjvRXmIVosafkro8wncVzX07/nx7MnBIXtYJRE+Cv+9Vg==",
"requires": {
"@atlaskit/analytics-next": "1.1.3",
"@atlaskit/theme": "2.4.1",
"babel-runtime": "6.26.0",
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/theme": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-2.4.1.tgz",
"integrity": "sha512-I4AMg+asUUwaruwsWDaUzub2zhEKYHhZ5tRlaNQ+8dhATpO1RSu6g9FSgwnAZ20M4Cf+YITVND/wyNpAON5HCQ==",
"requires": {
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
}
}
},
"@atlaskit/field-base": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/@atlaskit/field-base/-/field-base-8.2.1.tgz",
"integrity": "sha512-s18bwvmGug6sYZ6nuHLP33T2AWYZemteZCIaFPwlEKT4MkTXNX3KX9bYy8TRJmQo2+NC4uPVUUT2U8MeF4wBnQ==",
"requires": {
"@atlaskit/icon": "10.12.2",
"@atlaskit/inline-dialog": "5.3.2",
"@atlaskit/spinner": "4.2.1",
"@atlaskit/theme": "2.4.1",
"babel-runtime": "6.26.0",
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/theme": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-2.4.1.tgz",
"integrity": "sha512-I4AMg+asUUwaruwsWDaUzub2zhEKYHhZ5tRlaNQ+8dhATpO1RSu6g9FSgwnAZ20M4Cf+YITVND/wyNpAON5HCQ==",
"requires": {
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
}
}
},
"@atlaskit/icon": {
"version": "10.12.2",
"resolved": "https://registry.npmjs.org/@atlaskit/icon/-/icon-10.12.2.tgz",
"integrity": "sha512-OGNCVg8CdyaF/pD6DyhSaiK2pfb5R0jTWezlKCeKNXpmNiLUF4/B5MkOJFBMAHZc1A3EpBr9YNqH/uhJvG9G/Q==",
"requires": {
"@atlaskit/theme": "2.4.1",
"babel-runtime": "6.26.0",
"styled-components": "1.4.6",
"uuid": "3.1.0"
},
"dependencies": {
"@atlaskit/theme": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-2.4.1.tgz",
"integrity": "sha512-I4AMg+asUUwaruwsWDaUzub2zhEKYHhZ5tRlaNQ+8dhATpO1RSu6g9FSgwnAZ20M4Cf+YITVND/wyNpAON5HCQ==",
"requires": {
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
}
}
},
"@atlaskit/inline-dialog": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/@atlaskit/inline-dialog/-/inline-dialog-5.3.2.tgz",
"integrity": "sha512-FUZZBi9x6Cr1bpd/f00Q262wLlTfl0Sl7PJUQLoHxy0u7Zg9lBLrUEoxuGh0OlOlqvQA99lJK23igZYOXDe9xA==",
"requires": {
"@atlaskit/layer": "2.9.1",
"@atlaskit/theme": "2.4.1",
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/theme": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-2.4.1.tgz",
"integrity": "sha512-I4AMg+asUUwaruwsWDaUzub2zhEKYHhZ5tRlaNQ+8dhATpO1RSu6g9FSgwnAZ20M4Cf+YITVND/wyNpAON5HCQ==",
"requires": {
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
}
}
},
"@atlaskit/layer": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@atlaskit/layer/-/layer-2.9.1.tgz",
"integrity": "sha512-nyIVGeS2OhuGR5gIMTYUfRmCG8z/9KMgUzTpbpsB70sH6+d4KSFhfkz+KhKNIa8gvKI6zBc+3UBYSlUW1t1qmQ==",
"requires": {
"styled-components": "1.4.6"
}
},
"@atlaskit/spinner": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@atlaskit/spinner/-/spinner-4.2.1.tgz",
"integrity": "sha512-PWJ3hFMl+7rls0vvwFtH6qOu9HKeIHQFtDvJ/+HykmhzIp1tQ+S5DQ+et4ciHfNSh2On2GNiLcZHwXahHvR5WA==",
"requires": {
"@atlaskit/theme": "2.4.1",
"babel-runtime": "6.26.0",
"react-transition-group": "2.2.1",
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/theme": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-2.4.1.tgz",
"integrity": "sha512-I4AMg+asUUwaruwsWDaUzub2zhEKYHhZ5tRlaNQ+8dhATpO1RSu6g9FSgwnAZ20M4Cf+YITVND/wyNpAON5HCQ==",
"requires": {
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
}
}
},
"react-transition-group": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.2.1.tgz",
"integrity": "sha512-q54UBM22bs/CekG8r3+vi9TugSqh0t7qcEVycaRc9M0p0aCEu+h6rp/RFiW7fHfgd1IKpd9oILFTl5QK+FpiPA==",
"requires": {
"chain-function": "1.0.0",
"classnames": "2.2.5",
"dom-helpers": "3.3.1",
"loose-envify": "1.3.1",
"prop-types": "15.6.0",
"warning": "3.0.0"
}
}
}
},
"@atlaskit/flag": {
@@ -232,12 +378,12 @@
"requires": {
"@atlaskit/button": "5.4.2",
"@atlaskit/icon": "7.1.0",
"@atlaskit/theme": "2.2.0",
"@atlaskit/theme": "2.4.0",
"@atlaskit/util-shared-styles": "2.10.6",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"react-transition-group": "1.2.1",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/icon": {
@@ -247,7 +393,7 @@
"requires": {
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
}
}
}
@@ -259,7 +405,7 @@
"requires": {
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
}
},
"@atlaskit/inline-dialog": {
@@ -268,10 +414,10 @@
"integrity": "sha1-xwPi9seo0M+nrcPXgJK0bE7ShkQ=",
"requires": {
"@atlaskit/layer": "2.7.2",
"@atlaskit/theme": "2.2.0",
"@atlaskit/theme": "2.4.0",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
}
},
"@atlaskit/inline-message": {
@@ -282,10 +428,10 @@
"@atlaskit/button": "3.6.0",
"@atlaskit/icon": "7.1.0",
"@atlaskit/inline-dialog": "5.0.2",
"@atlaskit/theme": "2.2.0",
"@atlaskit/theme": "2.4.0",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/button": {
@@ -297,7 +443,7 @@
"babel-runtime": "6.26.0",
"classnames": "2.2.5",
"prop-types": "15.6.0",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
}
},
"@atlaskit/icon": {
@@ -307,7 +453,7 @@
"requires": {
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
}
}
}
@@ -375,6 +521,33 @@
}
}
},
"@atlaskit/layer-manager": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/@atlaskit/layer-manager/-/layer-manager-2.8.0.tgz",
"integrity": "sha512-/o4c+zyF3nOeR3xJGFeUpOh8TUbcX293285Nvxcbo1F+knM7WA1JhE786XkTK05nZ2HCqK6zHKSlyIoKdJYtkw==",
"requires": {
"focusin": "2.0.0",
"prop-types": "15.6.0",
"react-transition-group": "2.2.1",
"styled-components": "1.4.6",
"tabbable": "1.1.2"
},
"dependencies": {
"react-transition-group": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.2.1.tgz",
"integrity": "sha512-q54UBM22bs/CekG8r3+vi9TugSqh0t7qcEVycaRc9M0p0aCEu+h6rp/RFiW7fHfgd1IKpd9oILFTl5QK+FpiPA==",
"requires": {
"chain-function": "1.0.0",
"classnames": "2.2.5",
"dom-helpers": "3.3.1",
"loose-envify": "1.3.1",
"prop-types": "15.6.0",
"warning": "3.0.0"
}
}
}
},
"@atlaskit/lozenge": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/@atlaskit/lozenge/-/lozenge-3.4.2.tgz",
@@ -383,7 +556,7 @@
"@atlaskit/util-shared-styles": "1.7.1",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/util-shared-styles": {
@@ -397,15 +570,100 @@
}
},
"@atlaskit/modal-dialog": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@atlaskit/modal-dialog/-/modal-dialog-2.6.0.tgz",
"integrity": "sha1-gqXtcN8W5s6ttZHQDeYvTzx3jiE=",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@atlaskit/modal-dialog/-/modal-dialog-3.4.0.tgz",
"integrity": "sha512-5KOZrwCanzXD3b9+kQYIS4tACAj/HjcwY+yLGY1SCxlDxj94uUNr+tTAznng3EKRf64UbqshrRQXBj8Fcol4sw==",
"requires": {
"@atlaskit/blanket": "2.4.3",
"@atlaskit/util-shared-styles": "2.10.6",
"babel-runtime": "6.26.0",
"@atlaskit/blanket": "4.1.1",
"@atlaskit/button": "6.6.3",
"@atlaskit/icon": "10.12.2",
"@atlaskit/layer-manager": "2.8.0",
"@atlaskit/theme": "2.4.0",
"focusin": "2.0.0",
"prop-types": "15.6.0",
"styled-components": "1.3.0"
"raf-schd": "2.1.0",
"react-transition-group": "2.2.1",
"styled-components": "1.4.6",
"tabbable": "1.1.2"
},
"dependencies": {
"@atlaskit/blanket": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@atlaskit/blanket/-/blanket-4.1.1.tgz",
"integrity": "sha512-LPqRNVkR6+mI67EutF6iQIUARf1MWpjdVxeLaMJZOG/ej8u9dYIkoOOVeliXx/bx+4X5NfnKikeg34MB6VU5DQ==",
"requires": {
"@atlaskit/theme": "2.4.1",
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/theme": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-2.4.1.tgz",
"integrity": "sha512-I4AMg+asUUwaruwsWDaUzub2zhEKYHhZ5tRlaNQ+8dhATpO1RSu6g9FSgwnAZ20M4Cf+YITVND/wyNpAON5HCQ==",
"requires": {
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
}
}
},
"@atlaskit/button": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/@atlaskit/button/-/button-6.6.3.tgz",
"integrity": "sha512-+NLQrWv6HeJPC32Q6o+3w59F3secYWPT+9AM9nblPOjvRXmIVosafkro8wncVzX07/nx7MnBIXtYJRE+Cv+9Vg==",
"requires": {
"@atlaskit/analytics-next": "1.1.3",
"@atlaskit/theme": "2.4.1",
"babel-runtime": "6.26.0",
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/theme": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-2.4.1.tgz",
"integrity": "sha512-I4AMg+asUUwaruwsWDaUzub2zhEKYHhZ5tRlaNQ+8dhATpO1RSu6g9FSgwnAZ20M4Cf+YITVND/wyNpAON5HCQ==",
"requires": {
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
}
}
},
"@atlaskit/icon": {
"version": "10.12.2",
"resolved": "https://registry.npmjs.org/@atlaskit/icon/-/icon-10.12.2.tgz",
"integrity": "sha512-OGNCVg8CdyaF/pD6DyhSaiK2pfb5R0jTWezlKCeKNXpmNiLUF4/B5MkOJFBMAHZc1A3EpBr9YNqH/uhJvG9G/Q==",
"requires": {
"@atlaskit/theme": "2.4.1",
"babel-runtime": "6.26.0",
"styled-components": "1.4.6",
"uuid": "3.1.0"
},
"dependencies": {
"@atlaskit/theme": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-2.4.1.tgz",
"integrity": "sha512-I4AMg+asUUwaruwsWDaUzub2zhEKYHhZ5tRlaNQ+8dhATpO1RSu6g9FSgwnAZ20M4Cf+YITVND/wyNpAON5HCQ==",
"requires": {
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
}
}
},
"react-transition-group": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.2.1.tgz",
"integrity": "sha512-q54UBM22bs/CekG8r3+vi9TugSqh0t7qcEVycaRc9M0p0aCEu+h6rp/RFiW7fHfgd1IKpd9oILFTl5QK+FpiPA==",
"requires": {
"chain-function": "1.0.0",
"classnames": "2.2.5",
"dom-helpers": "3.3.1",
"loose-envify": "1.3.1",
"prop-types": "15.6.0",
"warning": "3.0.0"
}
}
}
},
"@atlaskit/multi-select": {
@@ -419,12 +677,12 @@
"@atlaskit/spinner": "3.4.2",
"@atlaskit/tag": "3.1.3",
"@atlaskit/tag-group": "3.5.3",
"@atlaskit/theme": "2.2.0",
"@atlaskit/theme": "2.4.0",
"@atlaskit/util-shared-styles": "2.10.6",
"babel-runtime": "6.26.0",
"classnames": "2.2.5",
"prop-types": "15.6.0",
"styled-components": "1.3.0",
"styled-components": "1.4.6",
"uid": "0.0.2"
},
"dependencies": {
@@ -437,7 +695,7 @@
"classnames": "2.2.5",
"create-error": "0.3.1",
"prop-types": "15.6.0",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
}
},
"@atlaskit/spinner": {
@@ -450,7 +708,7 @@
"prop-types": "15.6.0",
"prop-types-extra": "1.0.1",
"react-transition-group": "1.2.1",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
}
}
}
@@ -460,12 +718,12 @@
"resolved": "https://registry.npmjs.org/@atlaskit/spinner/-/spinner-4.0.0.tgz",
"integrity": "sha1-u8MaEjYPwzNhegZsMPhmRmJoOhA=",
"requires": {
"@atlaskit/theme": "2.2.0",
"@atlaskit/theme": "2.4.0",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"prop-types-extra": "1.0.1",
"react-transition-group": "1.2.1",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
}
},
"@atlaskit/tabs": {
@@ -473,14 +731,14 @@
"resolved": "https://registry.npmjs.org/@atlaskit/tabs/-/tabs-4.0.1.tgz",
"integrity": "sha1-vM2WQ8nSO9kR+0GCK4U1oYJHHM8=",
"requires": {
"@atlaskit/theme": "2.2.0",
"@atlaskit/theme": "2.4.0",
"@atlaskit/util-shared-styles": "1.7.1",
"classnames": "2.2.5",
"css-element-queries": "0.3.2",
"debounce": "1.1.0",
"keycode": "2.1.9",
"prop-types": "15.6.0",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/util-shared-styles": {
@@ -571,13 +829,12 @@
}
},
"@atlaskit/theme": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-2.2.0.tgz",
"integrity": "sha1-uHNsuA3U7TnizAkPLbFdz4eEgBo=",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-2.4.0.tgz",
"integrity": "sha512-JORaZ4/9Bm8V2qsSC4O8Zn6aTHM0MJA4nVJPzxlvFJlz0CX6GTQ+CJiPPssJGidBR8Y78j0LvNXASH5ijLBChw==",
"requires": {
"@atlaskit/util-shared-styles": "2.10.6",
"prop-types": "15.6.0",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
}
},
"@atlaskit/tooltip": {
@@ -586,11 +843,11 @@
"integrity": "sha1-rn3xMCmvO1iZqcuKNPPIT8K7mpc=",
"requires": {
"@atlaskit/layer": "2.7.2",
"@atlaskit/theme": "2.2.0",
"@atlaskit/theme": "2.4.0",
"@atlaskit/util-shared-styles": "2.10.6",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.3.0"
"styled-components": "1.4.6"
}
},
"@atlaskit/util-common": {
@@ -603,6 +860,19 @@
"keycode": "2.1.9"
}
},
"@atlaskit/util-readme": {
"version": "3.6.5",
"resolved": "https://registry.npmjs.org/@atlaskit/util-readme/-/util-readme-3.6.5.tgz",
"integrity": "sha1-M6jgyQEI9liaYtiZdOH30J59zuc=",
"requires": {
"@atlaskit/util-shared-styles": "2.10.6",
"decamelize": "1.2.0",
"prop-types": "15.6.0",
"react-docgen": "2.20.1",
"react-syntax-highlighter": "3.0.2",
"styled-components": "1.4.6"
}
},
"@atlaskit/util-shared-styles": {
"version": "2.10.6",
"resolved": "https://registry.npmjs.org/@atlaskit/util-shared-styles/-/util-shared-styles-2.10.6.tgz",
@@ -1107,6 +1377,11 @@
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
},
"ast-types": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.10.1.tgz",
"integrity": "sha512-UY7+9DPzlJ9VM8eY0b2TUZcZvF+1pO0hzMtAyjBYKhOmnvRlqYNYnWdtsMj0V16CGaMlpL0G1jnLbLo4AyotuQ=="
},
"async": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/async/-/async-0.9.0.tgz",
@@ -3342,14 +3617,6 @@
"resolved": "https://registry.npmjs.org/css-element-queries/-/css-element-queries-0.3.2.tgz",
"integrity": "sha1-U12AiRs68hOV0AXxmQX+UVS1zP8="
},
"css-in-js-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.0.tgz",
"integrity": "sha512-yuWmPMD9FLi50Xf3k8W8oO3WM1eVnxEGCldCLyfusQ+CgivFk0s23yst4ooW6tfxMuSa03S6uUEga9UhX6GRrA==",
"requires": {
"hyphenate-style-name": "1.0.2"
}
},
"css-loader": {
"version": "0.28.7",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.28.7.tgz",
@@ -3675,7 +3942,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
"dev": true,
"requires": {
"esutils": "2.0.2"
}
@@ -5017,6 +5283,11 @@
}
}
},
"focusin": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/focusin/-/focusin-2.0.0.tgz",
"integrity": "sha1-WMARgN+xlJ8D493u4GNzn8M7b/w="
},
"for-in": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -5995,29 +6266,6 @@
"integrity": "sha1-nMj/ABF3lXoRcmq1CNQVu4Cxi88=",
"dev": true
},
"glamor": {
"version": "2.20.40",
"resolved": "https://registry.npmjs.org/glamor/-/glamor-2.20.40.tgz",
"integrity": "sha512-DNXCd+c14N9QF8aAKrfl4xakPk5FdcFwmH7sD0qnC0Pr7xoZ5W9yovhUrY/dJc3psfGGXC58vqQyRtuskyUJxA==",
"requires": {
"fbjs": "0.8.16",
"inline-style-prefixer": "3.0.8",
"object-assign": "4.1.1",
"prop-types": "15.6.0",
"through": "2.3.8"
},
"dependencies": {
"inline-style-prefixer": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-3.0.8.tgz",
"integrity": "sha1-hVG45bTVcyROZqNLBPfTIHaitTQ=",
"requires": {
"bowser": "1.9.2",
"css-in-js-utils": "2.0.0"
}
}
}
},
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
@@ -6236,6 +6484,11 @@
"sntp": "2.1.0"
}
},
"highlight.js": {
"version": "9.8.0",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.8.0.tgz",
"integrity": "sha1-OO7vQM1F6t2+yMnlI4+3p4OjtoU="
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@@ -7220,7 +7473,7 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#410ad5e76d33b1422ce34f83719eda7dfd584e22",
"version": "github:jitsi/lib-jitsi-meet#0503ec4d3f175b154b1c6fd7037520ce0768fa58",
"requires": {
"async": "0.9.0",
"current-executing-script": "0.1.3",
@@ -7467,6 +7720,21 @@
"signal-exit": "3.0.2"
}
},
"lowlight": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.9.1.tgz",
"integrity": "sha512-CpDhyVhI+xHjruiGvH2F/Fr5q5aTn5A6Oyh7MI+4oI8G0A1E7p9a3Zqv9Hzx9WByK8gAiNifEueAXz+cA2xdEA==",
"requires": {
"highlight.js": "9.12.0"
},
"dependencies": {
"highlight.js": {
"version": "9.12.0",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz",
"integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4="
}
}
},
"lru-cache": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz",
@@ -8071,6 +8339,14 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz",
"integrity": "sha1-Jp1cR2gQ7JLtvntsLygxY4T5p+g="
},
"node-dir": {
"version": "0.1.17",
"resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz",
"integrity": "sha1-X1Zl2TNRM1yqvvjxxVRRbPXx5OU=",
"requires": {
"minimatch": "3.0.4"
}
},
"node-fetch": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
@@ -9601,6 +9877,11 @@
"integrity": "sha1-DPf4T5Rj/wrlHExLFC2VvjdyTZw=",
"dev": true
},
"raf-schd": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-2.1.0.tgz",
"integrity": "sha512-6OXM6ObuPnyqjiHVz/XaFTm/yEYC1PXYMt/UFCwMvLQK1Wqlc2f7eU4+8JbgjfZLiQNQU9miFEGPnFUEMEx2hA=="
},
"railroad-diagrams": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz",
@@ -9754,6 +10035,35 @@
}
}
},
"react-docgen": {
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-2.20.1.tgz",
"integrity": "sha512-NYmD8nDPMWpIpqWqhSZjQ3P5iQml55IMkDG0ZInyWP7JD2ljaNhrxNWZnXPrOezUu6bYlcZUCxjw19s7zhE2uw==",
"requires": {
"async": "2.6.0",
"babel-runtime": "6.26.0",
"babylon": "5.8.38",
"commander": "2.14.1",
"doctrine": "2.1.0",
"node-dir": "0.1.17",
"recast": "0.12.9"
},
"dependencies": {
"async": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz",
"integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==",
"requires": {
"lodash": "4.17.4"
}
},
"babylon": {
"version": "5.8.38",
"resolved": "https://registry.npmjs.org/babylon/-/babylon-5.8.38.tgz",
"integrity": "sha1-7JsSCxG/bM1Bc6GL8hfmC3mFn/0="
}
}
},
"react-dom": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.2.0.tgz",
@@ -9984,6 +10294,16 @@
}
}
},
"react-syntax-highlighter": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-3.0.2.tgz",
"integrity": "sha1-d8VWOFu/cdwul0upa/3Q7e9cMZI=",
"requires": {
"babel-runtime": "6.26.0",
"highlight.js": "9.8.0",
"lowlight": "1.9.1"
}
},
"react-timer-mixin": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/react-timer-mixin/-/react-timer-mixin-0.13.3.tgz",
@@ -10084,6 +10404,30 @@
}
}
},
"recast": {
"version": "0.12.9",
"resolved": "https://registry.npmjs.org/recast/-/recast-0.12.9.tgz",
"integrity": "sha512-y7ANxCWmMW8xLOaiopiRDlyjQ9ajKRENBH+2wjntIbk3A6ZR1+BLQttkmSHMY7Arl+AAZFwJ10grg2T6f1WI8A==",
"requires": {
"ast-types": "0.10.1",
"core-js": "2.5.3",
"esprima": "4.0.0",
"private": "0.1.8",
"source-map": "0.6.1"
},
"dependencies": {
"esprima": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
"integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw=="
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
}
}
},
"redent": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
@@ -11341,17 +11685,17 @@
}
},
"styled-components": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-1.3.0.tgz",
"integrity": "sha1-BxG1qC/nhLRY/W5QVLhCdRxUOms=",
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-1.4.6.tgz",
"integrity": "sha1-WPMuimq1EPsUgekB6DjgR38UiwY=",
"requires": {
"buffer": "5.1.0",
"css-to-react-native": "1.0.6",
"fbjs": "0.8.16",
"glamor": "2.20.40",
"inline-style-prefixer": "2.0.5",
"is-function": "1.0.1",
"is-plain-object": "2.0.4",
"prop-types": "15.6.0",
"supports-color": "3.2.3"
}
},
@@ -11397,6 +11741,11 @@
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
},
"tabbable": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-1.1.2.tgz",
"integrity": "sha512-77oqsKEPrxIwgRcXUwipkj9W5ItO97L6eUT1Ar7vh+El16Zm4M6V+YU1cbipHEa6q0Yjw8O3Hoh8oRgatV5s7A=="
},
"table": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz",

View File

@@ -20,16 +20,18 @@
"@atlaskit/dropdown-menu": "3.10.2",
"@atlaskit/droplist": "4.11.1",
"@atlaskit/field-text": "4.0.1",
"@atlaskit/field-text-area": "1.2.0",
"@atlaskit/flag": "6.1.0",
"@atlaskit/icon": "10.0.0",
"@atlaskit/inline-dialog": "5.0.2",
"@atlaskit/inline-message": "3.0.1",
"@atlaskit/layer-manager": "2.8.0",
"@atlaskit/lozenge": "3.4.2",
"@atlaskit/modal-dialog": "2.6.0",
"@atlaskit/modal-dialog": "3.4.0",
"@atlaskit/multi-select": "7.1.3",
"@atlaskit/spinner": "4.0.0",
"@atlaskit/tabs": "4.0.1",
"@atlaskit/theme": "2.2.0",
"@atlaskit/theme": "2.4.0",
"@atlaskit/tooltip": "6.0.0",
"autosize": "1.18.13",
"es6-iterator": "2.0.3",
@@ -44,7 +46,7 @@
"jquery-i18next": "1.2.0",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#410ad5e76d33b1422ce34f83719eda7dfd584e22",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#0503ec4d3f175b154b1c6fd7037520ce0768fa58",
"lodash": "4.17.4",
"moment": "2.19.4",
"nuclear-js": "1.4.0",
@@ -71,7 +73,7 @@
"redux-thunk": "2.2.0",
"strophe.js": "github:jitsi/strophejs#1.2.14-1",
"strophejs-plugin-disco": "0.0.2",
"styled-components": "1.3.0",
"styled-components": "1.4.6",
"url-polyfill": "github:github/url-polyfill#39734186de44612bc5a16eb25f5407adcc5b2e7c",
"uuid": "3.1.0",
"xmldom": "0.1.27"

View File

@@ -1,10 +1,13 @@
/* @flow */
// @flow
import type { Dispatch } from 'redux';
import {
CONFIG_WILL_LOAD,
LOAD_CONFIG_ERROR,
SET_CONFIG
} from './actionTypes';
import { setConfigFromURLParams } from './functions';
/**
* Signals that the configuration for a specific locationURL will be loaded now.
@@ -51,14 +54,36 @@ export function loadConfigError(error: Error, locationURL: string | URL) {
*
* @param {Object} config - The configuration to be represented by the feature
* base/config.
* @returns {{
* type: SET_CONFIG,
* config: Object
* }}
* @returns {Function}
*/
export function setConfig(config: Object = {}) {
return {
type: SET_CONFIG,
config
return (dispatch: Dispatch<*>, getState: Function) => {
const { locationURL } = getState()['features/base/connection'];
// Now that the loading of the config was successful override the values
// with the parameters passed in the hash part of the location URI.
// TODO We're still in the middle ground between old Web with config,
// interfaceConfig, and loggingConfig used via global variables and new
// Web and mobile reading the respective values from the redux store.
// On React Native there's no interfaceConfig at all yet and
// loggingConfig is not loaded but there's a default value in the redux
// store.
// Only the config will be overridden on React Native, as the other
// globals will be undefined here. It's intentional - we do not care to
// override those configs yet.
locationURL
&& setConfigFromURLParams(
// On Web the config also comes from the window.config global,
// but it is resolved in the loadConfig procedure.
config,
window.interfaceConfig,
window.loggingConfig,
locationURL);
dispatch({
type: SET_CONFIG,
config
});
};
}

View File

@@ -179,7 +179,7 @@ export function obtainConfig(
* @returns {void}
*/
export function overrideConfigJSON(
config: Object, interfaceConfig: Object, loggingConfig: Object,
config: ?Object, interfaceConfig: ?Object, loggingConfig: ?Object,
json: Object) {
for (const configName of Object.keys(json)) {
let configObj;
@@ -212,6 +212,8 @@ export function overrideConfigJSON(
}
}
/* eslint-enable max-params, no-shadow */
/**
* Whitelist only config.js, skips this for others configs
* (interfaceConfig, loggingConfig).
@@ -220,9 +222,9 @@ export function overrideConfigJSON(
* @param {string} configName - The config name, one of config,
* interfaceConfig, loggingConfig.
* @param {Object} configJSON - The object with keys and values to override.
* @private
* @returns {Object} - The result object only with the keys
* that are whitelisted.
* @private
*/
function _getWhitelistedJSON(configName, configJSON) {
if (configName !== 'config') {
@@ -232,40 +234,48 @@ function _getWhitelistedJSON(configName, configJSON) {
return _.pick(configJSON, WHITELISTED_KEYS);
}
/* eslint-enable max-params, no-shadow */
/* eslint-disable max-params */
/**
* Converts 'URL_PARAMS' to JSON object.
* We have:
* {
* "config.disableAudioLevels": false,
* "config.channelLastN": -1,
* "interfaceConfig.APP_NAME": "Jitsi Meet"
* }.
* We want to have:
* {
* "config": {
* "disableAudioLevels": false,
* "channelLastN": -1
* },
* interfaceConfig: {
* "APP_NAME": "Jitsi Meet"
* }
* }.
* Inspects the hash part of the location URI and overrides values specified
* there in the corresponding config objects given as the arguments. The syntax
* is: {@code https://server.com/room#config.debug=true
* &interfaceConfig.showButton=false&loggingConfig.something=1}.
*
* In the hash part each parameter will be parsed to JSON and then the root
* object will be matched with the corresponding config object given as the
* argument to this function.
*
* @param {Object} config - This is the general config.
* @param {Object} interfaceConfig - This is the interface config.
* @param {Object} loggingConfig - The logging config.
* @param {URI} location - The new location to which the app is navigating to.
* @returns {void}
*/
export function setConfigFromURLParams() {
const params = parseURLParams(window.location);
const { config, interfaceConfig, loggingConfig } = window;
export function setConfigFromURLParams(
config: ?Object,
interfaceConfig: ?Object,
loggingConfig: ?Object,
location: Object) {
const params = parseURLParams(location);
const json = {};
// TODO We're still in the middle ground between old Web with config,
// interfaceConfig, and loggingConfig used via global variables and new Web
// and mobile reading the respective values from the redux store. On React
// Native there's no interfaceConfig at all yet and loggingConfig is not
// loaded but there's a default value in the redux store.
// At this point we have:
// params = {
// "config.disableAudioLevels": false,
// "config.channelLastN": -1,
// "interfaceConfig.APP_NAME": "Jitsi Meet"
// }
// We want to have:
// json = {
// config: {
// "disableAudioLevels": false,
// "channelLastN": -1
// },
// interfaceConfig: {
// "APP_NAME": "Jitsi Meet"
// }
// }
config && (json.config = {});
interfaceConfig && (json.interfaceConfig = {});
loggingConfig && (json.loggingConfig = {});
@@ -284,3 +294,5 @@ export function setConfigFromURLParams() {
overrideConfigJSON(config, interfaceConfig, loggingConfig, json);
}
/* eslint-enable max-params */

View File

@@ -2,4 +2,5 @@ export * from './actions';
export * from './actionTypes';
export * from './functions';
import './middleware';
import './reducer';

View File

@@ -0,0 +1,52 @@
// @flow
import { MiddlewareRegistry } from '../redux';
import { SET_CONFIG } from './actionTypes';
/**
* The middleware of the feature {@code base/config}.
*
* @param {Store} store - The redux store.
* @private
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case SET_CONFIG:
return _setConfig(store, next, action);
}
return next(action);
});
/**
* Notifies the feature {@code base/config} that the {@link SET_CONFIG} redux
* action is being {@code dispatch}ed in a specific redux store.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
* specified {@code action} in the specified {@code store}.
* @param {Action} action - The redux action which is being {@code dispatch}ed
* in the specified {@code store}.
* @private
* @returns {*} The return value of {@code next(action)}.
*/
function _setConfig({ getState }, next, action) {
// The reducer is doing some alterations to the config passed in the action,
// so make sure it's the final state by waiting for the action to be
// reduced.
const result = next(action);
// FIXME On Web we rely on the global 'config' variable which gets altered
// multiple times, before it makes it to the reducer. At some point it may
// not be the global variable which is being modified anymore due to
// different merge methods being used along the way. The global variable
// must be synchronized with the final state resolved by the reducer.
if (typeof window.config !== 'undefined') {
window.config = getState()['features/base/config'];
}
return result;
}

View File

@@ -157,7 +157,7 @@ function _translateLegacyConfig(oldValue: Object) {
/* eslint-enable indent */
if (oldKey in newValue) {
if (oldKey in newValue && !(newKey in newValue.p2p)) {
const v = newValue[oldKey];
// Do not modify oldValue.

View File

@@ -1,9 +1,10 @@
// @flow
import Button, { ButtonGroup } from '@atlaskit/button';
import ModalDialog from '@atlaskit/modal-dialog';
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import { withContextFromProps } from '@atlaskit/layer-manager';
import Modal, { ModalFooter } from '@atlaskit/modal-dialog';
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { translate } from '../../i18n';
@@ -30,6 +31,8 @@ const OK_BUTTON_ID = 'modal-dialog-ok-button';
type Props = {
...DialogProps,
i18n: Object,
/**
* Disables dismissing the dialog when the blanket is clicked. Enabled
* by default.
@@ -57,10 +60,26 @@ type Props = {
width: string
};
/**
* ContexTypes is used as a workaround for Atlaskit's modal being displayed
* outside of the normal App hierarchy, thereby losing context. ContextType
* is responsible for taking its props and passing them into children.
*
* @type {ReactElement}
*/
const ContextProvider = withContextFromProps({
i18n: PropTypes.object
});
/**
* Web dialog that uses atlaskit modal-dialog to display dialogs.
*/
class StatelessDialog extends Component<Props> {
/**
* The functional component to be used for rendering the modal footer.
*/
_Footer: ?Function
_dialogElement: ?HTMLElement;
/**
@@ -78,30 +97,24 @@ class StatelessDialog extends Component<Props> {
this._onKeyDown = this._onKeyDown.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._setDialogElement = this._setDialogElement.bind(this);
this._Footer = this._createFooterConstructor(props);
}
/**
* React Component method that executes once component is mounted.
* React Component method that executes before the component is updated.
*
* @inheritdoc
*/
componentDidMount() {
this._updateButtonFocus();
}
/**
* React Component method that executes once component is updated.
*
* @param {Object} prevProps - The previous properties, before the update.
* @param {Object} nextProps - The next properties, before the update.
* @returns {void}
*/
componentDidUpdate(prevProps) {
// if there is an update in any of the buttons enable/disable props
// update the focus if needed
if (prevProps.okDisabled !== this.props.okDisabled
|| prevProps.cancelDisabled !== this.props.cancelDisabled
|| prevProps.submitDisabled !== this.props.submitDisabled) {
this._updateButtonFocus();
componentWillUpdate(nextProps) {
// If button states have changed, update the Footer constructor function
// so buttons of the proper state are rendered.
if (nextProps.okDisabled !== this.props.okDisabled
|| nextProps.cancelDisabled !== this.props.cancelDisabled
|| nextProps.submitDisabled !== this.props.submitDisabled) {
this._Footer = this._createFooterConstructor(nextProps);
}
}
@@ -112,36 +125,86 @@ class StatelessDialog extends Component<Props> {
* @returns {ReactElement}
*/
render() {
return (
const {
children,
t /* The following fixes a flow error: */ = _.identity,
titleString,
titleKey,
width
} = this.props;
/**
* Enabled light theme for dialogs until all in-dialog components
* support dark theme.
*/
<AtlasKitThemeProvider mode = 'light'>
<div
onKeyDown = { this._onKeyDown }
ref = { this._setDialogElement }>
<ModalDialog
footer = { this._renderFooter() }
header = { this._renderHeader() }
isOpen = { true }
onDialogDismissed = { this._onDialogDismissed }
width = { this.props.width || 'medium' }>
<div>
<form
className = 'modal-dialog-form'
id = 'modal-dialog-form'
onSubmit = { this._onSubmit }>
{ this.props.children }
</form>
</div>
</ModalDialog>
</div>
</AtlasKitThemeProvider>
return (
<Modal
autoFocus = { true }
footer = { this._Footer }
heading = { titleString || t(titleKey) }
i18n = { this.props.i18n }
onClose = { this._onDialogDismissed }
onDialogDismissed = { this._onDialogDismissed }
shouldCloseOnEscapePress = { true }
width = { width || 'medium' }>
{
/**
* Wrapping the contents of {@link Modal} with
* {@link ContextProvider} is a workaround for the
* i18n context becoming undefined as modal gets rendered
* outside of the normal react app context.
*/
}
<ContextProvider i18n = { this.props.i18n }>
<div
onKeyDown = { this._onKeyDown }
ref = { this._setDialogElement }>
<form
className = 'modal-dialog-form'
id = 'modal-dialog-form'
onSubmit = { this._onSubmit }>
{ children }
</form>
</div>
</ContextProvider>
</Modal>
);
}
_onCancel: () => Function;
/**
* Returns a functional component to be used for the modal footer.
*
* @param {Object} options - The configuration for how the buttons in the
* footer should display. Essentially {@code StatelessDialog} props should
* be passed in.
* @private
* @returns {ReactElement}
*/
_createFooterConstructor(options) {
// Filter out falsy (null) values because {@code ButtonGroup} will error
// if passed in anything but buttons with valid type props.
const buttons = [
this._renderOKButton(options),
this._renderCancelButton(options)
].filter(Boolean);
return function Footer(modalFooterProps) {
return (
<ModalFooter showKeyline = { modalFooterProps.showKeyline } >
{
/**
* Atlaskit has this empty span (JustifySim) so...
*/
}
<span />
<ButtonGroup>
{ buttons }
</ButtonGroup>
</ModalFooter>
);
};
}
_onCancel: () => void;
/**
@@ -188,11 +251,19 @@ class StatelessDialog extends Component<Props> {
/**
* Renders Cancel button.
*
* @param {Object} options - The configuration for the Cancel button.
* @param {boolean} options.cancelDisabled - True if the cancel button
* should not be rendered.
* @param {string} options.cancelTitleKey - The translation key to use as
* text on the button.
* @param {boolean} options.isModal - True if the cancel button should not
* be rendered.
* @private
* @returns {*} The Cancel button if enabled and dialog is not modal.
* @returns {ReactElement|null} The Cancel button if enabled and dialog is
* not modal.
*/
_renderCancelButton() {
if (this.props.cancelDisabled || this.props.isModal) {
_renderCancelButton(options = {}) {
if (options.cancelDisabled || options.isModal) {
return null;
}
@@ -207,62 +278,26 @@ class StatelessDialog extends Component<Props> {
key = 'cancel'
onClick = { this._onCancel }
type = 'button'>
{ t(this.props.cancelTitleKey || 'dialog.Cancel') }
{ t(options.cancelTitleKey || 'dialog.Cancel') }
</Button>
);
}
/**
* Renders component in dialog footer.
*
* @private
* @returns {ReactElement}
*/
_renderFooter() {
// Filter out falsy (null) values because {@code ButtonGroup} will error
// if passed in anything but buttons with valid type props.
const buttons = [
this._renderCancelButton(),
this._renderOKButton()
].filter(Boolean);
return (
<footer className = 'modal-dialog-footer'>
<ButtonGroup>
{ buttons }
</ButtonGroup>
</footer>
);
}
/**
* Renders component in dialog header.
*
* @private
* @returns {ReactElement}
*/
_renderHeader() {
const {
t /* The following fixes a flow error: */ = _.identity
} = this.props;
return (
<header>
<h3>
{ this.props.titleString || t(this.props.titleKey) }
</h3>
</header>
);
}
/**
* Renders OK button.
*
* @param {Object} options - The configuration for the OK button.
* @param {boolean} options.okDisabled - True if the button should display
* as disabled and clicking should have no effect.
* @param {string} options.okTitleKey - The translation key to use as text
* on the button.
* @param {boolean} options.submitDisabled - True if the button should not
* be rendered.
* @private
* @returns {*} The OK button if enabled.
* @returns {ReactElement|null} The OK button if enabled.
*/
_renderOKButton() {
if (this.props.submitDisabled) {
_renderOKButton(options = {}) {
if (options.submitDisabled) {
return null;
}
@@ -275,11 +310,11 @@ class StatelessDialog extends Component<Props> {
appearance = 'primary'
form = 'modal-dialog-form'
id = { OK_BUTTON_ID }
isDisabled = { this.props.okDisabled }
isDisabled = { options.okDisabled }
key = 'submit'
onClick = { this._onSubmit }
type = 'button'>
{ t(this.props.okTitleKey || 'dialog.Ok') }
{ t(options.okTitleKey || 'dialog.Ok') }
</Button>
);
}
@@ -327,40 +362,6 @@ class StatelessDialog extends Component<Props> {
}
}
}
/**
* Updates focused button, if we have a reference to the dialog element.
* Focus on available button if there is no focus already.
*
* @private
* @returns {void}
*/
_updateButtonFocus() {
const dialogElement = this._dialogElement;
if (dialogElement) {
// if we have a focused element inside the dialog, skip changing
// the focus
if (dialogElement.contains(document.activeElement)) {
return;
}
let buttonToFocus;
if (this.props.submitDisabled) {
buttonToFocus
= dialogElement.querySelector(`[id=${CANCEL_BUTTON_ID}]`);
} else if (!this.props.okDisabled) {
buttonToFocus
= dialogElement.querySelector(`[id=${OK_BUTTON_ID}]`);
}
if (buttonToFocus) {
buttonToFocus.focus();
}
}
}
}
export default translate(StatelessDialog);

View File

@@ -1,6 +1,5 @@
// @flow
import { setConfigFromURLParams } from '../config';
import { toState } from '../redux';
import { loadScript } from '../util';
@@ -12,7 +11,7 @@ const JitsiConferenceErrors = JitsiMeetJS.errors.conference;
const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
/**
* Creates a JitsiLocalTrack model from the given device id.
* Creates a {@link JitsiLocalTrack} model from the given device id.
*
* @param {string} type - The media type of track being created. Expected values
* are "video" or "audio".
@@ -107,9 +106,9 @@ export function isFatalJitsiConnectionError(error: Object | string) {
* Loads config.js from a specific remote server.
*
* @param {string} url - The URL to load.
* @param {number} [timeout] - The timeout for the configuration to be loaded,
* in milliseconds. If not specified, a default value deamed appropriate for the
* purpsoe is used.
* @param {number} [timeout] - The timeout in milliseconds for the {@code url}
* to load. If not specified, a default value deemed appropriate for the purpose
* is used.
* @returns {Promise<Object>}
*/
export function loadConfig(
@@ -145,14 +144,5 @@ export function loadConfig(
promise = Promise.resolve(window.config);
}
// FIXME It's neither here nor there at the time of this writing where
// config, interfaceConfig, and loggingConfig should be overwritten by URL
// params.
promise = promise.then(value => {
setConfigFromURLParams();
return value;
});
return promise;
}

View File

@@ -1,17 +1,17 @@
// @flow
declare var JitsiMeetJS: Object;
import { loadScript as loadScriptF } from 'js-utils';
/**
* Loads a script from a specific URL. The script will be interpreted upon load.
*
* @param {string} url - The url to be loaded.
* @returns {Promise} Resolved with no arguments when the script is loaded and
* rejected with the error from JitsiMeetJS.ScriptUtil.loadScript method.
* rejected with the error from loadScriptF method.
*/
export function loadScript(url: string): Promise<void> {
return new Promise((resolve, reject) =>
JitsiMeetJS.util.ScriptUtil.loadScript(
loadScriptF(
url,
/* async */ true,
/* prepend */ false,

View File

@@ -1,5 +1,6 @@
/* global JitsiMeetJS */
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import Logger from 'jitsi-meet-logger';
import React from 'react';
import ReactDOM from 'react-dom';
@@ -234,7 +235,9 @@ export default class DeviceSelectionPopup {
ReactDOM.render(
<I18nextProvider
i18n = { this._i18next }>
<DeviceSelectionDialogBase { ...props } />
<AtlasKitThemeProvider mode = 'dark'>
<DeviceSelectionDialogBase { ...props } />
</AtlasKitThemeProvider>
</I18nextProvider>,
document.getElementById('react'));
}

View File

@@ -1,5 +1,6 @@
/* global interfaceConfig */
import { FieldTextAreaStateless } from '@atlaskit/field-text-area';
import StarIcon from '@atlaskit/icon/glyph/star';
import StarFilledIcon from '@atlaskit/icon/glyph/star-filled';
import PropTypes from 'prop-types';
@@ -219,12 +220,14 @@ class FeedbackDialog extends Component {
</div>
</div>
<div className = 'details'>
<textarea
<FieldTextAreaStateless
autoFocus = { true }
className = 'input-control'
id = 'feedbackTextArea'
isLabelHidden = { true }
onChange = { this._onMessageChange }
placeholder = { t('dialog.feedbackHelp') }
shouldFitContainer = { true }
value = { message } />
</div>
</div>

View File

@@ -2,6 +2,8 @@
import { NativeModules } from 'react-native';
import { Platform } from '../../base/react';
import {
ENTER_PICTURE_IN_PICTURE,
_SET_EMITTER_SUBSCRIPTIONS
@@ -28,10 +30,12 @@ export function enterPictureInPicture() {
&& (conference || joining)) {
const { PictureInPicture } = NativeModules;
const p
= PictureInPicture
? PictureInPicture.enterPictureInPicture()
: Promise.reject(
new Error('Picture-in-Picture not supported'));
= Platform.OS === 'android'
? PictureInPicture
? PictureInPicture.enterPictureInPicture()
: Promise.reject(
new Error('Picture-in-Picture not supported'))
: Promise.resolve();
p.then(
() => dispatch({ type: ENTER_PICTURE_IN_PICTURE }),

View File

@@ -0,0 +1,168 @@
import {
DropdownItem,
DropdownItemGroup,
DropdownMenuStateless
} from '@atlaskit/dropdown-menu';
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { translate } from '../../../base/i18n';
/**
* A dropdown to select a YouTube broadcast.
*
* @extends Component
*/
class BroadcastsDropdown extends PureComponent {
/**
* Default values for {@code StreamKeyForm} component's properties.
*
* @static
*/
static defaultProps = {
broadcasts: []
};
/**
* {@code BroadcastsDropdown} component's property types.
*/
static propTypes = {
/**
* Broadcasts available for selection. Each broadcast item should be an
* object with a title for display in the dropdown and a boundStreamID
* to return in the {@link onBroadcastSelected} callback.
*/
broadcasts: PropTypes.array,
/**
* Callback invoked when an item in the dropdown is selected. The
* selected broadcast's boundStreamID will be passed back.
*/
onBroadcastSelected: PropTypes.func,
/**
* The boundStreamID of the broadcast that should display as selected in
* the dropdown.
*/
selectedBroadcastID: PropTypes.string,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* The initial state of a {@code StreamKeyForm} instance.
*
* @type {{
* isDropdownOpen: boolean
* }}
*/
state = {
isDropdownOpen: false
};
/**
* Initializes a new {@code BroadcastsDropdown} instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* the new {@code BroadcastsDropdown} instance with.
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onDropdownOpenChange = this._onDropdownOpenChange.bind(this);
this._onSelect = this._onSelect.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { broadcasts, selectedBroadcastID, t } = this.props;
const dropdownItems = broadcasts.map(broadcast =>
// eslint-disable-next-line react/jsx-wrap-multilines
<DropdownItem
key = { broadcast.boundStreamID }
// eslint-disable-next-line react/jsx-no-bind
onClick = { () => this._onSelect(broadcast.boundStreamID) }>
{ broadcast.title }
</DropdownItem>
);
const selected = this.props.broadcasts.find(
broadcast => broadcast.boundStreamID === selectedBroadcastID);
const triggerText = (selected && selected.title)
|| t('liveStreaming.choose');
return (
<div className = 'broadcast-dropdown'>
<DropdownMenuStateless
isOpen = { this.state.isDropdownOpen }
onItemActivated = { this._onSelect }
onOpenChange = { this._onDropdownOpenChange }
shouldFitContainer = { true }
trigger = { triggerText }
triggerButtonProps = {{
className: 'broadcast-dropdown-trigger',
shouldFitContainer: true
}}
triggerType = 'button'>
<DropdownItemGroup>
{ dropdownItems }
</DropdownItemGroup>
</DropdownMenuStateless>
</div>
);
}
/**
* Transforms the passed in broadcasts into an array of objects that can
* be parsed by {@code DropdownMenuStateless}.
*
* @param {Array<Object>} broadcasts - The YouTube broadcasts to display.
* @private
* @returns {Array<Object>}
*/
_formatBroadcasts(broadcasts) {
return broadcasts.map(broadcast => {
return {
content: broadcast.title,
value: broadcast
};
});
}
/**
* Sets the dropdown to be displayed or not based on the passed in event.
*
* @param {Object} dropdownEvent - The event passed from
* {@code DropdownMenuStateless} indicating if the dropdown should be open
* or closed.
* @private
* @returns {void}
*/
_onDropdownOpenChange(dropdownEvent) {
this.setState({
isDropdownOpen: dropdownEvent.isOpen
});
}
/**
* Callback invoked when an item has been clicked in the dropdown menu.
*
* @param {Object} boundStreamID - The bound stream ID for the selected
* broadcast.
* @returns {void}
*/
_onSelect(boundStreamID) {
this.props.onBroadcastSelected(boundStreamID);
}
}
export default translate(BroadcastsDropdown);

View File

@@ -0,0 +1,47 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
/**
* A React Component showing a button to sign in with Google.
*
* @extends Component
*/
export default class GoogleSignInButton extends Component {
/**
* {@code GoogleSignInButton} component's property types.
*
* @static
*/
static propTypes = {
/**
* The callback to invoke when the button is clicked.
*/
onClick: PropTypes.func,
/**
* The text to display in the button.
*/
text: PropTypes.string
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<div
className = 'google-sign-in'
onClick = { this.props.onClick }>
<img
className = 'google-logo'
src = 'images/googleLogo.svg' />
<div className = 'google-cta'>
{ this.props.text }
</div>
</div>
);
}
}

View File

@@ -0,0 +1,462 @@
/* globals APP, interfaceConfig */
import Spinner from '@atlaskit/spinner';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import googleApi from '../../googleApi';
import BroadcastsDropdown from './BroadcastsDropdown';
import GoogleSignInButton from './GoogleSignInButton';
import StreamKeyForm from './StreamKeyForm';
/**
* An enumeration of the different states the Google API can be in while
* interacting with {@code StartLiveStreamDialog}.
*
* @private
* @type {Object}
*/
const GOOGLE_API_STATES = {
/**
* The state in which the Google API still needs to be loaded.
*/
NEEDS_LOADING: 0,
/**
* The state in which the Google API is loaded and ready for use.
*/
LOADED: 1,
/**
* The state in which a user has been logged in through the Google API.
*/
SIGNED_IN: 2,
/**
* The state in which the Google API encountered an error either loading
* or with an API request.
*/
ERROR: 3
};
/**
* A React Component for requesting a YouTube stream key to use for live
* streaming of the current conference.
*
* @extends Component
*/
class StartLiveStreamDialog extends Component {
/**
* {@code StartLiveStreamDialog} component's property types.
*
* @static
*/
static propTypes = {
/**
* The ID for the Google web client application used for making stream
* key related requests.
*/
_googleApiApplicationClientID: PropTypes.string,
/**
* Callback to invoke when the dialog is dismissed without submitting a
* stream key.
*/
onCancel: PropTypes.func,
/**
* Callback to invoke when a stream key is submitted for use.
*/
onSubmit: PropTypes.func,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* {@code StartLiveStreamDialog} component's local state.
*
* @property {boolean} googleAPIState - The current state of interactions
* with the Google API. Determines what Google related UI should display.
* @property {Object[]|undefined} broadcasts - Details about the broadcasts
* available for use for the logged in Google user's YouTube account.
* @property {string} googleProfileEmail - The email of the user currently
* logged in to the Google web client application.
* @property {string} streamKey - The selected or entered stream key to use
* for YouTube live streaming.
*/
state = {
broadcasts: undefined,
googleAPIState: GOOGLE_API_STATES.NEEDS_LOADING,
googleProfileEmail: '',
selectedBroadcastID: undefined,
streamKey: ''
};
/**
* Initializes a new {@code StartLiveStreamDialog} instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* the new {@code StartLiveStreamDialog} instance with.
*/
constructor(props) {
super(props);
/**
* Instance variable used to flag whether the component is or is not
* mounted. Used as a hack to avoid setting state on an unmounted
* component.
*
* @private
* @type {boolean}
*/
this._isMounted = false;
// Bind event handlers so they are only bound once per instance.
this._onCancel = this._onCancel.bind(this);
this._onGetYouTubeBroadcasts = this._onGetYouTubeBroadcasts.bind(this);
this._onInitializeGoogleApi = this._onInitializeGoogleApi.bind(this);
this._onRequestGoogleSignIn = this._onRequestGoogleSignIn.bind(this);
this._onStreamKeyChange = this._onStreamKeyChange.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._onYouTubeBroadcastIDSelected
= this._onYouTubeBroadcastIDSelected.bind(this);
}
/**
* Implements {@link Component#componentDidMount()}. Invoked immediately
* after this component is mounted.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
this._isMounted = true;
if (this.props._googleApiApplicationClientID) {
this._onInitializeGoogleApi();
}
}
/**
* Implements React's {@link Component#componentWillUnmount()}. Invoked
* immediately before this component is unmounted and destroyed.
*
* @inheritdoc
*/
componentWillUnmount() {
this._isMounted = false;
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _googleApiApplicationClientID } = this.props;
return (
<Dialog
cancelTitleKey = 'dialog.Cancel'
okTitleKey = 'dialog.startLiveStreaming'
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
titleKey = 'liveStreaming.start'
width = { 'small' }>
<div className = 'live-stream-dialog'>
{ _googleApiApplicationClientID
? this._renderYouTubePanel() : null }
<StreamKeyForm
helpURL = { interfaceConfig.LIVE_STREAMING_HELP_LINK }
onChange = { this._onStreamKeyChange }
value = { this.state.streamKey } />
</div>
</Dialog>
);
}
/**
* Loads the Google web client application used for fetching stream keys.
* If the user is already logged in, then a request for available YouTube
* broadcasts is also made.
*
* @private
* @returns {Promise}
*/
_onInitializeGoogleApi() {
return googleApi.get()
.then(() => googleApi.initializeClient(
this.props._googleApiApplicationClientID))
.then(() => this._setStateIfMounted({
googleAPIState: GOOGLE_API_STATES.LOADED
}))
.then(() => googleApi.isSignedIn())
.then(isSignedIn => {
if (isSignedIn) {
return this._onGetYouTubeBroadcasts();
}
})
.catch(() => {
this._setStateIfMounted({
googleAPIState: GOOGLE_API_STATES.ERROR
});
});
}
/**
* Invokes the passed in {@link onCancel} callback and closes
* {@code StartLiveStreamDialog}.
*
* @private
* @returns {boolean} True is returned to close the modal.
*/
_onCancel() {
this.props.onCancel(APP.UI.messageHandler.CANCEL);
return true;
}
/**
* Asks the user to sign in, if not already signed in, and then requests a
* list of the user's YouTube broadcasts.
*
* @private
* @returns {Promise}
*/
_onGetYouTubeBroadcasts() {
return googleApi.get()
.then(() => googleApi.signInIfNotSignedIn())
.then(() => googleApi.getCurrentUserProfile())
.then(profile => {
this._setStateIfMounted({
googleProfileEmail: profile.getEmail(),
googleAPIState: GOOGLE_API_STATES.SIGNED_IN
});
})
.then(() => googleApi.requestAvailableYouTubeBroadcasts())
.then(response => {
const broadcasts = response.result.items.map(item => {
return {
title: item.snippet.title,
boundStreamID: item.contentDetails.boundStreamId,
status: item.status.lifeCycleStatus
};
});
this._setStateIfMounted({
broadcasts
});
if (broadcasts.length === 1 && !this.state.streamKey) {
const broadcast = broadcasts[0];
this._onYouTubeBroadcastIDSelected(broadcast.boundStreamID);
}
})
.catch(response => {
// Only show an error if an external request was made with the
// Google api. Do not error if the login in canceled.
if (response && response.result) {
this._setStateIfMounted({
googleAPIState: GOOGLE_API_STATES.ERROR
});
}
});
}
/**
* Forces the Google web client application to prompt for a sign in, such as
* when changing account, and will then fetch available YouTube broadcasts.
*
* @private
* @returns {Promise}
*/
_onRequestGoogleSignIn() {
return googleApi.showAccountSelection()
.then(() => this._setStateIfMounted({ broadcasts: undefined }))
.then(() => this._onGetYouTubeBroadcasts());
}
/**
* Callback invoked to update the {@code StartLiveStreamDialog} component's
* display of the entered YouTube stream key.
*
* @param {Object} event - DOM Event for value change.
* @private
* @returns {void}
*/
_onStreamKeyChange(event) {
this._setStateIfMounted({
streamKey: event.target.value,
selectedBroadcastID: undefined
});
}
/**
* Invokes the passed in {@link onSubmit} callback with the entered stream
* key, and then closes {@code StartLiveStreamDialog}.
*
* @private
* @returns {boolean} False if no stream key is entered to preventing
* closing, true to close the modal.
*/
_onSubmit() {
if (!this.state.streamKey) {
return false;
}
this.props.onSubmit(this.state.streamKey);
return true;
}
/**
* Fetches the stream key for a YouTube broadcast and updates the internal
* state to display the associated stream key as being entered.
*
* @param {string} boundStreamID - The bound stream ID associated with the
* broadcast from which to get the stream key.
* @private
* @returns {Promise}
*/
_onYouTubeBroadcastIDSelected(boundStreamID) {
return googleApi.requestLiveStreamsForYouTubeBroadcast(boundStreamID)
.then(response => {
const found = response.result.items[0];
const streamKey = found.cdn.ingestionInfo.streamName;
this._setStateIfMounted({
streamKey,
selectedBroadcastID: boundStreamID
});
});
}
/**
* Renders a React Element for authenticating with the Google web client.
*
* @private
* @returns {ReactElement}
*/
_renderYouTubePanel() {
const { t } = this.props;
const {
broadcasts,
googleProfileEmail,
selectedBroadcastID
} = this.state;
let googleContent, helpText;
switch (this.state.googleAPIState) {
case GOOGLE_API_STATES.LOADED:
googleContent = ( // eslint-disable-line no-extra-parens
<GoogleSignInButton
onClick = { this._onGetYouTubeBroadcasts }
text = { t('liveStreaming.signIn') } />
);
helpText = t('liveStreaming.signInCTA');
break;
case GOOGLE_API_STATES.SIGNED_IN:
googleContent = ( // eslint-disable-line no-extra-parens
<BroadcastsDropdown
broadcasts = { broadcasts }
onBroadcastSelected = { this._onYouTubeBroadcastIDSelected }
selectedBroadcastID = { selectedBroadcastID } />
);
/**
* FIXME: Ideally this help text would be one translation string
* that also accepts the anchor. This can be done using the Trans
* component of react-i18next but I couldn't get it working...
*/
helpText = ( // eslint-disable-line no-extra-parens
<div>
{ `${t('liveStreaming.chooseCTA',
{ email: googleProfileEmail })} ` }
<a onClick = { this._onRequestGoogleSignIn }>
{ t('liveStreaming.changeSignIn') }
</a>
</div>
);
break;
case GOOGLE_API_STATES.ERROR:
googleContent = ( // eslint-disable-line no-extra-parens
<GoogleSignInButton
onClick = { this._onRequestGoogleSignIn }
text = { t('liveStreaming.signIn') } />
);
helpText = t('liveStreaming.errorAPI');
break;
case GOOGLE_API_STATES.NEEDS_LOADING:
default:
googleContent = ( // eslint-disable-line no-extra-parens
<Spinner
isCompleting = { false }
size = 'medium' />
);
break;
}
return (
<div className = 'google-panel'>
<div className = 'live-stream-cta'>
{ helpText }
</div>
<div className = 'google-api'>
{ googleContent }
</div>
</div>
);
}
/**
* Updates the internal state if the component is still mounted. This is a
* workaround for all the state setting that occurs after ajax.
*
* @param {Object} newState - The new state to merge into the existing
* state.
* @private
* @returns {void}
*/
_setStateIfMounted(newState) {
if (this._isMounted) {
this.setState(newState);
}
}
}
/**
* Maps (parts of) the redux state to the React {@code Component} props of
* {@code StartLiveStreamDialog}.
*
* @param {Object} state - The redux state.
* @protected
* @returns {{
* _googleApiApplicationClientID: string
* }}
*/
function _mapStateToProps(state) {
return {
_googleApiApplicationClientID:
state['features/base/config'].googleApiApplicationClientID
};
}
export default translate(connect(_mapStateToProps)(StartLiveStreamDialog));

View File

@@ -0,0 +1,82 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
/**
* A React Component for confirming the participant wishes to stop the currently
* active live stream of the conference.
*
* @extends Component
*/
class StopLiveStreamDialog extends Component {
/**
* {@code StopLiveStreamDialog} component's property types.
*
* @static
*/
static propTypes = {
/**
* Callback to invoke when the dialog is dismissed without confirming
* the live stream should be stopped.
*/
onCancel: PropTypes.func,
/**
* Callback to invoke when confirming the live stream should be stopped.
*/
onSubmit: PropTypes.func,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* Initializes a new {@code StopLiveStreamDialog} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onSubmit = this._onSubmit.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<Dialog
okTitleKey = 'dialog.stopLiveStreaming'
onCancel = { this.props.onCancel }
onSubmit = { this._onSubmit }
titleKey = 'dialog.liveStreaming'
width = 'small'>
{ this.props.t('dialog.stopStreamingWarning') }
</Dialog>
);
}
/**
* Callback invoked when stopping of live streaming is confirmed.
*
* @private
* @returns {boolean} True to close the modal.
*/
_onSubmit() {
this.props.onSubmit();
return true;
}
}
export default translate(StopLiveStreamDialog);

View File

@@ -0,0 +1,115 @@
import { FieldTextStateless } from '@atlaskit/field-text';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { translate } from '../../../base/i18n';
/**
* A React Component for entering a key for starting a YouTube live stream.
*
* @extends Component
*/
class StreamKeyForm extends Component {
/**
* {@code StreamKeyForm} component's property types.
*
* @static
*/
static propTypes = {
/**
* The URL to the page with more information for manually finding the
* stream key for a YouTube broadcast.
*/
helpURL: PropTypes.string,
/**
* Callback invoked when the entered stream key has changed.
*/
onChange: PropTypes.func,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func,
/**
* The stream key value to display as having been entered so far.
*/
value: PropTypes.string
};
/**
* Initializes a new {@code StreamKeyForm} instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* the new {@code StreamKeyForm} instance with.
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onInputChange = this._onInputChange.bind(this);
this._onOpenHelp = this._onOpenHelp.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
return (
<div className = 'stream-key-form'>
<FieldTextStateless
autoFocus = { true }
compact = { true }
label = { t('dialog.streamKey') }
name = 'streamId'
okDisabled = { !this.props.value }
onChange = { this._onInputChange }
placeholder = { t('liveStreaming.enterStreamKey') }
shouldFitContainer = { true }
type = 'text'
value = { this.props.value } />
{ this.props.helpURL
? <div className = 'form-footer'>
<a
className = 'helper-link'
onClick = { this._onOpenHelp }>
{ t('liveStreaming.streamIdHelp') }
</a>
</div>
: null
}
</div>
);
}
/**
* Callback invoked when the value of the input field has updated through
* user input.
*
* @param {Object} event - DOM Event for value change.
* @private
* @returns {void}
*/
_onInputChange(event) {
this.props.onChange(event);
}
/**
* Opens a new tab with information on how to manually locate a YouTube
* broadcast stream key.
*
* @private
* @returns {void}
*/
_onOpenHelp() {
window.open(this.props.helpURL, 'noopener');
}
}
export default translate(StreamKeyForm);

View File

@@ -0,0 +1,2 @@
export { default as StartLiveStreamDialog } from './StartLiveStreamDialog';
export { default as StopLiveStreamDialog } from './StopLiveStreamDialog';

View File

@@ -1 +1,2 @@
export { StartLiveStreamDialog, StopLiveStreamDialog } from './LiveStream';
export { default as RecordingLabel } from './RecordingLabel';

View File

@@ -0,0 +1,230 @@
const GOOGLE_API_CLIENT_LIBRARY_URL = 'https://apis.google.com/js/api.js';
const GOOGLE_API_SCOPES = [
'https://www.googleapis.com/auth/youtube.readonly'
].join(' ');
/**
* A promise for dynamically loading the Google API Client Library.
*
* @private
* @type {Promise}
*/
let googleClientLoadPromise;
/**
* A singleton for loading and interacting with the Google API.
*/
const googleApi = {
/**
* Obtains Google API Client Library, loading the library dynamically if
* needed.
*
* @returns {Promise}
*/
get() {
const globalGoogleApi = this._getGoogleApiClient();
if (!globalGoogleApi) {
return this.load();
}
return Promise.resolve(globalGoogleApi);
},
/**
* Gets the profile for the user signed in to the Google API Client Library.
*
* @returns {Promise}
*/
getCurrentUserProfile() {
return this.get()
.then(() => this.isSignedIn())
.then(isSignedIn => {
if (!isSignedIn) {
return null;
}
return this._getGoogleApiClient()
.auth2.getAuthInstance()
.currentUser.get()
.getBasicProfile();
});
},
/**
* Sets the Google Web Client ID used for authenticating with Google and
* making Google API requests.
*
* @param {string} clientId - The client ID to be used with the API library.
* @returns {Promise}
*/
initializeClient(clientId) {
return this.get()
.then(api => new Promise((resolve, reject) => {
// setTimeout is used as a workaround for api.client.init not
// resolving consistently when the Google API Client Library is
// loaded asynchronously. See:
// github.com/google/google-api-javascript-client/issues/399
setTimeout(() => {
api.client.init({
clientId,
scope: GOOGLE_API_SCOPES
})
.then(resolve)
.catch(reject);
}, 500);
}));
},
/**
* Checks whether a user is currently authenticated with Google through an
* initialized Google API Client Library.
*
* @returns {Promise}
*/
isSignedIn() {
return this.get()
.then(api => Boolean(api
&& api.auth2
&& api.auth2.getAuthInstance
&& api.auth2.getAuthInstance().isSignedIn
&& api.auth2.getAuthInstance().isSignedIn.get()));
},
/**
* Generates a script tag and downloads the Google API Client Library.
*
* @returns {Promise}
*/
load() {
if (googleClientLoadPromise) {
return googleClientLoadPromise;
}
googleClientLoadPromise = new Promise((resolve, reject) => {
const scriptTag = document.createElement('script');
scriptTag.async = true;
scriptTag.addEventListener('error', () => {
scriptTag.remove();
googleClientLoadPromise = null;
reject();
});
scriptTag.addEventListener('load', resolve);
scriptTag.type = 'text/javascript';
scriptTag.src = GOOGLE_API_CLIENT_LIBRARY_URL;
document.head.appendChild(scriptTag);
})
.then(() => new Promise((resolve, reject) =>
this._getGoogleApiClient().load('client:auth2', {
callback: resolve,
onerror: reject
})))
.then(() => this._getGoogleApiClient());
return googleClientLoadPromise;
},
/**
* Executes a request for a list of all YouTube broadcasts associated with
* user currently signed in to the Google API Client Library.
*
* @returns {Promise}
*/
requestAvailableYouTubeBroadcasts() {
const url = this._getURLForLiveBroadcasts();
return this.get()
.then(api => api.client.request(url));
},
/**
* Executes a request to get all live streams associated with a broadcast
* in YouTube.
*
* @param {string} boundStreamID - The bound stream ID associated with a
* broadcast in YouTube.
* @returns {Promise}
*/
requestLiveStreamsForYouTubeBroadcast(boundStreamID) {
const url = this._getURLForLiveStreams(boundStreamID);
return this.get()
.then(api => api.client.request(url));
},
/**
* Prompts the participant to sign in to the Google API Client Library, even
* if already signed in.
*
* @returns {Promise}
*/
showAccountSelection() {
return this.get()
.then(api => api.auth2.getAuthInstance().signIn());
},
/**
* Prompts the participant to sign in to the Google API Client Library, if
* not already signed in.
*
* @returns {Promise}
*/
signInIfNotSignedIn() {
return this.get()
.then(() => this.isSignedIn())
.then(isSignedIn => {
if (!isSignedIn) {
return this.showAccountSelection();
}
});
},
/**
* Returns the global Google API Client Library object. Direct use of this
* method is discouraged; instead use the {@link get} method.
*
* @private
* @returns {Object|undefined}
*/
_getGoogleApiClient() {
return window.gapi;
},
/**
* Returns the URL to the Google API endpoint for retrieving the currently
* signed in user's YouTube broadcasts.
*
* @private
* @returns {string}
*/
_getURLForLiveBroadcasts() {
return [
'https://content.googleapis.com/youtube/v3/liveBroadcasts',
'?broadcastType=persistent',
'&mine=true&part=id%2Csnippet%2CcontentDetails%2Cstatus'
].join('');
},
/**
* Returns the URL to the Google API endpoint for retrieving the live
* streams associated with a YouTube broadcast's bound stream.
*
* @param {string} boundStreamID - The bound stream ID associated with a
* broadcast in YouTube.
* @returns {string}
*/
_getURLForLiveStreams(boundStreamID) {
return [
'https://content.googleapis.com/youtube/v3/liveStreams',
'?part=id%2Csnippet%2Ccdn%2Cstatus',
`&id=${boundStreamID}`
].join('');
}
};
export default googleApi;

View File

@@ -1,4 +1,4 @@
/* global interfaceConfig */
/* global APP, config, interfaceConfig, JitsiMeetJS */
import Button from '@atlaskit/button';
import { FieldTextStateless } from '@atlaskit/field-text';
@@ -6,7 +6,9 @@ import { AtlasKitThemeProvider } from '@atlaskit/theme';
import React from 'react';
import { connect } from 'react-redux';
import { initAnalytics } from '../../analytics';
import { translate } from '../../base/i18n';
import { isAnalyticsEnabled } from '../../base/lib-jitsi-meet';
import { Watermarks } from '../../base/react';
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
@@ -66,6 +68,17 @@ class WelcomePage extends AbstractWelcomePage {
* @returns {void}
*/
componentDidMount() {
// FIXME: This is not the best place for this logic. Ideally we should
// use features/base/lib-jitsi-meet#initLib() action for this use case.
// But currently lib-jitsi-meet#initLib()'s logic works for mobile only
// (on web it ends up with infinite loop over initLib).
JitsiMeetJS.init({
enableAnalyticsLogging: isAnalyticsEnabled(APP.store),
...config
}).then(() => {
initAnalytics(APP.store);
});
if (this.state.generateRoomnames) {
this._updateRoomname();
}

View File

@@ -7,6 +7,7 @@ local st = require "util.stanza";
local get_room_from_jid = module:require "util".get_room_from_jid;
local wrap_async_run = module:require "util".wrap_async_run;
local timer = require "util.timer";
local MUC_NS = "http://jabber.org/protocol/muc";
-- Options
local poltergeist_component
@@ -200,12 +201,12 @@ end);
-- @param avatar the avatar to use for the new occupant (optional)
-- @param status the initial status to use for the new occupant (optional)
function create_poltergeist_occupant(room, nick, name, avatar, status)
log("debug", "create_poltergeist_occupant %s:", nick);
log("debug", "create_poltergeist_occupant %s", nick);
-- Join poltergeist occupant to room, with the invited JID as their nick
local join_presence = st.presence({
to = room.jid.."/"..nick,
from = poltergeist_component.."/"..nick
}):tag("x", { xmlns = "http://jabber.org/protocol/muc" }):up();
}):tag("x", { xmlns = MUC_NS }):up();
if (name) then
join_presence:tag(
@@ -219,6 +220,13 @@ function create_poltergeist_occupant(room, nick, name, avatar, status)
join_presence:tag("status"):text(status):up();
end
-- If the room has a password set, let the poltergeist enter using it
local room_password = room:get_password();
if room_password then
local join = join_presence:get_child("x", MUC_NS);
join:tag("password", { xmlns = MUC_NS }):text(room_password);
end
room:handle_first_presence(
prosody.hosts[poltergeist_component], join_presence);

View File

@@ -0,0 +1,40 @@
local stanza = require "util.stanza";
-- For all received presence messages, if the jitsi_meet_context_(user|group)
-- values are set in the session, then insert them into the presence messages
-- for that session.
function on_message(event)
if event and event["stanza"] then
if event.origin and event.origin.jitsi_meet_context_user then
-- First remove any 'identity' element if it already
-- exists
event.stanza:maptags(
function(tag)
for k, v in pairs(tag) do
if k == "name" and v == "identity" then
return nil
end
end
return tag
end
)
module:log("debug", "Presence after previous identity stripped: %s", tostring(event.stanza))
event.stanza:tag("identity"):tag("user")
for k, v in pairs(event.origin.jitsi_meet_context_user) do
event.stanza:tag(k):text(v):up()
end
event.stanza:up()
-- Add the group information if it is present
if event.origin.jitsi_meet_context_group then
event.stanza:tag("group"):text(event.origin.jitsi_meet_context_group)
end
module:log("debug", "Sending presence with identity inserted %s", tostring(event.stanza))
end
end
end
module:hook("pre-presence/bare", on_message);
module:hook("pre-presence/full", on_message);