Compare commits

...

40 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
Saumeya Katyal
2334eb9967 doc: Add dev server steps (#2610)
* doc: Add webpack-dev-server steps
2018-03-19 10:16:01 -05:00
Emil Ivov
04bd4a9038 Merge pull request #2617 from virtuacoplenny/lenny/info-dialog-again
fix(info): update copy text to find correct var
2018-03-15 12:16:20 -05:00
virtuacoplenny
eb8f34cee8 Merge pull request #2612 from jitsi/no_protocol_in_intent_uri
feat(UnsupportedMobileBrowser): do not include protocol in the intent
2018-03-15 10:01:31 -07:00
Leonard Kim
b9379f5996 fix(info): update copy text to find correct var 2018-03-15 09:42:39 -07:00
paweldomas
40d7d0c9cb feat(UnsupportedMobileBrowser): do not include protocol in the intent
Do not include the protocol part in the intent URL.
2018-03-14 17:44:32 -05:00
zbettenbuk
357f173e85 Remove obsolate PlatformElements.native.js 2018-03-13 18:04:17 -05:00
zbettenbuk
7da26042b3 Avoid asking for calendar permission on app start 2018-03-13 18:04:17 -05:00
zbettenbuk
c86c7beb24 Refactor i18n calendar formatter 2018-03-13 18:04:17 -05:00
zbettenbuk
1020a54a33 Add Android navigation bar 2018-03-13 18:04:17 -05:00
zbettenbuk
c84abd543e Add support for app link scheme 2018-03-13 18:04:16 -05:00
zbettenbuk
4b17c6f015 Add pull-to-refresh functionality 2018-03-13 18:04:16 -05:00
zbettenbuk
cb973b61aa Implement adaptive known domain list 2018-03-13 18:04:16 -05:00
zbettenbuk
b096622995 Unify recent and meeting lists 2018-03-13 18:04:16 -05:00
zbettenbuk
ae0bf876a8 Add conference notification 2018-03-13 18:04:16 -05:00
zbettenbuk
bba480f329 Add calendar-sync feature 2018-03-13 18:04:14 -05:00
106 changed files with 6303 additions and 1010 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

@@ -90,6 +90,19 @@ cd jitsi-meet
npm unlink lib-jitsi-meet
npm install
```
## Running with webpack-dev-server for development
Use it at the CLI, type
```
node_modules/.bin/webpack-dev-server
```
By default the backend deployment used is `beta.meet.jit.si`, you can point the Jitsi-Meet app at a different backend by using a proxy server. To do this set the WEBPACK_DEV_SERVER_PROXY_TARGET variable, type
```
WEBPACK_DEV_SERVER_PROXY_TARGET=https://your-example-server.com node_modules/.bin/webpack-dev-server
```
The app should be running at https://localhost:8080/
## Contributing

View File

@@ -23,6 +23,8 @@ import org.jitsi.meet.sdk.JitsiMeetActivity;
import org.jitsi.meet.sdk.JitsiMeetView;
import org.jitsi.meet.sdk.JitsiMeetViewListener;
import com.calendarevents.CalendarEventsPackage;
import java.util.Map;
/**
@@ -103,4 +105,10 @@ public class MainActivity extends JitsiMeetActivity {
super.onCreate(savedInstanceState);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
CalendarEventsPackage.onRequestPermissionsResult(requestCode, permissions, grantResults);
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}

View File

@@ -32,6 +32,7 @@ dependencies {
compile project(':react-native-sound')
compile project(':react-native-vector-icons')
compile project(':react-native-webrtc')
compile project(':react-native-calendar-events')
}
// Build process helpers

View File

@@ -114,6 +114,7 @@ public class JitsiMeetView extends FrameLayout {
.setApplication(application)
.setBundleAssetName("index.android.bundle")
.setJSMainModulePath("index.android")
.addPackage(new com.calendarevents.CalendarEventsPackage())
.addPackage(new com.corbt.keepawake.KCKeepAwakePackage())
.addPackage(new com.facebook.react.shell.MainReactPackage())
.addPackage(new com.i18n.reactnativei18n.ReactNativeI18n())

View File

@@ -17,3 +17,5 @@ include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
include ':react-native-webrtc'
project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android')
include ':react-native-calendar-events'
project(':react-native-calendar-events').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-calendar-events/android')

View File

@@ -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

@@ -36,9 +36,15 @@
.icon-navigate_before:before {
content: "\e408";
}
.icon-navigate_next:before {
content: "\e409";
}
.icon-public:before {
content: "\e80b";
}
.icon-restore:before {
content: "\e8b3";
}
.icon-timer:before {
content: "\e425";
}

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;

Binary file not shown.

View File

@@ -12,6 +12,7 @@
<glyph unicode="&#xe1aa;" glyph-name="bluetooth" d="M550 328l-80 82v-162zM470 776v-162l80 82zM670 696l-184-184 184-184-244-242h-42v324l-196-196-60 60 238 238-238 238 60 60 196-196v324h42zM834 738c40-64 62-142 62-222 0-84-24-160-66-226l-50 50c26 52 42 110 42 172s-16 120-42 172zM608 512l98 98c12-30 20-64 20-98s-8-70-20-100z" />
<glyph unicode="&#xe310;" glyph-name="headset" d="M512 982c212 0 384-172 384-384v-300c0-70-58-128-128-128h-128v342h170v86c0 166-132 298-298 298s-298-132-298-298v-86h170v-342h-128c-70 0-128 58-128 128v300c0 212 172 384 384 384z" />
<glyph unicode="&#xe408;" glyph-name="navigate_before" d="M658 708l-196-196 196-196-60-60-256 256 256 256z" />
<glyph unicode="&#xe409;" glyph-name="navigate_next" d="M426 768l256-256-256-256-60 60 196 196-196 196z" />
<glyph unicode="&#xe425;" glyph-name="timer" d="M512 170c166 0 298 134 298 300s-132 298-298 298-298-132-298-298 132-300 298-300zM812 708c52-66 84-148 84-238 0-212-172-384-384-384s-384 172-384 384 172 384 384 384c90 0 174-34 240-86l60 62c22-18 42-38 60-60zM470 426v256h84v-256h-84zM640 982v-86h-256v86h256z" />
<glyph unicode="&#xe5c4;" glyph-name="arrow_back" d="M854 554v-84h-520l238-240-60-60-342 342 342 342 60-60-238-240h520z" />
<glyph unicode="&#xe5d2;" glyph-name="menu" d="M128 768h768v-86h-768v86zM128 470v84h768v-84h-768zM128 256v86h768v-86h-768z" />
@@ -22,6 +23,7 @@
<glyph unicode="&#xe616;" glyph-name="event_note" d="M598 426v-84h-300v84h300zM810 214v468h-596v-468h596zM810 896c46 0 86-40 86-86v-596c0-46-40-86-86-86h-596c-48 0-86 40-86 86v596c0 46 38 86 86 86h42v86h86v-86h340v86h86v-86h42zM726 598v-86h-428v86h428z" />
<glyph unicode="&#xe61d;" glyph-name="phone-talk" d="M640 512c0 70-58 128-128 128v86c118 0 214-96 214-214h-86zM810 512c0 166-132 298-298 298v86c212 0 384-172 384-384h-86zM854 362c24 0 42-18 42-42v-150c0-24-18-42-42-42-400 0-726 326-726 726 0 24 18 42 42 42h150c24 0 42-18 42-42 0-54 8-104 24-152 4-14 2-32-10-44l-94-94c62-122 162-220 282-282l94 94c12 12 30 14 44 10 48-16 98-24 152-24z" />
<glyph unicode="&#xe80b;" glyph-name="public" d="M764 282c56 60 90 142 90 230 0 142-88 266-214 316v-18c0-46-40-84-86-84h-84v-86c0-24-20-42-44-42h-84v-86h256c24 0 42-18 42-42v-128h42c38 0 70-26 82-60zM470 174v82c-46 0-86 40-86 86v42l-204 204c-6-24-10-50-10-76 0-174 132-318 300-338zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
<glyph unicode="&#xe8b3;" glyph-name="restore" d="M512 682h64v-180l150-90-32-52-182 110v212zM554 896c212 0 384-172 384-384s-172-384-384-384c-106 0-200 42-270 112l60 62c54-54 128-88 210-88 166 0 300 132 300 298s-134 298-300 298-298-132-298-298h128l-172-172-4 6-166 166h128c0 212 172 384 384 384z" />
<glyph unicode="&#xe901;" glyph-name="avatar" d="M512 204c106 0 200 56 256 138-2 84-172 132-256 132-86 0-254-48-256-132 56-82 150-138 256-138zM512 810c-70 0-128-58-128-128s58-128 128-128 128 58 128 128-58 128-128 128zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
<glyph unicode="&#xe902;" glyph-name="download" d="M726 470h-128v170h-172v-170h-128l214-214zM826 596c110-8 198-100 198-212 0-118-96-214-214-214h-554c-142 0-256 114-256 256 0 132 100 240 228 254 54 102 160 174 284 174 156 0 284-110 314-258z" />
<glyph unicode="&#xe903;" glyph-name="mic-camera-combined" d="M756.704 628.138l267.296 202.213v-635.075l-267.296 202.213v-191.923c0-12.085-11.296-21.863-25.216-21.863h-706.272c-13.92 0-25.216 9.777-25.216 21.863v612.25c0 12.085 11.296 21.863 25.216 21.863h706.272c13.92 0 25.216-9.777 25.216-21.863v-189.679zM371.338 376.228c47.817 0 86.529 40.232 86.529 89.811v184.835c0 49.651-38.713 89.883-86.529 89.883-47.788 0-86.515-40.232-86.515-89.883v-184.835c0-49.579 38.756-89.811 86.515-89.811v0zM356.754 314.070v-32.78h33.718v33.412c73.858 9.606 131.235 73.73 131.235 151.351v88.232h-30.636v-88.232c0-67.57-53.696-122.534-119.734-122.534-66.024 0-119.691 54.964-119.691 122.534v88.232h-30.636v-88.232c0-79.215 59.674-144.502 135.744-151.969v-0.014z" />

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,60 @@
{
"IcoMoonType": "selection",
"icons": [
{
"icon": {
"paths": [
"M512 342h64v180l150 90-32 52-182-110v-212zM554 128c212 0 384 172 384 384s-172 384-384 384c-106 0-200-42-270-112l60-62c54 54 128 88 210 88 166 0 300-132 300-298s-134-298-300-298-298 132-298 298h128l-172 172-4-6-166-166h128c0-212 172-384 384-384z"
],
"attrs": [],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"restore"
],
"defaultCode": 59571,
"grid": 24
},
"attrs": [],
"properties": {
"ligatures": "history, restore",
"id": 385,
"order": 930,
"prevSize": 24,
"code": 59571,
"name": "restore"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 385
},
{
"icon": {
"paths": [
"M426 256l256 256-256 256-60-60 196-196-196-196z"
],
"attrs": [],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"navigate_next"
],
"defaultCode": 58377,
"grid": 24
},
"attrs": [],
"properties": {
"ligatures": "chevron_right, navigate_next",
"id": 153,
"order": 927,
"prevSize": 24,
"code": 58377,
"name": "navigate_next"
},
"setIdx": 1,
"setId": 1,
"iconIdx": 0
},
{
"icon": {
"paths": [
@@ -24,9 +78,9 @@
"code": 58834,
"name": "menu"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 489
"setIdx": 1,
"setId": 1,
"iconIdx": 1
},
{
"icon": {
@@ -53,7 +107,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 0
"iconIdx": 2
},
{
"icon": {
@@ -80,7 +134,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 1
"iconIdx": 3
},
{
"icon": {
@@ -107,7 +161,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 2
"iconIdx": 4
},
{
"icon": {
@@ -134,7 +188,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 3
"iconIdx": 5
},
{
"icon": {
@@ -154,14 +208,14 @@
"properties": {
"ligatures": "timer",
"id": 760,
"order": 916,
"order": 928,
"prevSize": 24,
"code": 58405,
"name": "timer"
},
"setIdx": 1,
"setId": 1,
"iconIdx": 4
"iconIdx": 6
},
{
"icon": {
@@ -188,7 +242,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 5
"iconIdx": 7
},
{
"icon": {
@@ -215,7 +269,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 6
"iconIdx": 8
},
{
"icon": {
@@ -242,7 +296,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 7
"iconIdx": 9
},
{
"icon": {
@@ -269,7 +323,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 8
"iconIdx": 10
},
{
"icon": {
@@ -298,7 +352,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 9
"iconIdx": 11
},
{
"icon": {
@@ -325,7 +379,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 10
"iconIdx": 12
},
{
"icon": {
@@ -352,7 +406,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 11
"iconIdx": 13
},
{
"icon": {
@@ -381,7 +435,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 12
"iconIdx": 14
},
{
"icon": {
@@ -410,7 +464,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 13
"iconIdx": 15
},
{
"icon": {
@@ -439,7 +493,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 14
"iconIdx": 16
},
{
"icon": {
@@ -468,7 +522,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 15
"iconIdx": 17
},
{
"icon": {
@@ -497,7 +551,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 16
"iconIdx": 18
},
{
"icon": {
@@ -523,7 +577,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 17
"iconIdx": 19
},
{
"icon": {
@@ -549,7 +603,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 18
"iconIdx": 20
},
{
"icon": {
@@ -575,7 +629,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 19
"iconIdx": 21
},
{
"icon": {
@@ -601,7 +655,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 20
"iconIdx": 22
},
{
"icon": {
@@ -627,7 +681,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 21
"iconIdx": 23
},
{
"icon": {
@@ -653,7 +707,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 22
"iconIdx": 24
},
{
"icon": {
@@ -679,7 +733,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 23
"iconIdx": 25
},
{
"icon": {
@@ -705,7 +759,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 24
"iconIdx": 26
},
{
"icon": {
@@ -731,7 +785,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 25
"iconIdx": 27
},
{
"icon": {
@@ -757,7 +811,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 26
"iconIdx": 28
},
{
"icon": {
@@ -783,7 +837,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 27
"iconIdx": 29
},
{
"icon": {
@@ -809,7 +863,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 28
"iconIdx": 30
},
{
"icon": {
@@ -835,7 +889,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 29
"iconIdx": 31
},
{
"icon": {
@@ -861,7 +915,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 30
"iconIdx": 32
},
{
"icon": {
@@ -887,7 +941,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 31
"iconIdx": 33
},
{
"icon": {
@@ -913,7 +967,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 32
"iconIdx": 34
},
{
"icon": {
@@ -939,7 +993,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 33
"iconIdx": 35
},
{
"icon": {
@@ -965,7 +1019,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 34
"iconIdx": 36
},
{
"icon": {
@@ -991,7 +1045,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 35
"iconIdx": 37
},
{
"icon": {
@@ -1017,7 +1071,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 36
"iconIdx": 38
},
{
"icon": {
@@ -1043,7 +1097,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 37
"iconIdx": 39
},
{
"icon": {
@@ -1069,7 +1123,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 38
"iconIdx": 40
},
{
"icon": {
@@ -1095,7 +1149,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 39
"iconIdx": 41
},
{
"icon": {
@@ -1121,7 +1175,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 40
"iconIdx": 42
},
{
"icon": {
@@ -1147,7 +1201,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 41
"iconIdx": 43
},
{
"icon": {
@@ -1173,7 +1227,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 42
"iconIdx": 44
},
{
"icon": {
@@ -1199,7 +1253,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 43
"iconIdx": 45
},
{
"icon": {
@@ -1225,7 +1279,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 44
"iconIdx": 46
},
{
"icon": {
@@ -1251,7 +1305,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 45
"iconIdx": 47
},
{
"icon": {
@@ -1280,7 +1334,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 46
"iconIdx": 48
},
{
"icon": {
@@ -1310,7 +1364,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 47
"iconIdx": 49
},
{
"icon": {
@@ -1340,7 +1394,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 48
"iconIdx": 50
},
{
"icon": {
@@ -1366,7 +1420,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 49
"iconIdx": 51
},
{
"icon": {
@@ -1392,7 +1446,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 50
"iconIdx": 52
},
{
"icon": {
@@ -1418,7 +1472,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 51
"iconIdx": 53
}
],
"height": 1024,

BIN
images/calendar@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
images/calendar@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

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

@@ -30,6 +30,8 @@ target 'JitsiMeet' do
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
pod 'RNSound', :path => '../node_modules/react-native-sound'
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
pod 'react-native-calendar-events',
:path => '../node_modules/react-native-calendar-events'
end
post_install do |installer|

View File

@@ -3,6 +3,8 @@ PODS:
- React/Core (= 0.51.0)
- react-native-background-timer (2.0.0):
- React
- react-native-calendar-events (1.4.3):
- React
- react-native-fetch-blob (0.10.6):
- React/Core
- react-native-keep-awake (2.0.6):
@@ -52,6 +54,7 @@ PODS:
DEPENDENCIES:
- react-native-background-timer (from `../node_modules/react-native-background-timer`)
- react-native-calendar-events (from `../node_modules/react-native-calendar-events`)
- react-native-fetch-blob (from `../node_modules/react-native-fetch-blob`)
- react-native-keep-awake (from `../node_modules/react-native-keep-awake`)
- react-native-locale-detector (from `../node_modules/react-native-locale-detector`)
@@ -75,6 +78,8 @@ EXTERNAL SOURCES:
:path: ../node_modules/react-native
react-native-background-timer:
:path: ../node_modules/react-native-background-timer
react-native-calendar-events:
:path: ../node_modules/react-native-calendar-events
react-native-fetch-blob:
:path: ../node_modules/react-native-fetch-blob
react-native-keep-awake:
@@ -93,6 +98,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
React: 541ba768b9855e10cdc76f55427a5cd0653ca806
react-native-background-timer: 63dcbf37dbcf294b5c6c071afcdc661fa06a7594
react-native-calendar-events: fe6fbc8ed337a7423c98f2c9012b25f20444de09
react-native-fetch-blob: 63394b1d7b0781547b3e4463b3195790177b1222
react-native-keep-awake: 0de4bd66de0c23178107dce0c2fcc3354b2a8e94
react-native-locale-detector: d1b2c6fe5abb56e3a1efb6c2d6f308c05c4251f1
@@ -101,6 +107,6 @@ SPEC CHECKSUMS:
RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44
yoga: 17521bbb0dd54a47c0b3ac43253e78cdac7488e0
PODFILE CHECKSUM: 1e6ce4da1b385720c726f3f131a6aaf08bf9c0ba
PODFILE CHECKSUM: 4a5a310403b99b9c2d619e0b18da89bf0fe5858c
COCOAPODS: 1.4.0

View File

@@ -55,12 +55,14 @@
</dict>
</dict>
</dict>
<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

@@ -51,6 +51,7 @@
"audio": "Voice",
"video": "Video"
},
"calendar": "Calendar",
"go": "GO",
"join": "JOIN",
"privacy": "Privacy",
@@ -277,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",
@@ -294,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 weren’t 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.",
@@ -395,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":
@@ -502,7 +510,7 @@
"dialInConferenceID": "PIN: __conferenceID__#",
"dialInNotSupported": "Sorry, dialing in is currently not suppported.",
"genericError": "Whoops, something went wrong.",
"invitePhone": "To join by phone, dial __number__ and enter this PIN: __pin__#",
"invitePhone": "To join by phone, dial __number__ and enter this PIN: __conferenceID__#",
"invitePhoneAlternatives": "To view more phone numbers, click this link: __url__",
"inviteURL": "To join the video meeting, click this link: __url__",
"moreNumbers": "More numbers",
@@ -526,5 +534,16 @@
"serverURL": "Server URL",
"startWithAudioMuted": "Start with audio muted",
"startWithVideoMuted": "Start with video muted"
},
"calendarSync": {
"later": "Later",
"next": "Upcoming",
"nextMeeting": "next meeting",
"now": "Now"
},
"recentList": {
"today": "Today",
"yesterday": "Yesterday",
"earlier": "Earlier"
}
}

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;

530
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",
@@ -9845,6 +10155,11 @@
"resolved": "https://registry.npmjs.org/react-native-background-timer/-/react-native-background-timer-2.0.0.tgz",
"integrity": "sha512-vLNJIedXQZN4p3ChFsAgVHacnJqQMnLl+wBsnZuliRkmsjEHo8kQOA9fnLih/OoiDi1O3eHQvXC5L8f+RYiKgw=="
},
"react-native-calendar-events": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/react-native-calendar-events/-/react-native-calendar-events-1.4.3.tgz",
"integrity": "sha1-KYBOi0TWlG5pq1ogkC2USe0xXEc="
},
"react-native-callstats": {
"version": "3.27.0",
"resolved": "https://registry.npmjs.org/react-native-callstats/-/react-native-callstats-3.27.0.tgz",
@@ -9979,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",
@@ -10079,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",
@@ -11336,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"
}
},
@@ -11392,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",
@@ -55,6 +57,7 @@
"react-i18next": "4.8.0",
"react-native": "0.51.0",
"react-native-background-timer": "2.0.0",
"react-native-calendar-events": "1.4.3",
"react-native-callstats": "3.27.0",
"react-native-fetch-blob": "0.10.8",
"react-native-img-cache": "1.5.2",
@@ -70,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,60 @@
{
"IcoMoonType": "selection",
"icons": [
{
"icon": {
"paths": [
"M512 342h64v180l150 90-32 52-182-110v-212zM554 128c212 0 384 172 384 384s-172 384-384 384c-106 0-200-42-270-112l60-62c54 54 128 88 210 88 166 0 300-132 300-298s-134-298-300-298-298 132-298 298h128l-172 172-4-6-166-166h128c0-212 172-384 384-384z"
],
"attrs": [],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"restore"
],
"defaultCode": 59571,
"grid": 24
},
"attrs": [],
"properties": {
"ligatures": "history, restore",
"id": 385,
"order": 930,
"prevSize": 24,
"code": 59571,
"name": "restore"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 385
},
{
"icon": {
"paths": [
"M426 256l256 256-256 256-60-60 196-196-196-196z"
],
"attrs": [],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"navigate_next"
],
"defaultCode": 58377,
"grid": 24
},
"attrs": [],
"properties": {
"ligatures": "chevron_right, navigate_next",
"id": 153,
"order": 927,
"prevSize": 24,
"code": 58377,
"name": "navigate_next"
},
"setIdx": 1,
"setId": 1,
"iconIdx": 0
},
{
"icon": {
"paths": [
@@ -24,9 +78,9 @@
"code": 58834,
"name": "menu"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 489
"setIdx": 1,
"setId": 1,
"iconIdx": 1
},
{
"icon": {
@@ -53,7 +107,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 0
"iconIdx": 2
},
{
"icon": {
@@ -80,7 +134,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 1
"iconIdx": 3
},
{
"icon": {
@@ -107,7 +161,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 2
"iconIdx": 4
},
{
"icon": {
@@ -134,7 +188,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 3
"iconIdx": 5
},
{
"icon": {
@@ -154,14 +208,14 @@
"properties": {
"ligatures": "timer",
"id": 760,
"order": 916,
"order": 928,
"prevSize": 24,
"code": 58405,
"name": "timer"
},
"setIdx": 1,
"setId": 1,
"iconIdx": 4
"iconIdx": 6
},
{
"icon": {
@@ -188,7 +242,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 5
"iconIdx": 7
},
{
"icon": {
@@ -215,7 +269,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 6
"iconIdx": 8
},
{
"icon": {
@@ -242,7 +296,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 7
"iconIdx": 9
},
{
"icon": {
@@ -269,7 +323,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 8
"iconIdx": 10
},
{
"icon": {
@@ -298,7 +352,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 9
"iconIdx": 11
},
{
"icon": {
@@ -325,7 +379,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 10
"iconIdx": 12
},
{
"icon": {
@@ -352,7 +406,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 11
"iconIdx": 13
},
{
"icon": {
@@ -381,7 +435,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 12
"iconIdx": 14
},
{
"icon": {
@@ -410,7 +464,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 13
"iconIdx": 15
},
{
"icon": {
@@ -439,7 +493,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 14
"iconIdx": 16
},
{
"icon": {
@@ -468,7 +522,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 15
"iconIdx": 17
},
{
"icon": {
@@ -497,7 +551,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 16
"iconIdx": 18
},
{
"icon": {
@@ -523,7 +577,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 17
"iconIdx": 19
},
{
"icon": {
@@ -549,7 +603,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 18
"iconIdx": 20
},
{
"icon": {
@@ -575,7 +629,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 19
"iconIdx": 21
},
{
"icon": {
@@ -601,7 +655,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 20
"iconIdx": 22
},
{
"icon": {
@@ -627,7 +681,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 21
"iconIdx": 23
},
{
"icon": {
@@ -653,7 +707,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 22
"iconIdx": 24
},
{
"icon": {
@@ -679,7 +733,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 23
"iconIdx": 25
},
{
"icon": {
@@ -705,7 +759,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 24
"iconIdx": 26
},
{
"icon": {
@@ -731,7 +785,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 25
"iconIdx": 27
},
{
"icon": {
@@ -757,7 +811,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 26
"iconIdx": 28
},
{
"icon": {
@@ -783,7 +837,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 27
"iconIdx": 29
},
{
"icon": {
@@ -809,7 +863,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 28
"iconIdx": 30
},
{
"icon": {
@@ -835,7 +889,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 29
"iconIdx": 31
},
{
"icon": {
@@ -861,7 +915,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 30
"iconIdx": 32
},
{
"icon": {
@@ -887,7 +941,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 31
"iconIdx": 33
},
{
"icon": {
@@ -913,7 +967,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 32
"iconIdx": 34
},
{
"icon": {
@@ -939,7 +993,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 33
"iconIdx": 35
},
{
"icon": {
@@ -965,7 +1019,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 34
"iconIdx": 36
},
{
"icon": {
@@ -991,7 +1045,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 35
"iconIdx": 37
},
{
"icon": {
@@ -1017,7 +1071,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 36
"iconIdx": 38
},
{
"icon": {
@@ -1043,7 +1097,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 37
"iconIdx": 39
},
{
"icon": {
@@ -1069,7 +1123,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 38
"iconIdx": 40
},
{
"icon": {
@@ -1095,7 +1149,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 39
"iconIdx": 41
},
{
"icon": {
@@ -1121,7 +1175,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 40
"iconIdx": 42
},
{
"icon": {
@@ -1147,7 +1201,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 41
"iconIdx": 43
},
{
"icon": {
@@ -1173,7 +1227,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 42
"iconIdx": 44
},
{
"icon": {
@@ -1199,7 +1253,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 43
"iconIdx": 45
},
{
"icon": {
@@ -1225,7 +1279,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 44
"iconIdx": 46
},
{
"icon": {
@@ -1251,7 +1305,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 45
"iconIdx": 47
},
{
"icon": {
@@ -1280,7 +1334,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 46
"iconIdx": 48
},
{
"icon": {
@@ -1310,7 +1364,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 47
"iconIdx": 49
},
{
"icon": {
@@ -1340,7 +1394,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 48
"iconIdx": 50
},
{
"icon": {
@@ -1366,7 +1420,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 49
"iconIdx": 51
},
{
"icon": {
@@ -1392,7 +1446,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 50
"iconIdx": 52
},
{
"icon": {
@@ -1418,7 +1472,7 @@
},
"setIdx": 1,
"setId": 1,
"iconIdx": 51
"iconIdx": 53
}
],
"height": 1024,

View File

@@ -0,0 +1,82 @@
// @flow
import moment from 'moment';
import i18next from './i18next';
// MomentJS uses static language bundle loading, so in order to support dynamic
// language selection in the app we need to load all bundles that we support in
// the app.
require('moment/locale/bg');
require('moment/locale/de');
require('moment/locale/eo');
require('moment/locale/es');
require('moment/locale/fr');
require('moment/locale/hy-am');
require('moment/locale/it');
require('moment/locale/nb');
// OC is not available. Please submit OC translation to the MomentJS project.
require('moment/locale/pl');
require('moment/locale/pt');
require('moment/locale/pt-br');
require('moment/locale/ru');
require('moment/locale/sk');
require('moment/locale/sl');
require('moment/locale/sv');
require('moment/locale/tr');
require('moment/locale/zh-cn');
/**
* Returns a localized date formatter initialized with a specific {@code Date}
* or timestamp ({@code number}).
*
* @private
* @param {Date | number} dateOrTimeStamp - The date or unix timestamp (ms)
* to format.
* @returns {Object}
*/
export function getLocalizedDateFormatter(dateOrTimeStamp: Date | number) {
return moment(dateOrTimeStamp).locale(_getSupportedLocale());
}
/**
* Returns a localized duration formatter initialized with a
* specific duration ({@code number}).
*
* @private
* @param {number} duration - The duration (ms)
* to format.
* @returns {Object}
*/
export function getLocalizedDurationFormatter(duration: number) {
return moment.duration(duration).locale(_getSupportedLocale());
}
/**
* A lenient locale matcher to match language and dialect if possible.
*
* @private
* @returns {string}
*/
function _getSupportedLocale() {
const i18nLocale = i18next.language;
let supportedLocale;
if (i18nLocale) {
const localeRegexp = new RegExp('^([a-z]{2,2})(-)*([a-z]{2,2})*$');
const localeResult = localeRegexp.exec(i18nLocale.toLowerCase());
if (localeResult) {
const currentLocaleRegexp
= new RegExp(
`^${localeResult[1]}(-)*${`(${localeResult[3]})*` || ''}`);
supportedLocale
= moment.locales().find(lang => currentLocaleRegexp.exec(lang));
}
}
return supportedLocale || 'en';
}

View File

@@ -1,4 +1,4 @@
export * from './dateUtil';
export * from './functions';
// TODO Eventually (e.g. when the non-React Web app is rewritten into React), it

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

@@ -0,0 +1,295 @@
// @flow
import React, { Component } from 'react';
import {
SafeAreaView,
SectionList,
Text,
TouchableHighlight,
View
} from 'react-native';
import styles, { UNDERLAY_COLOR } from './styles';
type Props = {
/**
* Indicates if the list is disabled or not.
*/
disabled: boolean,
/**
* Function to be invoked when an item is pressed. The item's URL is passed.
*/
onPress: Function,
/**
* Function to be invoked when pull-to-refresh is performed.
*/
onRefresh: Function,
/**
* Sections to be rendered in the following format:
*
* [
* {
* title: string, <- section title
* key: string, <- unique key for the section
* data: [ <- Array of items in the section
* {
* colorBase: string, <- the color base of the avatar
* title: string, <- item title
* url: string, <- item url
* lines: Array<string> <- additional lines to be rendered
* }
* ]
* }
* ]
*/
sections: Array<Object>
}
/**
* Implements a general section list to display items that have a URL
* property and navigates to (probably) meetings, such as the recent list
* or the meeting list components.
*/
export default class NavigateSectionList extends Component<Props> {
/**
* Constructor of the NavigateSectionList component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._getAvatarColor = this._getAvatarColor.bind(this);
this._getItemKey = this._getItemKey.bind(this);
this._onPress = this._onPress.bind(this);
this._onRefresh = this._onRefresh.bind(this);
this._renderItem = this._renderItem.bind(this);
this._renderItemLine = this._renderItemLine.bind(this);
this._renderItemLines = this._renderItemLines.bind(this);
this._renderSection = this._renderSection.bind(this);
}
/**
* Implements React's Component.render function.
* Note: we don't use the refreshing value yet, because refreshing of these
* lists is super quick, no need to complicate the code - yet.
*
* @inheritdoc
*/
render() {
const { sections } = this.props;
return (
<SafeAreaView
style = { styles.container } >
<SectionList
keyExtractor = { this._getItemKey }
onRefresh = { this._onRefresh }
refreshing = { false }
renderItem = { this._renderItem }
renderSectionHeader = { this._renderSection }
sections = { sections }
style = { styles.list } />
</SafeAreaView>
);
}
/**
* Creates an empty section object.
*
* @private
* @param {string} title - The title of the section.
* @param {string} key - The key of the section. It must be unique.
* @returns {Object}
*/
static createSection(title, key) {
return {
data: [],
key,
title
};
}
_getAvatarColor: string => Object
/**
* Returns a style (color) based on the string that determines the
* color of the avatar.
*
* @param {string} colorBase - The string that is the base of the color.
* @private
* @returns {Object}
*/
_getAvatarColor(colorBase) {
if (!colorBase) {
return null;
}
let nameHash = 0;
for (let i = 0; i < colorBase.length; i++) {
nameHash += colorBase.codePointAt(i);
}
return styles[`avatarColor${(nameHash % 5) + 1}`];
}
_getItemKey: (Object, number) => string;
/**
* Generates a unique id to every item.
*
* @private
* @param {Object} item - The item.
* @param {number} index - The item index.
* @returns {string}
*/
_getItemKey(item, index) {
return `${index}-${item.key}`;
}
_onPress: string => Function
/**
* Returns a function that is used in the onPress callback of the items.
*
* @private
* @param {string} url - The URL of the item to navigate to.
* @returns {Function}
*/
_onPress(url) {
return () => {
const { disabled, onPress } = this.props;
!disabled && url && typeof onPress === 'function' && onPress(url);
};
}
_onRefresh: () => void
/**
* Invokes the onRefresh callback if present.
*
* @private
* @returns {void}
*/
_onRefresh() {
const { onRefresh } = this.props;
if (typeof onRefresh === 'function') {
onRefresh();
}
}
_renderItem: Object => Object;
/**
* Renders a single item in the list.
*
* @private
* @param {Object} listItem - The item to render.
* @returns {Component}
*/
_renderItem(listItem) {
const { item } = listItem;
return (
<TouchableHighlight
onPress = { this._onPress(item.url) }
underlayColor = { UNDERLAY_COLOR }>
<View style = { styles.listItem }>
<View style = { styles.avatarContainer } >
<View
style = { [
styles.avatar,
this._getAvatarColor(item.colorBase)
] } >
<Text style = { styles.avatarContent }>
{ item.title.substr(0, 1).toUpperCase() }
</Text>
</View>
</View>
<View style = { styles.listItemDetails }>
<Text
numberOfLines = { 1 }
style = { [
styles.listItemText,
styles.listItemTitle
] }>
{ item.title }
</Text>
{
this._renderItemLines(item.lines)
}
</View>
</View>
</TouchableHighlight>
);
}
_renderItemLine: (string, number) => React$Node;
/**
* Renders a single line from the additional lines.
*
* @private
* @param {string} line - The line text.
* @param {number} index - The index of the line.
* @returns {React$Node}
*/
_renderItemLine(line, index) {
if (!line) {
return null;
}
return (
<Text
key = { index }
numberOfLines = { 1 }
style = { styles.listItemText }>
{ line }
</Text>
);
}
_renderItemLines: (Array<string>) => Array<React$Node>;
/**
* Renders the additional item lines, if any.
*
* @private
* @param {Array<string>} lines - The lines to render.
* @returns {Array<React$Node>}
*/
_renderItemLines(lines) {
if (lines && lines.length) {
return lines.map((line, index) =>
this._renderItemLine(line, index)
);
}
return null;
}
_renderSection: Object => Object
/**
* Renders a section title.
*
* @private
* @param {Object} section - The section being rendered.
* @returns {React$Node}
*/
_renderSection(section) {
return (
<View style = { styles.listSection }>
<Text style = { styles.listSectionText }>
{ section.section.title }
</Text>
</View>
);
}
}

View File

@@ -1,5 +1,6 @@
export { default as Container } from './Container';
export { default as Header } from './Header';
export { default as NavigateSectionList } from './NavigateSectionList';
export { default as Link } from './Link';
export { default as LoadingIndicator } from './LoadingIndicator';
export { default as SideBar } from './SideBar';

View File

@@ -4,20 +4,20 @@ import {
createStyleSheet
} from '../../../styles';
const AVATAR_OPACITY = 0.4;
const AVATAR_SIZE = 65;
const HEADER_COLOR = ColorPalette.blue;
// Header height is from iOS guidelines. Also, this looks good.
const HEADER_HEIGHT = 44;
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
export const HEADER_PADDING = BoxModel.padding;
export const STATUSBAR_COLOR = ColorPalette.blueHighlight;
export const SIDEBAR_WIDTH = 250;
export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)';
/**
* The styles of the generic React {@code Components} of the app.
*/
export default createStyleSheet({
const HEADER_STYLES = {
/**
* Platform specific header button (e.g. back, menu...etc).
*/
@@ -68,8 +68,124 @@ export default createStyleSheet({
height: HEADER_HEIGHT,
justifyContent: 'flex-start',
padding: HEADER_PADDING
}
};
const SECTION_LIST_STYLES = {
/**
* The style of the actual avatar.
*/
avatar: {
alignItems: 'center',
backgroundColor: `rgba(23, 160, 219, ${AVATAR_OPACITY})`,
borderRadius: AVATAR_SIZE,
height: AVATAR_SIZE,
justifyContent: 'center',
width: AVATAR_SIZE
},
/**
* List of styles of the avatar of a remote meeting (not the default
* server). The number of colors are limited because they should match
* nicely.
*/
avatarColor1: {
backgroundColor: `rgba(232, 105, 156, ${AVATAR_OPACITY})`
},
avatarColor2: {
backgroundColor: `rgba(255, 198, 115, ${AVATAR_OPACITY})`
},
avatarColor3: {
backgroundColor: `rgba(128, 128, 255, ${AVATAR_OPACITY})`
},
avatarColor4: {
backgroundColor: `rgba(105, 232, 194, ${AVATAR_OPACITY})`
},
avatarColor5: {
backgroundColor: `rgba(234, 255, 128, ${AVATAR_OPACITY})`
},
/**
* The style of the avatar container that makes the avatar rounded.
*/
avatarContainer: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-around',
padding: 5
},
/**
* Simple {@code Text} content of the avatar (the actual initials).
*/
avatarContent: {
backgroundColor: 'rgba(0, 0, 0, 0)',
color: OVERLAY_FONT_COLOR,
fontSize: 32,
fontWeight: '100',
textAlign: 'center'
},
/**
* The top level container style of the list.
*/
container: {
flex: 1
},
list: {
flex: 1,
flexDirection: 'column'
},
listItem: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
paddingVertical: 5
},
listItemDetails: {
flex: 1,
flexDirection: 'column',
overflow: 'hidden',
paddingHorizontal: 5
},
listItemText: {
color: OVERLAY_FONT_COLOR,
fontSize: 14
},
listItemTitle: {
fontWeight: 'bold',
fontSize: 16
},
listSection: {
alignItems: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.2)',
flex: 1,
flexDirection: 'row',
padding: 5
},
listSectionText: {
color: OVERLAY_FONT_COLOR,
fontSize: 14,
fontWeight: 'normal'
},
touchableView: {
flexDirection: 'row'
}
};
const SIDEBAR_STYLES = {
/**
* The topmost container of the side bar.
*/
@@ -105,4 +221,14 @@ export default createStyleSheet({
sideMenuShadowTouchable: {
flex: 1
}
};
/**
* The styles of the React {@code Components} of the generic components
* in the app.
*/
export default createStyleSheet({
...HEADER_STYLES,
...SECTION_LIST_STYLES,
...SIDEBAR_STYLES
});

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,11 @@
// @flow
/**
* The app linking scheme.
* TODO: This should be read from the manifest files later.
*/
export const APP_LINK_SCHEME = 'org.jitsi.meet:';
/**
* The {@link RegExp} pattern of the authority of a URI.
*
@@ -19,10 +25,14 @@ const _URI_PATH_PATTERN = '([^?#]*)';
/**
* The {@link RegExp} pattern of the protocol of a URI.
*
* @private
* FIXME: The URL class exposed by JavaScript will not include the colon in
* the protocol field. Also in other places (at the time of this writing:
* the UnsupportedMobileBrowser.js) the APP_LINK_SCHEME does not include
* the double dots, so things are inconsistent.
*
* @type {string}
*/
const _URI_PROTOCOL_PATTERN = '([a-z][a-z0-9\\.\\+-]*:)';
export const URI_PROTOCOL_PATTERN = '([a-z][a-z0-9\\.\\+-]*:)';
/**
* Fixes the hier-part of a specific URI (string) so that the URI is well-known.
@@ -41,7 +51,7 @@ function _fixURIStringHierPart(uri) {
// hipchat.com
let regex
= new RegExp(
`^${_URI_PROTOCOL_PATTERN}//hipchat\\.com/video/call/`,
`^${URI_PROTOCOL_PATTERN}//hipchat\\.com/video/call/`,
'gi');
let match: Array<string> | null = regex.exec(uri);
@@ -49,7 +59,7 @@ function _fixURIStringHierPart(uri) {
// enso.me
regex
= new RegExp(
`^${_URI_PROTOCOL_PATTERN}//enso\\.me/(?:call|meeting)/`,
`^${URI_PROTOCOL_PATTERN}//enso\\.me/(?:call|meeting)/`,
'gi');
match = regex.exec(uri);
}
@@ -81,7 +91,7 @@ function _fixURIStringHierPart(uri) {
* @returns {string}
*/
function _fixURIStringScheme(uri: string) {
const regex = new RegExp(`^${_URI_PROTOCOL_PATTERN}+`, 'gi');
const regex = new RegExp(`^${URI_PROTOCOL_PATTERN}+`, 'gi');
const match: Array<string> | null = regex.exec(uri);
if (match) {
@@ -185,7 +195,7 @@ export function parseStandardURIString(str: string) {
str = str.replace(/\s/g, '');
// protocol
regex = new RegExp(`^${_URI_PROTOCOL_PATTERN}`, 'gi');
regex = new RegExp(`^${URI_PROTOCOL_PATTERN}`, 'gi');
match = regex.exec(str);
if (match) {
obj.protocol = match[1].toLowerCase();
@@ -395,7 +405,7 @@ export function urlObjectToString(o: Object): ?string {
// XXX The value of domain in supposed to be host/hostname
// and, optionally, pathname. Make sure it is not taken for
// a pathname only.
_fixURIStringScheme(`org.jitsi.meet://${domain}`));
_fixURIStringScheme(`${APP_LINK_SCHEME}//${domain}`));
// authority
if (host) {

View File

@@ -0,0 +1,17 @@
// @flow
/**
* Action to update the current calendar entry list in the store.
*/
export const NEW_CALENDAR_ENTRY_LIST = Symbol('NEW_CALENDAR_ENTRY_LIST');
/**
* Action to add a new known domain to the list.
*/
export const NEW_KNOWN_DOMAIN = Symbol('NEW_KNOWN_DOMAIN');
/**
* Action to refresh (re-fetch) the entry list.
*/
export const REFRESH_CALENDAR_ENTRY_LIST
= Symbol('REFRESH_CALENDAR_ENTRY_LIST');

View File

@@ -0,0 +1,51 @@
// @flow
import {
NEW_CALENDAR_ENTRY_LIST,
NEW_KNOWN_DOMAIN,
REFRESH_CALENDAR_ENTRY_LIST
} from './actionTypes';
/**
* Sends an action to add a new known domain if not present yet.
*
* @param {string} domainName - The new domain.
* @returns {{
* type: NEW_KNOWN_DOMAIN,
* domainName: string
* }}
*/
export function maybeAddNewKnownDomain(domainName: string) {
return {
type: NEW_KNOWN_DOMAIN,
domainName
};
}
/**
* Sends an action to refresh the entry list (fetches new data).
*
* @returns {{
* type: REFRESH_CALENDAR_ENTRY_LIST
* }}
*/
export function refreshCalendarEntryList() {
return {
type: REFRESH_CALENDAR_ENTRY_LIST
};
}
/**
* Sends an action to update the current calendar list in redux.
*
* @param {Array<Object>} events - The new list.
* @returns {{
* type: NEW_CALENDAR_ENTRY_LIST,
* events: Array<Object>
* }}
*/
export function updateCalendarEntryList(events: Array<Object>) {
return {
type: NEW_CALENDAR_ENTRY_LIST,
events
};
}

View File

@@ -0,0 +1,283 @@
// @flow
import React, { Component } from 'react';
import {
Text,
TouchableOpacity,
View
} from 'react-native';
import { connect } from 'react-redux';
import { appNavigate } from '../../app';
import { getURLWithoutParams } from '../../base/connection';
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
import { Icon } from '../../base/font-icons';
import { ASPECT_RATIO_NARROW } from '../../base/responsive-ui';
import styles from './styles';
const ALERT_MILLISECONDS = 5 * 60 * 1000;
type Props = {
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* The current aspect ratio of the screen.
*/
_aspectRatio: Symbol,
/**
* The URL of the current conference without params.
*/
_currentConferenceURL: string,
/**
* The calendar event list.
*/
_eventList: Array<Object>,
/**
* The translate function.
*/
t: Function
};
type State = {
/**
* The event object to display the notification for.
*/
event?: Object
};
/**
* Component to display a permanent badge-like notification on the
* conference screen when another meeting is about to start.
*/
class ConferenceNotification extends Component<Props, State> {
updateIntervalId: number;
/**
* Constructor of the ConferenceNotification component.
*
* @inheritdoc
*/
constructor(props) {
super(props);
this.state = {
event: undefined
};
this._getNotificationContentStyle
= this._getNotificationContentStyle.bind(this);
this._getNotificationPosition
= this._getNotificationPosition.bind(this);
this._maybeDisplayNotification
= this._maybeDisplayNotification.bind(this);
this._onGoToNext = this._onGoToNext.bind(this);
}
/**
* Implements React Component's componentDidMount.
*
* @inheritdoc
*/
componentDidMount() {
this.updateIntervalId = setInterval(
this._maybeDisplayNotification,
10 * 1000
);
}
/**
* Implements React Component's componentWillUnmount.
*
* @inheritdoc
*/
componentWillUnmount() {
clearTimeout(this.updateIntervalId);
}
/**
* Implements the React Components's render method.
*
* @inheritdoc
*/
render() {
const { event } = this.state;
const { t } = this.props;
if (event) {
return (
<View
style = { [
styles.notificationContainer,
this._getNotificationPosition()
] } >
<View
style = { this._getNotificationContentStyle() }>
<TouchableOpacity
onPress = { this._onGoToNext } >
<View style = { styles.touchableView }>
<View
style = {
styles.notificationTextContainer
}>
<Text style = { styles.notificationText }>
{ t('calendarSync.nextMeeting') }
</Text>
<Text style = { styles.notificationText }>
{
getLocalizedDateFormatter(
event.startDate
).fromNow()
}
</Text>
</View>
<View
style = {
styles.notificationIconContainer
}>
<Icon
name = 'navigate_next'
style = { styles.notificationIcon } />
</View>
</View>
</TouchableOpacity>
</View>
</View>
);
}
return null;
}
_getNotificationContentStyle: () => Array<Object>
/**
* Decides the color of the notification and some additional
* styles based on notificationPosition.
*
* @private
* @returns {Array<Object>}
*/
_getNotificationContentStyle() {
const { event } = this.state;
const { _aspectRatio } = this.props;
const now = Date.now();
const style = [
styles.notificationContent
];
if (event && event.startDate < now && event.endDate > now) {
style.push(styles.notificationContentPast);
} else {
style.push(styles.notificationContentNext);
}
if (_aspectRatio === ASPECT_RATIO_NARROW) {
style.push(styles.notificationContentSide);
} else {
style.push(styles.notificationContentTop);
}
return style;
}
_getNotificationPosition: () => Object;
/**
* Decides the position of the notification.
*
* @private
* @returns {Object}
*/
_getNotificationPosition() {
const { _aspectRatio } = this.props;
if (_aspectRatio === ASPECT_RATIO_NARROW) {
return styles.notificationContainerSide;
}
return styles.notificationContainerTop;
}
_maybeDisplayNotification: () => void;
/**
* Periodically checks if there is an event in the calendar for which we
* need to show a notification.
*
* @private
* @returns {void}
*/
_maybeDisplayNotification() {
const { _currentConferenceURL, _eventList } = this.props;
let eventToShow;
if (_eventList && _eventList.length) {
const now = Date.now();
for (const event of _eventList) {
if (event.url !== _currentConferenceURL) {
if ((!eventToShow
&& event.startDate > now
&& event.startDate < now + ALERT_MILLISECONDS)
|| (event.startDate < now && event.endDate > now)
) {
eventToShow = event;
}
}
}
}
this.setState({
event: eventToShow
});
}
_onGoToNext: () => void;
/**
* Opens the meeting URL that the notification shows.
*
* @private
* @param {string} url - The URL to open.
* @returns {void}
*/
_onGoToNext() {
const { event } = this.state;
if (event && event.url) {
this.props.dispatch(appNavigate(event.url));
}
}
}
/**
* Maps redux state to component props.
*
* @param {Object} state - The redux state.
* @returns {{
* _aspectRatio: Symbol,
* _currentConferenceURL: string,
* _eventList: Array
* }}
*/
export function _mapStateToProps(state: Object) {
const { locationURL } = state['features/base/connection'];
return {
_aspectRatio: state['features/base/responsive-ui'].aspectRatio,
_currentConferenceURL:
locationURL ? getURLWithoutParams(locationURL)._url : '',
_eventList: state['features/calendar-sync'].events
};
}
export default translate(connect(_mapStateToProps)(ConferenceNotification));

View File

@@ -0,0 +1,245 @@
// @flow
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { refreshCalendarEntryList } from '../actions';
import { appNavigate } from '../../app';
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
import { NavigateSectionList } from '../../base/react';
type Props = {
/**
* Indicates if the list is disabled or not.
*/
disabled: boolean,
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* Tells the component if it's being displayed at the moment, or not.
* Note: as an example, on Android it can happen that the component
* is rendered but not displayed, because components like ViewPagerAndroid
* render their children even if they are not visible at the moment.
*/
displayed: boolean,
/**
* The calendar event list.
*/
_eventList: Array<Object>,
/**
* The translate function.
*/
t: Function
};
/**
* Component to display a list of events from the (mobile) user's calendar.
*/
class MeetingList extends Component<Props> {
_initialLoaded: boolean
/**
* Default values for the component's props.
*/
static defaultProps = {
_eventList: []
};
/**
* Constructor of the MeetingList component.
*
* @inheritdoc
*/
constructor(props) {
super(props);
this._onPress = this._onPress.bind(this);
this._onRefresh = this._onRefresh.bind(this);
this._toDisplayableItem = this._toDisplayableItem.bind(this);
this._toDisplayableList = this._toDisplayableList.bind(this);
this._toDateString = this._toDateString.bind(this);
}
/**
* Implements React Component's componentWillReceiveProps function.
*
* @inheritdoc
*/
componentWillReceiveProps(newProps) {
// This is a conditional logic to refresh the calendar entries (thus
// to request access to calendar) on component first receives a
// displayed=true prop - to avoid requesting calendar access on
// app start.
if (!this._initialLoaded
&& newProps.displayed
&& !this.props.displayed) {
const { dispatch } = this.props;
this._initialLoaded = true;
dispatch(refreshCalendarEntryList());
}
}
/**
* Implements the React Components's render method.
*
* @inheritdoc
*/
render() {
const { disabled } = this.props;
return (
<NavigateSectionList
disabled = { disabled }
onPress = { this._onPress }
onRefresh = { this._onRefresh }
sections = { this._toDisplayableList() } />
);
}
_onPress: string => Function
/**
* Handles the list's navigate action.
*
* @private
* @param {string} url - The url string to navigate to.
* @returns {void}
*/
_onPress(url) {
const { dispatch } = this.props;
dispatch(appNavigate(url));
}
_onRefresh: () => void
/**
* Callback to execute when the list is doing a pull-to-refresh.
*
* @private
* @returns {void}
*/
_onRefresh() {
const { dispatch } = this.props;
dispatch(refreshCalendarEntryList());
}
_toDisplayableItem: Object => Object
/**
* Creates a displayable object from an event.
*
* @private
* @param {Object} event - The calendar event.
* @returns {Object}
*/
_toDisplayableItem(event) {
return {
key: `${event.id}-${event.startDate}`,
lines: [
event.url,
this._toDateString(event)
],
title: event.title,
url: event.url
};
}
_toDisplayableList: () => Array<Object>
/**
* Transforms the event list to a displayable list
* with sections.
*
* @private
* @returns {Array<Object>}
*/
_toDisplayableList() {
const { _eventList, t } = this.props;
const now = Date.now();
const nowSection = NavigateSectionList.createSection(
t('calendarSync.now'),
'now'
);
const nextSection = NavigateSectionList.createSection(
t('calendarSync.next'),
'next'
);
const laterSection = NavigateSectionList.createSection(
t('calendarSync.later'),
'later'
);
for (const event of _eventList) {
const displayableEvent = this._toDisplayableItem(event);
if (event.startDate < now && event.endDate > now) {
nowSection.data.push(displayableEvent);
} else if (event.startDate > now) {
if (nextSection.data.length
&& nextSection.data[0].startDate !== event.startDate) {
laterSection.data.push(displayableEvent);
} else {
nextSection.data.push(displayableEvent);
}
}
}
const sectionList = [];
for (const section of [
nowSection,
nextSection,
laterSection
]) {
if (section.data.length) {
sectionList.push(section);
}
}
return sectionList;
}
_toDateString: Object => string
/**
* Generates a date (interval) string for a given event.
*
* @private
* @param {Object} event - The event.
* @returns {string}
*/
_toDateString(event) {
/* eslint-disable max-len */
const startDateTime = getLocalizedDateFormatter(event.startDate).format('lll');
const endTime = getLocalizedDateFormatter(event.endDate).format('LT');
return `${startDateTime} - ${endTime}`;
/* eslint-enable max-len */
}
}
/**
* Maps redux state to component props.
*
* @param {Object} state - The redux state.
* @returns {{
* _eventList: Array
* }}
*/
export function _mapStateToProps(state: Object) {
return {
_eventList: state['features/calendar-sync'].events
};
}
export default translate(connect(_mapStateToProps)(MeetingList));

View File

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

View File

@@ -0,0 +1,126 @@
import { createStyleSheet } from '../../base/styles';
const NOTIFICATION_SIZE = 55;
/**
* The styles of the React {@code Component}s of the feature meeting-list i.e.
* {@code MeetingList}.
*/
export default createStyleSheet({
/**
* The top level container of the notification.
*/
notificationContainer: {
alignSelf: 'flex-start',
flexDirection: 'row',
justifyContent: 'center',
overflow: 'hidden',
position: 'absolute'
},
/**
* Additional style for the container when the notification is displayed
* on the side (narrow view).
*/
notificationContainerSide: {
top: 100
},
/**
* Additional style for the container when the notification is displayed
* on the top (wide view).
*/
notificationContainerTop: {
justifyContent: 'center',
left: 0,
right: 0,
top: 0
},
/**
* The top level container of the notification.
*/
notificationContent: {
alignSelf: 'flex-start',
flexDirection: 'row',
height: NOTIFICATION_SIZE,
justifyContent: 'center',
paddingHorizontal: 10
},
/**
* Color for upcoming meeting notification.
*/
notificationContentNext: {
backgroundColor: '#eeb231'
},
/**
* Color for already ongoing meeting notifications.
*/
notificationContentPast: {
backgroundColor: 'red'
},
/**
* Additional style for the content when the notification is displayed
* on the side (narrow view).
*/
notificationContentSide: {
borderBottomRightRadius: NOTIFICATION_SIZE,
borderTopRightRadius: NOTIFICATION_SIZE
},
/**
* Additional style for the content when the notification is displayed
* on the top (wide view).
*/
notificationContentTop: {
borderBottomLeftRadius: NOTIFICATION_SIZE / 2,
borderBottomRightRadius: NOTIFICATION_SIZE / 2,
paddingHorizontal: 20
},
/**
* The icon of the notification.
*/
notificationIcon: {
color: 'white',
fontSize: 25
},
/**
* The container that contains the icon.
*/
notificationIconContainer: {
alignItems: 'center',
flexDirection: 'row',
height: NOTIFICATION_SIZE,
justifyContent: 'center'
},
/**
* A single line of text of the notification.
*/
notificationText: {
color: 'white',
fontSize: 13
},
/**
* The container for all the lines if the norification.
*/
notificationTextContainer: {
flexDirection: 'column',
height: NOTIFICATION_SIZE,
justifyContent: 'center'
},
/**
* The touchable component.
*/
touchableView: {
flexDirection: 'row'
}
});

View File

@@ -0,0 +1,4 @@
export * from './components';
import './middleware';
import './reducer';

View File

@@ -0,0 +1,211 @@
// @flow
import Logger from 'jitsi-meet-logger';
import RNCalendarEvents from 'react-native-calendar-events';
import { SET_ROOM } from '../base/conference';
import { MiddlewareRegistry } from '../base/redux';
import { APP_LINK_SCHEME, parseURIString } from '../base/util';
import { APP_WILL_MOUNT } from '../app';
import { maybeAddNewKnownDomain, updateCalendarEntryList } from './actions';
import { REFRESH_CALENDAR_ENTRY_LIST } from './actionTypes';
const FETCH_END_DAYS = 10;
const FETCH_START_DAYS = -1;
const MAX_LIST_LENGTH = 10;
const logger = Logger.getLogger(__filename);
MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) {
case APP_WILL_MOUNT:
_ensureDefaultServer(store);
_fetchCalendarEntries(store, false);
break;
case REFRESH_CALENDAR_ENTRY_LIST:
_fetchCalendarEntries(store, true);
break;
case SET_ROOM:
_parseAndAddDomain(store);
}
return result;
});
/**
* Ensures calendar access if possible and resolves the promise if it's granted.
*
* @private
* @param {boolean} promptForPermission - Flag to tell the app if it should
* prompt for a calendar permission if it wasn't granted yet.
* @returns {Promise}
*/
function _ensureCalendarAccess(promptForPermission) {
return new Promise((resolve, reject) => {
RNCalendarEvents.authorizationStatus()
.then(status => {
if (status === 'authorized') {
resolve();
} else if (promptForPermission) {
RNCalendarEvents.authorizeEventStore()
.then(result => {
if (result === 'authorized') {
resolve();
} else {
reject(result);
}
})
.catch(error => {
reject(error);
});
} else {
reject(status);
}
})
.catch(error => {
reject(error);
});
});
}
/**
* Ensures presence of the default server in the known domains list.
*
* @private
* @param {Object} store - The redux store.
* @returns {Promise}
*/
function _ensureDefaultServer(store) {
const state = store.getState();
const defaultURL = parseURIString(
state['features/app'].app._getDefaultURL()
);
store.dispatch(maybeAddNewKnownDomain(defaultURL.host));
}
/**
* Reads the user's calendar and updates the stored entries if need be.
*
* @private
* @param {Object} store - The redux store.
* @param {boolean} promptForPermission - Flag to tell the app if it should
* prompt for a calendar permission if it wasn't granted yet.
* @returns {void}
*/
function _fetchCalendarEntries(store, promptForPermission) {
_ensureCalendarAccess(promptForPermission)
.then(() => {
const startDate = new Date();
const endDate = new Date();
startDate.setDate(startDate.getDate() + FETCH_START_DAYS);
endDate.setDate(endDate.getDate() + FETCH_END_DAYS);
RNCalendarEvents.fetchAllEvents(
startDate.getTime(),
endDate.getTime(),
[]
)
.then(events => {
const { knownDomains } = store.getState()['features/calendar-sync'];
const eventList = [];
if (events && events.length) {
for (const event of events) {
const jitsiURL = _getURLFromEvent(event, knownDomains);
const now = Date.now();
if (jitsiURL) {
const eventStartDate = Date.parse(event.startDate);
const eventEndDate = Date.parse(event.endDate);
if (isNaN(eventStartDate) || isNaN(eventEndDate)) {
logger.warn(
'Skipping calendar event due to invalid dates',
event.title,
event.startDate,
event.endDate
);
} else if (eventEndDate > now) {
eventList.push({
endDate: eventEndDate,
id: event.id,
startDate: eventStartDate,
title: event.title,
url: jitsiURL
});
}
}
}
}
store.dispatch(updateCalendarEntryList(eventList.sort((a, b) =>
a.startDate - b.startDate
).slice(0, MAX_LIST_LENGTH)));
})
.catch(error => {
logger.error('Error fetching calendar.', error);
});
})
.catch(reason => {
logger.error('Error accessing calendar.', reason);
});
}
/**
* Retreives a jitsi URL from an event if present.
*
* @private
* @param {Object} event - The event to parse.
* @param {Array<string>} knownDomains - The known domain names.
* @returns {string}
*
*/
function _getURLFromEvent(event, knownDomains) {
const linkTerminatorPattern = '[^\\s<>$]';
/* eslint-disable max-len */
const urlRegExp
= new RegExp(`http(s)?://(${knownDomains.join('|')})/${linkTerminatorPattern}+`, 'gi');
/* eslint-enable max-len */
const schemeRegExp
= new RegExp(`${APP_LINK_SCHEME}${linkTerminatorPattern}+`, 'gi');
const fieldsToSearch = [
event.title,
event.url,
event.location,
event.notes,
event.description
];
let matchArray;
for (const field of fieldsToSearch) {
if (typeof field === 'string') {
if (
(matchArray
= urlRegExp.exec(field) || schemeRegExp.exec(field))
!== null
) {
return matchArray[0];
}
}
}
return null;
}
/**
* Retreives the domain name of a room upon join and stores it
* in the known domain list, if not present yet.
*
* @private
* @param {Object} store - The redux store.
* @returns {Promise}
*/
function _parseAndAddDomain(store) {
const { locationURL } = store.getState()['features/base/connection'];
store.dispatch(maybeAddNewKnownDomain(locationURL.host));
}

View File

@@ -0,0 +1,69 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import { PersistenceRegistry } from '../base/storage';
import { NEW_CALENDAR_ENTRY_LIST, NEW_KNOWN_DOMAIN } from './actionTypes';
/**
* ZB: this is an object, as further data is to come here, like:
* - known domain list
*/
const DEFAULT_STATE = {
events: [],
knownDomains: []
};
const MAX_DOMAIN_LIST_SIZE = 10;
const STORE_NAME = 'features/calendar-sync';
PersistenceRegistry.register(STORE_NAME, {
knownDomains: true
});
ReducerRegistry.register(
STORE_NAME,
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case NEW_CALENDAR_ENTRY_LIST:
return {
...state,
events: action.events
};
case NEW_KNOWN_DOMAIN:
return _maybeAddNewDomain(state, action);
default:
return state;
}
});
/**
* Adds a new domain to the known domain list if not present yet.
*
* @private
* @param {Object} state - The redux state.
* @param {Object} action - The redux action.
* @returns {Object}
*/
function _maybeAddNewDomain(state, action) {
let { domainName } = action;
const { knownDomains } = state;
if (domainName && domainName.length) {
domainName = domainName.toLowerCase();
if (knownDomains.indexOf(domainName) === -1) {
knownDomains.push(domainName);
// Ensure the list doesn't exceed a/the maximum size.
knownDomains.splice(0, knownDomains.length - MAX_DOMAIN_LIST_SIZE);
}
}
return {
...state,
knownDomains
};
}

