Compare commits

..

7 Commits

Author SHA1 Message Date
paweldomas
c2191e3a28 callkit with base/session 2018-08-07 12:20:38 -05:00
paweldomas
72e3e8593d feat(base/session): store 'room' in the session
Stores name of the conference room in the session when it's being
created.
2018-08-07 12:20:38 -05:00
paweldomas
67a8b4915d feat(base/session): add SESSION_CONFIGURED event
The SESSION_CONFIGURED event is fired once the config has been set,
after either being loaded or restored from the storage.
2018-08-07 12:20:38 -05:00
paweldomas
468d4a7150 ref(mobile/external-api): use base/session 2018-08-07 12:20:38 -05:00
paweldomas
2a01e29fec feat: add features/base/session 2018-08-07 12:20:38 -05:00
paweldomas
90a64d30dc ref(base/config): keep 'locationURL' after SET_CONFIG
This is required for the session feature to be able to tell what's
the latest URL the app is working with.
2018-08-07 12:20:38 -05:00
paweldomas
31905d4f63 debug actions 2018-08-07 12:20:38 -05:00
185 changed files with 2072 additions and 8694 deletions

View File

@@ -2,7 +2,6 @@ BUILD_DIR = build
CLEANCSS = ./node_modules/.bin/cleancss
DEPLOY_DIR = libs
LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet/
LIBFLAC_DIR = node_modules/libflacjs/dist/min/
NODE_SASS = ./node_modules/.bin/node-sass
NPM = npm
OUTPUT_DIR = .
@@ -20,7 +19,7 @@ compile:
clean:
rm -fr $(BUILD_DIR)
deploy: deploy-init deploy-appbundle deploy-lib-jitsi-meet deploy-libflac deploy-css deploy-local
deploy: deploy-init deploy-appbundle deploy-lib-jitsi-meet deploy-css deploy-local
deploy-init:
rm -fr $(DEPLOY_DIR)
@@ -34,8 +33,6 @@ deploy-appbundle:
$(BUILD_DIR)/do_external_connect.min.map \
$(BUILD_DIR)/external_api.min.js \
$(BUILD_DIR)/external_api.min.map \
$(BUILD_DIR)/flacEncodeWorker.min.js \
$(BUILD_DIR)/flacEncodeWorker.min.map \
$(BUILD_DIR)/device_selection_popup_bundle.min.js \
$(BUILD_DIR)/device_selection_popup_bundle.min.map \
$(BUILD_DIR)/dial_in_info_bundle.min.js \
@@ -53,12 +50,6 @@ deploy-lib-jitsi-meet:
$(LIBJITSIMEET_DIR)/modules/browser/capabilities.json \
$(DEPLOY_DIR)
deploy-libflac:
cp \
$(LIBFLAC_DIR)/libflac4-1.3.2.min.js \
$(LIBFLAC_DIR)/libflac4-1.3.2.min.js.mem \
$(DEPLOY_DIR)
deploy-css:
$(NODE_SASS) $(STYLES_MAIN) $(STYLES_BUNDLE) && \
$(CLEANCSS) $(STYLES_BUNDLE) > $(STYLES_DESTINATION) ; \
@@ -67,7 +58,7 @@ deploy-css:
deploy-local:
([ ! -x deploy-local.sh ] || ./deploy-local.sh)
dev: deploy-init deploy-css deploy-lib-jitsi-meet deploy-libflac
dev: deploy-init deploy-css deploy-lib-jitsi-meet
$(WEBPACK_DEV_SERVER)
source-package:

View File

@@ -704,7 +704,7 @@ export default {
track.mute();
}
});
logger.log(`initialized with ${tracks.length} local tracks`);
logger.log('initialized with %s local tracks', tracks.length);
this._localTracksInitialized = true;
con.addEventListener(
JitsiConnectionEvents.CONNECTION_FAILED,
@@ -1678,7 +1678,7 @@ export default {
role: user.getRole()
}));
logger.log(`USER ${id} connnected:`, user);
logger.log('USER %s connnected', id, user);
APP.API.notifyUserJoined(id, {
displayName,
formattedDisplayName: appendSuffix(
@@ -1698,7 +1698,7 @@ export default {
}
APP.store.dispatch(participantLeft(id, room));
logger.log(`USER ${id} LEFT:`, user);
logger.log('USER %s LEFT', id, user);
APP.API.notifyUserLeft(id);
APP.UI.messageHandler.participantNotification(
user.getDisplayName(),

View File

@@ -256,10 +256,6 @@ var config = {
// maintenance at 01:00 AM GMT,
// noticeMessage: '',
// Enables calendar integration, depends on googleApiApplicationClientID
// and microsoftApiApplicationClientID
// enableCalendarIntegration: false,
// Stats
//
@@ -351,36 +347,6 @@ var config = {
// userRegion: "asia"
}
// Local Recording
//
// localRecording: {
// Enables local recording.
// Additionally, 'localrecording' (all lowercase) needs to be added to
// TOOLBAR_BUTTONS in interface_config.js for the Local Recording
// button to show up on the toolbar.
//
// enabled: true,
//
// The recording format, can be one of 'ogg', 'flac' or 'wav'.
// format: 'flac'
//
// }
// Options related to end-to-end (participant to participant) ping.
// e2eping: {
// // The interval in milliseconds at which pings will be sent.
// // Defaults to 10000, set to <= 0 to disable.
// pingInterval: 10000,
//
// // The interval in milliseconds at which analytics events
// // with the measured RTT will be sent. Defaults to 60000, set
// // to <= 0 to disable.
// analyticsInterval: 60000,
// }
// List of undocumented settings used in jitsi-meet
/**
_immediateReloadThreshold
@@ -402,7 +368,6 @@ var config = {
googleApiApplicationClientID
iAmRecorder
iAmSipGateway
microsoftApiApplicationClientID
peopleSearchQueryTypes
peopleSearchUrl
requireDisplayName
@@ -431,7 +396,6 @@ var config = {
nick
startBitrate
*/
};
/* eslint-enable no-unused-vars, no-var */

View File

@@ -10,31 +10,3 @@
-ms-transform: translateX(0) translateY(100%) translateY(16px) !important;
-webkit-transform: translateX(0) translateY(100%) translateY(16px) !important;
}
/**
* Welcome page tab color adjustments.
*/
.welcome {
/**
* The text color of the selected tab and hovered tabs.
*/
li.bcVmZW,
li.bcVmZW:hover,
li.kheoEp:hover {
color: #172B4D;
}
/**
* The color of the inactive tab text.
*/
li.kheoEp {
color: #FFFFFF;
}
/**
* The color of the underline of a selected tab.
*/
li>span.kByArU {
background-color: #172B4D;
}
}

View File

@@ -13,35 +13,20 @@
float: left;
}
.navigate-section-list-tile {
background-color: #1754A9;
height: 90px;
width: 260px;
border-radius: 4px;
box-sizing: border-box;
display: inline-flex;
height: 100px;
margin-bottom: 8px;
background-color: #1754A9;
margin-right: 8px;
padding: 16px;
width: 100%;
&.with-click-handler {
cursor: pointer;
}
&.with-click-handler:hover {
background-color: #1a5dbb;
}
i {
cursor: inherit;
}
display: inline-block;
box-sizing: border-box;
cursor: pointer;
}
.navigate-section-tile-body {
@extend %navigate-section-list-tile-text;
font-weight: normal;
}
.navigate-section-list-tile-info {
flex: 1;
}
.navigate-section-tile-title {
@extend %navigate-section-list-tile-text;
font-weight: bold;
@@ -55,8 +40,4 @@
position: relative;
margin-top: 36px;
margin-bottom: 36px;
width: 100%;
}
.navigate-section-list-empty {
text-align: center;
}

View File

@@ -2,42 +2,6 @@
vertical-align: top;
}
.recording-dialog {
.authorization-panel {
border-bottom: 2px solid rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
margin-bottom: 10px;
padding-bottom: 10px;
.dropbox-sign-in {
align-items: center;
border: 1px solid #4285f4;
background-color: white;
border-radius: 2px;
cursor: pointer;
display: inline-flex;
padding: 10px;
font-size: 18px;
font-weight: 600;
margin: 10px 0px;
color: #4285f4;
.dropbox-logo {
background-color: white;
border-radius: 2px;
display: inline-block;
padding-right: 5px;
height: 18px;
}
}
.logged-in-pannel {
padding: 10px;
}
}
}
.live-stream-dialog {
/**
* Set font-size to be consistent with Atlaskit FieldText.
@@ -70,6 +34,39 @@
color: $errorColor;
}
/**
* 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);

View File

@@ -2,19 +2,12 @@
bottom: 10%;
font-size: 16px;
font-weight: 1000;
left: 50%;
max-width: 50vw;
opacity: 0.80;
pointer-events: none;
position: absolute;
text-shadow: 0px 0px 1px rgba(0,0,0,0.3),
0px 1px 1px rgba(0,0,0,0.3),
1px 0px 1px rgba(0,0,0,0.3),
0px 0px 1px rgba(0,0,0,0.3);
transform: translateX(-50%);
z-index: $filmstripVideosZ + 1;
span {
background: black;
}
width: 100%;
z-index: $zindex2;
}

View File

@@ -45,7 +45,6 @@ body.welcome-page {
font-size: 1rem;
font-weight: 400;
line-height: 24px;
margin-bottom: 20px;
}
#enter_room {
@@ -63,30 +62,12 @@ body.welcome-page {
width: 100%;
}
}
.tab-container {
font-size: 16px;
position: relative;
text-align: left;
width: 650px;
}
}
.welcome-page-button {
font-size: 16px;
}
.welcome-page-settings {
color: $welcomePageDescriptionColor;
position: absolute;
right: 10px;
z-index: $zindex2;
* {
cursor: pointer;
}
}
.welcome-watermark {
position: absolute;
width: 100%;

View File

@@ -14,9 +14,14 @@
* Focused video thumbnail.
*/
&.videoContainerFocused {
border: $thumbnailVideoBorder solid $videoThumbnailSelected;
transition-duration: 0.5s;
-webkit-transition-duration: 0.5s;
-webkit-animation-name: greyPulse;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: 1;
border: $thumbnailVideoBorder solid $videoThumbnailSelected !important;
box-shadow: inset 0 0 3px $videoThumbnailSelected,
0 0 3px $videoThumbnailSelected;
0 0 3px $videoThumbnailSelected !important;
}
.remotevideomenu > .icon-menu {
@@ -26,7 +31,7 @@
/**
* Hovered video thumbnail.
*/
&:hover:not(.videoContainerFocused):not(.active-speaker) {
&:hover {
cursor: hand;
border: $thumbnailVideoBorder solid $videoThumbnailHovered;
box-shadow: inset 0 0 3px $videoThumbnailHovered,

View File

@@ -1,113 +0,0 @@
/**
* CSS styles that are specific to the filmstrip that shows the thumbnail tiles.
*/
.tile-view {
/**
* Add a border around the active speaker to make the thumbnail easier to
* see.
*/
.active-speaker {
box-shadow: 0 0 5px 3px $videoThumbnailSelected
}
#filmstripRemoteVideos {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
}
.filmstrip__videos .videocontainer {
&:not(.active-speaker),
&:hover:not(.active-speaker) {
border: none;
box-shadow: none;
}
}
#remoteVideos {
/**
* Height is modified with an inline style in horizontal filmstrip mode
* so !important is used to override that.
*/
height: 100% !important;
width: 100%;
}
.filmstrip {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
left: 0;
position: fixed;
top: 0;
width: 100%;
z-index: $filmstripVideosZ
}
/**
* Regardless of the user setting, do not let the filmstrip be in a hidden
* state.
*/
.filmstrip__videos.hidden {
display: block;
}
#filmstripRemoteVideos {
box-sizing: border-box;
/**
* Allow vertical scrolling of the thumbnails.
*/
overflow-x: hidden;
overflow-y: auto;
}
/**
* The size of the thumbnails should be set with javascript, based on
* desired column count and window width. The rows are created using flex
* and allowing the thumbnails to wrap.
*/
#filmstripRemoteVideosContainer {
align-content: center;
align-items: center;
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
height: 100vh;
justify-content: center;
padding: 100px 0;
.videocontainer {
border: 0;
box-sizing: border-box;
display: block;
margin: 5px;
}
video {
object-fit: contain;
}
}
.has-overflow#filmstripRemoteVideosContainer {
align-content: baseline;
}
.has-overflow .videocontainer {
align-self: baseline;
}
/**
* Firefox flex acts a little differently. To make sure the bottom row of
* thumbnails is not overlapped by the horizontal toolbar, margin is added
* to the local thumbnail to keep it from the bottom of the screen. It is
* assumed the local thumbnail will always be on the bottom row.
*/
.has-overflow #localVideoContainer {
margin-bottom: 100px !important;
}
}

View File

@@ -1,47 +0,0 @@
/**
* Various overrides outside of the filmstrip to style the app to support a
* tiled thumbnail experience.
*/
.tile-view {
/**
* Let the avatar grow with the tile.
*/
.userAvatar {
max-height: initial;
max-width: initial;
}
/**
* Hide various features that should not be displayed while in tile view.
*/
#dominantSpeaker,
#filmstripLocalVideoThumbnail,
#largeVideoElementsContainer,
#sharedVideo,
.filmstrip__toolbar {
display: none;
}
#localConnectionMessage,
#remoteConnectionMessage,
.watermark {
z-index: $filmstripVideosZ + 1;
}
/**
* The follow styling uses !important to override inline styles set with
* javascript.
*
* TODO: These overrides should be more easy to remove and should be removed
* when the components are in react so their rendering done declaratively,
* making conditional styling easier to apply.
*/
#largeVideoElementsContainer,
#remoteConnectionMessage,
#remotePresenceMessage {
display: none !important;
}
#largeVideoContainer {
background-color: $defaultBackground !important;
}
}

View File

@@ -45,7 +45,6 @@
@import 'modals/settings/settings';
@import 'modals/speaker_stats/speaker_stats';
@import 'modals/video-quality/video-quality';
@import 'modals/local-recording/local-recording';
@import 'videolayout_default';
@import 'notice';
@import 'popup_menu';
@@ -73,8 +72,6 @@
@import 'filmstrip/filmstrip_toolbar';
@import 'filmstrip/horizontal_filmstrip';
@import 'filmstrip/small_video';
@import 'filmstrip/tile_view';
@import 'filmstrip/tile_view_overrides';
@import 'filmstrip/vertical_filmstrip';
@import 'filmstrip/vertical_filmstrip_overrides';
@import 'unsupported-browser/main';
@@ -82,7 +79,4 @@
@import 'deep-linking/main';
@import 'transcription-subtitles';
@import 'navigate_section_list';
@import 'third-party-branding/google';
@import 'third-party-branding/microsoft';
/* Modules END */

View File

@@ -1,92 +0,0 @@
.localrec-participant-stats {
list-style: none;
padding: 0;
width: 100%;
font-weight: 500;
.localrec-participant-stats-item__status-dot {
position: relative;
display: block;
width: 9px;
height: 9px;
border-radius: 50%;
margin: 0 auto;
&.status-on {
background: green;
}
&.status-off {
background: gray;
}
&.status-unknown {
background: darkgoldenrod;
}
&.status-error {
background: darkred;
}
}
.localrec-participant-stats-item__status,
.localrec-participant-stats-item__name,
.localrec-participant-stats-item__sessionid {
display: inline-block;
margin: 5px 0;
vertical-align: middle;
}
.localrec-participant-stats-item__status {
width: 5%;
}
.localrec-participant-stats-item__name {
width: 40%;
}
.localrec-participant-stats-item__sessionid {
width: 55%;
}
.localrec-participant-stats-item__name,
.localrec-participant-stats-item__sessionid {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.localrec-control-info-label {
font-weight: bold;
}
.localrec-control-info-label:after {
content: ' ';
}
.localrec-control-action-link {
display: inline-block;
line-height: 1.5em;
a {
cursor: pointer;
vertical-align: middle;
}
}
.localrec-control-action-link:before {
color: $linkFontColor;
content: '\2022';
font-size: 1.5em;
padding: 0 10px;
vertical-align: middle;
}
.localrec-control-action-link:first-child:before {
content: '';
padding: 0;
}
.localrec-control-action-links {
font-weight: bold;
margin-top: 10px;
white-space: nowrap;
}

View File

@@ -10,7 +10,6 @@
margin-bottom: 4px;
}
.calendar-tab,
.device-selection {
margin-top: 20px;
}
@@ -23,7 +22,6 @@
padding: 20px 0px 4px 0px;
}
.calendar-tab,
.more-tab,
.profile-edit {
display: flex;
@@ -42,20 +40,4 @@
.language-settings {
max-width: 50%;
}
.calendar-tab {
align-items: center;
flex-direction: column;
font-size: 14px;
min-height: 100px;
text-align: center;
}
.calendar-tab-sign-in {
margin-top: 20px;
}
.sign-out-cta {
margin-bottom: 20px;
}
}

View File

@@ -168,10 +168,6 @@
background: #FF5630;
}
.circular-label.local-rec {
background: #FF5630;
}
.circular-label.stream {
background: #0065FF;
}

View File

@@ -1,32 +0,0 @@
/**
* 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;
}
}

View File

@@ -1,28 +0,0 @@
/**
* The Microsoft sign in button must follow Microsoft's brand guidelines.
* See: https://docs.microsoft.com/en-us/azure/active-directory/
* develop/active-directory-branding-guidelines
*/
.microsoft-sign-in {
align-items: center;
background: #FFFFFF;
border: 1px solid #8C8C8C;
box-sizing: border-box;
cursor: pointer;
display: inline-flex;
font-family: Segoe UI, Roboto, arial, sans-serif;
height: 41px;
padding: 12px;
.microsoft-cta {
display: inline-block;
color: #5E5E5E;
font-size: 15px;
line-height: 41px;
}
.microsoft-logo {
display: inline-block;
margin-right: 12px;
}
}

View File

@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="324px" height="63.8px" viewBox="0 0 324 63.8" style="enable-background:new 0 0 324 63.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#0061FF;}
.st1{display:none;}
.st2{display:inline;}
.st3{fill:none;}
</style>
<path class="st0" d="M37.6,12L18.8,24l18.8,12L18.8,48L0,35.9l18.8-12L0,12L18.8,0L37.6,12z M18.7,51.8l18.8-12l18.8,12l-18.8,12
L18.7,51.8z M37.6,35.9l18.8-12L37.6,12L56.3,0l18.8,12L56.3,24l18.8,12L56.3,48L37.6,35.9z"/>
<path d="M89.8,12H105c9.7,0,17.7,5.6,17.7,18.4v2.7c0,12.9-7.5,18.7-17.4,18.7H89.8V12z M98.3,19.2v25.3h6.5c5.5,0,9.2-3.6,9.2-11.6
v-2.1c0-8-3.9-11.6-9.5-11.6H98.3z M127.2,19.6h6.8l1.1,7.5c1.3-5.1,4.6-7.8,10.6-7.8h2.1v8.6h-3.5c-6.9,0-8.6,2.4-8.6,9.2v14.8
h-8.4V19.6H127.2z M149.5,36.4v-0.9c0-10.8,6.9-16.7,16.3-16.7c9.6,0,16.3,5.9,16.3,16.7v0.9c0,10.6-6.5,16.3-16.3,16.3
C155.4,52.6,149.5,47,149.5,36.4z M173.5,36.3v-0.8c0-6-3-9.6-7.8-9.6c-4.7,0-7.8,3.3-7.8,9.6v0.8c0,5.8,3,9.1,7.8,9.1
C170.5,45.3,173.5,42.1,173.5,36.3z M186.5,19.6h7l0.8,6.1c1.7-4.1,5.3-6.9,10.6-6.9c8.2,0,13.6,5.9,13.6,16.8v0.9
c0,10.6-6,16.2-13.6,16.2c-5.1,0-8.6-2.3-10.3-6V63h-8.2L186.5,19.6L186.5,19.6z M210,36.3v-0.7c0-6.4-3.3-9.6-7.7-9.6
c-4.7,0-7.8,3.6-7.8,9.6v0.6c0,5.7,3,9.3,7.7,9.3C207,45.4,210,42.3,210,36.3z M230.9,45.9l-0.7,5.9H223v-43h8.2v16.5
c1.8-4.2,5.4-6.5,10.5-6.5c7.7,0.1,13.4,5.4,13.4,16.1v1c0,10.7-5.4,16.8-13.6,16.8C236.1,52.6,232.6,50.1,230.9,45.9z M246.5,35.9
v-0.8c0-5.9-3.2-9.2-7.7-9.2c-4.6,0-7.8,3.7-7.8,9.3v0.7c0,6,3.1,9.5,7.7,9.5C243.6,45.4,246.5,42.3,246.5,35.9z M258.7,36.4v-0.9
c0-10.8,6.9-16.7,16.3-16.7c9.6,0,16.3,5.9,16.3,16.7v0.9c0,10.6-6.6,16.3-16.3,16.3C264.6,52.6,258.7,47,258.7,36.4z M282.8,36.3
v-0.8c0-6-3-9.6-7.8-9.6c-4.7,0-7.8,3.3-7.8,9.6v0.8c0,5.8,3,9.1,7.8,9.1C279.8,45.3,282.8,42.1,282.8,36.3z M302.3,35.1L291,19.6
h9.7l6.5,9.7l6.6-9.7h9.6L311.9,35L324,51.8h-9.5l-7.4-10.7l-7.2,10.7H290L302.3,35.1z"/>
<g id="Editble" class="st1">
<g class="st2">
<rect x="-105" y="5" class="st3" width="506" height="71.8"/>
<path d="M0.2,13.6h16.3c10.4,0,19,6.1,19,19.8v2.9c0,13.8-8,20-18.7,20H0.2V13.6z M9.4,21.3v27.2h7c5.9,0,9.9-3.9,9.9-12.5v-2.2
c0-8.6-4.1-12.5-10.2-12.5H9.4z M40.4,21.8h7.3l1.1,8c1.4-5.5,4.9-8.3,11.3-8.3h2.2v9.2h-3.7c-7.4,0-9.2,2.6-9.2,9.9v15.8h-9
C40.4,56.4,40.4,21.8,40.4,21.8z M64.3,39.8v-1c0-11.6,7.4-17.9,17.5-17.9c10.3,0,17.5,6.4,17.5,17.9v1c0,11.4-7,17.5-17.5,17.5
C70.6,57.3,64.3,51.2,64.3,39.8z M90.1,39.7v-0.8c0-6.5-3.2-10.3-8.3-10.3c-5,0-8.4,3.5-8.4,10.3v0.8c0,6.2,3.2,9.7,8.3,9.7
C86.9,49.4,90.1,46,90.1,39.7z M104,21.8h7.6l0.9,6.6c1.9-4.4,5.7-7.4,11.4-7.4c8.8,0,14.6,6.4,14.6,18v1
c0,11.4-6.4,17.3-14.6,17.3c-5.5,0-9.2-2.5-11-6.5v17.5H104V21.8z M129.3,39.8V39c0-6.9-3.5-10.3-8.3-10.3c-5,0-8.4,3.8-8.4,10.3
v0.7c0,6.1,3.2,10,8.2,10C126,49.5,129.3,46.1,129.3,39.8z M151.7,50.1l-0.7,6.3h-7.8V10.2h8.8V28c1.9-4.5,5.8-7,11.2-7
c8.2,0.1,14.3,5.8,14.3,17.3v1c0,11.5-5.8,18-14.6,18C157.3,57.3,153.5,54.5,151.7,50.1z M168.5,39.3v-0.8c0-6.4-3.5-9.8-8.3-9.8
c-5,0-8.4,4-8.4,10v0.7c0,6.5,3.3,10.2,8.3,10.2C165.3,49.5,168.5,46.1,168.5,39.3z M181.6,39.8v-1c0-11.6,7.4-17.9,17.5-17.9
c10.3,0,17.5,6.4,17.5,17.9v1c0,11.4-7.1,17.5-17.5,17.5C187.9,57.3,181.6,51.2,181.6,39.8z M207.4,39.7v-0.8
c0-6.5-3.2-10.3-8.3-10.3c-5,0-8.4,3.5-8.4,10.3v0.8c0,6.2,3.2,9.7,8.3,9.7C204.2,49.4,207.4,46,207.4,39.7z M228.3,38.4
l-12.1-16.7h10.4l7,10.4l7.1-10.4H251l-12.3,16.6l13,18h-10.2l-8-11.5l-7.7,11.5h-10.6L228.3,38.4z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 21 21"><title>MS-SymbolLockup</title><rect x="1" y="1" width="9" height="9" fill="#f25022"/><rect x="1" y="11" width="9" height="9" fill="#00a4ef"/><rect x="11" y="1" width="9" height="9" fill="#7fba00"/><rect x="11" y="11" width="9" height="9" fill="#ffb900"/></svg>

Before

Width:  |  Height:  |  Size: 343 B

View File

@@ -48,11 +48,10 @@ var interfaceConfig = {
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
'tileview'
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts'
],
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile' ],
// Determines how the video would fit the screen. 'both' would fit the whole
// screen, 'height' would fit the original video height to the height of the
@@ -173,12 +172,6 @@ var interfaceConfig = {
*/
RECENT_LIST_ENABLED: true
/**
* How many columns the tile view can expand to. The respected range is
* between 1 and 5.
*/
// TILE_VIEW_MAX_COLUMNS: 5,
/**
* Specify custom URL for downloading android mobile app.
*/

View File

@@ -11,7 +11,7 @@ PODS:
- React/Core (= 0.55.4)
- react-native-background-timer (2.0.0):
- React
- react-native-calendar-events (1.6.2):
- react-native-calendar-events (1.6.0):
- React
- react-native-fast-image (4.0.14):
- FLAnimatedImage
@@ -22,7 +22,7 @@ PODS:
- React
- react-native-locale-detector (1.0.0):
- React
- react-native-webrtc (1.63.0):
- react-native-webrtc (1.58.2):
- React
- React/Core (0.55.4):
- yoga (= 0.55.4.React)
@@ -152,7 +152,7 @@ SPEC CHECKSUMS:
react-native-keep-awake: 0de4bd66de0c23178107dce0c2fcc3354b2a8e94
react-native-locale-detector: d1b2c6fe5abb56e3a1efb6c2d6f308c05c4251f1
react-native-webrtc: 31b6d3f1e3e2ce373aa43fd682b04367250f807d
ReactNativePermissions: 9ef3f0c74a373fdbfae21c067098a8348d9aa15f
ReactNativePermissions: 9f2d9c45c98800795e6c2ed330e25d11a66a8169
RNSound: b360b3862d3118ed1c74bb9825696b5957686ac4
RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44
SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681

View File

@@ -43,8 +43,7 @@
"mute": "Mute or unmute your microphone",
"fullScreen": "View or exit full screen",
"videoMute": "Start or stop your camera",
"showSpeakerStats": "Show speaker stats",
"localRecording": "Show or hide local recording controls"
"showSpeakerStats": "Show speaker stats"
},
"welcomepage":{
"accessibilityLabel": {
@@ -57,8 +56,6 @@
"video": "Video"
},
"calendar": "Calendar",
"connectCalendarText": "Connect your calendar to view all your meetings in __app__. Plus, add __app__ meetings to your calendar and start them with one click.",
"connectCalendarButton": "Connect your calendar",
"go": "GO",
"join": "JOIN",
"privacy": "Privacy",
@@ -90,7 +87,6 @@
"fullScreen": "Toggle full screen",
"hangup": "Leave the call",
"invite": "Invite people",
"localRecording": "Toggle local recording controls",
"lockRoom": "Toggle room lock",
"moreActions": "Toggle more actions menu",
"moreActionsMenu": "More actions menu",
@@ -106,7 +102,6 @@
"shortcuts": "Toggle shortcuts",
"speakerStats": "Toggle speaker statistics",
"toggleCamera": "Toggle camera",
"tileView": "Toggle tile view",
"videomute": "Toggle mute video"
},
"addPeople": "Add people to your call",
@@ -149,7 +144,6 @@
"raiseHand": "Raise / Lower your hand",
"shortcuts": "View shortcuts",
"speakerStats": "Speaker stats",
"tileViewToggle": "Toggle tile view",
"invite": "Invite people"
},
"chat":{
@@ -159,14 +153,8 @@
},
"messagebox": "Enter text..."
},
"settings": {
"calendar": {
"about": "The __appName__ calendar integration is used to securely access your calendar so it can read upcoming events.",
"disconnect": "Disconnect",
"microsoftSignIn": "Sign in with Microsoft",
"signedIn": "Currently accessing calendar events for __email__. Click the Disconnect button below to stop accessing calendar events.",
"title": "Calendar"
},
"settings":
{
"title": "Settings",
"update": "Update",
"name": "Name",
@@ -210,7 +198,6 @@
"packetloss": "Packet loss:",
"resolution": "Resolution:",
"framerate": "Frame rate:",
"e2e_rtt": "E2E RTT:",
"less": "Show less",
"more": "Show more",
"address": "Address:",
@@ -426,11 +413,6 @@
],
"and": "and"
},
"share":
{
"mainText": "Click the following link to join the meeting:\n__roomUrl__",
"dialInfoText": "\n\n=====\n\nJust want to dial in on your phone?\n\nClick this link to see the dial in phone numbers for this meetings\n__dialInfoPageUrl__"
},
"connection":
{
"ERROR": "Error",
@@ -463,13 +445,7 @@
"on": "Recording",
"pending": "Preparing to record the meeting...",
"rec": "REC",
"authDropboxText": "Upload your recording to Dropbox.",
"authDropboxCompletedText": "Your recording file will appear in your Dropbox shortly after the recording has finished.",
"serviceName": "Recording service",
"signOut": "Sign Out",
"signIn": "sign in",
"loggedIn": "Logged in as __userName__",
"availableSpace": "Available space: __spaceLeft__ MB (approximately __duration__ minutes of recording)",
"startRecordingBody": "Are you sure you would like to start recording?",
"unavailable": "Oops! The __serviceName__ is currently unavailable. We're working on resolving the issue. Please try again later.",
"unavailableTitle": "Recording unavailable"
@@ -641,14 +617,13 @@
"startWithVideoMuted": "Start with video muted"
},
"calendarSync": {
"addMeetingURL": "Add a meeting link",
"today": "Today",
"later": "Later",
"next": "Upcoming",
"nextMeeting": "next meeting",
"noEvents": "There are no upcoming events scheduled.",
"now": "Now",
"ongoingMeeting": "ongoing meeting",
"permissionButton": "Open settings",
"permissionMessage": "The Calendar permission is required to see your meetings in the app.",
"refresh": "Refresh calendar"
"permissionMessage": "The Calendar permission is required to see your meetings in the app."
},
"recentList": {
"joinPastMeeting": "Join A Past Meeting"
@@ -690,34 +665,5 @@
"decline": "Dismiss",
"productLabel": "from Jitsi Meet",
"videoCallTitle": "Incoming video call"
},
"localRecording": {
"localRecording": "Local Recording",
"dialogTitle": "Local Recording Controls",
"start": "Start Recording",
"stop": "Stop Recording",
"moderator": "Moderator",
"me": "Me",
"duration": "Duration",
"durationNA": "N/A",
"encoding": "Encoding",
"participantStats": "Participant Stats",
"participant": "Participant",
"sessionToken": "Session Token",
"clientState": {
"on": "On",
"off": "Off",
"unknown": "Unknown"
},
"messages": {
"engaged": "Local recording engaged.",
"finished": "Recording session __token__ finished. Please send the recorded file to the moderator.",
"finishedModerator": "Recording session __token__ finished. The recording of the local track has been saved. Please ask the other participants to submit their recordings.",
"notModerator": "You are not the moderator. You cannot start or stop local recording."
},
"yes": "Yes",
"no": "No",
"label": "LOR",
"labelToolTip": "Local recording is engaged"
}
}

View File