View File

@@ -12,6 +12,7 @@ import { DialogContainer } from '../../base/dialog';
import { CalleeInfoContainer } from '../../base/jwt';
import { Container, LoadingIndicator, TintedView } from '../../base/react';
import { createDesiredLocalTracks } from '../../base/tracks';
import { ConferenceNotification } from '../../calendar-sync';
import { Filmstrip } from '../../filmstrip';
import { LargeVideo } from '../../large-video';
import { setToolboxVisible, Toolbox } from '../../toolbox';
@@ -233,6 +234,8 @@ class Conference extends Component<Props> {
<Filmstrip />
</View>
<ConferenceNotification />
{/*
* The dialogs are in the topmost stacking layers.
*/

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

@@ -1,75 +0,0 @@
// @flow
import { Component } from 'react';
import { appNavigate } from '../../app';
/**
* The type of the React {@code Component} props of {@link AbstractRecentList}
*/
type Props = {
_defaultURL: string,
_recentList: Array<Object>,
/**
* The redux store's {@code dispatch} function.
*/
dispatch: Dispatch<*>,
/**
* Whether {@code AbstractRecentList} is enabled.
*/
enabled: boolean
};
/**
* Implements a React {@link Component} which represents the list of conferences
* recently joined, similar to how a list of last dialed numbers list would do
* on a mobile device.
*
* @extends Component
*/
export default class AbstractRecentList extends Component<Props> {
/**
* Joins the selected room.
*
* @param {string} room - The selected room.
* @protected
* @returns {void}
*/
_onJoin(room) {
const { dispatch, enabled } = this.props;
enabled && room && dispatch(appNavigate(room));
}
/**
* Creates a bound onPress action for the list item.
*
* @param {string} room - The selected room.
* @protected
* @returns {Function}
*/
_onSelect(room) {
return this._onJoin.bind(this, room);
}
}
/**
* Maps (parts of) the redux state into {@code AbstractRecentList}'s React
* {@code Component} props.
*
* @param {Object} state - The redux state.
* @returns {{
* _defaultURL: string,
* _recentList: Array
* }}
*/
export function _mapStateToProps(state: Object) {
return {
_defaultURL: state['features/app'].app._getDefaultURL(),
_recentList: state['features/recent-list']
};
}

View File

@@ -1,202 +1,240 @@
import React from 'react';
import { ListView, Text, TouchableHighlight, View } from 'react-native';
// @flow
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Icon } from '../../base/font-icons';
import { appNavigate } from '../../app';
import {
getLocalizedDateFormatter,
getLocalizedDurationFormatter,
translate
} from '../../base/i18n';
import { NavigateSectionList } from '../../base/react';
import { parseURIString } from '../../base/util';
import AbstractRecentList, { _mapStateToProps } from './AbstractRecentList';
import { getRecentRooms } from '../functions';
import styles, { UNDERLAY_COLOR } from './styles';
/**
* The type of the React {@code Component} props of {@link RecentList}
*/
type Props = {
/**
* Renders the list disabled.
*/
disabled: boolean,
/**
* The redux store's {@code dispatch} function.
*/
dispatch: Dispatch<*>,
/**
* The translate function.
*/
t: Function,
/**
* The default server URL.
*/
_defaultServerURL: string,
/**
* The recent list from the Redux store.
*/
_recentList: Array<Object>
};
/**
* The native container rendering the list of the recently joined rooms.
*
* @extends AbstractRecentList
*/
class RecentList extends AbstractRecentList {
/**
* The datasource wrapper to be used for the display.
*/
dataSource = new ListView.DataSource({
rowHasChanged: (r1, r2) =>
r1.conference !== r2.conference
&& r1.dateTimeStamp !== r2.dateTimeStamp
});
class RecentList extends Component<Props> {
/**
* Initializes a new {@code RecentList} instance.
*
* @inheritdoc
*/
constructor(props) {
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._getAvatarStyle = this._getAvatarStyle.bind(this);
this._onSelect = this._onSelect.bind(this);
this._renderConfDuration = this._renderConfDuration.bind(this);
this._renderRow = this._renderRow.bind(this);
this._renderServerInfo = this._renderServerInfo.bind(this);
this._onPress = this._onPress.bind(this);
this._toDateString = this._toDateString.bind(this);
this._toDurationString = this._toDurationString.bind(this);
this._toDisplayableItem = this._toDisplayableItem.bind(this);
this._toDisplayableList = this._toDisplayableList.bind(this);
}
/**
* Implements React's {@link Component#render()}. Renders a list of recently
* joined rooms.
* Implements the React Components's render method.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { enabled, _recentList } = this.props;
if (!_recentList) {
return null;
}
const listViewDataSource
= this.dataSource.cloneWithRows(getRecentRooms(_recentList));
const { disabled } = this.props;
return (
<View
style = { [
styles.container,
enabled ? null : styles.containerDisabled
] }>
<ListView
dataSource = { listViewDataSource }
enableEmptySections = { true }
renderRow = { this._renderRow } />
</View>
<NavigateSectionList
disabled = { disabled }
onPress = { this._onPress }
sections = { this._toDisplayableList() } />
);
}
_onPress: string => Function
/**
* Assembles the style array of the avatar based on if the conference was
* hosted on the default Jitsi Meet deployment or on a non-default one
* (based on current app setting).
* Handles the list's navigate action.
*
* @private
* @param {string} url - The url string to navigate to.
* @returns {void}
*/
_onPress(url) {
const { dispatch } = this.props;
dispatch(appNavigate(url));
}
_toDisplayableItem: Object => Object
/**
* Creates a displayable list item of a recent list entry.
*
* @private
* @param {Object} item - The recent list entry.
* @returns {Object}
*/
_toDisplayableItem(item) {
const { _defaultServerURL } = this.props;
const location = parseURIString(item.conference);
const baseURL = `${location.protocol}//${location.host}`;
const serverName = baseURL === _defaultServerURL ? null : location.host;
return {
colorBase: serverName,
key: `key-${item.conference}-${item.date}`,
lines: [
this._toDateString(item.date),
this._toDurationString(item.duration),
serverName
],
title: location.room,
url: item.conference
};
}
_toDisplayableList: () => Array<Object>
/**
* Transforms the history list to a displayable list
* with sections.
*
* @param {Object} recentListEntry - The recent list entry being rendered.
* @private
* @returns {Array<Object>}
*/
_getAvatarStyle({ baseURL, serverName }) {
const avatarStyles = [ styles.avatar ];
if (baseURL !== this.props._defaultURL) {
avatarStyles.push(this._getColorForServerName(serverName));
}
return avatarStyles;
}
/**
* Returns a style (color) based on the server name, so then the same server
* will always be rendered with the same avatar color.
*
* @param {string} serverName - The recent list entry being rendered.
* @private
* @returns {Object}
*/
_getColorForServerName(serverName) {
let nameHash = 0;
for (let i = 0; i < serverName.length; i++) {
nameHash += serverName.codePointAt(i);
}
return styles[`avatarRemoteServer${(nameHash % 5) + 1}`];
}
/**
* Renders the conference duration if available.
*
* @param {Object} recentListEntry - The recent list entry being rendered.
* @private
* @returns {ReactElement}
*/
_renderConfDuration({ durationString }) {
if (durationString) {
return (
<View style = { styles.infoWithIcon } >
<Icon
name = 'timer'
style = { styles.inlineIcon } />
<Text style = { styles.confLength }>
{ durationString }
</Text>
</View>
);
}
return null;
}
/**
* Renders the list of recently joined rooms.
*
* @param {Object} data - The row data to be rendered.
* @private
* @returns {ReactElement}
*/
_renderRow(data) {
return (
<TouchableHighlight
onPress = { this._onSelect(data.conference) }
underlayColor = { UNDERLAY_COLOR } >
<View style = { styles.row } >
<View style = { styles.avatarContainer } >
<View style = { this._getAvatarStyle(data) } >
<Text style = { styles.avatarContent }>
{ data.initials }
</Text>
</View>
</View>
<View style = { styles.detailsContainer } >
<Text
numberOfLines = { 1 }
style = { styles.roomName }>
{ data.room }
</Text>
<View style = { styles.infoWithIcon } >
<Icon
name = 'event_note'
style = { styles.inlineIcon } />
<Text style = { styles.date }>
{ data.dateString }
</Text>
</View>
{ this._renderConfDuration(data) }
{ this._renderServerInfo(data) }
</View>
</View>
</TouchableHighlight>
_toDisplayableList() {
const { _recentList, t } = this.props;
const todaySection = NavigateSectionList.createSection(
t('recentList.today'),
'today'
);
const yesterdaySection = NavigateSectionList.createSection(
t('recentList.yesterday'),
'yesterday'
);
const earlierSection = NavigateSectionList.createSection(
t('recentList.earlier'),
'earlier'
);
const today = new Date().toDateString();
const yesterdayDate = new Date();
yesterdayDate.setDate(yesterdayDate.getDate() - 1);
const yesterday = yesterdayDate.toDateString();
for (const item of _recentList) {
const itemDay = new Date(item.date).toDateString();
const displayableItem = this._toDisplayableItem(item);
if (itemDay === today) {
todaySection.data.push(displayableItem);
} else if (itemDay === yesterday) {
yesterdaySection.data.push(displayableItem);
} else {
earlierSection.data.push(displayableItem);
}
}
const displayableList = [];
if (todaySection.data.length) {
todaySection.data.reverse();
displayableList.push(todaySection);
}
if (yesterdaySection.data.length) {
yesterdaySection.data.reverse();
displayableList.push(yesterdaySection);
}
if (earlierSection.data.length) {
earlierSection.data.reverse();
displayableList.push(earlierSection);
}
return displayableList;
}
_toDateString: number => string
/**
* Renders the server info component based on whether the entry was on a
* different server.
* Generates a date string for the item.
*
* @param {Object} recentListEntry - The recent list entry being rendered.
* @private
* @returns {ReactElement}
* @param {number} itemDate - The item's timestamp.
* @returns {string}
*/
_renderServerInfo({ baseURL, serverName }) {
if (baseURL !== this.props._defaultURL) {
return (
<View style = { styles.infoWithIcon } >
<Icon
name = 'public'
style = { styles.inlineIcon } />
<Text style = { styles.serverName }>
{ serverName }
</Text>
</View>
);
_toDateString(itemDate) {
const date = new Date(itemDate);
const m = getLocalizedDateFormatter(itemDate);
if (date.toDateString() === new Date().toDateString()) {
// The date is today, we use fromNow format.
return m.fromNow();
}
return m.format('lll');
}
_toDurationString: number => string
/**
* Generates a duration string for the item.
*
* @private
* @param {number} duration - The item's duration.
* @returns {string}
*/
_toDurationString(duration) {
if (duration) {
return getLocalizedDurationFormatter(duration).humanize();
}
return null;
}
}
export default connect(_mapStateToProps)(RecentList);
/**
* Maps redux state to component props.
*
* @param {Object} state - The redux state.
* @returns {{
* _defaultServerURL: string,
* _recentList: Array
* }}
*/
export function _mapStateToProps(state: Object) {
return {
_defaultServerURL: state['features/app'].app._getDefaultURL(),
_recentList: state['features/recent-list']
};
}
export default translate(connect(_mapStateToProps)(RecentList));

View File

@@ -1,166 +0,0 @@
// @flow
import moment from 'moment';
// MomentJS uses static language bundle loading, so in order to support dynamic
// language selection in the app we need to load all bundles that we support in
// the app.
// FIXME: If we decide to support MomentJS in other features as well we may need
// to move this import and the lenient matcher to the i18n feature.
require('moment/locale/bg');
require('moment/locale/de');
require('moment/locale/eo');
require('moment/locale/es');
require('moment/locale/fr');
require('moment/locale/hy-am');
require('moment/locale/it');
require('moment/locale/nb');
// OC is not available. Please submit OC translation to the MomentJS project.
require('moment/locale/pl');
require('moment/locale/pt');
require('moment/locale/pt-br');
require('moment/locale/ru');
require('moment/locale/sk');
require('moment/locale/sl');
require('moment/locale/sv');
require('moment/locale/tr');
require('moment/locale/zh-cn');
import { i18next } from '../base/i18n';
import { parseURIString } from '../base/util';
/**
* Retrieves the recent room list and generates all the data needed to be
* displayed.
*
* @param {Array<Object>} list - The stored recent list retrieved from redux.
* @returns {Array}
*/
export function getRecentRooms(list: Array<Object>): Array<Object> {
const recentRoomDS = [];
if (list.length) {
// We init the locale on every list render, so then it changes
// immediately if a language change happens in the app.
const locale = _getSupportedLocale();
for (const e of list) {
const uri = parseURIString(e.conference);
if (uri && uri.room && uri.hostname) {
const duration
= e.duration || /* legacy */ e.conferenceDuration || 0;
recentRoomDS.push({
baseURL: `${uri.protocol}//${uri.host}`,
conference: e.conference,
dateString: _getDateString(e.date, locale),
dateTimeStamp: e.date,
duration,
durationString: _getDurationString(duration, locale),
initials: _getInitials(uri.room),
room: uri.room,
serverName: uri.hostname
});
}
}
}
return recentRoomDS.reverse();
}
/**
* Returns a well formatted date string to be displayed in the list.
*
* @param {number} dateTimeStamp - The UTC timestamp to be converted to String.
* @param {string} locale - The locale to init the formatter with. Note: This
* locale must be supported by the formatter so ensure this prerequisite before
* invoking the function.
* @private
* @returns {string}
*/
function _getDateString(dateTimeStamp: number, locale: string) {
const date = new Date(dateTimeStamp);
const m = _getLocalizedFormatter(date, locale);
if (date.toDateString() === new Date().toDateString()) {
// The date is today, we use fromNow format.
return m.fromNow();
}
return m.format('lll');
}
/**
* Returns a well formatted duration string to be displayed as the conference
* length.
*
* @param {number} duration - The duration in MS.
* @param {string} locale - The locale to init the formatter with. Note: This
* locale must be supported by the formatter so ensure this prerequisite before
* invoking the function.
* @private
* @returns {string}
*/
function _getDurationString(duration: number, locale: string) {
return _getLocalizedFormatter(duration, locale).humanize();
}
/**
* Returns the initials supposed to be used based on the room name.
*
* @param {string} room - The room name.
* @private
* @returns {string}
*/
function _getInitials(room: string) {
return room && room.charAt(0) ? room.charAt(0).toUpperCase() : '?';
}
/**
* Returns a localized date formatter initialized with a specific {@code Date}
* or duration ({@code number}).
*
* @private
* @param {Date|number} dateOrDuration - The date or duration to format.
* @param {string} locale - The locale to init the formatter with. Note: The
* specified locale must be supported by the formatter so ensure the
* prerequisite is met before invoking the function.
* @returns {Object}
*/
function _getLocalizedFormatter(dateOrDuration: Date | number, locale: string) {
const m
= typeof dateOrDuration === 'number'
? moment.duration(dateOrDuration)
: moment(dateOrDuration);
return m.locale(locale);
}
/**
* A lenient locale matcher to match language and dialect if possible.
*
* @private
* @returns {string}
*/
function _getSupportedLocale() {
const i18nLocale = i18next.language;
let supportedLocale;
if (i18nLocale) {
const localeRegexp = new RegExp('^([a-z]{2,2})(-)*([a-z]{2,2})*$');
const localeResult = localeRegexp.exec(i18nLocale.toLowerCase());
if (localeResult) {
const currentLocaleRegexp
= new RegExp(
`^${localeResult[1]}(-)*${`(${localeResult[3]})*` || ''}`);
supportedLocale
= moment.locales().find(lang => currentLocaleRegexp.exec(lang));
}
}
return supportedLocale || 'en';
}

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

@@ -6,6 +6,7 @@ import { connect } from 'react-redux';
import { translate, translateToHTML } from '../../base/i18n';
import { Platform } from '../../base/react';
import { URI_PROTOCOL_PATTERN } from '../../base/util';
import { DialInSummary } from '../../invite';
import HideNotificationBarStyle from './HideNotificationBarStyle';
@@ -82,7 +83,11 @@ class UnsupportedMobileBrowser extends Component<*, *> {
// appears to be a link with an app-specific scheme, not a Universal
// Link.
const appScheme = interfaceConfig.MOBILE_APP_SCHEME || 'org.jitsi.meet';
const joinURL = `${appScheme}:${window.location.href}`;
// Replace the protocol part with the app scheme.
const joinURL
= window.location.href.replace(
new RegExp(`^${URI_PROTOCOL_PATTERN}`), `${appScheme}:`);
this.setState({
joinURL

View File

@@ -0,0 +1,48 @@
// @flow
import { Component } from 'react';
/**
* The page to be displayed on render.
*/
export const DEFAULT_PAGE = 0;
type Props = {
/**
* Indicates if the list is disabled or not.
*/
disabled: boolean,
/**
* The i18n translate function
*/
t: Function
}
type State = {
/**
* The currently selected page.
*/
pageIndex: number
}
/**
* Abstract class for the platform specific paged lists.
*/
export default class AbstractPagedList extends Component<Props, State> {
/**
* Constructor of the component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.state = {
pageIndex: DEFAULT_PAGE
};
}
}

View File

@@ -0,0 +1,177 @@
// @flow
import React from 'react';
import { Text, TouchableOpacity, View, ViewPagerAndroid } from 'react-native';
import { Icon } from '../../base/font-icons';
import { MeetingList } from '../../calendar-sync';
import { RecentList } from '../../recent-list';
import AbstractPagedList, { DEFAULT_PAGE } from './AbstractPagedList';
import styles from './styles';
/**
* A platform specific component to render a paged or tabbed list/view.
*
* @extends PagedList
*/
export default class PagedList extends AbstractPagedList {
/**
* A reference to the viewpager.
*/
_viewPager: Object;
/**
* Constructor of the PagedList Component.
*
* @inheritdoc
*/
constructor(props) {
super(props);
this._getIndicatorStyle = this._getIndicatorStyle.bind(this);
this._onPageSelected = this._onPageSelected.bind(this);
this._onSelectPage = this._onSelectPage.bind(this);
this._setPagerReference = this._setPagerReference.bind(this);
}
/**
* Renders the paged list.
*
* @inheritdoc
*/
render() {
const { disabled } = this.props;
const { pageIndex } = this.state;
return (
<View
style = { [
styles.pagedListContainer,
disabled ? styles.pagedListContainerDisabled : null
] }>
<ViewPagerAndroid
initialPage = { DEFAULT_PAGE }
keyboardDismissMode = 'on-drag'
onPageSelected = { this._onPageSelected }
peekEnabled = { true }
ref = { this._setPagerReference }
style = { styles.pagedList }>
<View key = { 0 }>
<RecentList disabled = { disabled } />
</View>
<View key = { 1 }>
<MeetingList
disabled = { disabled }
displayed = { pageIndex === 1 } />
</View>
</ViewPagerAndroid>
<View style = { styles.pageIndicatorContainer }>
<TouchableOpacity
onPress = { this._onSelectPage(0) }
style = { styles.pageIndicator } >
<View style = { styles.pageIndicator }>
<Icon
name = 'restore'
style = { [
styles.pageIndicatorIcon,
this._getIndicatorStyle(0)
] } />
<Text
style = { [
styles.pageIndicatorText,
this._getIndicatorStyle(0)
] }>
History
</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress = { this._onSelectPage(1) }
style = { styles.pageIndicator } >
<View style = { styles.pageIndicator }>
<Icon
name = 'event_note'
style = { [
styles.pageIndicatorIcon,
this._getIndicatorStyle(1)
] } />
<Text
style = { [
styles.pageIndicatorText,
this._getIndicatorStyle(1)
] }>
Calendar
</Text>
</View>
</TouchableOpacity>
</View>
</View>
);
}
_getIndicatorStyle: number => Object;
/**
* Constructs the style of an indicator.
*
* @private
* @param {number} indicatorIndex - The index of the indicator.
* @returns {Object}
*/
_getIndicatorStyle(indicatorIndex) {
if (this.state.pageIndex === indicatorIndex) {
return styles.pageIndicatorTextActive;
}
return null;
}
_onPageSelected: Object => void;
/**
* Updates the index of the currently selected page.
*
* @private
* @param {Object} event - The native event of the callback.
* @returns {void}
*/
_onPageSelected({ nativeEvent: { position } }) {
if (this.state.pageIndex !== position) {
this.setState({
pageIndex: position
});
}
}
_onSelectPage: number => Function
/**
* Constructs a function to be used as a callback for the tab bar.
*
* @private
* @param {number} pageIndex - The index of the page to activate via the
* callback.
* @returns {Function}
*/
_onSelectPage(pageIndex) {
return () => {
this._viewPager.setPage(pageIndex);
this.setState({
pageIndex
});
};
}
_setPagerReference: Object => void
/**
* Sets the pager's reference for direct modification.
*
* @private
* @param {React@Node} component - The pager component.
* @returns {void}
*/
_setPagerReference(component) {
this._viewPager = component;
}
}

Some files were not shown because too many files have changed in this diff Show More