@@ -21,7 +21,6 @@ import {
getPinnedParticipant,
pinParticipant
} from '../react/features/base/participants';
import { setTileView } from '../react/features/video-layout';
import UIEvents from '../service/UI/UIEvents';
import VideoLayout from './UI/videolayout/VideoLayout';
@@ -118,31 +117,6 @@ class State {
}
}
/**
* A getter for this object instance to know the state of tile view.
*
* @returns {boolean} True if tile view is enabled.
*/
get tileViewEnabled() {
return this._tileViewEnabled;
}
/**
* A setter for {@link tileViewEnabled}. Fires a property change event for
* other participants to follow.
*
* @param {boolean} b - Whether or not tile view is enabled.
* @returns {void}
*/
set tileViewEnabled(b) {
const oldValue = this._tileViewEnabled;
if (oldValue !== b) {
this._tileViewEnabled = b;
this._firePropertyChange('tileViewEnabled', oldValue, b);
}
}
/**
* Invokes {_propertyChangeCallback} to notify it that {property} had its
* value changed from {oldValue} to {newValue}.
@@ -215,10 +189,6 @@ class FollowMe {
this._sharedDocumentToggled
.bind(this, this._UI.getSharedDocumentManager().isVisible());
}
this._tileViewToggled.bind(
this,
APP.store.getState()['features/video-layout'].tileViewEnabled);
}
/**
@@ -244,10 +214,6 @@ class FollowMe {
this.sharedDocEventHandler = this._sharedDocumentToggled.bind(this);
this._UI.addListener(UIEvents.TOGGLED_SHARED_DOCUMENT,
this.sharedDocEventHandler);
this.tileViewEventHandler = this._tileViewToggled.bind(this);
this._UI.addListener(UIEvents.TOGGLED_TILE_VIEW,
this.tileViewEventHandler);
}
/**
@@ -261,8 +227,6 @@ class FollowMe {
this.sharedDocEventHandler);
this._UI.removeListener(UIEvents.PINNED_ENDPOINT,
this.pinnedEndpointEventHandler);
this._UI.removeListener(UIEvents.TOGGLED_TILE_VIEW,
this.tileViewEventHandler);
}
/**
@@ -302,18 +266,6 @@ class FollowMe {
this._local.sharedDocumentVisible = sharedDocumentVisible;
}
/**
* Notifies this instance that the tile view mode has been enabled or
* disabled.
*
* @param {boolean} enabled - True if tile view has been enabled, false
* if has been disabled.
* @returns {void}
*/
_tileViewToggled(enabled) {
this._local.tileViewEnabled = enabled;
}
/**
* Changes the nextOnStage property value.
*
@@ -364,8 +316,7 @@ class FollowMe {
attributes: {
filmstripVisible: local.filmstripVisible,
nextOnStage: local.nextOnStage,
sharedDocumentVisible: local.sharedDocumentVisible,
tileViewEnabled: local.tileViewEnabled
sharedDocumentVisible: local.sharedDocumentVisible
}
});
}
@@ -404,7 +355,6 @@ class FollowMe {
this._onFilmstripVisible(attributes.filmstripVisible);
this._onNextOnStage(attributes.nextOnStage);
this._onSharedDocumentVisible(attributes.sharedDocumentVisible);
this._onTileViewEnabled(attributes.tileViewEnabled);
}
/**
@@ -484,21 +434,6 @@ class FollowMe {
}
}
/**
* Process a tile view enabled / disabled event received from FOLLOW-ME.
*
* @param {boolean} enabled - Whether or not tile view should be shown.
* @private
* @returns {void}
*/
_onTileViewEnabled(enabled) {
if (typeof enabled === 'undefined') {
return;
}
APP.store.dispatch(setTileView(enabled === 'true'));
}
/**
* Pins / unpins the video thumbnail given by clickId.
*

View File

@@ -1,6 +1,4 @@
/* global $, APP */
import { shouldDisplayTileView } from '../../../react/features/video-layout';
/* global $ */
import SmallVideo from '../videolayout/SmallVideo';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -66,9 +64,7 @@ SharedVideoThumb.prototype.createContainer = function(spanId) {
* The thumb click handler.
*/
SharedVideoThumb.prototype.videoClick = function() {
if (!shouldDisplayTileView(APP.store.getState())) {
this._togglePin();
}
this._togglePin();
};
/**

View File

@@ -1,13 +1,6 @@
/* global $, APP, interfaceConfig */
import { setFilmstripVisible } from '../../../react/features/filmstrip';
import {
LAYOUTS,
getCurrentLayout,
getMaxColumnCount,
getTileViewGridDimensions,
shouldDisplayTileView
} from '../../../react/features/video-layout';
import UIEvents from '../../../service/UI/UIEvents';
import UIUtil from '../util/UIUtil';
@@ -240,10 +233,6 @@ const Filmstrip = {
* @returns {*|{localVideo, remoteVideo}}
*/
calculateThumbnailSize() {
if (shouldDisplayTileView(APP.store.getState())) {
return this._calculateThumbnailSizeForTileView();
}
const availableSizes = this.calculateAvailableSize();
const width = availableSizes.availableWidth;
const height = availableSizes.availableHeight;
@@ -258,10 +247,11 @@ const Filmstrip = {
* @returns {{availableWidth: number, availableHeight: number}}
*/
calculateAvailableSize() {
const state = APP.store.getState();
const currentLayout = getCurrentLayout(state);
const isHorizontalFilmstripView
= currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW;
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
const thumbs = this.getThumbs(true);
const numvids = thumbs.remoteThumbs.length;
const localVideoContainer = $('#localVideoContainer');
/**
* If the videoAreaAvailableWidth is set we use this one to calculate
@@ -278,15 +268,10 @@ const Filmstrip = {
- UIUtil.parseCssInt(this.filmstrip.css('borderRightWidth'), 10)
- 5;
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
let availableWidth = videoAreaAvailableWidth;
const thumbs = this.getThumbs(true);
// If local thumb is not hidden
if (thumbs.localThumb) {
const localVideoContainer = $('#localVideoContainer');
availableWidth = Math.floor(
videoAreaAvailableWidth - (
UIUtil.parseCssInt(
@@ -304,12 +289,10 @@ const Filmstrip = {
);
}
// If the number of videos is 0 or undefined or we're not in horizontal
// If the number of videos is 0 or undefined or we're in vertical
// filmstrip mode we don't need to calculate further any adjustments
// to width based on the number of videos present.
const numvids = thumbs.remoteThumbs.length;
if (numvids && isHorizontalFilmstripView) {
if (numvids && !interfaceConfig.VERTICAL_FILMSTRIP) {
const remoteVideoContainer = thumbs.remoteThumbs.eq(0);
availableWidth = Math.floor(
@@ -339,10 +322,8 @@ const Filmstrip = {
availableHeight
= Math.min(maxHeight, window.innerHeight - 18);
return {
availableHeight,
availableWidth
};
return { availableWidth,
availableHeight };
},
/**
@@ -453,51 +434,6 @@ const Filmstrip = {
};
},
/**
* Calculates the size for thumbnails when in tile view layout.
*
* @returns {{localVideo, remoteVideo}}
*/
_calculateThumbnailSizeForTileView() {
const tileAspectRatio = 16 / 9;
// The distance from the top and bottom of the screen, as set by CSS, to
// avoid overlapping UI elements.
const topBottomPadding = 200;
// Minimum space to keep between the sides of the tiles and the sides
// of the window.
const sideMargins = 30 * 2;
const state = APP.store.getState();
const viewWidth = document.body.clientWidth - sideMargins;
const viewHeight = document.body.clientHeight - topBottomPadding;
const {
columns,
visibleRows
} = getTileViewGridDimensions(state, getMaxColumnCount());
const initialWidth = viewWidth / columns;
const aspectRatioHeight = initialWidth / tileAspectRatio;
const heightOfEach = Math.min(
aspectRatioHeight,
viewHeight / visibleRows);
const widthOfEach = tileAspectRatio * heightOfEach;
return {
localVideo: {
thumbWidth: widthOfEach,
thumbHeight: heightOfEach
},
remoteVideo: {
thumbWidth: widthOfEach,
thumbHeight: heightOfEach
}
};
},
/**
* Resizes thumbnails
* @param local
@@ -507,28 +443,6 @@ const Filmstrip = {
*/
// eslint-disable-next-line max-params
resizeThumbnails(local, remote, forceUpdate = false) {
const state = APP.store.getState();
if (shouldDisplayTileView(state)) {
// The size of the side margins for each tile as set in CSS.
const sideMargins = 10 * 2;
const {
columns,
rows
} = getTileViewGridDimensions(state, getMaxColumnCount());
const hasOverflow = rows > columns;
// Width is set so that the flex layout can automatically wrap
// tiles onto new rows.
this.filmstripRemoteVideos.css({
width: (local.thumbWidth * columns) + (columns * sideMargins)
});
this.filmstripRemoteVideos.toggleClass('has-overflow', hasOverflow);
} else {
this.filmstripRemoteVideos.css('width', '');
}
const thumbs = this.getThumbs(!forceUpdate);
if (thumbs.localThumb) {
@@ -552,15 +466,13 @@ const Filmstrip = {
});
}
const currentLayout = getCurrentLayout(APP.store.getState());
// Let CSS take care of height in vertical filmstrip mode.
if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
if (interfaceConfig.VERTICAL_FILMSTRIP) {
$('#filmstripLocalVideo').css({
// adds 4 px because of small video 2px border
width: `${local.thumbWidth + 4}px`
});
} else if (currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
} else {
this.filmstrip.css({
// adds 4 px because of small video 2px border
height: `${remote.thumbHeight + 4}px`

View File

@@ -11,7 +11,6 @@ import {
getAvatarURLByParticipantId
} from '../../../react/features/base/participants';
import { updateSettings } from '../../../react/features/base/settings';
import { shouldDisplayTileView } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -27,7 +26,7 @@ function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
this.streamEndedCallback = streamEndedCallback;
this.container = this.createContainer();
this.$container = $(this.container);
this.updateDOMLocation();
$('#filmstripLocalVideoThumbnail').append(this.container);
this.localVideoId = null;
this.bindHoverHandler();
@@ -110,7 +109,16 @@ LocalVideo.prototype.changeVideo = function(stream) {
this.localVideoId = `localVideo_${stream.getId()}`;
this._updateVideoElement();
const localVideoContainer = document.getElementById('localVideoWrapper');
ReactDOM.render(
<Provider store = { APP.store }>
<VideoTrack
id = { this.localVideoId }
videoTrack = {{ jitsiTrack: stream }} />
</Provider>,
localVideoContainer
);
// eslint-disable-next-line eqeqeq
const isVideo = stream.videoType != 'desktop';
@@ -120,14 +128,12 @@ LocalVideo.prototype.changeVideo = function(stream) {
this.setFlipX(isVideo ? settings.localFlipX : false);
const endedHandler = () => {
const localVideoContainer
= document.getElementById('localVideoWrapper');
// Only remove if there is no video and not a transition state.
// Previous non-react logic created a new video element with each track
// removal whereas react reuses the video component so it could be the
// stream ended but a new one is being used.
if (localVideoContainer && this.videoStream.isEnded()) {
if (this.videoStream.isEnded()) {
ReactDOM.unmountComponentAtNode(localVideoContainer);
}
@@ -229,29 +235,6 @@ LocalVideo.prototype._enableDisableContextMenu = function(enable) {
}
};
/**
* Places the {@code LocalVideo} in the DOM based on the current video layout.
*
* @returns {void}
*/
LocalVideo.prototype.updateDOMLocation = function() {
if (!this.container) {
return;
}
if (this.container.parentElement) {
this.container.parentElement.removeChild(this.container);
}
const appendTarget = shouldDisplayTileView(APP.store.getState())
? document.getElementById('localVideoTileViewContainer')
: document.getElementById('filmstripLocalVideoThumbnail');
appendTarget && appendTarget.appendChild(this.container);
this._updateVideoElement();
};
/**
* Callback invoked when the thumbnail is clicked. Will directly call
* VideoLayout to handle thumbnail click if certain elements have not been
@@ -275,9 +258,7 @@ LocalVideo.prototype._onContainerClick = function(event) {
= $source.parents('.displayNameContainer').length > 0;
const clickedOnPopover = $source.parents('.popover').length > 0
|| classList.contains('popover');
const ignoreClick = clickedOnDisplayName
|| clickedOnPopover
|| shouldDisplayTileView(APP.store.getState());
const ignoreClick = clickedOnDisplayName || clickedOnPopover;
if (event.stopPropagation && !ignoreClick) {
event.stopPropagation();
@@ -288,28 +269,4 @@ LocalVideo.prototype._onContainerClick = function(event) {
}
};
/**
* Renders the React Element for displaying video in {@code LocalVideo}.
*
*/
LocalVideo.prototype._updateVideoElement = function() {
const localVideoContainer = document.getElementById('localVideoWrapper');
ReactDOM.render(
<Provider store = { APP.store }>
<VideoTrack
id = 'localVideo_container'
videoTrack = {{ jitsiTrack: this.videoStream }} />
</Provider>,
localVideoContainer
);
// Ensure the video gets play() called on it. This may be necessary in the
// case where the local video container was moved and re-attached, in which
// case video does not autoplay.
const video = this.container.querySelector('video');
video && video.play();
};
export default LocalVideo;

View File

@@ -20,11 +20,6 @@ import {
REMOTE_CONTROL_MENU_STATES,
RemoteVideoMenuTriggerButton
} from '../../../react/features/remote-video-menu';
import {
LAYOUTS,
getCurrentLayout,
shouldDisplayTileView
} from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -168,17 +163,8 @@ RemoteVideo.prototype._generatePopupContent = function() {
const onVolumeChange = this._setAudioVolume;
const { isModerator } = APP.conference;
const participantID = this.id;
const currentLayout = getCurrentLayout(APP.store.getState());
let remoteMenuPosition;
if (currentLayout === LAYOUTS.TILE_VIEW) {
remoteMenuPosition = 'left top';
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
remoteMenuPosition = 'left bottom';
} else {
remoteMenuPosition = 'top center';
}
const menuPosition = interfaceConfig.VERTICAL_FILMSTRIP
? 'left bottom' : 'top center';
ReactDOM.render(
<Provider store = { APP.store }>
@@ -188,7 +174,7 @@ RemoteVideo.prototype._generatePopupContent = function() {
initialVolumeValue = { initialVolumeValue }
isAudioMuted = { this.isAudioMuted }
isModerator = { isModerator }
menuPosition = { remoteMenuPosition }
menuPosition = { menuPosition }
onMenuDisplay
= {this._onRemoteVideoMenuDisplay.bind(this)}
onRemoteControlToggle = { onRemoteControlToggle }
@@ -627,8 +613,7 @@ RemoteVideo.prototype._onContainerClick = function(event) {
const { classList } = event.target;
const ignoreClick = $source.parents('.popover').length > 0
|| classList.contains('popover')
|| shouldDisplayTileView(APP.store.getState());
|| classList.contains('popover');
if (!ignoreClick) {
this._togglePin();

View File

@@ -27,11 +27,6 @@ import {
RaisedHandIndicator,
VideoMutedIndicator
} from '../../../react/features/filmstrip';
import {
LAYOUTS,
getCurrentLayout,
shouldDisplayTileView
} from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -333,21 +328,7 @@ SmallVideo.prototype.setVideoMutedView = function(isMuted) {
SmallVideo.prototype.updateStatusBar = function() {
const statusBarContainer
= this.container.querySelector('.videocontainer__toolbar');
if (!statusBarContainer) {
return;
}
const currentLayout = getCurrentLayout(APP.store.getState());
let tooltipPosition;
if (currentLayout === LAYOUTS.TILE_VIEW) {
tooltipPosition = 'right';
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
tooltipPosition = 'left';
} else {
tooltipPosition = 'top';
}
const tooltipPosition = interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top';
ReactDOM.render(
<I18nextProvider i18n = { i18next }>
@@ -566,8 +547,7 @@ SmallVideo.prototype.isVideoPlayable = function() {
*/
SmallVideo.prototype.selectDisplayMode = function() {
// Display name is always and only displayed when user is on the stage
if (this.isCurrentlyOnLargeVideo()
&& !shouldDisplayTileView(APP.store.getState())) {
if (this.isCurrentlyOnLargeVideo()) {
return this.isVideoPlayable() && !APP.conference.isAudioOnly()
? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
} else if (this.isVideoPlayable()
@@ -705,10 +685,7 @@ SmallVideo.prototype.showDominantSpeakerIndicator = function(show) {
this._showDominantSpeaker = show;
this.$container.toggleClass('active-speaker', this._showDominantSpeaker);
this.updateIndicators();
this.updateView();
};
/**
@@ -788,18 +765,6 @@ SmallVideo.prototype.initBrowserSpecificProperties = function() {
}
};
/**
* Helper function for re-rendering multiple react components of the small
* video.
*
* @returns {void}
*/
SmallVideo.prototype.rerender = function() {
this.updateIndicators();
this.updateStatusBar();
this.updateView();
};
/**
* Updates the React element responsible for showing connection status, dominant
* speaker, and raised hand icons. Uses instance variables to get the necessary
@@ -819,19 +784,7 @@ SmallVideo.prototype.updateIndicators = function() {
const iconSize = UIUtil.getIndicatorFontSize();
const showConnectionIndicator = this.videoIsHovered
|| !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
const currentLayout = getCurrentLayout(APP.store.getState());
let statsPopoverPosition, tooltipPosition;
if (currentLayout === LAYOUTS.TILE_VIEW) {
statsPopoverPosition = 'right top';
tooltipPosition = 'right';
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
statsPopoverPosition = this.statsPopoverLocation;
tooltipPosition = 'left';
} else {
statsPopoverPosition = this.statsPopoverLocation;
tooltipPosition = 'top';
}
const tooltipPosition = interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top';
ReactDOM.render(
<I18nextProvider i18n = { i18next }>
@@ -846,7 +799,7 @@ SmallVideo.prototype.updateIndicators = function() {
enableStatsDisplay
= { !interfaceConfig.filmStripOnly }
statsPopoverPosition
= { statsPopoverPosition }
= { this.statsPopoverLocation }
userID = { this.id } />
: null }
{ this._showRaisedHand

View File

@@ -1,10 +1,6 @@
/* global APP, $, interfaceConfig */
const logger = require('jitsi-meet-logger').getLogger(__filename);
import {
getNearestReceiverVideoQualityLevel,
setMaxReceiverVideoQuality
} from '../../../react/features/base/conference';
import {
JitsiParticipantConnectionStatus
} from '../../../react/features/base/lib-jitsi-meet';
@@ -13,9 +9,6 @@ import {
getPinnedParticipant,
pinParticipant
} from '../../../react/features/base/participants';
import {
shouldDisplayTileView
} from '../../../react/features/video-layout';
import { SHARED_VIDEO_CONTAINER_TYPE } from '../shared_video/SharedVideo';
import SharedVideoThumb from '../shared_video/SharedVideoThumb';
@@ -601,19 +594,12 @@ const VideoLayout = {
Filmstrip.resizeThumbnails(localVideo, remoteVideo, forceUpdate);
if (shouldDisplayTileView(APP.store.getState())) {
const height
= (localVideo && localVideo.thumbHeight)
|| (remoteVideo && remoteVideo.thumbnHeight)
|| 0;
const qualityLevel = getNearestReceiverVideoQualityLevel(height);
APP.store.dispatch(setMaxReceiverVideoQuality(qualityLevel));
}
if (onComplete && typeof onComplete === 'function') {
onComplete();
}
return { localVideo,
remoteVideo };
},
/**
@@ -1156,22 +1142,6 @@ const VideoLayout = {
);
},
/**
* Helper method to invoke when the video layout has changed and elements
* have to be re-arranged and resized.
*
* @returns {void}
*/
refreshLayout() {
localVideoThumbnail && localVideoThumbnail.updateDOMLocation();
VideoLayout.resizeVideoArea();
localVideoThumbnail && localVideoThumbnail.rerender();
Object.values(remoteVideos).forEach(
remoteVideo => remoteVideo.rerender()
);
},
/**
* Triggers an update of large video if the passed in participant is
* currently displayed on large video.

59
package-lock.json generated
View File

@@ -3003,15 +3003,6 @@
"sdp-transform": "2.3.0"
}
},
"@microsoft/microsoft-graph-client": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@microsoft/microsoft-graph-client/-/microsoft-graph-client-1.1.0.tgz",
"integrity": "sha512-sDgchKZz1l3QJVNdkE1P1KpwTjupNt1mS9h1T0CiP+ayMN7IeFKfElB8IYtxFplNalZTmEq+iqoQFqUVpVMLfQ==",
"requires": {
"es6-promise": "^4.1.0",
"isomorphic-fetch": "^2.2.1"
}
},
"@webcomponents/url": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@webcomponents/url/-/url-0.7.1.tgz",
@@ -6033,15 +6024,6 @@
"domelementtype": "1"
}
},
"dropbox": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/dropbox/-/dropbox-4.0.9.tgz",
"integrity": "sha512-UeaKw7DY24ZGLRV8xboZvbZXhbTVrFjPjfpr0LfF/KVOzBUad9vJJwqz3udqTLNxD0FXbFlC9rlNLLNXaj9msg==",
"requires": {
"buffer": "^5.0.8",
"moment": "^2.19.3"
}
},
"duplexify": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.4.tgz",
@@ -6281,11 +6263,6 @@
"event-emitter": "~0.3.5"
}
},
"es6-promise": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz",
"integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ=="
},
"es6-set": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz",
@@ -6472,8 +6449,8 @@
}
},
"eslint-config-jitsi": {
"version": "github:jitsi/eslint-config-jitsi#7474f6668515eb5852f1273dc5a50b940a550d3f",
"from": "github:jitsi/eslint-config-jitsi#7474f6668515eb5852f1273dc5a50b940a550d3f",
"version": "github:jitsi/eslint-config-jitsi#3d193df6476a73f827582e137a67a8612130a455",
"from": "github:jitsi/eslint-config-jitsi#v0.1.0",
"dev": true
},
"eslint-import-resolver-node": {
@@ -9666,11 +9643,6 @@
"verror": "1.10.0"
}
},
"jsrsasign": {
"version": "8.0.12",
"resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-8.0.12.tgz",
"integrity": "sha1-Iqu5ZW00owuVMENnIINeicLlwxY="
},
"jssha": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/jssha/-/jssha-2.3.1.tgz",
@@ -9747,8 +9719,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#41592c774ab234cc45a0a083e3a3f0bf7f7b54fe",
"from": "github:jitsi/lib-jitsi-meet#41592c774ab234cc45a0a083e3a3f0bf7f7b54fe",
"version": "github:jitsi/lib-jitsi-meet#2be752fc88ff71e454c6b9178b21a33b59c53f41",
"from": "github:jitsi/lib-jitsi-meet#2be752fc88ff71e454c6b9178b21a33b59c53f41",
"requires": {
"@jitsi/sdp-interop": "0.1.13",
"@jitsi/sdp-simulcast": "0.2.1",
@@ -9764,10 +9736,6 @@
"yaeti": "1.0.1"
}
},
"libflacjs": {
"version": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"from": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d"
},
"load-json-file": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
@@ -12744,8 +12712,8 @@
"integrity": "sha512-vLNJIedXQZN4p3ChFsAgVHacnJqQMnLl+wBsnZuliRkmsjEHo8kQOA9fnLih/OoiDi1O3eHQvXC5L8f+RYiKgw=="
},
"react-native-calendar-events": {
"version": "github:wmcmahan/react-native-calendar-events#cb2731db6684a49b4343e09de7f9c2fcc68bcd9b",
"from": "github:wmcmahan/react-native-calendar-events#cb2731db6684a49b4343e09de7f9c2fcc68bcd9b"
"version": "github:jitsi/react-native-calendar-events#cad37355f36d17587d84af72b0095e8cc5fd3df9",
"from": "github:jitsi/react-native-calendar-events#cad37355f36d17587d84af72b0095e8cc5fd3df9"
},
"react-native-callstats": {
"version": "3.52.0",
@@ -12787,8 +12755,9 @@
"from": "github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9"
},
"react-native-permissions": {
"version": "github:lyubomir/react-native-permissions#3462430addce3f2c8297c15da14182568194a216",
"from": "github:lyubomir/react-native-permissions#3462430addce3f2c8297c15da14182568194a216"
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-1.1.1.tgz",
"integrity": "sha512-t0Ujm177bagjUOSzhpmkSz+LqFW04HnY9TeZFavDCmV521fQvFz82aD+POXqWsAdsJVOK3umJYBNNqCjC3g0hQ=="
},
"react-native-prompt": {
"version": "1.0.0",
@@ -12833,8 +12802,8 @@
}
},
"react-native-webrtc": {
"version": "github:jitsi/react-native-webrtc#048b852d340877370d849e44b8ca4d59ff4b1429",
"from": "github:jitsi/react-native-webrtc#048b852d340877370d849e44b8ca4d59ff4b1429",
"version": "github:jitsi/react-native-webrtc#6b0ea124414f6f5b7f234a7d5cec75d30f5f6312",
"from": "github:jitsi/react-native-webrtc#6b0ea124414f6f5b7f234a7d5cec75d30f5f6312",
"requires": {
"base64-js": "^1.1.2",
"event-target-shim": "^1.0.5",
@@ -14329,9 +14298,9 @@
}
},
"sdp": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.8.0.tgz",
"integrity": "sha512-wRSES07rAwKWAR7aev9UuClT7kdf9ZTdeUK5gTgHue9vlhs19Fbm3ccNEGJO4y2IitH4/JzS4sdzyPl6H2KQLw=="
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.7.4.tgz",
"integrity": "sha512-0+wTfgvUUEGcvvFoHIC0aiGbx6gzwAUm8FkKt5Oqqkjf9mEEDLgwnoDKX7MYTGXrNNwzikVbutJ+OVNAGmJBQw=="
},
"sdp-transform": {
"version": "2.3.0",

View File

@@ -34,10 +34,8 @@
"@atlaskit/tabs": "4.0.1",
"@atlaskit/theme": "2.4.0",
"@atlaskit/tooltip": "9.1.1",
"@microsoft/microsoft-graph-client": "1.1.0",
"@webcomponents/url": "0.7.1",
"autosize": "1.18.13",
"dropbox": "4.0.9",
"i18next": "8.4.3",
"i18next-browser-languagedetector": "2.0.0",
"i18next-xhr-backend": "1.4.2",
@@ -48,10 +46,8 @@
"jquery-i18next": "1.2.0",
"js-md5": "0.6.1",
"jsc-android": "224109.1.0",
"jsrsasign": "8.0.12",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#41592c774ab234cc45a0a083e3a3f0bf7f7b54fe",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#2be752fc88ff71e454c6b9178b21a33b59c53f41",
"lodash": "4.17.4",
"moment": "2.19.4",
"moment-duration-format": "2.2.2",
@@ -62,18 +58,18 @@
"react-i18next": "4.8.0",
"react-native": "0.55.4",
"react-native-background-timer": "2.0.0",
"react-native-calendar-events": "github:wmcmahan/react-native-calendar-events#cb2731db6684a49b4343e09de7f9c2fcc68bcd9b",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#cad37355f36d17587d84af72b0095e8cc5fd3df9",
"react-native-callstats": "3.52.0",
"react-native-fast-image": "4.0.14",
"react-native-immersive": "1.1.0",
"react-native-keep-awake": "2.0.6",
"react-native-linear-gradient": "2.4.0",
"react-native-locale-detector": "github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9",
"react-native-permissions": "github:lyubomir/react-native-permissions#3462430addce3f2c8297c15da14182568194a216",
"react-native-permissions": "1.1.1",
"react-native-prompt": "1.0.0",
"react-native-sound": "0.10.9",
"react-native-vector-icons": "4.4.2",
"react-native-webrtc": "github:jitsi/react-native-webrtc#048b852d340877370d849e44b8ca4d59ff4b1429",
"react-native-webrtc": "github:jitsi/react-native-webrtc#6b0ea124414f6f5b7f234a7d5cec75d30f5f6312",
"react-redux": "5.0.7",
"redux": "4.0.0",
"redux-thunk": "2.2.0",
@@ -91,7 +87,7 @@
"clean-css": "3.4.25",
"css-loader": "0.28.7",
"eslint": "4.12.1",
"eslint-config-jitsi": "github:jitsi/eslint-config-jitsi#7474f6668515eb5852f1273dc5a50b940a550d3f",
"eslint-config-jitsi": "github:jitsi/eslint-config-jitsi#v0.1.0",
"eslint-plugin-flowtype": "2.39.1",
"eslint-plugin-import": "2.8.0",
"eslint-plugin-jsdoc": "3.2.0",

View File

@@ -124,8 +124,6 @@ function _appNavigateToOptionalLocation(
// FIXME Turn location's host, hostname, and port properties into
// setters in order to reduce the risks of inconsistent state.
location.hostname = defaultLocation.hostname;
location.pathname
= defaultLocation.pathname + location.pathname.substr(1);
location.port = defaultLocation.port;
location.protocol = defaultLocation.protocol;
} else {

View File

@@ -8,8 +8,7 @@ import {
AVATAR_ID_COMMAND,
AVATAR_URL_COMMAND,
EMAIL_COMMAND,
JITSI_CONFERENCE_URL_KEY,
VIDEO_QUALITY_LEVELS
JITSI_CONFERENCE_URL_KEY
} from './constants';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -103,38 +102,6 @@ export function getCurrentConference(stateful: Function | Object) {
: joining);
}
/**
* Finds the nearest match for the passed in {@link availableHeight} to am
* enumerated value in {@code VIDEO_QUALITY_LEVELS}.
*
* @param {number} availableHeight - The height to which a matching video
* quality level should be found.
* @returns {number} The closest matching value from
* {@code VIDEO_QUALITY_LEVELS}.
*/
export function getNearestReceiverVideoQualityLevel(availableHeight: number) {
const qualityLevels = [
VIDEO_QUALITY_LEVELS.HIGH,
VIDEO_QUALITY_LEVELS.STANDARD,
VIDEO_QUALITY_LEVELS.LOW
];
let selectedLevel = qualityLevels[0];
for (let i = 1; i < qualityLevels.length; i++) {
const previousValue = qualityLevels[i - 1];
const currentValue = qualityLevels[i];
const diffWithCurrent = Math.abs(availableHeight - currentValue);
const diffWithPrevious = Math.abs(availableHeight - previousValue);
if (diffWithCurrent < diffWithPrevious) {
selectedLevel = currentValue;
}
}
return selectedLevel;
}
/**
* Handle an error thrown by the backend (i.e. lib-jitsi-meet) while
* manipulating a conference participant (e.g. pin or select participant).

View File

@@ -96,7 +96,6 @@ const WHITELISTED_KEYS = [
'disableRtx',
'disableSuspendVideo',
'displayJids',
'e2eping',
'enableDisplayNameInStats',
'enableLayerSuspension',
'enableLipSync',

View File

@@ -37,6 +37,9 @@ const INITIAL_RN_STATE = {
// fastest to merely disable them.
disableAudioLevels: true,
// FIXME flow complains about missing 'locationURL' missing in _setConfig
locationURL: undefined,
p2p: {
disableH264: false,
preferH264: true
@@ -126,8 +129,10 @@ function _setConfig(state, { config }) {
const newState = _.merge(
{},
config,
{ error: undefined },
config, {
error: undefined,
locationURL: state.locationURL
},
// The config of _getInitialState() is meant to override the config
// downloaded from the Jitsi Meet deployment because the former contains

View File

@@ -9,7 +9,7 @@ import {
getCurrentConference
} from '../conference';
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
import { parseURIString } from '../util';
import { parseStandardURIString } from '../util';
import {
CONNECTION_DISCONNECTED,
@@ -276,32 +276,18 @@ function _connectionWillConnect(connection) {
* {@code JitsiConnection}.
*/
function _constructOptions(state) {
// Deep clone the options to make sure we don't modify the object in the
// redux store.
const options = _.cloneDeep(state['features/base/config']);
const defaultOptions = state['features/base/connection'].options;
const options = _.merge(
{},
defaultOptions,
// Normalize the BOSH URL.
// Lib-jitsi-meet wants the config passed in multiple places and here is
// the latest one I have discovered.
state['features/base/config'],
);
let { bosh } = options;
if (bosh) {
if (bosh.startsWith('//')) {
// By default our config.js doesn't include the protocol.
const { locationURL } = state['features/base/connection'];
bosh = `${locationURL.protocol}${bosh}`;
} else if (bosh.startsWith('/')) {
// Handle relative URLs, which won't work on mobile.
const { locationURL } = state['features/base/connection'];
const {
protocol,
hostname,
contextRoot
} = parseURIString(locationURL.href);
// eslint-disable-next-line max-len
bosh = `${protocol}//${hostname}${contextRoot || '/'}${bosh.substr(1)}`;
}
// Append room to the URL's search.
const { room } = state['features/base/conference'];
@@ -310,6 +296,16 @@ function _constructOptions(state) {
// not ignore case themselves.
room && (bosh += `?room=${room.toLowerCase()}`);
// XXX By default, config.js does not add a protocol to the BOSH URL.
// Which trips React Native. Make sure there is a protocol in order to
// satisfy React Native.
if (bosh !== defaultOptions.bosh
&& !parseStandardURIString(bosh).protocol) {
const { protocol } = parseStandardURIString(defaultOptions.bosh);
protocol && (bosh = protocol + bosh);
}
options.bosh = bosh;
}

View File

@@ -2,7 +2,8 @@
import { SET_ROOM } from '../conference';
import { JitsiConnectionErrors } from '../lib-jitsi-meet';
import { assign, set, ReducerRegistry } from '../redux';
import { assign, ReducerRegistry } from '../redux';
import { parseURIString } from '../util';
import {
CONNECTION_DISCONNECTED,
@@ -152,6 +153,50 @@ function _connectionWillConnect(
});
}
/**
* Constructs options to be passed to the constructor of {@code JitsiConnection}
* based on a specific location URL.
*
* @param {string} locationURL - The location URL with which the returned
* options are to be constructed.
* @private
* @returns {Object} The options to be passed to the constructor of
* {@code JitsiConnection} based on the location URL.
*/
function _constructOptions(locationURL: URL) {
const locationURI = parseURIString(locationURL.href);
// FIXME The HTTPS scheme for the BOSH URL works with meet.jit.si on both
// mobile & Web. It also works with beta.meet.jit.si on Web. Unfortunately,
// it doesn't work with beta.meet.jit.si on mobile. Temporarily, use the
// HTTP scheme for the BOSH URL with beta.meet.jit.si on mobile.
let { protocol } = locationURI;
const domain = locationURI.hostname;
if (!protocol && domain === 'beta.meet.jit.si') {
const windowLocation = window.location;
windowLocation && (protocol = windowLocation.protocol);
protocol || (protocol = 'http:');
}
// Default to the HTTPS scheme for the BOSH URL.
protocol || (protocol = 'https:');
return {
bosh:
`${String(protocol)}//${domain}${
locationURI.contextRoot || '/'}http-bind`,
hosts: {
domain,
// Required by:
// - lib-jitsi-meet/modules/xmpp/xmpp.js
muc: `conference.${domain}`
}
};
}
/**
* The current (similar to getCurrentConference in base/conference/functions.js)
* connection which is {@code connection} or {@code connecting}.
@@ -178,7 +223,10 @@ function _getCurrentConnection(baseConnectionState: Object): ?Object {
function _setLocationURL(
state: Object,
{ locationURL }: { locationURL: ?URL }) {
return set(state, 'locationURL', locationURL);
return assign(state, {
locationURL,
options: locationURL ? _constructOptions(locationURL) : undefined
});
}
/**

View File

@@ -113,7 +113,7 @@ class Dialog extends AbstractDialog<Props, State> {
[_TAG_KEY]: _SUBMIT_TEXT_TAG_VALUE
};
let el: ?React$Element<*> = (
let el: ?React$Element<*> = ( // eslint-disable-line no-extra-parens
<Prompt
cancelButtonTextStyle = { cancelButtonTextStyle }
cancelText = { t(cancelTitleKey) }

View File

@@ -212,7 +212,7 @@ class DialogWithTabs extends Component<Props, State> {
const { onSubmit, tabs } = this.props;
tabs.forEach(({ submit }, idx) => {
submit && submit(this.state.tabStates[idx]);
submit(this.state.tabStates[idx]);
});
onSubmit();

View File

@@ -14,7 +14,6 @@ export const JitsiConnectionErrors = JitsiMeetJS.errors.connection;
export const JitsiConnectionEvents = JitsiMeetJS.events.connection;
export const JitsiConnectionQualityEvents
= JitsiMeetJS.events.connectionQuality;
export const JitsiE2ePingEvents = JitsiMeetJS.events.e2eping;
export const JitsiMediaDevicesEvents = JitsiMeetJS.events.mediaDevices;
export const JitsiParticipantConnectionStatus
= JitsiMeetJS.constants.participantConnectionStatus;

View File

@@ -73,6 +73,14 @@ export default function _RTCPeerConnection(...args: any[]) {
_RTCPeerConnection.prototype = Object.create(RTCPeerConnection.prototype);
_RTCPeerConnection.prototype.constructor = _RTCPeerConnection;
_RTCPeerConnection.prototype.addIceCandidate
= _makePromiseAware(RTCPeerConnection.prototype.addIceCandidate, 1, 0);
_RTCPeerConnection.prototype.createAnswer
= _makePromiseAware(RTCPeerConnection.prototype.createAnswer, 0, 1);
_RTCPeerConnection.prototype.createOffer
= _makePromiseAware(RTCPeerConnection.prototype.createOffer, 0, 1);
_RTCPeerConnection.prototype._invokeOnaddstream = function(...args) {
const onaddstream = this._onaddstream;
@@ -96,14 +104,32 @@ _RTCPeerConnection.prototype._queueOnaddstream = function(...args) {
this._onaddstreamQueue.push(Array.from(args));
};
_RTCPeerConnection.prototype.setRemoteDescription = function(description) {
_RTCPeerConnection.prototype.setLocalDescription
= _makePromiseAware(RTCPeerConnection.prototype.setLocalDescription, 1, 0);
_RTCPeerConnection.prototype.setRemoteDescription = function(
sessionDescription,
successCallback,
errorCallback) {
// If the deprecated callback-based version is used, translate it to the
// Promise-based version.
if (typeof successCallback !== 'undefined'
|| typeof errorCallback !== 'undefined') {
// XXX Returning a Promise is not necessary. But I don't see why it'd
// hurt (much).
return (
_RTCPeerConnection.prototype.setRemoteDescription.call(
this,
sessionDescription)
.then(successCallback, errorCallback));
}
return (
_synthesizeIPv6Addresses(description)
_synthesizeIPv6Addresses(sessionDescription)
.catch(reason => {
reason && _LOGE(reason);
return description;
return sessionDescription;
})
.then(value => _setRemoteDescription.bind(this)(value)));
@@ -119,18 +145,61 @@ function _LOGE(...args) {
logger.error(...args);
}
/**
* Makes a {@code Promise}-returning function out of a specific void function
* with {@code successCallback} and {@code failureCallback}.
*
* @param {Function} f - The (void) function with {@code successCallback} and
* {@code failureCallback}.
* @param {number} beforeCallbacks - The number of arguments before
* {@code successCallback} and {@code failureCallback}.
* @param {number} afterCallbacks - The number of arguments after
* {@code successCallback} and {@code failureCallback}.
* @returns {Promise}
*/
function _makePromiseAware(
f: Function,
beforeCallbacks: number,
afterCallbacks: number) {
return function(...args) {
return new Promise((resolve, reject) => {
if (args.length <= beforeCallbacks + afterCallbacks) {
args.splice(beforeCallbacks, 0, resolve, reject);
}
let fPromise;
try {
// eslint-disable-next-line no-invalid-this
fPromise = f.apply(this, args);
} catch (e) {
reject(e);
}
// If the super implementation returns a Promise from the deprecated
// invocation by any chance, try to make sense of it.
if (fPromise) {
const { then } = fPromise;
typeof then === 'function'
&& then.call(fPromise, resolve, reject);
}
});
};
}
/**
* Adapts react-native-webrtc's {@link RTCPeerConnection#setRemoteDescription}
* implementation which uses the deprecated, callback-based version to the
* {@code Promise}-based version.
*
* @param {RTCSessionDescription} description - The RTCSessionDescription
* @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription
* which specifies the configuration of the remote end of the connection.
* @private
* @private
* @returns {Promise}
*/
function _setRemoteDescription(description) {
function _setRemoteDescription(sessionDescription) {
return new Promise((resolve, reject) => {
/* eslint-disable no-invalid-this */
@@ -139,8 +208,10 @@ function _setRemoteDescription(description) {
// setRemoteDescription calls. I shouldn't be but... anyway.
this._onaddstreamQueue = [];
RTCPeerConnection.prototype.setRemoteDescription.call(this, description)
.then((...args) => {
RTCPeerConnection.prototype.setRemoteDescription.call(
this,
sessionDescription,
(...args) => {
let q;
try {
@@ -151,7 +222,8 @@ function _setRemoteDescription(description) {
}
this._invokeQueuedOnaddstream(q);
}, (...args) => {
},
(...args) => {
this._onaddstreamQueue = undefined;
reject(...args);

View File

@@ -126,6 +126,7 @@ function _visitNode(node, callback) {
//
// Required by:
// - jQuery
// - lib-jitsi-meet/modules/RTC/adapter.screenshare.js
// - Strophe
if (typeof global.document === 'undefined') {
const document
@@ -150,6 +151,14 @@ function _visitNode(node, callback) {
document.cookie = '';
}
// document.implementation
//
// Required by:
// - jQuery
if (typeof document.implementation === 'undefined') {
document.implementation = {};
}
// document.implementation.createHTMLDocument
//
// Required by:
@@ -353,9 +362,26 @@ function _visitNode(node, callback) {
const { navigator } = global;
if (navigator) {
// platform
//
// Required by:
// - lib-jitsi-meet/modules/RTC/adapter.screenshare.js
if (typeof navigator.platform === 'undefined') {
navigator.platform = '';
}
// plugins
//
// Required by:
// - lib-jitsi-meet/modules/RTC/adapter.screenshare.js
if (typeof navigator.plugins === 'undefined') {
navigator.plugins = [];
}
// userAgent
//
// Required by:
// - lib-jitsi-meet/modules/RTC/adapter.screenshare.js
// - lib-jitsi-meet/modules/browser/BrowserDetection.js
let userAgent = navigator.userAgent || '';

View File

@@ -3,14 +3,14 @@ import {
MediaStreamTrack,
RTCSessionDescription,
RTCIceCandidate,
mediaDevices
getUserMedia
} from 'react-native-webrtc';
import RTCPeerConnection from './RTCPeerConnection';
(global => {
if (typeof global.MediaStream === 'undefined') {
global.MediaStream = MediaStream;
if (typeof global.webkitMediaStream === 'undefined') {
global.webkitMediaStream = MediaStream;
}
if (typeof global.MediaStreamTrack === 'undefined') {
global.MediaStreamTrack = MediaStreamTrack;
@@ -21,7 +21,7 @@ import RTCPeerConnection from './RTCPeerConnection';
if (typeof global.RTCPeerConnection === 'undefined') {
global.RTCPeerConnection = RTCPeerConnection;
}
if (typeof global.RTCPeerConnection === 'undefined') {
if (typeof global.webkitRTCPeerConnection === 'undefined') {
global.webkitRTCPeerConnection = RTCPeerConnection;
}
if (typeof global.RTCSessionDescription === 'undefined') {
@@ -31,8 +31,8 @@ import RTCPeerConnection from './RTCPeerConnection';
const navigator = global.navigator;
if (navigator) {
if (typeof navigator.mediaDevices === 'undefined') {
navigator.mediaDevices = mediaDevices;
if (typeof navigator.webkitGetUserMedia === 'undefined') {
navigator.webkitGetUserMedia = getUserMedia;
}
}

View File

@@ -93,7 +93,7 @@ export default class Video extends Component<*> {
? 'contain'
: (style && style.objectFit) || 'cover';
const rtcView
= (
= ( // eslint-disable-line no-extra-parens
<RTCView
mirror = { this.props.mirror }
objectFit = { objectFit }

View File

@@ -12,11 +12,6 @@ export type Item = {
*/
colorBase: string,
/**
* An optional react element to append to the end of the Item.
*/
elementAfter?: ?ComponentType<any>,
/**
* Item title
*/

View File

@@ -87,7 +87,7 @@ class NavigateSectionList extends Component<Props> {
*/
render() {
const {
renderListEmptyComponent = this._renderListEmptyComponent(),
renderListEmptyComponent = this._renderListEmptyComponent,
sections
} = this.props;
@@ -128,13 +128,11 @@ class NavigateSectionList extends Component<Props> {
* @returns {Function}
*/
_onPress(url) {
const { disabled, onPress } = this.props;
return () => {
const { disabled, onPress } = this.props;
if (!disabled && url && typeof onPress === 'function') {
return () => onPress(url);
}
return null;
!disabled && url && typeof onPress === 'function' && onPress(url);
};
}
_onRefresh: () => void;

View File

@@ -41,7 +41,7 @@ class InlineDialogFailure extends Component<*> {
const supportString = t('inlineDialogFailure.supportMsg');
const supportLinkElem
= supportLink
? (
? ( // eslint-disable-line no-extra-parens
<div className = 'inline-dialog-error-text'>
<span>{ supportString.padEnd(supportString.length + 1) }
</span>

View File

@@ -244,7 +244,7 @@ class MultiSelectAutocomplete extends Component {
if (!this.state.error) {
return null;
}
const content = (
const content = ( // eslint-disable-line no-extra-parens
<div className = 'autocomplete-error'>
<InlineDialogFailure
onRetry = { this._onRetry } />

View File

@@ -6,22 +6,18 @@ import Container from './Container';
import Text from './Text';
import type { Item } from '../../Types';
/**
* The type of the React {@code Component} props of
* {@link NavigateSectionListItem}.
*/
type Props = {
/**
* Function to be invoked when an item is pressed. The item's URL is passed.
*/
onPress: ?Function,
onPress: Function,
/**
* A item containing data to be rendered
*/
item: Item
};
}
/**
* Implements a React/Web {@link Component} for displaying an item in a
@@ -29,16 +25,14 @@ type Props = {
*
* @extends Component
*/
export default class NavigateSectionListItem<P: Props>
extends Component<P> {
export default class NavigateSectionListItem extends Component<Props> {
/**
* Renders the content of this component.
*
* @returns {ReactElement}
*/
render() {
const { elementAfter, lines, title } = this.props.item;
const { lines, title } = this.props.item;
const { onPress } = this.props;
/**
@@ -58,28 +52,22 @@ export default class NavigateSectionListItem<P: Props>
duration = lines[1];
}
const rootClassName = `navigate-section-list-tile ${
onPress ? 'with-click-handler' : 'without-click-handler'}`;
return (
<Container
className = { rootClassName }
className = 'navigate-section-list-tile'
onClick = { onPress }>
<Container className = 'navigate-section-list-tile-info'>
<Text
className = 'navigate-section-tile-title'>
{ title }
</Text>
<Text
className = 'navigate-section-tile-body'>
{ date }
</Text>
<Text
className = 'navigate-section-tile-body'>
{ duration }
</Text>
</Container>
{ elementAfter || null }
<Text
className = 'navigate-section-tile-title'>
{ title }
</Text>
<Text
className = 'navigate-section-tile-body'>
{ date }
</Text>
<Text
className = 'navigate-section-tile-body'>
{ duration }
</Text>
</Container>
);
}

View File

@@ -7,11 +7,6 @@ import type { Section } from '../../Types';
type Props = {
/**
* Rendered when the list is empty. Should be a rendered element.
*/
ListEmptyComponent: Object,
/**
* Used to extract a unique key for a given item at the specified index.
* Key is used for caching and as the react key to track item re-ordering.
@@ -54,7 +49,6 @@ export default class SectionList extends Component<Props> {
*/
render() {
const {
ListEmptyComponent,
renderSectionHeader,
renderItem,
sections,
@@ -62,34 +56,34 @@ export default class SectionList extends Component<Props> {
} = this.props;
/**
* If there are no recent items we don't want to display anything
* If there are no recent items we dont want to display anything
*/
if (sections) {
return (
/* eslint-disable no-extra-parens */
<Container
className = 'navigate-section-list'>
{
sections.length === 0
? ListEmptyComponent
: sections.map((section, sectionIndex) => (
<Container
key = { sectionIndex }>
{ renderSectionHeader(section) }
{ section.data
.map((item, listIndex) => {
const listItem = {
item
};
sections.map((section, sectionIndex) => (
<Container
key = { sectionIndex }>
{ renderSectionHeader(section) }
{ section.data
.map((item, listIndex) => {
const listItem = {
item
};
return renderItem(listItem,
keyExtractor(section,
listIndex));
}) }
</Container>
)
)
return renderItem(listItem,
keyExtractor(section,
listIndex));
}) }
</Container>
)
)
}
</Container>
/* eslint-enable no-extra-parens */
);
}

View File

@@ -105,7 +105,7 @@ class Watermarks extends Component<*, *> {
let reactElement = null;
if (this.state.showBrandWatermark) {
reactElement = (
reactElement = ( // eslint-disable-line no-extra-parens
<div
className = 'watermark rightwatermark'
style = { _RIGHT_WATERMARK_STYLE } />
@@ -114,7 +114,7 @@ class Watermarks extends Component<*, *> {
const { brandWatermarkLink } = this.state;
if (brandWatermarkLink) {
reactElement = (
reactElement = ( // eslint-disable-line no-extra-parens
<a
href = { brandWatermarkLink }
target = '_new'>
@@ -144,7 +144,7 @@ class Watermarks extends Component<*, *> {
const { jitsiWatermarkLink } = this.state;
if (jitsiWatermarkLink) {
reactElement = (
reactElement = ( // eslint-disable-line no-extra-parens
<a
href = { jitsiWatermarkLink }
target = '_new'>

View File

@@ -0,0 +1,14 @@
/**
* FIXME.
*
* {
* type: SET_SESSION,
* session: {
* url: {string},
* state: {string},
* ...data
* }
* }
* @public
*/
export const SET_SESSION = Symbol('SET_SESSION');

View File

@@ -0,0 +1,16 @@
import { SET_SESSION } from './actionTypes';
/**
* FIXME.
*
* @param {string} session - FIXME.
* @returns {{
* type: SET_SESSION
* }}
*/
export function setSession(session) {
return {
type: SET_SESSION,
session
};
}

View File

@@ -0,0 +1,12 @@
export const SESSION_CONFIGURED = Symbol('SESSION_CONFIGURED');
export const SESSION_ENDED = Symbol('SESSION_ENDED');
export const SESSION_FAILED = Symbol('SESSION_FAILED');
export const SESSION_STARTED = Symbol('SESSION_STARTED');
export const SESSION_WILL_END = Symbol('SESSION_WILL_END');
export const SESSION_WILL_START = Symbol('SESSION_WILL_START');

View File

@@ -0,0 +1,36 @@
// @flow
import { toState } from '../redux';
import { toURLString } from '../util';
/**
* FIXME.
*
* @param {Function|Object} stateful - FIXME.
* @param {string} url - FIXME.
* @returns {*}
*/
export function getSession(stateful: Function | Object, url: string): ?Object {
const state = toState(stateful);
const session = state['features/base/session'].get(url);
if (!session) {
console.info(`SESSION NOT FOUND FOR URL: ${url}`);
}
return session;
}
/**
* FIXME.
*
* @param {Function | Object} stateful - FIXME.
* @returns {Object}
*/
export function getCurrentSession(stateful: Function | Object): ?Object {
const state = toState(stateful);
const { locationURL } = state['features/base/config'];
return getSession(state, toURLString(locationURL));
}

View File

@@ -1,7 +1,7 @@
export * from './actions';
export * from './actionTypes';
export * from './components';
export * from './controller';
export * from './constants';
export * from './functions';
import './middleware';
import './reducer';

View File

@@ -0,0 +1,349 @@
// @flow
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE,
JITSI_CONFERENCE_URL_KEY,
isRoomValid
} from '../../base/conference';
import {
CONNECTION_DISCONNECTED,
CONNECTION_FAILED,
CONNECTION_WILL_CONNECT
} from '../../base/connection';
import {
MiddlewareRegistry,
toState
} from '../../base/redux';
import { parseURIString, toURLString } from '../../base/util';
import {
SESSION_CONFIGURED,
SESSION_ENDED,
SESSION_FAILED,
SESSION_STARTED,
SESSION_WILL_END,
SESSION_WILL_START
} from './constants';
import { setSession } from './actions';
import { CONFIG_WILL_LOAD, LOAD_CONFIG_ERROR, SET_CONFIG } from '../config';
import { getCurrentSession, getSession } from './functions';
/**
* Middleware that maintains conference sessions. The features spans across
* three features strictly related to the conference lifecycle.
* The first one is the base/config which configures the session. It's
* 'locationURL' state is used to tell what's the current conference URL the app
* is working with. The session starts as soon as {@link CONFIG_WILL_LOAD} event
* arrives. The {@code locationURL} instance is stored in the session to
* associate the load config request with the session and be able to distinguish
* between the current and outdated load config request failures. After the
* config is loaded the lifecycle moves to the base/connection feature which
* creates a {@code JitsiConnection} and tries to connect to the server. On
* {@code CONNECTION_WILL_CONNECT} the connection instance is stored in the
* session and used later to filter the events similar to what's done for
* the load config requests. The base/conference feature adds the last part to
* the session's lifecycle. A {@code JitsiConference} instance is stored in the
* session on the {@code CONFERENCE_WILL_JOIN} action. A session is considered
* alive as long as either connection or conference is available and
* operational.
*
* @param {Store} store - Redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const result = next(action);
const { type } = action;
switch (type) {
case CONFERENCE_WILL_JOIN: {
const { conference } = action;
const { locationURL } = store.getState()['features/base/connection'];
const url = toURLString(locationURL);
const session = getSession(store, url);
if (session) {
store.dispatch(setSession({
url: session.url,
conference
}));
} else {
console.info(`IGNORED WILL_JOIN FOR: ${url}`);
}
break;
}
case CONFERENCE_JOINED: {
const { conference } = action;
const session = findSessionForConference(store, conference);
const state = session && session.state;
if (state === SESSION_CONFIGURED) {
store.dispatch(
setSession({
// Flow complains that the session can be undefined, but it
// can't if the state is defined.
// $FlowExpectedError
url: session.url,
state: SESSION_STARTED
}));
} else {
// eslint-disable-next-line max-len
console.info(`IGNORED CONF JOINED FOR: ${toURLString(conference[JITSI_CONFERENCE_URL_KEY])}`);
}
break;
}
case CONFERENCE_LEFT:
case CONFERENCE_FAILED: {
const { conference, error } = action;
const session = findSessionForConference(store, conference);
// FIXME update comments
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
// prevented the user from joining a specific conference but the app may
// be able to eventually join the conference. For example, the app will
// ask the user for a password upon
// JitsiConferenceErrors.PASSWORD_REQUIRED and will retry joining the
// conference afterwards. Such errors are to not reach the native
// counterpart of the External API (or at least not in the
// fatality/finality semantics attributed to
// conferenceFailed:/onConferenceFailed).
if (session) {
if (!error || isGameOver(store, session, error)) {
if (session.connection) {
store.dispatch(
setSession({
url: session.url,
conference: undefined
}));
} else {
store.dispatch(
setSession({
url: session.url,
state: error ? SESSION_FAILED : SESSION_ENDED,
error
}));
}
}
} else {
// eslint-disable-next-line max-len
console.info(`IGNORED FAILED/LEFT for ${toURLString(conference[JITSI_CONFERENCE_URL_KEY])}`, error);
}
break;
}
// NOTE WILL_JOIN is fired on SET_ROOM
// case CONFERENCE_WILL_JOIN:
case CONFERENCE_WILL_LEAVE: {
const { conference } = action;
const url = toURLString(conference[JITSI_CONFERENCE_URL_KEY]);
const session = findSessionForConference(store, conference);
const state = session && session.state;
if (state && state !== SESSION_WILL_END) {
store.dispatch(
setSession({
// Flow complains that the session can be undefined, but it
// can't if the state is defined.
// $FlowExpectedError
url: session.url,
state: SESSION_WILL_END
}));
} else {
console.info(`IGNORED WILL LEAVE FOR ${url}`);
}
break;
}
case CONNECTION_WILL_CONNECT: {
const { connection } = action;
const { locationURL } = store.getState()['features/base/connection'];
const url = toURLString(locationURL);
const session = getSession(store, url);
if (session) {
store.dispatch(
setSession({
url: session.url,
connection,
conference: undefined // Detach from the old conference
}));
} else {
console.info(`IGNORED CONNECTION_WILL_CONNECT FOR: ${url}`);
}
break;
}
case CONNECTION_DISCONNECTED:
case CONNECTION_FAILED: {
const { connection, error } = action;
const session = findSessionForConnection(store, connection);
if (session) {
// Remove connection from the session, but wait for
// the conference to be removed as well.
if (!error || isGameOver(store, session, error)) {
if (session.conference) {
store.dispatch(
setSession({
url: session.url,
connection: undefined
}));
} else {
store.dispatch(
setSession({
url: session.url,
state: error ? SESSION_FAILED : SESSION_ENDED,
error
}));
}
}
} else {
console.info('Ignored DISCONNECTED/FAILED for connection');
}
break;
}
case SET_CONFIG: {
// XXX SET_CONFIG IS ALWAYS RELEVANT
const { locationURL } = store.getState()['features/base/config'];
const url = toURLString(locationURL);
const session = getSession(store, url);
const state = session && session.state;
if (state === SESSION_WILL_START) {
store.dispatch(
setSession({
url,
state: SESSION_CONFIGURED
}));
}
break;
}
case CONFIG_WILL_LOAD: {
const { locationURL } = action;
const url = toURLString(locationURL);
const session = getSession(store, url);
// The back and forth to string conversion is here, because there's no
// guarantee that the locationURL will be the exact custom structure
// which contains the room property.
let { room } = parseURIString(url);
// Validate the room
room = isRoomValid(room) ? room : undefined;
if (room && !session) {
store.dispatch(
setSession({
url,
state: SESSION_WILL_START,
locationURL,
room
}));
} else if (room && session) {
// Update to the new locationURL instance
store.dispatch(
setSession({
url,
locationURL
}));
} else {
console.info(`IGNORED CFG WILL LOAD FOR ${url}`);
}
break;
}
case LOAD_CONFIG_ERROR: {
const { error, locationURL } = action;
const url = toURLString(locationURL);
const session = getSession(store, url);
if (session && session.locationURL === locationURL) {
if (isGameOver(store, session, error)) {
store.dispatch(
setSession({
url,
state: SESSION_FAILED,
error
}));
}
} else {
console.info(`IGNORED LOAD_CONFIG_ERROR FOR: ${url}`);
}
break;
}
}
return result;
});
/**
* FIXME A session is to be terminated either when the recoverable flag is
* explicitly set to {@code false} or if the error arrives for a session which
* is no longer current (the app has started working with another session).
* This can happen when a conference which is being disconnected fails in which
* case the session needs to be ended even if the flag is not {@code false}
* because we know that there's no fatal error handling. This is kind of
* a contract between the fatal error feature and the session which probably
* indicates that the fatal error detection and handling should be incorporated
* into the session feature.
*
* @param {Object | Function} stateful - FIXME.
* @param {Object} session - FIXME.
* @param {Object} error - FIXME.
* @returns {boolean}
*/
function isGameOver(stateful, session, error) {
return getCurrentSession(stateful) !== session
|| error.recoverable === false;
}
/**
* FIXME.
*
* @param {Object | Function} stateful - FIXME.
* @param {JitsiConnection} connection - FIXME.
* @returns {Object|undefined}
*/
function findSessionForConnection(stateful, connection) {
const state = toState(stateful);
for (const session of state['features/base/session'].values()) {
if (session.connection === connection) {
return session;
}
}
console.info('Session not found for a connection');
return undefined;
}
/**
* FIXME.
*
* @param {Object | Function} stateful - FIXME.
* @param {JitsiConference} conference - FIXME.
* @returns {Object|undefined}
*/
function findSessionForConference(stateful, conference) {
const state = toState(stateful);
for (const session of state['features/base/session'].values()) {
if (session.conference === conference) {
return session;
}
}
console.info('Session not found for a conference');
return undefined;
}

View File

@@ -0,0 +1,71 @@
// @flow
import { assign, ReducerRegistry } from '../../base/redux';
import { getSymbolDescription } from '../util';
import { SET_SESSION } from './actionTypes';
import {
SESSION_FAILED,
SESSION_ENDED,
SESSION_WILL_START
} from './constants';
ReducerRegistry.register('features/base/session',
(state = new Map(), action) => {
switch (action.type) {
case SET_SESSION:
return _setSession(state, action);
}
return state;
});
/**
* FIXME.
*
* @param {Object} featureState - FIXME.
* @param {Object} action - FIXME.
* @returns {Map<any, any>} - FIXME.
* @private
*/
function _setSession(featureState, action) {
const { url, state, ...data } = action.session;
const session = featureState.get(url);
const nextState = new Map(featureState);
// Drop the whole action if the url is not defined
if (!url) {
console.error('SET SESSION - NO URL');
return nextState;
}
if (session) {
if (state === SESSION_ENDED || state === SESSION_FAILED) {
nextState.delete(url);
} else {
nextState.set(
url,
assign(session, {
url,
state: state ? state : session.state,
...data
}));
}
} else if (state === SESSION_WILL_START) {
nextState.set(
url, {
url,
state,
...data
});
}
console.info(
'SESSION STATE REDUCED: ',
new Map(nextState),
url,
state && getSymbolDescription(state),
action.session.error);
return nextState;
}

View File

@@ -67,7 +67,7 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
// XXX TouchableHighlight requires 1 child. If there's a need to
// show both the icon and the label, then these two need to be
// wrapped in a View.
children = (
children = ( // eslint-disable-line no-extra-parens
<View style = { style }>
{ children }
<Text style = { styles && styles.labelStyle }>

View File

@@ -35,6 +35,7 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
};
const elementType = showLabel ? 'li' : 'div';
const useTooltip = this.tooltip && this.tooltip.length > 0;
// eslint-disable-next-line no-extra-parens
let children = (
<Fragment>
{ this._renderIcon() }
@@ -46,6 +47,7 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
);
if (useTooltip) {
// eslint-disable-next-line no-extra-parens
children = (
<Tooltip
content = { this.tooltip }

View File

@@ -300,15 +300,7 @@ export function parseStandardURIString(str: string) {
* references a Jitsi Meet resource (location).
* @public
* @returns {{
* contextRoot: string,
* hash: string,
* host: string,
* hostname: string,
* pathname: string,
* port: string,
* protocol: string,
* room: (string|undefined),
* search: string
* room: (string|undefined)
* }}
*/
export function parseURIString(uri: ?string) {

View File

@@ -1,15 +1,5 @@
// @flow
/**
* Resets the state of calendar integration so stored events and selected
* calendar type are cleared.
*
* {
* type: CLEAR_CALENDAR_INTEGRATION
* }
*/
export const CLEAR_CALENDAR_INTEGRATION = Symbol('CLEAR_CALENDAR_INTEGRATION');
/**
* Action to refresh (re-fetch) the entry list.
*
@@ -42,48 +32,3 @@ export const SET_CALENDAR_AUTHORIZATION = Symbol('SET_CALENDAR_AUTHORIZATION');
* }
*/
export const SET_CALENDAR_EVENTS = Symbol('SET_CALENDAR_EVENTS');
/**
* Action to update calendar type to be used for web.
*
* {
* type: SET_CALENDAR_INTEGRATION,
* integrationReady: boolean,
* integrationType: string
* }
*/
export const SET_CALENDAR_INTEGRATION = Symbol('SET_CALENDAR_INTEGRATION');
/**
* The type of Redux action which changes Calendar API auth state.
*
* {
* type: SET_CALENDAR_AUTH_STATE
* }
* @public
*/
export const SET_CALENDAR_AUTH_STATE = Symbol('SET_CALENDAR_AUTH_STATE');
/**
* The type of Redux action which changes Calendar Profile email state.
*
* {
* type: SET_CALENDAR_PROFILE_EMAIL,
* email: string
* }
* @public
*/
export const SET_CALENDAR_PROFILE_EMAIL = Symbol('SET_CALENDAR_PROFILE_EMAIL');
/**
* The type of Redux action which denotes whether a request is in flight to get
* updated calendar events.
*
* {
* type: SET_LOADING_CALENDAR_EVENTS,
* isLoadingEvents: string
* }
* @public
*/
export const SET_LOADING_CALENDAR_EVENTS
= Symbol('SET_LOADING_CALENDAR_EVENTS');

View File

@@ -1,89 +1,10 @@
// @flow
import { loadGoogleAPI } from '../google-api';
import {
CLEAR_CALENDAR_INTEGRATION,
REFRESH_CALENDAR,
SET_CALENDAR_AUTH_STATE,
SET_CALENDAR_AUTHORIZATION,
SET_CALENDAR_EVENTS,
SET_CALENDAR_INTEGRATION,
SET_CALENDAR_PROFILE_EMAIL,
SET_LOADING_CALENDAR_EVENTS
SET_CALENDAR_EVENTS
} from './actionTypes';
import { _getCalendarIntegration, isCalendarEnabled } from './functions';
import { generateRoomWithoutSeparator } from '../welcome';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Sets the initial state of calendar integration by loading third party APIs
* and filling out any data that needs to be fetched.
*
* @returns {Function}
*/
export function bootstrapCalendarIntegration(): Function {
return (dispatch, getState) => {
const {
googleApiApplicationClientID
} = getState()['features/base/config'];
const {
integrationReady,
integrationType
} = getState()['features/calendar-sync'];
if (!isCalendarEnabled()) {
return Promise.reject();
}
return Promise.resolve()
.then(() => {
if (googleApiApplicationClientID) {
return dispatch(
loadGoogleAPI(googleApiApplicationClientID));
}
})
.then(() => {
if (!integrationType || integrationReady) {
return;
}
const integrationToLoad
= _getCalendarIntegration(integrationType);
if (!integrationToLoad) {
dispatch(clearCalendarIntegration());
return;
}
return dispatch(integrationToLoad._isSignedIn())
.then(signedIn => {
if (signedIn) {
dispatch(setIntegrationReady(integrationType));
dispatch(updateProfile(integrationType));
} else {
dispatch(clearCalendarIntegration());
}
});
});
};
}
/**
* Resets the state of calendar integration so stored events and selected
* calendar type are cleared.
*
* @returns {{
* type: CLEAR_CALENDAR_INTEGRATION
* }}
*/
export function clearCalendarIntegration() {
return {
type: CLEAR_CALENDAR_INTEGRATION
};
}
/**
* Sends an action to refresh the entry list (fetches new data).
@@ -107,23 +28,6 @@ export function refreshCalendar(
};
}
/**
* Sends an action to update the current calendar api auth state in redux.
* This is used only for microsoft implementation to store it auth state.
*
* @param {number} newState - The new state.
* @returns {{
* type: SET_CALENDAR_AUTH_STATE,
* msAuthState: Object
* }}
*/
export function setCalendarAPIAuthState(newState: ?Object) {
return {
type: SET_CALENDAR_AUTH_STATE,
msAuthState: newState
};
}
/**
* Sends an action to signal that a calendar access has been requested. For more
* info, see {@link SET_CALENDAR_AUTHORIZATION}.
@@ -157,155 +61,3 @@ export function setCalendarEvents(events: Array<Object>) {
events
};
}
/**
* Sends an action to update the current calendar profile email state in redux.
*
* @param {number} newEmail - The new email.
* @returns {{
* type: SET_CALENDAR_PROFILE_EMAIL,
* email: string
* }}
*/
export function setCalendarProfileEmail(newEmail: ?string) {
return {
type: SET_CALENDAR_PROFILE_EMAIL,
email: newEmail
};
}
/**
* Sends an to denote a request in is flight to get calendar events.
*
* @param {boolean} isLoadingEvents - Whether or not calendar events are being
* fetched.
* @returns {{
* type: SET_LOADING_CALENDAR_EVENTS,
* isLoadingEvents: boolean
* }}
*/
export function setLoadingCalendarEvents(isLoadingEvents: boolean) {
return {
type: SET_LOADING_CALENDAR_EVENTS,
isLoadingEvents
};
}
/**
* Sets the calendar integration type to be used by web and signals that the
* integration is ready to be used.
*
* @param {string|undefined} integrationType - The calendar type.
* @returns {{
* type: SET_CALENDAR_INTEGRATION,
* integrationReady: boolean,
* integrationType: string
* }}
*/
export function setIntegrationReady(integrationType: string) {
return {
type: SET_CALENDAR_INTEGRATION,
integrationReady: true,
integrationType
};
}
/**
* Signals signing in to the specified calendar integration.
*
* @param {string} calendarType - The calendar integration which should be
* signed into.
* @returns {Function}
*/
export function signIn(calendarType: string): Function {
return (dispatch: Dispatch<*>) => {
const integration = _getCalendarIntegration(calendarType);
if (!integration) {
return Promise.reject('No supported integration found');
}
return dispatch(integration.load())
.then(() => dispatch(integration.signIn()))
.then(() => dispatch(setIntegrationReady(calendarType)))
.then(() => dispatch(updateProfile(calendarType)))
.then(() => dispatch(refreshCalendar()))
.catch(error => {
logger.error(
'Error occurred while signing into calendar integration',
error);
return Promise.reject(error);
});
};
}
/**
* Signals to get current profile data linked to the current calendar
* integration that is in use.
*
* @param {string} calendarType - The calendar integration to which the profile
* should be updated.
* @returns {Function}
*/
export function updateProfile(calendarType: string): Function {
return (dispatch: Dispatch<*>) => {
const integration = _getCalendarIntegration(calendarType);
if (!integration) {
return Promise.reject('No integration found');
}
return dispatch(integration.getCurrentEmail())
.then(email => {
dispatch(setCalendarProfileEmail(email));
});
};
}
/**
* Updates calendar event by generating new invite URL and editing the event
* adding some descriptive text and location.
*
* @param {string} id - The event id.
* @param {string} calendarId - The id of the calendar to use.
* @returns {Function}
*/
export function updateCalendarEvent(id: string, calendarId: string): Function {
return (dispatch: Dispatch<*>, getState: Function) => {
const { integrationType } = getState()['features/calendar-sync'];
const integration = _getCalendarIntegration(integrationType);
if (!integration) {
return Promise.reject('No integration found');
}
const { locationURL } = getState()['features/base/connection'];
const newRoomName = generateRoomWithoutSeparator();
let href = locationURL.href;
href.endsWith('/') || (href += '/');
const roomURL = `${href}${newRoomName}`;
return dispatch(integration.updateCalendarEvent(
id, calendarId, roomURL))
.then(() => {
// make a copy of the array
const events
= getState()['features/calendar-sync'].events.slice(0);
const eventIx = events.findIndex(
e => e.id === id && e.calendarId === calendarId);
// clone the event we will modify
const newEvent = Object.assign({}, events[eventIx]);
newEvent.url = roomURL;
events[eventIx] = newEvent;
return dispatch(setCalendarEvents(events));
});
};
}

View File

@@ -1,23 +0,0 @@
// @flow
import { Component } from 'react';
/**
* A React Component for adding a meeting URL to an existing calendar meeting.
*
* @extends Component
*/
class AddMeetingUrlButton extends Component<*> {
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
// Not yet implemented.
return null;
}
}
export default AddMeetingUrlButton;

View File

@@ -1,89 +0,0 @@
// @flow
import Button from '@atlaskit/button';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Tooltip from '@atlaskit/tooltip';
import { translate } from '../../base/i18n';
import { updateCalendarEvent } from '../actions';
/**
* The type of the React {@code Component} props of {@link AddMeetingUrlButton}.
*/
type Props = {
/**
* The calendar ID associated with the calendar event.
*/
calendarId: string,
/**
* Invoked to add a meeting URL to a calendar event.
*/
dispatch: Dispatch<*>,
/**
* The ID of the calendar event that will have a meeting URL added on click.
*/
eventId: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* A React Component for adding a meeting URL to an existing calendar event.
*
* @extends Component
*/
class AddMeetingUrlButton extends Component<Props> {
/**
* Initializes a new {@code AddMeetingUrlButton} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onClick = this._onClick.bind(this);
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
return (
<Tooltip content = { this.props.t('calendarSync.addMeetingURL') }>
<Button
appearance = 'primary'
onClick = { this._onClick }
type = 'button'>
<i className = { 'icon-add' } />
</Button>
</Tooltip>
);
}
_onClick: () => void;
/**
* Dispatches an action to adding a meeting URL to a calendar event.
*
* @returns {void}
*/
_onClick() {
const { calendarId, dispatch, eventId } = this.props;
dispatch(updateCalendarEvent(eventId, calendarId));
}
}
export default translate(connect()(AddMeetingUrlButton));

View File

@@ -1,126 +0,0 @@
// @flow
import React, { Component } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { connect } from 'react-redux';
import { openSettings } from '../../mobile/permissions';
import { translate } from '../../base/i18n';
import { isCalendarEnabled } from '../functions';
import styles from './styles';
import BaseCalendarList from './BaseCalendarList';
/**
* The tyoe of the React {@code Component} props of {@link CalendarList}.
*/
type Props = {
/**
* The current state of the calendar access permission.
*/
_authorization: ?string,
/**
* Indicates if the list is disabled or not.
*/
disabled: boolean,
/**
* The translate function.
*/
t: Function
};
/**
* Component to display a list of events from the (mobile) user's calendar.
*/
class CalendarList extends Component<Props> {
/**
* Initializes a new {@code CalendarList} instance.
*
* @inheritdoc
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._getRenderListEmptyComponent
= this._getRenderListEmptyComponent.bind(this);
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { disabled } = this.props;
return (
BaseCalendarList
? <BaseCalendarList
disabled = { disabled }
renderListEmptyComponent
= { this._getRenderListEmptyComponent() } />
: null
);
}
_getRenderListEmptyComponent: () => Object;
/**
* Returns a list empty component if a custom one has to be rendered instead
* of the default one in the {@link NavigateSectionList}.
*
* @private
* @returns {?React$Component}
*/
_getRenderListEmptyComponent() {
const { _authorization, t } = this.props;
// If we don't provide a list specific renderListEmptyComponent, then
// the default empty component of the NavigateSectionList will be
// rendered, which (atm) is a simple "Pull to refresh" message.
if (_authorization !== 'denied') {
return undefined;
}
return (
<View style = { styles.noPermissionMessageView }>
<Text style = { styles.noPermissionMessageText }>
{ t('calendarSync.permissionMessage') }
</Text>
<TouchableOpacity
onPress = { openSettings }
style = { styles.noPermissionMessageButton } >
<Text style = { styles.noPermissionMessageButtonText }>
{ t('calendarSync.permissionButton') }
</Text>
</TouchableOpacity>
</View>
);
}
}
/**
* Maps redux state to component props.
*
* @param {Object} state - The redux state.
* @returns {{
* _authorization: ?string,
* _eventList: Array<Object>
* }}
*/
function _mapStateToProps(state: Object) {
const { authorization } = state['features/calendar-sync'];
return {
_authorization: authorization
};
}
export default isCalendarEnabled()
? translate(connect(_mapStateToProps)(CalendarList))
: undefined;

View File

@@ -1,194 +0,0 @@
// @flow
import Button from '@atlaskit/button';
import Spinner from '@atlaskit/spinner';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { translate } from '../../base/i18n';
import { openSettingsDialog, SETTINGS_TABS } from '../../settings';
import { refreshCalendar } from '../actions';
import { isCalendarEnabled } from '../functions';
import BaseCalendarList from './BaseCalendarList';
declare var interfaceConfig: Object;
/**
* The type of the React {@code Component} props of {@link CalendarList}.
*/
type Props = {
/**
* Whether or not a calendar may be connected for fetching calendar events.
*/
_hasIntegrationSelected: boolean,
/**
* Whether or not events have been fetched from a calendar.
*/
_hasLoadedEvents: boolean,
/**
* Indicates if the list is disabled or not.
*/
disabled: boolean,
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* The translate function.
*/
t: Function
};
/**
* Component to display a list of events from the user's calendar.
*/
class CalendarList extends Component<Props> {
/**
* Initializes a new {@code CalendarList} instance.
*
* @inheritdoc
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._getRenderListEmptyComponent
= this._getRenderListEmptyComponent.bind(this);
this._onOpenSettings = this._onOpenSettings.bind(this);
this._onRefreshEvents = this._onRefreshEvents.bind(this);
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { disabled } = this.props;
return (
BaseCalendarList
? <BaseCalendarList
disabled = { disabled }
renderListEmptyComponent
= { this._getRenderListEmptyComponent() } />
: null
);
}
_getRenderListEmptyComponent: () => Object;
/**
* Returns a list empty component if a custom one has to be rendered instead
* of the default one in the {@link NavigateSectionList}.
*
* @private
* @returns {React$Component}
*/
_getRenderListEmptyComponent() {
const { _hasIntegrationSelected, _hasLoadedEvents, t } = this.props;
if (_hasIntegrationSelected && _hasLoadedEvents) {
return (
<div className = 'navigate-section-list-empty'>
<div>{ t('calendarSync.noEvents') }</div>
<Button
appearance = 'primary'
className = 'calendar-button'
id = 'connect_calendar_button'
onClick = { this._onRefreshEvents }
type = 'button'>
{ t('calendarSync.refresh') }
</Button>
</div>
);
} else if (_hasIntegrationSelected && !_hasLoadedEvents) {
return (
<div className = 'navigate-section-list-empty'>
<Spinner
invertColor = { true }
isCompleting = { false }
size = 'medium' />
</div>
);
}
return (
<div className = 'navigate-section-list-empty'>
<p className = 'header-text-description'>
{ t('welcomepage.connectCalendarText', {
app: interfaceConfig.APP_NAME
}) }
</p>
<Button
appearance = 'primary'
className = 'calendar-button'
id = 'connect_calendar_button'
onClick = { this._onOpenSettings }
type = 'button'>
{ t('welcomepage.connectCalendarButton') }
</Button>
</div>
);
}
_onOpenSettings: () => void;
/**
* Opens {@code SettingsDialog}.
*
* @private
* @returns {void}
*/
_onOpenSettings() {
this.props.dispatch(openSettingsDialog(SETTINGS_TABS.CALENDAR));
}
_onRefreshEvents: () => void;
/**
* Gets an updated list of calendar events.
*
* @private
* @returns {void}
*/
_onRefreshEvents() {
this.props.dispatch(refreshCalendar(true));
}
}
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code CalendarList} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _hasIntegrationSelected: boolean,
* _hasLoadedEvents: boolean
* }}
*/
function _mapStateToProps(state) {
const {
events,
integrationType,
isLoadingEvents
} = state['features/calendar-sync'];
return {
_hasIntegrationSelected: Boolean(integrationType),
_hasLoadedEvents: Boolean(events) || !isLoadingEvents
};
}
export default isCalendarEnabled()
? translate(connect(_mapStateToProps)(CalendarList))
: undefined;

View File

@@ -10,7 +10,7 @@ import { Icon } from '../../base/font-icons';
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
import { ASPECT_RATIO_NARROW } from '../../base/responsive-ui';
import { isCalendarEnabled } from '../functions';
import { CALENDAR_ENABLED } from '../constants';
import styles from './styles';
const ALERT_MILLISECONDS = 5 * 60 * 1000;
@@ -293,6 +293,6 @@ function _mapStateToProps(state: Object) {
};
}
export default isCalendarEnabled()
export default CALENDAR_ENABLED
? translate(connect(_mapStateToProps)(ConferenceNotification))
: undefined;

View File

@@ -1,24 +1,28 @@
// @flow
import React, { Component } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { connect } from 'react-redux';
import { appNavigate } from '../../app';
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
import { NavigateSectionList } from '../../base/react';
import { openSettings } from '../../mobile/permissions';
import { refreshCalendar } from '../actions';
import { isCalendarEnabled } from '../functions';
import AddMeetingUrlButton from './AddMeetingUrlButton';
import { CALENDAR_ENABLED } from '../constants';
import styles from './styles';
/**
* The type of the React {@code Component} props of
* {@link BaseCalendarList}.
* The tyoe of the React {@code Component} props of {@link MeetingList}.
*/
type Props = {
/**
* The current state of the calendar access permission.
*/
_authorization: ?string,
/**
* The calendar event list.
*/
@@ -34,11 +38,6 @@ type Props = {
*/
dispatch: Function,
/**
*
*/
renderListEmptyComponent: Function,
/**
* The translate function.
*/
@@ -46,9 +45,9 @@ type Props = {
};
/**
* Component to display a list of events from a connected calendar.
* Component to display a list of events from the (mobile) user's calendar.
*/
class BaseCalendarList extends Component<Props> {
class MeetingList extends Component<Props> {
/**
* Default values for the component's props.
*/
@@ -76,7 +75,7 @@ class BaseCalendarList extends Component<Props> {
}
/**
* Initializes a new {@code BaseCalendarList} instance.
* Initializes a new {@code MeetingList} instance.
*
* @inheritdoc
*/
@@ -84,12 +83,13 @@ class BaseCalendarList extends Component<Props> {
super(props);
// Bind event handlers so they are only bound once per instance.
this._getRenderListEmptyComponent
= this._getRenderListEmptyComponent.bind(this);
this._onPress = this._onPress.bind(this);
this._onRefresh = this._onRefresh.bind(this);
this._toDateString = this._toDateString.bind(this);
this._toDisplayableItem = this._toDisplayableItem.bind(this);
this._toDisplayableList = this._toDisplayableList.bind(this);
this._toTimeString = this._toTimeString.bind(this);
}
/**
@@ -98,7 +98,7 @@ class BaseCalendarList extends Component<Props> {
* @inheritdoc
*/
render() {
const { disabled, renderListEmptyComponent } = this.props;
const { disabled } = this.props;
return (
<NavigateSectionList
@@ -106,11 +106,46 @@ class BaseCalendarList extends Component<Props> {
onPress = { this._onPress }
onRefresh = { this._onRefresh }
renderListEmptyComponent
= { renderListEmptyComponent }
= { this._getRenderListEmptyComponent() }
sections = { this._toDisplayableList() } />
);
}
_getRenderListEmptyComponent: () => Object;
/**
* Returns a list empty component if a custom one has to be rendered instead
* of the default one in the {@link NavigateSectionList}.
*
* @private
* @returns {?React$Component}
*/
_getRenderListEmptyComponent() {
const { _authorization, t } = this.props;
// If we don't provide a list specific renderListEmptyComponent, then
// the default empty component of the NavigateSectionList will be
// rendered, which (atm) is a simple "Pull to refresh" message.
if (_authorization !== 'denied') {
return undefined;
}
return (
<View style = { styles.noPermissionMessageView }>
<Text style = { styles.noPermissionMessageText }>
{ t('calendarSync.permissionMessage') }
</Text>
<TouchableOpacity
onPress = { openSettings }
style = { styles.noPermissionMessageButton } >
<Text style = { styles.noPermissionMessageButtonText }>
{ t('calendarSync.permissionButton') }
</Text>
</TouchableOpacity>
</View>
);
}
_onPress: string => Function;
/**
@@ -139,7 +174,7 @@ class BaseCalendarList extends Component<Props> {
_toDateString: Object => string;
/**
* Generates a date string for a given event.
* Generates a date (interval) string for a given event.
*
* @param {Object} event - The event.
* @private
@@ -147,9 +182,11 @@ class BaseCalendarList extends Component<Props> {
*/
_toDateString(event) {
const startDateTime
= getLocalizedDateFormatter(event.startDate).format('MMM Do, YYYY');
= getLocalizedDateFormatter(event.startDate).format('lll');
const endTime
= getLocalizedDateFormatter(event.endDate).format('LT');
return `${startDateTime}`;
return `${startDateTime} - ${endTime}`;
}
_toDisplayableItem: Object => Object;
@@ -163,15 +200,10 @@ class BaseCalendarList extends Component<Props> {
*/
_toDisplayableItem(event) {
return {
elementAfter: event.url ? undefined : (
<AddMeetingUrlButton
calendarId = { event.calendarId }
eventId = { event.id } />
),
key: `${event.id}-${event.startDate}`,
lines: [
event.url,
this._toTimeString(event)
this._toDateString(event)
],
title: event.title,
url: event.url
@@ -189,60 +221,39 @@ class BaseCalendarList extends Component<Props> {
_toDisplayableList() {
const { _eventList, t } = this.props;
const now = new Date();
const now = Date.now();
const { createSection } = NavigateSectionList;
const TODAY_SECTION = 'today';
const sectionMap = new Map();
const nowSection = createSection(t('calendarSync.now'), 'now');
const nextSection = createSection(t('calendarSync.next'), 'next');
const laterSection = createSection(t('calendarSync.later'), 'later');
for (const event of _eventList) {
const displayableEvent = this._toDisplayableItem(event);
const startDate = new Date(event.startDate).getDate();
if (startDate === now.getDate()) {
let todaySection = sectionMap.get(TODAY_SECTION);
if (!todaySection) {
todaySection
= createSection(t('calendarSync.today'), TODAY_SECTION);
sectionMap.set(TODAY_SECTION, todaySection);
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);
}
todaySection.data.push(displayableEvent);
} else if (sectionMap.has(startDate)) {
const section = sectionMap.get(startDate);
if (section) {
section.data.push(displayableEvent);
}
} else {
const newSection
= createSection(this._toDateString(event), startDate);
sectionMap.set(startDate, newSection);
newSection.data.push(displayableEvent);
}
}
return Array.from(sectionMap.values());
}
const sectionList = [];
_toTimeString: Object => string;
for (const section of [
nowSection,
nextSection,
laterSection
]) {
section.data.length && sectionList.push(section);
}
/**
* Generates a time (interval) string for a given event.
*
* @param {Object} event - The event.
* @private
* @returns {string}
*/
_toTimeString(event) {
const startDateTime
= getLocalizedDateFormatter(event.startDate).format('lll');
const endTime
= getLocalizedDateFormatter(event.endDate).format('LT');
return `${startDateTime} - ${endTime}`;
return sectionList;
}
}
@@ -251,15 +262,19 @@ class BaseCalendarList extends Component<Props> {
*
* @param {Object} state - The redux state.
* @returns {{
* _authorization: ?string,
* _eventList: Array<Object>
* }}
*/
function _mapStateToProps(state: Object) {
const { authorization, events } = state['features/calendar-sync'];
return {
_eventList: state['features/calendar-sync'].events
_authorization: authorization,
_eventList: events
};
}
export default isCalendarEnabled()
? translate(connect(_mapStateToProps)(BaseCalendarList))
export default CALENDAR_ENABLED
? translate(connect(_mapStateToProps)(MeetingList))
: undefined;

View File

@@ -1,44 +0,0 @@
// @flow
import React, { Component } from 'react';
/**
* The type of the React {@code Component} props of
* {@link MicrosoftSignInButton}.
*/
type Props = {
// The callback to invoke when {@code MicrosoftSignInButton} is clicked.
onClick: Function,
// The text to display within {@code MicrosoftSignInButton}.
text: string
};
/**
* A React Component showing a button to sign in with Microsoft.
*
* @extends Component
*/
export default class MicrosoftSignInButton extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<div
className = 'microsoft-sign-in'
onClick = { this.props.onClick }>
<img
className = 'microsoft-logo'
src = 'images/microsoftLogo.svg' />
<div className = 'microsoft-cta'>
{ this.props.text }
</div>
</div>
);
}
}

View File

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

View File

@@ -4,7 +4,7 @@ const NOTIFICATION_SIZE = 55;
/**
* The styles of the React {@code Component}s of the feature meeting-list i.e.
* {@code CalendarList}.
* {@code MeetingList}.
*/
export default createStyleSheet({

View File

@@ -1,26 +1,37 @@
// @flow
import { NativeModules } from 'react-native';
/**
* An enumeration of support calendar integration types.
* The indicator which determines whether the calendar feature is enabled by the
* app.
*
* @enum {string}
* @type {boolean}
*/
export const CALENDAR_TYPE = {
GOOGLE: 'google',
MICROSOFT: 'microsoft'
export const CALENDAR_ENABLED = _isCalendarEnabled();
/**
* The default state of the calendar.
*
* NOTE: This is defined here, to be reusable by functions.js as well (see file
* for details).
*/
export const DEFAULT_STATE = {
authorization: undefined,
events: []
};
/**
* The number of days to fetch.
* Determines whether the calendar feature is enabled by the app. For
* example, Apple through its App Store requires
* {@code NSCalendarsUsageDescription} in the app's Info.plist or App Store
* rejects the app.
*
* @returns {boolean} If the app has enabled the calendar feature, {@code true};
* otherwise, {@code false}.
*/
export const FETCH_END_DAYS = 10;
function _isCalendarEnabled() {
const { calendarEnabled } = NativeModules.AppInfo;
/**
* The number of days to go back when fetching.
*/
export const FETCH_START_DAYS = -1;
/**
* The max number of events to fetch from the calendar.
*/
export const MAX_LIST_LENGTH = 10;
return typeof calendarEnabled === 'undefined' ? true : calendarEnabled;
}

View File

@@ -1,171 +0,0 @@
// @flow
import md5 from 'js-md5';
import { setCalendarEvents } from './actions';
import { APP_LINK_SCHEME, parseURIString } from '../base/util';
import { MAX_LIST_LENGTH } from './constants';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Updates the calendar entries in redux when new list is received. The feature
* calendar-sync doesn't display all calendar events, it displays unique
* title, URL, and start time tuples i.e. it doesn't display subsequent
* occurrences of recurring events, and the repetitions of events coming from
* multiple calendars.
*
* XXX The function's {@code this} is the redux store.
*
* @param {Array<CalendarEntry>} events - The new event list.
* @private
* @returns {void}
*/
export function _updateCalendarEntries(events: Array<Object>) {
if (!events || !events.length) {
return;
}
// eslint-disable-next-line no-invalid-this
const { dispatch, getState } = this;
const knownDomains = getState()['features/base/known-domains'];
const now = Date.now();
const entryMap = new Map();
for (const event of events) {
const entry = _parseCalendarEntry(event, knownDomains);
if (entry && entry.endDate > now) {
// As was stated above, we don't display subsequent occurrences of
// recurring events, and the repetitions of events coming from
// multiple calendars.
const key = md5.hex(JSON.stringify([
// Obviously, we want to display different conference/meetings
// URLs. URLs are the very reason why we implemented the feature
// calendar-sync in the first place.
entry.url,
// We probably want to display one and the same URL to people if
// they have it under different titles in their Calendar.
// Because maybe they remember the title of the meeting, not the
// URL so they expect to see the title without realizing that
// they have the same URL already under a different title.
entry.title,
// XXX Eventually, given that the URL and the title are the
// same, what sets one event apart from another is the start
// time of the day (note the use of toTimeString() bellow)! The
// day itself is not important because we don't want multiple
// occurrences of a recurring event or repetitions of an even
// from multiple calendars.
new Date(entry.startDate).toTimeString()
]));
const existingEntry = entryMap.get(key);
// We want only the earliest occurrence (which hasn't ended in the
// past, that is) of a recurring event.
if (!existingEntry || existingEntry.startDate > entry.startDate) {
entryMap.set(key, entry);
}
}
}
dispatch(
setCalendarEvents(
Array.from(entryMap.values())
.sort((a, b) => a.startDate - b.startDate)
.slice(0, MAX_LIST_LENGTH)));
}
/**
* Updates the calendar entries in Redux when new list is received.
*
* @param {Object} event - An event returned from the native calendar.
* @param {Array<string>} knownDomains - The known domain list.
* @private
* @returns {CalendarEntry}
*/
function _parseCalendarEntry(event, knownDomains) {
if (event) {
const url = _getURLFromEvent(event, knownDomains);
// we only filter events without url on mobile, this is temporary
// till we implement event edit on mobile
if (url || navigator.product !== 'ReactNative') {
const startDate = Date.parse(event.startDate);
const endDate = Date.parse(event.endDate);
// we want to hide all events that
// - has no start or end date
// - for web, if there is no url and we cannot edit the event (has
// no calendarId)
if (isNaN(startDate)
|| isNaN(endDate)
|| (navigator.product !== 'ReactNative'
&& !url
&& !event.calendarId)) {
logger.debug(
'Skipping invalid calendar event',
event.title,
event.startDate,
event.endDate,
url,
event.calendarId
);
} else {
return {
calendarId: event.calendarId,
endDate,
id: event.id,
startDate,
title: event.title,
url
};
}
}
}
return null;
}
/**
* Retrieves a Jitsi Meet URL from an event if present.
*
* @param {Object} event - The event to parse.
* @param {Array<string>} knownDomains - The known domain names.
* @private
* @returns {string}
*/
function _getURLFromEvent(event, knownDomains) {
const linkTerminatorPattern = '[^\\s<>$]';
const urlRegExp
= new RegExp(
`http(s)?://(${knownDomains.join('|')})/${linkTerminatorPattern}+`,
'gi');
const schemeRegExp
= new RegExp(`${APP_LINK_SCHEME}${linkTerminatorPattern}+`, 'gi');
const fieldsToSearch = [
event.title,
event.url,
event.location,
event.notes,
event.description
];
for (const field of fieldsToSearch) {
if (typeof field === 'string') {
const matches = urlRegExp.exec(field) || schemeRegExp.exec(field);
if (matches) {
const url = parseURIString(matches[0]);
if (url) {
return url.toString();
}
}
}
}
return null;
}

View File

@@ -0,0 +1,18 @@
// @flow
import { toState } from '../base/redux';
import { CALENDAR_ENABLED, DEFAULT_STATE } from './constants';
/**
* Returns the calendar state, considering the enabled/disabled state of the
* feature. Since that is the normal Redux behaviour, this function will always
* return an object (the default state if the feature is disabled).
*
* @param {Object | Function} stateful - An object or a function that can be
* resolved to a Redux state by {@code toState}.
* @returns {Object}
*/
export function getCalendarState(stateful: Object | Function) {
return CALENDAR_ENABLED
? toState(stateful)['features/calendar-sync'] : DEFAULT_STATE;
}

View File

@@ -1,99 +0,0 @@
import { NativeModules } from 'react-native';
import RNCalendarEvents from 'react-native-calendar-events';
import { setCalendarAuthorization } from './actions';
import { FETCH_END_DAYS, FETCH_START_DAYS } from './constants';
import { _updateCalendarEntries } from './functions';
export * from './functions.any';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Determines whether the calendar feature is enabled by the app. For
* example, Apple through its App Store requires
* {@code NSCalendarsUsageDescription} in the app's Info.plist or App Store
* rejects the app.
*
* @returns {boolean} If the app has enabled the calendar feature, {@code true};
* otherwise, {@code false}.
*/
export function isCalendarEnabled() {
const { calendarEnabled } = NativeModules.AppInfo;
return typeof calendarEnabled === 'undefined' ? true : calendarEnabled;
}
/**
* Reads the user's calendar and updates the stored entries if need be.
*
* @param {Object} store - The redux store.
* @param {boolean} maybePromptForPermission - Flag to tell the app if it should
* prompt for a calendar permission if it wasn't granted yet.
* @param {boolean|undefined} forcePermission - Whether to force to re-ask for
* the permission or not.
* @private
* @returns {void}
*/
export function _fetchCalendarEntries(
store,
maybePromptForPermission,
forcePermission) {
const { dispatch, getState } = store;
const promptForPermission
= (maybePromptForPermission
&& !getState()['features/calendar-sync'].authorization)
|| forcePermission;
_ensureCalendarAccess(promptForPermission, dispatch)
.then(accessGranted => {
if (accessGranted) {
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(_updateCalendarEntries.bind(store))
.catch(error =>
logger.error('Error fetching calendar.', error));
} else {
logger.warn('Calendar access not granted.');
}
})
.catch(reason => logger.error('Error accessing calendar.', reason));
}
/**
* Ensures calendar access if possible and resolves the promise if it's granted.
*
* @param {boolean} promptForPermission - Flag to tell the app if it should
* prompt for a calendar permission if it wasn't granted yet.
* @param {Function} dispatch - The Redux dispatch function.
* @private
* @returns {Promise}
*/
function _ensureCalendarAccess(promptForPermission, dispatch) {
return new Promise((resolve, reject) => {
RNCalendarEvents.authorizationStatus()
.then(status => {
if (status === 'authorized') {
resolve(true);
} else if (promptForPermission) {
RNCalendarEvents.authorizeEventStore()
.then(result => {
dispatch(setCalendarAuthorization(result));
resolve(result === 'authorized');
})
.catch(reject);
} else {
resolve(false);
}
})
.catch(reject);
});
}

View File

@@ -1,97 +0,0 @@
// @flow
import { setLoadingCalendarEvents } from './actions';
export * from './functions.any';
import {
CALENDAR_TYPE,
FETCH_END_DAYS,
FETCH_START_DAYS
} from './constants';
import { _updateCalendarEntries } from './functions';
import { googleCalendarApi } from './web/googleCalendar';
import { microsoftCalendarApi } from './web/microsoftCalendar';
const logger = require('jitsi-meet-logger').getLogger(__filename);
declare var config: Object;
/**
* Determines whether the calendar feature is enabled by the web.
*
* @returns {boolean} If the app has enabled the calendar feature, {@code true};
* otherwise, {@code false}.
*/
export function isCalendarEnabled() {
return Boolean(
config.enableCalendarIntegration
&& (config.googleApiApplicationClientID
|| config.microsoftApiApplicationClientID));
}
/* eslint-disable no-unused-vars */
/**
* Reads the user's calendar and updates the stored entries if need be.
*
* @param {Object} store - The redux store.
* @param {boolean} maybePromptForPermission - Flag to tell the app if it should
* prompt for a calendar permission if it wasn't granted yet.
* @param {boolean|undefined} forcePermission - Whether to force to re-ask for
* the permission or not.
* @private
* @returns {void}
*/
export function _fetchCalendarEntries(
store,
maybePromptForPermission,
forcePermission) {
/* eslint-enable no-unused-vars */
const { dispatch, getState } = store;
const { integrationType } = getState()['features/calendar-sync'];
const integration = _getCalendarIntegration(integrationType);
if (!integration) {
logger.debug('No calendar type available');
return;
}
dispatch(setLoadingCalendarEvents(true));
dispatch(integration.load())
.then(() => dispatch(integration._isSignedIn()))
.then(signedIn => {
if (signedIn) {
return Promise.resolve();
}
return Promise.reject('Not authorized, please sign in!');
})
.then(() => dispatch(integration.getCalendarEntries(
FETCH_START_DAYS, FETCH_END_DAYS)))
.then(events => _updateCalendarEntries.call({
dispatch,
getState
}, events))
.catch(error =>
logger.error('Error fetching calendar.', error))
.then(() => dispatch(setLoadingCalendarEvents(false)));
}
/**
* Returns the calendar API implementation by specified type.
*
* @param {string} calendarType - The calendar type API as defined in
* the constant {@link CALENDAR_TYPE}.
* @private
* @returns {Object|undefined}
*/
export function _getCalendarIntegration(calendarType: string) {
switch (calendarType) {
case CALENDAR_TYPE.GOOGLE:
return googleCalendarApi;
case CALENDAR_TYPE.MICROSOFT:
return microsoftCalendarApi;
}
}

View File

@@ -1,7 +1,5 @@
export * from './actions';
export * from './components';
export * from './constants';
export { isCalendarEnabled } from './functions';
export * from './functions';
import './middleware';
import './reducer';

View File

@@ -1,15 +1,36 @@
// @flow
import { SET_CONFIG } from '../base/config';
import md5 from 'js-md5';
import RNCalendarEvents from 'react-native-calendar-events';
import { APP_WILL_MOUNT } from '../base/app';
import { ADD_KNOWN_DOMAINS, addKnownDomains } from '../base/known-domains';
import { equals, MiddlewareRegistry } from '../base/redux';
import { APP_STATE_CHANGED } from '../mobile/background/actionTypes';
import { MiddlewareRegistry } from '../base/redux';
import { APP_LINK_SCHEME, parseURIString } from '../base/util';
import { APP_STATE_CHANGED } from '../mobile/background';
import { setCalendarAuthorization } from './actions';
import { setCalendarAuthorization, setCalendarEvents } from './actions';
import { REFRESH_CALENDAR } from './actionTypes';
import { _fetchCalendarEntries, isCalendarEnabled } from './functions';
import { CALENDAR_ENABLED } from './constants';
isCalendarEnabled()
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* The number of days to fetch.
*/
const FETCH_END_DAYS = 10;
/**
* The number of days to go back when fetching.
*/
const FETCH_START_DAYS = -1;
/**
* The max number of events to fetch from the calendar.
*/
const MAX_LIST_LENGTH = 10;
CALENDAR_ENABLED
&& MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case ADD_KNOWN_DOMAINS: {
@@ -20,8 +41,7 @@ isCalendarEnabled()
const result = next(action);
const newValue = getState()['features/base/known-domains'];
equals(oldValue, newValue)
|| _fetchCalendarEntries(store, false, false);
oldValue === newValue || _fetchCalendarEntries(store, false, false);
return result;
}
@@ -34,9 +54,7 @@ isCalendarEnabled()
return result;
}
case SET_CONFIG: {
const result = next(action);
case APP_WILL_MOUNT: {
// For legacy purposes, we've allowed the deserialization of
// knownDomains and now we're to translate it to base/known-domains.
const state = store.getState()['features/calendar-sync'];
@@ -51,7 +69,7 @@ isCalendarEnabled()
_fetchCalendarEntries(store, false, false);
return result;
return next(action);
}
case REFRESH_CALENDAR: {
@@ -67,6 +85,121 @@ isCalendarEnabled()
return next(action);
});
/**
* Ensures calendar access if possible and resolves the promise if it's granted.
*
* @param {boolean} promptForPermission - Flag to tell the app if it should
* prompt for a calendar permission if it wasn't granted yet.
* @param {Function} dispatch - The Redux dispatch function.
* @private
* @returns {Promise}
*/
function _ensureCalendarAccess(promptForPermission, dispatch) {
return new Promise((resolve, reject) => {
RNCalendarEvents.authorizationStatus()
.then(status => {
if (status === 'authorized') {
resolve(true);
} else if (promptForPermission) {
RNCalendarEvents.authorizeEventStore()
.then(result => {
dispatch(setCalendarAuthorization(result));
resolve(result === 'authorized');
})
.catch(reject);
} else {
resolve(false);
}
})
.catch(reject);
});
}
/**
* Reads the user's calendar and updates the stored entries if need be.
*
* @param {Object} store - The redux store.
* @param {boolean} maybePromptForPermission - Flag to tell the app if it should
* prompt for a calendar permission if it wasn't granted yet.
* @param {boolean|undefined} forcePermission - Whether to force to re-ask for
* the permission or not.
* @private
* @returns {void}
*/
function _fetchCalendarEntries(
store,
maybePromptForPermission,
forcePermission) {
const { dispatch, getState } = store;
const promptForPermission
= (maybePromptForPermission
&& !getState()['features/calendar-sync'].authorization)
|| forcePermission;
_ensureCalendarAccess(promptForPermission, dispatch)
.then(accessGranted => {
if (accessGranted) {
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(_updateCalendarEntries.bind(store))
.catch(error =>
logger.error('Error fetching calendar.', error));
} else {
logger.warn('Calendar access not granted.');
}
})
.catch(reason => logger.error('Error accessing calendar.', reason));
}
/**
* Retrieves a Jitsi Meet URL from an event if present.
*
* @param {Object} event - The event to parse.
* @param {Array<string>} knownDomains - The known domain names.
* @private
* @returns {string}
*/
function _getURLFromEvent(event, knownDomains) {
const linkTerminatorPattern = '[^\\s<>$]';
const urlRegExp
= new RegExp(
`http(s)?://(${knownDomains.join('|')})/${linkTerminatorPattern}+`,
'gi');
const schemeRegExp
= new RegExp(`${APP_LINK_SCHEME}${linkTerminatorPattern}+`, 'gi');
const fieldsToSearch = [
event.title,
event.url,
event.location,
event.notes,
event.description
];
for (const field of fieldsToSearch) {
if (typeof field === 'string') {
const matches = urlRegExp.exec(field) || schemeRegExp.exec(field);
if (matches) {
const url = parseURIString(matches[0]);
if (url) {
return url.toString();
}
}
}
}
return null;
}
/**
* Clears the calendar access status when the app comes back from the
* background. This is needed as some users may never quit the app, but puts it
@@ -82,3 +215,111 @@ function _maybeClearAccessStatus(store, { appState }) {
appState === 'background'
&& store.dispatch(setCalendarAuthorization(undefined));
}
/**
* Updates the calendar entries in Redux when new list is received.
*
* @param {Object} event - An event returned from the native calendar.
* @param {Array<string>} knownDomains - The known domain list.
* @private
* @returns {CalendarEntry}
*/
function _parseCalendarEntry(event, knownDomains) {
if (event) {
const url = _getURLFromEvent(event, knownDomains);
if (url) {
const startDate = Date.parse(event.startDate);
const endDate = Date.parse(event.endDate);
if (isNaN(startDate) || isNaN(endDate)) {
logger.warn(
'Skipping invalid calendar event',
event.title,
event.startDate,
event.endDate
);
} else {
return {
endDate,
id: event.id,
startDate,
title: event.title,
url
};
}
}
}
return null;
}
/**
* Updates the calendar entries in redux when new list is received. The feature
* calendar-sync doesn't display all calendar events, it displays unique
* title, URL, and start time tuples i.e. it doesn't display subsequent
* occurrences of recurring events, and the repetitions of events coming from
* multiple calendars.
*
* XXX The function's {@code this} is the redux store.
*
* @param {Array<CalendarEntry>} events - The new event list.
* @private
* @returns {void}
*/
function _updateCalendarEntries(events) {
if (!events || !events.length) {
return;
}
// eslint-disable-next-line no-invalid-this
const { dispatch, getState } = this;
const knownDomains = getState()['features/base/known-domains'];
const now = Date.now();
const entryMap = new Map();
for (const event of events) {
const entry = _parseCalendarEntry(event, knownDomains);
if (entry && entry.endDate > now) {
// As was stated above, we don't display subsequent occurrences of
// recurring events, and the repetitions of events coming from
// multiple calendars.
const key = md5.hex(JSON.stringify([
// Obviously, we want to display different conference/meetings
// URLs. URLs are the very reason why we implemented the feature
// calendar-sync in the first place.
entry.url,
// We probably want to display one and the same URL to people if
// they have it under different titles in their Calendar.
// Because maybe they remember the title of the meeting, not the
// URL so they expect to see the title without realizing that
// they have the same URL already under a different title.
entry.title,
// XXX Eventually, given that the URL and the title are the
// same, what sets one event apart from another is the start
// time of the day (note the use of toTimeString() bellow)! The
// day itself is not important because we don't want multiple
// occurrences of a recurring event or repetitions of an even
// from multiple calendars.
new Date(entry.startDate).toTimeString()
]));
const existingEntry = entryMap.get(key);
// We want only the earliest occurrence (which hasn't ended in the
// past, that is) of a recurring event.
if (!existingEntry || existingEntry.startDate > entry.startDate) {
entryMap.set(key, entry);
}
}
}
dispatch(
setCalendarEvents(
Array.from(entryMap.values())
.sort((a, b) => a.startDate - b.startDate)
.slice(0, MAX_LIST_LENGTH)));
}

View File

@@ -5,37 +5,21 @@ import { ReducerRegistry, set } from '../base/redux';
import { PersistenceRegistry } from '../base/storage';
import {
CLEAR_CALENDAR_INTEGRATION,
SET_CALENDAR_AUTH_STATE,
SET_CALENDAR_AUTHORIZATION,
SET_CALENDAR_EVENTS,
SET_CALENDAR_INTEGRATION,
SET_CALENDAR_PROFILE_EMAIL,
SET_LOADING_CALENDAR_EVENTS
SET_CALENDAR_EVENTS
} from './actionTypes';
import { isCalendarEnabled } from './functions';
/**
* The default state of the calendar feature.
*
* @type {Object}
*/
const DEFAULT_STATE = {
authorization: undefined,
events: [],
integrationReady: false,
integrationType: undefined,
msAuthState: undefined
};
import { CALENDAR_ENABLED, DEFAULT_STATE } from './constants';
/**
* Constant for the Redux subtree of the calendar feature.
*
* NOTE: This feature can be disabled and in that case, accessing this subtree
* directly will return undefined and will need a bunch of repetitive type
* checks in other features. Make sure you take care of those checks, or
* consider using the {@code isCalendarEnabled} value to gate features if
* needed.
* NOTE: Please do not access this subtree directly outside of this feature.
* This feature can be disabled (see {@code constants.js} for details), and in
* that case, accessing this subtree directly will return undefined and will
* need a bunch of repetitive type checks in other features. Use the
* {@code getCalendarState} function instead, or make sure you take care of
* those checks, or consider using the {@code CALENDAR_ENABLED} const to gate
* features if needed.
*/
const STORE_NAME = 'features/calendar-sync';
@@ -47,14 +31,12 @@ const STORE_NAME = 'features/calendar-sync';
* runtime value to see if we need to re-request the calendar permission from
* the user.
*/
isCalendarEnabled()
CALENDAR_ENABLED
&& PersistenceRegistry.register(STORE_NAME, {
integrationType: true,
knownDomains: true,
msAuthState: true
knownDomains: true
});
isCalendarEnabled()
CALENDAR_ENABLED
&& ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
switch (action.type) {
case APP_WILL_MOUNT:
@@ -67,39 +49,11 @@ isCalendarEnabled()
}
break;
case CLEAR_CALENDAR_INTEGRATION:
return DEFAULT_STATE;
case SET_CALENDAR_AUTH_STATE: {
if (!action.msAuthState) {
// received request to delete the state
return set(state, 'msAuthState', undefined);
}
return set(state, 'msAuthState', {
...state.msAuthState,
...action.msAuthState
});
}
case SET_CALENDAR_AUTHORIZATION:
return set(state, 'authorization', action.authorization);
case SET_CALENDAR_EVENTS:
return set(state, 'events', action.events);
case SET_CALENDAR_INTEGRATION:
return {
...state,
integrationReady: action.integrationReady,
integrationType: action.integrationType
};
case SET_CALENDAR_PROFILE_EMAIL:
return set(state, 'profileEmail', action.email);
case SET_LOADING_CALENDAR_EVENTS:
return set(state, 'isLoadingEvents', action.isLoadingEvents);
}
return state;

View File

@@ -1,78 +0,0 @@
/* @flow */
import {
getCalendarEntries,
googleApi,
loadGoogleAPI,
signIn,
updateCalendarEvent,
updateProfile
} from '../../google-api';
/**
* A stateless collection of action creators that implements the expected
* interface for interacting with the Google API in order to get calendar data.
*
* @type {Object}
*/
export const googleCalendarApi = {
/**
* Retrieves the current calendar events.
*
* @param {number} fetchStartDays - The number of days to go back
* when fetching.
* @param {number} fetchEndDays - The number of days to fetch.
* @returns {function(): Promise<CalendarEntries>}
*/
getCalendarEntries,
/**
* Returns the email address for the currently logged in user.
*
* @returns {function(Dispatch<*>): Promise<string|never>}
*/
getCurrentEmail() {
return updateProfile();
},
/**
* Initializes the google api if needed.
*
* @returns {function(Dispatch<*>, Function): Promise<void>}
*/
load() {
return (dispatch: Dispatch<*>, getState: Function) => {
const { googleApiApplicationClientID }
= getState()['features/base/config'];
return dispatch(loadGoogleAPI(googleApiApplicationClientID));
};
},
/**
* Prompts the participant to sign in to the Google API Client Library.
*
* @returns {function(Dispatch<*>): Promise<string|never>}
*/
signIn,
/**
* Returns whether or not the user is currently signed in.
*
* @returns {function(): Promise<boolean>}
*/
_isSignedIn() {
return () => googleApi.isSignedIn();
},
/**
* Updates calendar event by generating new invite URL and editing the event
* adding some descriptive text and location.
*
* @param {string} id - The event id.
* @param {string} calendarId - The id of the calendar to use.
* @param {string} location - The location to save to the event.
* @returns {function(Dispatch<*>): Promise<string|never>}
*/
updateCalendarEvent
};

View File

@@ -1,593 +0,0 @@
/* @flow */
import { Client } from '@microsoft/microsoft-graph-client';
import rs from 'jsrsasign';
import { createDeferred } from '../../../../modules/util/helpers';
import parseURLParams from '../../base/config/parseURLParams';
import { parseStandardURIString } from '../../base/util';
import { getShareInfoText } from '../../invite';
import { setCalendarAPIAuthState } from '../actions';
/**
* Constants used for interacting with the Microsoft API.
*
* @private
* @type {object}
*/
const MS_API_CONFIGURATION = {
/**
* The URL to use when authenticating using Microsoft API.
* @type {string}
*/
AUTH_ENDPOINT:
'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?',
CALENDAR_ENDPOINT: '/me/calendars',
/**
* The Microsoft API scopes to request access for calendar.
*
* @type {string}
*/
MS_API_SCOPES: 'openid profile Calendars.ReadWrite',
/**
* See https://docs.microsoft.com/en-us/azure/active-directory/develop/
* v2-oauth2-implicit-grant-flow#send-the-sign-in-request. This value is
* needed for passing in the proper domain_hint value when trying to refresh
* a token silently.
*
*
* @type {string}
*/
MS_CONSUMER_TENANT: '9188040d-6c67-4c5b-b112-36a304b66dad',
/**
* The redirect URL to be used by the Microsoft API on successful
* authentication.
*
* @type {string}
*/
REDIRECT_URI: `${window.location.origin}/static/msredirect.html`
};
/**
* Store the window from an auth request. That way it can be reused if a new
* request comes in and it can be used to indicate a request is in progress.
*
* @private
* @type {Object|null}
*/
let popupAuthWindow = null;
/**
* A stateless collection of action creators that implements the expected
* interface for interacting with the Microsoft API in order to get calendar
* data.
*
* @type {Object}
*/
export const microsoftCalendarApi = {
/**
* Retrieves the current calendar events.
*
* @param {number} fetchStartDays - The number of days to go back
* when fetching.
* @param {number} fetchEndDays - The number of days to fetch.
* @returns {function(Dispatch<*>, Function): Promise<CalendarEntries>}
*/
getCalendarEntries(fetchStartDays: ?number, fetchEndDays: ?number) {
return (dispatch: Dispatch<*>, getState: Function): Promise<*> => {
const state = getState()['features/calendar-sync'] || {};
const token = state.msAuthState && state.msAuthState.accessToken;
if (!token) {
return Promise.reject('Not authorized, please sign in!');
}
const client = Client.init({
authProvider: done => done(null, token)
});
return client
.api(MS_API_CONFIGURATION.CALENDAR_ENDPOINT)
.get()
.then(response => {
const calendarIds = response.value.map(en => en.id);
const getEventsPromises = calendarIds.map(id =>
requestCalendarEvents(
client, id, fetchStartDays, fetchEndDays));
return Promise.all(getEventsPromises);
})
// get .value of every element from the array of results,
// which is an array of events and flatten it to one array
// of events
.then(result => [].concat(...result))
.then(entries => entries.map(e => formatCalendarEntry(e)));
};
},
/**
* Returns the email address for the currently logged in user.
*
* @returns {function(Dispatch<*, Function>): Promise<string>}
*/
getCurrentEmail(): Function {
return (dispatch: Dispatch<*>, getState: Function) => {
const { msAuthState = {} }
= getState()['features/calendar-sync'] || {};
const email = msAuthState.userSigninName || '';
return Promise.resolve(email);
};
},
/**
* Sets the application ID to use for interacting with the Microsoft API.
*
* @returns {function(): Promise<void>}
*/
load(): Function {
return () => Promise.resolve();
},
/**
* Prompts the participant to sign in to the Microsoft API Client Library.
*
* @returns {function(Dispatch<*>, Function): Promise<void>}
*/
signIn(): Function {
return (dispatch: Dispatch<*>, getState: Function) => {
// Ensure only one popup window at a time.
if (popupAuthWindow) {
popupAuthWindow.focus();
return Promise.reject('Sign in already in progress.');
}
const signInDeferred = createDeferred();
const guids = {
authState: generateGuid(),
authNonce: generateGuid()
};
dispatch(setCalendarAPIAuthState(guids));
const { microsoftApiApplicationClientID }
= getState()['features/base/config'];
const authUrl = getAuthUrl(
microsoftApiApplicationClientID,
guids.authState,
guids.authNonce);
const h = 600;
const w = 480;
popupAuthWindow = window.open(
authUrl,
'Auth M$',
`width=${w}, height=${h}, top=${
(screen.height / 2) - (h / 2)}, left=${
(screen.width / 2) - (w / 2)}`);
const windowCloseCheck = setInterval(() => {
if (popupAuthWindow && popupAuthWindow.closed) {
signInDeferred.reject(
'Popup closed before completing auth.');
popupAuthWindow = null;
window.removeEventListener('message', handleAuth);
clearInterval(windowCloseCheck);
} else if (!popupAuthWindow) {
// This case probably happened because the user completed
// auth.
clearInterval(windowCloseCheck);
}
}, 500);
/**
* Callback with scope access to other variables that are part of
* the sign in request.
*
* @param {Object} event - The event from the post message.
* @private
* @returns {void}
*/
function handleAuth({ data }) {
if (!data || data.type !== 'ms-login') {
return;
}
window.removeEventListener('message', handleAuth);
popupAuthWindow && popupAuthWindow.close();
popupAuthWindow = null;
const params = getParamsFromHash(data.url);
const tokenParts = getValidatedTokenParts(
params, guids, microsoftApiApplicationClientID);
if (!tokenParts) {
signInDeferred.reject('Invalid token received');
return;
}
dispatch(setCalendarAPIAuthState({
authState: undefined,
accessToken: tokenParts.accessToken,
idToken: tokenParts.idToken,
tokenExpires: params.tokenExpires,
userDomainType: tokenParts.userDomainType,
userSigninName: tokenParts.userSigninName
}));
signInDeferred.resolve();
}
window.addEventListener('message', handleAuth);
return signInDeferred.promise;
};
},
/**
* Returns whether or not the user is currently signed in.
*
* @returns {function(Dispatch<*>, Function): Promise<boolean>}
*/
_isSignedIn(): Function {
return (dispatch: Dispatch<*>, getState: Function) => {
const now = new Date().getTime();
const state
= getState()['features/calendar-sync'].msAuthState || {};
const tokenExpires = parseInt(state.tokenExpires, 10);
const isExpired = now > tokenExpires && !isNaN(tokenExpires);
if (state.accessToken && isExpired) {
// token expired, let's refresh it
return dispatch(this._refreshAuthToken())
.then(() => true)
.catch(() => false);
}
return Promise.resolve(state.accessToken && !isExpired);
};
},
/**
* Renews an existing auth token so it can continue to be used.
*
* @private
* @returns {function(Dispatch<*>, Function): Promise<void>}
*/
_refreshAuthToken(): Function {
return (dispatch: Dispatch<*>, getState: Function) => {
const { microsoftApiApplicationClientID }
= getState()['features/base/config'];
const { msAuthState = {} }
= getState()['features/calendar-sync'] || {};
const refreshAuthUrl = getAuthRefreshUrl(
microsoftApiApplicationClientID,
msAuthState.userDomainType,
msAuthState.userSigninName);
const iframe = document.createElement('iframe');
iframe.setAttribute('id', 'auth-iframe');
iframe.setAttribute('name', 'auth-iframe');
iframe.setAttribute('style', 'display: none');
iframe.setAttribute('src', refreshAuthUrl);
const signInPromise = new Promise(resolve => {
iframe.onload = () => {
resolve(iframe.contentWindow.location.hash);
};
});
// The check for body existence is done for flow, which also runs
// against native where document.body may not be defined.
if (!document.body) {
return Promise.reject(
'Cannot refresh auth token in this environment');
}
document.body.appendChild(iframe);
return signInPromise.then(hash => {
const params = getParamsFromHash(hash);
dispatch(setCalendarAPIAuthState({
accessToken: params.access_token,
idToken: params.id_token,
tokenExpires: params.tokenExpires
}));
});
};
},
/**
* Updates calendar event by generating new invite URL and editing the event
* adding some descriptive text and location.
*
* @param {string} id - The event id.
* @param {string} calendarId - The id of the calendar to use.
* @param {string} location - The location to save to the event.
* @returns {function(Dispatch<*>): Promise<string|never>}
*/
updateCalendarEvent(id: string, calendarId: string, location: string) {
return (dispatch: Dispatch<*>, getState: Function): Promise<*> => {
const state = getState()['features/calendar-sync'] || {};
const token = state.msAuthState && state.msAuthState.accessToken;
if (!token) {
return Promise.reject('Not authorized, please sign in!');
}
const { dialInNumbersUrl } = getState()['features/base/config'];
const text = getShareInfoText(
location, dialInNumbersUrl !== undefined, true/* use html */);
const client = Client.init({
authProvider: done => done(null, token)
});
return client
.api(`/me/events/${id}`)
.get()
.then(description => {
const body = description.body;
if (description.bodyPreview) {
body.content = `${description.bodyPreview}<br><br>`;
}
// replace all new lines from the text with html <br>
// to make it pretty
body.content += text.split('\n').join('<br>');
return client
.api(`/me/calendar/events/${id}`)
.patch({
body,
location: {
'displayName': location
}
});
});
};
}
};
/**
* Parses the Microsoft calendar entries to a known format.
*
* @param {Object} entry - The Microsoft calendar entry.
* @private
* @returns {{
* calendarId: string,
* description: string,
* endDate: string,
* id: string,
* location: string,
* startDate: string,
* title: string
* }}
*/
function formatCalendarEntry(entry) {
return {
calendarId: entry.calendarId,
description: entry.body.content,
endDate: entry.end.dateTime,
id: entry.id,
location: entry.location.displayName,
startDate: entry.start.dateTime,
title: entry.subject
};
}
/**
* Generate a guid to be used for verifying token validity.
*
* @private
* @returns {string} The generated string.
*/
function generateGuid() {
const buf = new Uint16Array(8);
window.crypto.getRandomValues(buf);
return `${s4(buf[0])}${s4(buf[1])}-${s4(buf[2])}-${s4(buf[3])}-${
s4(buf[4])}-${s4(buf[5])}${s4(buf[6])}${s4(buf[7])}`;
}
/**
* Constructs and returns the URL to use for renewing an auth token.
*
* @param {string} appId - The Microsoft application id to log into.
* @param {string} userDomainType - The domain type of the application as
* provided by Microsoft.
* @param {string} userSigninName - The email of the user signed into the
* integration with Microsoft.
* @private
* @returns {string} - The auth URL.
*/
function getAuthRefreshUrl(appId, userDomainType, userSigninName) {
return [
getAuthUrl(appId, 'undefined', 'undefined'),
'prompt=none',
`domain_hint=${userDomainType}`,
`login_hint=${userSigninName}`
].join('&');
}
/**
* Constructs and returns the auth URL to use for login.
*
* @param {string} appId - The Microsoft application id to log into.
* @param {string} authState - The authState guid to use.
* @param {string} authNonce - The authNonce guid to use.
* @private
* @returns {string} - The auth URL.
*/
function getAuthUrl(appId, authState, authNonce) {
const authParams = [
'response_type=id_token+token',
`client_id=${appId}`,
`redirect_uri=${MS_API_CONFIGURATION.REDIRECT_URI}`,
`scope=${MS_API_CONFIGURATION.MS_API_SCOPES}`,
`state=${authState}`,
`nonce=${authNonce}`,
'response_mode=fragment'
].join('&');
return `${MS_API_CONFIGURATION.AUTH_ENDPOINT}${authParams}`;
}
/**
* Converts a url from an auth redirect into an object of parameters passed
* into the url.
*
* @param {string} url - The string to parse.
* @private
* @returns {Object}
*/
function getParamsFromHash(url) {
const params = parseURLParams(parseStandardURIString(url), true, 'hash');
// Get the number of seconds the token is valid for, subtract 5 minutes
// to account for differences in clock settings and convert to ms.
const expiresIn = (parseInt(params.expires_in, 10) - 300) * 1000;
const now = new Date();
const expireDate = new Date(now.getTime() + expiresIn);
params.tokenExpires = expireDate.getTime().toString();
return params;
}
/**
* Converts the parameters from a Microsoft auth redirect into an object of
* token parts. The value "null" will be returned if the params do not produce
* a valid token.
*
* @param {Object} tokenInfo - The token object.
* @param {Object} guids - The guids for authState and authNonce that should
* match in the token.
* @param {Object} appId - The Microsoft application this token is for.
* @private
* @returns {Object|null}
*/
function getValidatedTokenParts(tokenInfo, guids, appId) {
// Make sure the token matches the request source by matching the GUID.
if (tokenInfo.state !== guids.authState) {
return null;
}
const idToken = tokenInfo.id_token;
// A token must exist to be valid.
if (!idToken) {
return null;
}
const tokenParts = idToken.split('.');
if (tokenParts.length !== 3) {
return null;
}
const payload
= rs.KJUR.jws.JWS.readSafeJSONString(rs.b64utoutf8(tokenParts[1]));
if (payload.nonce !== guids.authNonce
|| payload.aud !== appId
|| payload.iss
!== `https://login.microsoftonline.com/${payload.tid}/v2.0`) {
return null;
}
const now = new Date();
// Adjust by 5 minutes to allow for inconsistencies in system clocks.
const notBefore = new Date((payload.nbf - 300) * 1000);
const expires = new Date((payload.exp + 300) * 1000);
if (now < notBefore || now > expires) {
return null;
}
return {
accessToken: tokenInfo.access_token,
idToken,
userDisplayName: payload.name,
userDomainType:
payload.tid === MS_API_CONFIGURATION.MS_CONSUMER_TENANT
? 'consumers' : 'organizations',
userSigninName: payload.preferred_username
};
}
/**
* Retrieves calendar entries from a specific calendar.
*
* @param {Object} client - The Microsoft-graph-client initialized.
* @param {string} calendarId - The calendar ID to use.
* @param {number} fetchStartDays - The number of days to go back
* when fetching.
* @param {number} fetchEndDays - The number of days to fetch.
* @returns {Promise<any> | Promise}
* @private
*/
function requestCalendarEvents( // eslint-disable-line max-params
client,
calendarId,
fetchStartDays,
fetchEndDays): Promise<*> {
const startDate = new Date();
const endDate = new Date();
startDate.setDate(startDate.getDate() + fetchStartDays);
endDate.setDate(endDate.getDate() + fetchEndDays);
const filter = `Start/DateTime ge '${
startDate.toISOString()}' and End/DateTime lt '${
endDate.toISOString()}'`;
return client
.api(`/me/calendars/${calendarId}/events`)
.filter(filter)
.select('id,subject,start,end,location,body')
.orderby('createdDateTime DESC')
.get()
.then(result => result.value.map(item => {
return {
...item,
calendarId
};
}));
}
/**
* Converts the passed in number to a string and ensure it is at least 4
* characters in length, prepending 0's as needed.
*
* @param {number} num - The number to pad and convert to a string.
* @private
* @returns {string} - The number converted to a string.
*/
function s4(num) {
let ret = num.toString(16);
while (ret.length < 4) {
ret = `0${ret}`;
}
return ret;
}

View File

@@ -4,8 +4,6 @@ import _ from 'lodash';
import React, { Component } from 'react';
import { connect as reactReduxConnect } from 'react-redux';
import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
import { obtainConfig } from '../../base/config';
import { connect, disconnect } from '../../base/connection';
import { DialogContainer } from '../../base/dialog';
@@ -15,12 +13,6 @@ import { CalleeInfoContainer } from '../../invite';
import { LargeVideo } from '../../large-video';
import { NotificationsContainer } from '../../notifications';
import { SidePanel } from '../../side-panel';
import {
LAYOUTS,
getCurrentLayout,
shouldDisplayTileView
} from '../../video-layout';
import { default as Notice } from './Notice';
import {
Toolbox,
@@ -57,10 +49,9 @@ const FULL_SCREEN_EVENTS = [
* @private
* @type {Object}
*/
const LAYOUT_CLASSNAMES = {
[LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'horizontal-filmstrip',
[LAYOUTS.TILE_VIEW]: 'tile-view',
[LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'vertical-filmstrip'
const LAYOUT_CLASSES = {
HORIZONTAL_FILMSTRIP: 'horizontal-filmstrip',
VERTICAL_FILMSTRIP: 'vertical-filmstrip'
};
/**
@@ -77,18 +68,13 @@ type Props = {
* The CSS class to apply to the root of {@link Conference} to modify the
* application layout.
*/
_layoutClassName: string,
_layoutModeClassName: string,
/**
* Conference room name.
*/
_room: string,
/**
* Whether or not the current UI layout should be in tile view.
*/
_shouldDisplayTileView: boolean,
dispatch: Function,
t: Function
}
@@ -157,25 +143,6 @@ class Conference extends Component<Props> {
}
}
/**
* Calls into legacy UI to update the application layout, if necessary.
*
* @inheritdoc
* returns {void}
*/
componentDidUpdate(prevProps) {
if (this.props._shouldDisplayTileView
=== prevProps._shouldDisplayTileView) {
return;
}
// TODO: For now VideoLayout is being called as LargeVideo and Filmstrip
// sizing logic is still handled outside of React. Once all components
// are in react they should calculate size on their own as much as
// possible and pass down sizings.
VideoLayout.refreshLayout();
}
/**
* Disconnect from the conference when component will be
* unmounted.
@@ -213,7 +180,7 @@ class Conference extends Component<Props> {
return (
<div
className = { this.props._layoutClassName }
className = { this.props._layoutModeClassName }
id = 'videoconference_page'
onMouseMove = { this._onShowToolbar }>
<Notice />
@@ -290,19 +257,29 @@ class Conference extends Component<Props> {
* @private
* @returns {{
* _iAmRecorder: boolean,
* _layoutClassName: string,
* _room: ?string,
* _shouldDisplayTileView: boolean
* _room: ?string
* }}
*/
function _mapStateToProps(state) {
const currentLayout = getCurrentLayout(state);
const { room } = state['features/base/conference'];
const { iAmRecorder } = state['features/base/config'];
return {
_iAmRecorder: state['features/base/config'].iAmRecorder,
_layoutClassName: LAYOUT_CLASSNAMES[currentLayout],
_room: state['features/base/conference'].room,
_shouldDisplayTileView: shouldDisplayTileView(state)
/**
* Whether the local participant is recording the conference.
*
* @private
*/
_iAmRecorder: iAmRecorder,
_layoutModeClassName: interfaceConfig.VERTICAL_FILMSTRIP
? LAYOUT_CLASSES.VERTICAL_FILMSTRIP
: LAYOUT_CLASSES.HORIZONTAL_FILMSTRIP,
/**
* Conference room name.
*/
_room: room
};
}

View File

@@ -324,7 +324,6 @@ class ConnectionIndicator extends Component {
* @returns {void}
*/
_onStatsUpdated(stats = {}) {
// Rely on React to batch setState actions.
const { connectionQuality } = stats;
const newPercentageState = typeof connectionQuality === 'undefined'
? {} : { percent: connectionQuality };
@@ -338,6 +337,7 @@ class ConnectionIndicator extends Component {
stats: newStats
});
// Rely on React to batch setState actions.
this._updateIndicatorAutoHide(newStats.percent);
}
@@ -410,10 +410,8 @@ class ConnectionIndicator extends Component {
const {
bandwidth,
bitrate,
e2eRtt,
framerate,
packetLoss,
region,
resolution,
transport
} = this.state.stats;
@@ -423,12 +421,10 @@ class ConnectionIndicator extends Component {
bandwidth = { bandwidth }
bitrate = { bitrate }
connectionSummary = { this._getConnectionStatusTip() }
e2eRtt = { e2eRtt }
framerate = { framerate }
isLocalVideo = { this.props.isLocalVideo }
onShowMore = { this._onToggleShowMore }
packetLoss = { packetLoss }
region = { region }
resolution = { resolution }
shouldShowMore = { this.state.showMoreStats }
transport = { transport } />

View File

@@ -2,10 +2,7 @@
import _ from 'lodash';
import {
JitsiConnectionQualityEvents,
JitsiE2ePingEvents
} from '../base/lib-jitsi-meet';
import { JitsiConnectionQualityEvents } from '../base/lib-jitsi-meet';
declare var APP: Object;
@@ -36,17 +33,6 @@ const statsEmitter = {
conference.on(JitsiConnectionQualityEvents.REMOTE_STATS_UPDATED,
(id, stats) => this._emitStatsUpdate(id, stats));
conference.on(
JitsiE2ePingEvents.E2E_RTT_CHANGED,
(participant, e2eRtt) => {
const stats = {
e2eRtt,
region: participant.getProperty('region')
};
this._emitStatsUpdate(participant.getId(), stats);
});
},
/**

View File

@@ -39,12 +39,7 @@ class ConnectionStatsTable extends Component {
connectionSummary: PropTypes.string,
/**
* The end-to-end round-trip-time.
*/
e2eRtt: PropTypes.number,
/**
* Statistics related to frame rates for each ssrc.
* Statistics related to framerates for each ssrc.
* {{
* [ ssrc ]: Number
* }}
@@ -52,7 +47,7 @@ class ConnectionStatsTable extends Component {
framerate: PropTypes.object,
/**
* Whether or not the statistics are for local video.
* Whether or not the statitics are for local video.
*/
isLocalVideo: PropTypes.bool,
@@ -70,11 +65,6 @@ class ConnectionStatsTable extends Component {
*/
packetLoss: PropTypes.object,
/**
* The region.
*/
region: PropTypes.string,
/**
* Statistics related to display resolutions for each ssrc.
* {{
@@ -218,31 +208,6 @@ class ConnectionStatsTable extends Component {
);
}
/**
* Creates a table row as a ReactElement for displaying end-to-end RTT and
* the region.
*
* @returns {ReactElement}
* @private
*/
_renderE2eRtt() {
const { e2eRtt, region, t } = this.props;
let str = e2eRtt ? `${e2eRtt.toFixed(0)}ms` : 'N/A';
if (region) {
str += ` (${region})`;
}
return (
<tr>
<td>
<span>{ t('connectionindicator.e2e_rtt') }</span>
</td>
<td>{ str }</td>
</tr>
);
}
/**
* Creates a table row as a ReactElement for displaying frame rate related
* statistics.
@@ -280,6 +245,7 @@ class ConnectionStatsTable extends Component {
if (packetLoss) {
const { download, upload } = packetLoss;
// eslint-disable-next-line no-extra-parens
packetLossTableData = (
<td>
<span className = 'connection-info__download'>
@@ -364,15 +330,12 @@ class ConnectionStatsTable extends Component {
* @returns {ReactElement}
*/
_renderStatistics() {
const isRemoteVideo = !this.props.isLocalVideo;
return (
<table className = 'connection-info__container'>
<tbody>
{ this._renderConnectionSummary() }
{ this._renderBitrate() }
{ this._renderPacketLoss() }
{ isRemoteVideo ? this._renderE2eRtt() : null }
{ this._renderResolution() }
{ this._renderFrameRate() }
</tbody>
@@ -391,6 +354,7 @@ class ConnectionStatsTable extends Component {
const { t, transport } = this.props;
if (!transport || transport.length === 0) {
// eslint-disable-next-line no-extra-parens
const NA = (
<tr key = 'address'>
<td>

View File

@@ -61,16 +61,18 @@ class DesktopPickerPane extends Component {
const classNames
= `desktop-picker-pane default-scrollbar source-type-${type}`;
const previews
= sources
? sources.map(source => (
= sources ? sources.map(
source =>
// eslint-disable-next-line react/jsx-wrap-multilines
<DesktopSourcePreview
key = { source.id }
onClick = { onClick }
onDoubleClick = { onDoubleClick }
selected = { source.id === selectedSourceId }
source = { source }
type = { type } />))
: (
type = { type } />)
: ( // eslint-disable-line no-extra-parens
<div className = 'desktop-picker-pane-spinner'>
<Spinner
isCompleting = { false }

View File

@@ -1,11 +0,0 @@
// @flow
/**
* The type of (redux) action to update the dropbox access token.
*
* {
* type: UPDATE_DROPBOX_TOKEN,
* token: string
* }
*/
export const UPDATE_DROPBOX_TOKEN = Symbol('UPDATE_DROPBOX_TOKEN');

View File

@@ -1,77 +0,0 @@
// @flow
import { Dropbox } from 'dropbox';
import {
getJitsiMeetGlobalNS,
getLocationContextRoot,
parseStandardURIString
} from '../base/util';
import { parseURLParams } from '../base/config';
import { UPDATE_DROPBOX_TOKEN } from './actionTypes';
/**
* Executes the oauth flow.
*
* @param {string} authUrl - The URL to oauth service.
* @returns {Promise<string>} - The URL with the authorization details.
*/
function authorize(authUrl: string): Promise<string> {
const windowName = `oauth${Date.now()}`;
const gloabalNS = getJitsiMeetGlobalNS();
gloabalNS.oauthCallbacks = gloabalNS.oauthCallbacks || {};
return new Promise(resolve => {
const popup = window.open(authUrl, windowName);
gloabalNS.oauthCallbacks[windowName] = () => {
const returnURL = popup.location.href;
popup.close();
delete gloabalNS.oauthCallbacks.windowName;
resolve(returnURL);
};
});
}
/**
* Action to authorize the Jitsi Recording app in dropbox.
*
* @returns {Function}
*/
export function authorizeDropbox() {
return (dispatch: Function, getState: Function) => {
const state = getState();
const { locationURL } = state['features/base/connection'];
const { dropbox } = state['features/base/config'];
const redirectURI = `${locationURL.origin
+ getLocationContextRoot(locationURL)}static/oauth.html`;
const dropboxAPI = new Dropbox({ clientId: dropbox.clientId });
const url = dropboxAPI.getAuthenticationUrl(redirectURI);
authorize(url).then(returnUrl => {
const params
= parseURLParams(parseStandardURIString(returnUrl), true) || {};
dispatch(updateDropboxToken(params.access_token));
});
};
}
/**
* Action to update the dropbox access token.
*
* @param {string} token - The new token.
* @returns {{
* type: UPDATE_DROPBOX_TOKEN,
* token: string
* }}
*/
export function updateDropboxToken(token: string) {
return {
type: UPDATE_DROPBOX_TOKEN,
token
};
}

View File

@@ -1,39 +0,0 @@
// @flow
import { Dropbox } from 'dropbox';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Fetches information about the user's dropbox account.
*
* @param {string} token - The dropbox access token.
* @param {string} clientId - The Jitsi Recorder dropbox app ID.
* @returns {Promise<Object|undefined>}
*/
export function getDropboxData(
token: string,
clientId: string
): Promise<?Object> {
const dropboxAPI = new Dropbox({
accessToken: token,
clientId
});
return Promise.all(
[ dropboxAPI.usersGetCurrentAccount(), dropboxAPI.usersGetSpaceUsage() ]
).then(([ account, space ]) => {
const { allocation, used } = space;
const { allocated } = allocation;
return {
userName: account.name.display_name,
spaceLeft: Math.floor((allocated - used) / 1048576)// 1MiB=1048576B
};
}, error => {
logger.error(error);
return undefined;
});
}

View File

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

View File

@@ -1,28 +0,0 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import { PersistenceRegistry } from '../base/storage';
import { UPDATE_DROPBOX_TOKEN } from './actionTypes';
/**
* The redux subtree of this feature.
*/
const STORE_NAME = 'features/dropbox';
/**
* Sets up the persistence of the feature {@code dropbox}.
*/
PersistenceRegistry.register(STORE_NAME);
ReducerRegistry.register(STORE_NAME, (state = {}, action) => {
switch (action.type) {
case UPDATE_DROPBOX_TOKEN:
return {
...state,
token: action.token
};
default:
return state;
}
});

View File

@@ -121,15 +121,17 @@ class Filmstrip extends Component<Props> {
&& <LocalThumbnail />
}
{
/* eslint-disable react/jsx-wrap-multilines */
this._sort(
this.props._participants,
isNarrowAspectRatio_)
.map(p => (
.map(p =>
<Thumbnail
key = { p.id }
participant = { p } />))
participant = { p } />)
/* eslint-enable react/jsx-wrap-multilines */
}
{
!this._separateLocalThumbnail

View File

@@ -8,7 +8,6 @@ import { dockToolbox } from '../../../toolbox';
import { setFilmstripHovered } from '../../actions';
import { shouldRemoteVideosBeVisible } from '../../functions';
import Toolbar from './Toolbar';
declare var interfaceConfig: Object;
@@ -186,8 +185,9 @@ function _mapStateToProps(state) {
&& state['features/toolbox'].visible
&& interfaceConfig.TOOLBAR_BUTTONS.length;
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
reduceHeight ? 'reduce-height' : ''}`.trim();
reduceHeight ? 'reduce-height' : ''}`;
return {
_className: className,

View File

@@ -1,5 +1,4 @@
/* @flow */
import { getShareInfoText } from '../invite';
import {
SET_GOOGLE_API_PROFILE,
@@ -8,21 +7,6 @@ import {
import { GOOGLE_API_STATES } from './constants';
import googleApi from './googleApi';
/**
* Retrieves the current calendar events.
*
* @param {number} fetchStartDays - The number of days to go back when fetching.
* @param {number} fetchEndDays - The number of days to fetch.
* @returns {function(Dispatch<*>): Promise<CalendarEntries>}
*/
export function getCalendarEntries(
fetchStartDays: ?number, fetchEndDays: ?number) {
return () =>
googleApi.get()
.then(() =>
googleApi._getCalendarEntries(fetchStartDays, fetchEndDays));
}
/**
* Loads Google API.
*
@@ -30,16 +14,9 @@ export function getCalendarEntries(
* @returns {Function}
*/
export function loadGoogleAPI(clientId: string) {
return (dispatch: Dispatch<*>, getState: Function) =>
return (dispatch: Dispatch<*>) =>
googleApi.get()
.then(() => {
if (getState()['features/google-api'].googleAPIState
=== GOOGLE_API_STATES.NEEDS_LOADING) {
return googleApi.initializeClient(clientId);
}
return Promise.resolve();
})
.then(() => googleApi.initializeClient(clientId))
.then(() => dispatch({
type: SET_GOOGLE_API_STATE,
googleAPIState: GOOGLE_API_STATES.LOADED }))
@@ -53,6 +30,39 @@ export function loadGoogleAPI(clientId: string) {
});
}
/**
* Prompts the participant to sign in to the Google API Client Library.
*
* @returns {function(Dispatch<*>): Promise<string | never>}
*/
export function signIn() {
return (dispatch: Dispatch<*>) => googleApi.get()
.then(() => googleApi.signInIfNotSignedIn())
.then(() => dispatch({
type: SET_GOOGLE_API_STATE,
googleAPIState: GOOGLE_API_STATES.SIGNED_IN
}));
}
/**
* Updates the profile data that is currently used.
*
* @returns {function(Dispatch<*>): Promise<string | never>}
*/
export function updateProfile() {
return (dispatch: Dispatch<*>) => googleApi.get()
.then(() => googleApi.signInIfNotSignedIn())
.then(() => dispatch({
type: SET_GOOGLE_API_STATE,
googleAPIState: GOOGLE_API_STATES.SIGNED_IN
}))
.then(() => googleApi.getCurrentUserProfile())
.then(profile => dispatch({
type: SET_GOOGLE_API_PROFILE,
profileEmail: profile.getEmail()
}));
}
/**
* Executes a request for a list of all YouTube broadcasts associated with
* user currently signed in to the Google API Client Library.
@@ -127,82 +137,3 @@ export function showAccountSelection() {
return () =>
googleApi.showAccountSelection();
}
/**
* Prompts the participant to sign in to the Google API Client Library.
*
* @returns {function(Dispatch<*>): Promise<string | never>}
*/
export function signIn() {
return (dispatch: Dispatch<*>) => googleApi.get()
.then(() => googleApi.signInIfNotSignedIn())
.then(() => dispatch({
type: SET_GOOGLE_API_STATE,
googleAPIState: GOOGLE_API_STATES.SIGNED_IN
}));
}
/**
* Logs out the user.
*
* @returns {function(Dispatch<*>): Promise<string | never>}
*/
export function signOut() {
return (dispatch: Dispatch<*>) =>
googleApi.get()
.then(() => googleApi.signOut())
.then(() => {
dispatch({
type: SET_GOOGLE_API_STATE,
googleAPIState: GOOGLE_API_STATES.LOADED
});
dispatch({
type: SET_GOOGLE_API_PROFILE,
profileEmail: ''
});
});
}
/**
* Updates the profile data that is currently used.
*
* @returns {function(Dispatch<*>): Promise<string | never>}
*/
export function updateProfile() {
return (dispatch: Dispatch<*>) => googleApi.get()
.then(() => googleApi.signInIfNotSignedIn())
.then(() => dispatch({
type: SET_GOOGLE_API_STATE,
googleAPIState: GOOGLE_API_STATES.SIGNED_IN
}))
.then(() => googleApi.getCurrentUserProfile())
.then(profile => {
dispatch({
type: SET_GOOGLE_API_PROFILE,
profileEmail: profile.getEmail()
});
return profile.getEmail();
});
}
/**
* Updates the calendar event and adds a location and text.
*
* @param {string} id - The event id to update.
* @param {string} calendarId - The calendar id to use.
* @param {string} location - The location to add to the event.
* @returns {function(Dispatch<*>): Promise<string | never>}
*/
export function updateCalendarEvent(
id: string, calendarId: string, location: string) {
return (dispatch: Dispatch<*>, getState: Function) => {
const { dialInNumbersUrl } = getState()['features/base/config'];
const text = getShareInfoText(location, dialInNumbersUrl !== undefined);
return googleApi.get()
.then(() =>
googleApi._updateCalendarEntry(id, calendarId, location, text));
};
}

View File

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

View File

@@ -1,23 +1,14 @@
// @flow
/**
* The Google API scopes to request access for streaming and calendar.
* The Google API scopes to request access to for streaming.
*
* @type {Array<string>}
*/
export const GOOGLE_API_SCOPES = [
'https://www.googleapis.com/auth/youtube.readonly',
'https://www.googleapis.com/auth/calendar'
'https://www.googleapis.com/auth/youtube.readonly'
];
/**
* Array of API discovery doc URLs for APIs used by the googleApi.
*
* @type {string[]}
*/
export const DISCOVERY_DOCS
= [ 'https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest' ];
/**
* An enumeration of the different states the Google API can be in.
*

View File

@@ -1,4 +1,4 @@
import { GOOGLE_API_SCOPES, DISCOVERY_DOCS } from './constants';
import { GOOGLE_API_SCOPES } from './constants';
const GOOGLE_API_CLIENT_LIBRARY_URL = 'https://apis.google.com/js/api.js';
@@ -67,7 +67,6 @@ const googleApi = {
setTimeout(() => {
api.client.init({
clientId,
discoveryDocs: DISCOVERY_DOCS,
scope: GOOGLE_API_SCOPES.join(' ')
})
.then(resolve)
@@ -87,7 +86,6 @@ const googleApi = {
.then(api => Boolean(api
&& api.auth2
&& api.auth2.getAuthInstance
&& api.auth2.getAuthInstance()
&& api.auth2.getAuthInstance().isSignedIn
&& api.auth2.getAuthInstance().isSignedIn.get()));
},
@@ -185,165 +183,6 @@ const googleApi = {
});
},
/**
* Sign out from the Google API Client Library.
*
* @returns {Promise}
*/
signOut() {
return this.get()
.then(api =>
api.auth2
&& api.auth2.getAuthInstance
&& api.auth2.getAuthInstance()
&& api.auth2.getAuthInstance().signOut());
},
/**
* Parses the google calendar entries to a known format.
*
* @param {Object} entry - The google calendar entry.
* @returns {{
* calendarId: string,
* description: string,
* endDate: string,
* id: string,
* location: string,
* startDate: string,
* title: string}}
* @private
*/
_convertCalendarEntry(entry) {
return {
calendarId: entry.calendarId,
description: entry.description,
endDate: entry.end.dateTime,
id: entry.id,
location: entry.location,
startDate: entry.start.dateTime,
title: entry.summary
};
},
/**
* Retrieves calendar entries from all available calendars.
*
* @param {number} fetchStartDays - The number of days to go back
* when fetching.
* @param {number} fetchEndDays - The number of days to fetch.
* @returns {Promise<CalendarEntry>}
* @private
*/
_getCalendarEntries(fetchStartDays, fetchEndDays) {
return this.get()
.then(() => this.isSignedIn())
.then(isSignedIn => {
if (!isSignedIn) {
return null;
}
// user can edit the events, so we want only those that
// can be edited
return this._getGoogleApiClient()
.client.calendar.calendarList.list();
})
.then(calendarList => {
// no result, maybe not signed in
if (!calendarList) {
return Promise.resolve();
}
const calendarIds
= calendarList.result.items.map(en => {
return {
id: en.id,
accessRole: en.accessRole
};
});
const promises = calendarIds.map(({ id, accessRole }) => {
const startDate = new Date();
const endDate = new Date();
startDate.setDate(startDate.getDate() + fetchStartDays);
endDate.setDate(endDate.getDate() + fetchEndDays);
// retrieve the events and adds to the result the calendarId
return this._getGoogleApiClient()
.client.calendar.events.list({
'calendarId': id,
'timeMin': startDate.toISOString(),
'timeMax': endDate.toISOString(),
'showDeleted': false,
'singleEvents': true,
'orderBy': 'startTime'
})
.then(result => result.result.items
.map(item => {
const resultItem = { ...item };
// add the calendarId only for the events
// we can edit
if (accessRole === 'writer'
|| accessRole === 'owner') {
resultItem.calendarId = id;
}
return resultItem;
}));
});
return Promise.all(promises)
.then(results => [].concat(...results))
.then(entries =>
entries.map(e => this._convertCalendarEntry(e)));
});
},
/* eslint-disable max-params */
/**
* Updates the calendar event and adds a location and text.
*
* @param {string} id - The event id to update.
* @param {string} calendarId - The calendar id to use.
* @param {string} location - The location to add to the event.
* @param {string} text - The description text to set/append.
* @returns {Promise<T | never>}
* @private
*/
_updateCalendarEntry(id, calendarId, location, text) {
return this.get()
.then(() => this.isSignedIn())
.then(isSignedIn => {
if (!isSignedIn) {
return null;
}
return this._getGoogleApiClient()
.client.calendar.events.get({
'calendarId': calendarId,
'eventId': id
}).then(event => {
let newDescription = text;
if (event.result.description) {
newDescription = `${event.result.description}\n\n${
text}`;
}
return this._getGoogleApiClient()
.client.calendar.events.patch({
'calendarId': calendarId,
'eventId': id,
'description': newDescription,
'location': location
});
});
});
},
/* eslint-enable max-params */
/**
* Returns the global Google API Client Library object. Direct use of this
* method is discouraged; instead use the {@link get} method.

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