mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-15 03:00:19 +00:00
Compare commits
69 Commits
base_sessi
...
3003
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86caf52d08 | ||
|
|
f2cb15ba44 | ||
|
|
d62974b433 | ||
|
|
8ff33684f7 | ||
|
|
b8179102c5 | ||
|
|
c23c798f7a | ||
|
|
3c27d2ee54 | ||
|
|
7267f386dc | ||
|
|
dba7f2d429 | ||
|
|
a896d8f076 | ||
|
|
99d285519d | ||
|
|
2704b2f822 | ||
|
|
62544188bd | ||
|
|
df0e107ea6 | ||
|
|
f10d42f8e4 | ||
|
|
7eda31315f | ||
|
|
87c010a9bd | ||
|
|
8d0d92a437 | ||
|
|
faada0abae | ||
|
|
1d99abc4a4 | ||
|
|
9aed4df6d2 | ||
|
|
d92b720704 | ||
|
|
25aaa74edc | ||
|
|
195462a1a8 | ||
|
|
9c03e95bf1 | ||
|
|
c353e9377f | ||
|
|
913c56c408 | ||
|
|
2f1223f721 | ||
|
|
4f1aaf89bf | ||
|
|
df6df1c6c3 | ||
|
|
1e804e552e | ||
|
|
b284f25fde | ||
|
|
49bdd53bee | ||
|
|
0827e02de9 | ||
|
|
0410af9e5e | ||
|
|
5a051024e6 | ||
|
|
e2def5f88b | ||
|
|
1078fa9d05 | ||
|
|
dda7568a48 | ||
|
|
4550848eac | ||
|
|
7822831b1e | ||
|
|
e03126e422 | ||
|
|
61652c69b3 | ||
|
|
b6e1a49d33 | ||
|
|
e0ac3efb5c | ||
|
|
65c76dcde5 | ||
|
|
5daa91ec1b | ||
|
|
473ba28171 | ||
|
|
52b55d65a0 | ||
|
|
8ebf2b7e47 | ||
|
|
cc38fcc5d0 | ||
|
|
a277421ecb | ||
|
|
2f2e69a6f5 | ||
|
|
0490a3cf73 | ||
|
|
bfc8ecfaa6 | ||
|
|
42c827434c | ||
|
|
0f3b67e53e | ||
|
|
2dfb107c57 | ||
|
|
f8c01646c7 | ||
|
|
0f0f9ea1b2 | ||
|
|
ce308eaa8b | ||
|
|
337cea6488 | ||
|
|
e125861b29 | ||
|
|
3241c7a929 | ||
|
|
55a2ef30a0 | ||
|
|
ae0bd9e64e | ||
|
|
9c769a650e | ||
|
|
07bc70c2f5 | ||
|
|
2ee1bf9351 |
13
Makefile
13
Makefile
@@ -2,6 +2,7 @@ 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 = .
|
||||
@@ -19,7 +20,7 @@ compile:
|
||||
clean:
|
||||
rm -fr $(BUILD_DIR)
|
||||
|
||||
deploy: deploy-init deploy-appbundle deploy-lib-jitsi-meet deploy-css deploy-local
|
||||
deploy: deploy-init deploy-appbundle deploy-lib-jitsi-meet deploy-libflac deploy-css deploy-local
|
||||
|
||||
deploy-init:
|
||||
rm -fr $(DEPLOY_DIR)
|
||||
@@ -33,6 +34,8 @@ 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 \
|
||||
@@ -50,6 +53,12 @@ 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) ; \
|
||||
@@ -58,7 +67,7 @@ deploy-css:
|
||||
deploy-local:
|
||||
([ ! -x deploy-local.sh ] || ./deploy-local.sh)
|
||||
|
||||
dev: deploy-init deploy-css deploy-lib-jitsi-meet
|
||||
dev: deploy-init deploy-css deploy-lib-jitsi-meet deploy-libflac
|
||||
$(WEBPACK_DEV_SERVER)
|
||||
|
||||
source-package:
|
||||
|
||||
@@ -704,7 +704,7 @@ export default {
|
||||
track.mute();
|
||||
}
|
||||
});
|
||||
logger.log('initialized with %s local tracks', tracks.length);
|
||||
logger.log(`initialized with ${tracks.length} local tracks`);
|
||||
this._localTracksInitialized = true;
|
||||
con.addEventListener(
|
||||
JitsiConnectionEvents.CONNECTION_FAILED,
|
||||
@@ -1678,7 +1678,7 @@ export default {
|
||||
role: user.getRole()
|
||||
}));
|
||||
|
||||
logger.log('USER %s connnected', id, user);
|
||||
logger.log(`USER ${id} connnected:`, user);
|
||||
APP.API.notifyUserJoined(id, {
|
||||
displayName,
|
||||
formattedDisplayName: appendSuffix(
|
||||
@@ -1698,7 +1698,7 @@ export default {
|
||||
}
|
||||
|
||||
APP.store.dispatch(participantLeft(id, room));
|
||||
logger.log('USER %s LEFT', id, user);
|
||||
logger.log(`USER ${id} LEFT:`, user);
|
||||
APP.API.notifyUserLeft(id);
|
||||
APP.UI.messageHandler.participantNotification(
|
||||
user.getDisplayName(),
|
||||
|
||||
36
config.js
36
config.js
@@ -256,6 +256,10 @@ var config = {
|
||||
// maintenance at 01:00 AM GMT,
|
||||
// noticeMessage: '',
|
||||
|
||||
// Enables calendar integration, depends on googleApiApplicationClientID
|
||||
// and microsoftApiApplicationClientID
|
||||
// enableCalendarIntegration: false,
|
||||
|
||||
// Stats
|
||||
//
|
||||
|
||||
@@ -347,6 +351,36 @@ 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
|
||||
@@ -368,6 +402,7 @@ var config = {
|
||||
googleApiApplicationClientID
|
||||
iAmRecorder
|
||||
iAmSipGateway
|
||||
microsoftApiApplicationClientID
|
||||
peopleSearchQueryTypes
|
||||
peopleSearchUrl
|
||||
requireDisplayName
|
||||
@@ -396,6 +431,7 @@ var config = {
|
||||
nick
|
||||
startBitrate
|
||||
*/
|
||||
|
||||
};
|
||||
|
||||
/* eslint-enable no-unused-vars, no-var */
|
||||
|
||||
@@ -10,3 +10,31 @@
|
||||
-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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,20 +13,35 @@
|
||||
float: left;
|
||||
}
|
||||
.navigate-section-list-tile {
|
||||
height: 90px;
|
||||
width: 260px;
|
||||
border-radius: 4px;
|
||||
background-color: #1754A9;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
height: 100px;
|
||||
margin-bottom: 8px;
|
||||
margin-right: 8px;
|
||||
padding: 16px;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
|
||||
&.with-click-handler {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.with-click-handler:hover {
|
||||
background-color: #1a5dbb;
|
||||
}
|
||||
|
||||
i {
|
||||
cursor: inherit;
|
||||
}
|
||||
}
|
||||
.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;
|
||||
@@ -40,4 +55,8 @@
|
||||
position: relative;
|
||||
margin-top: 36px;
|
||||
margin-bottom: 36px;
|
||||
width: 100%;
|
||||
}
|
||||
.navigate-section-list-empty {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,42 @@
|
||||
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.
|
||||
@@ -34,39 +70,6 @@
|
||||
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);
|
||||
|
||||
@@ -2,12 +2,19 @@
|
||||
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);
|
||||
width: 100%;
|
||||
z-index: $zindex2;
|
||||
transform: translateX(-50%);
|
||||
z-index: $filmstripVideosZ + 1;
|
||||
|
||||
span {
|
||||
background: black;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ body.welcome-page {
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#enter_room {
|
||||
@@ -62,12 +63,30 @@ 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%;
|
||||
|
||||
@@ -14,14 +14,9 @@
|
||||
* Focused video thumbnail.
|
||||
*/
|
||||
&.videoContainerFocused {
|
||||
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;
|
||||
border: $thumbnailVideoBorder solid $videoThumbnailSelected;
|
||||
box-shadow: inset 0 0 3px $videoThumbnailSelected,
|
||||
0 0 3px $videoThumbnailSelected !important;
|
||||
0 0 3px $videoThumbnailSelected;
|
||||
}
|
||||
|
||||
.remotevideomenu > .icon-menu {
|
||||
@@ -31,7 +26,7 @@
|
||||
/**
|
||||
* Hovered video thumbnail.
|
||||
*/
|
||||
&:hover {
|
||||
&:hover:not(.videoContainerFocused):not(.active-speaker) {
|
||||
cursor: hand;
|
||||
border: $thumbnailVideoBorder solid $videoThumbnailHovered;
|
||||
box-shadow: inset 0 0 3px $videoThumbnailHovered,
|
||||
|
||||
113
css/filmstrip/_tile_view.scss
Normal file
113
css/filmstrip/_tile_view.scss
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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 {
|
||||
border: $thumbnailVideoBorder solid $videoThumbnailSelected;
|
||||
box-shadow: inset 0 0 3px $videoThumbnailSelected,
|
||||
0 0 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 scrolling of the thumbnails.
|
||||
*/
|
||||
overflow: 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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
47
css/filmstrip/_tile_view_overrides.scss
Normal file
47
css/filmstrip/_tile_view_overrides.scss
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@
|
||||
@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';
|
||||
@@ -72,6 +73,8 @@
|
||||
@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';
|
||||
@@ -79,4 +82,7 @@
|
||||
@import 'deep-linking/main';
|
||||
@import 'transcription-subtitles';
|
||||
@import 'navigate_section_list';
|
||||
@import 'third-party-branding/google';
|
||||
@import 'third-party-branding/microsoft';
|
||||
|
||||
/* Modules END */
|
||||
|
||||
92
css/modals/local-recording/_local-recording.scss
Normal file
92
css/modals/local-recording/_local-recording.scss
Normal file
@@ -0,0 +1,92 @@
|
||||
.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;
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.calendar-tab,
|
||||
.device-selection {
|
||||
margin-top: 20px;
|
||||
}
|
||||
@@ -22,6 +23,7 @@
|
||||
padding: 20px 0px 4px 0px;
|
||||
}
|
||||
|
||||
.calendar-tab,
|
||||
.more-tab,
|
||||
.profile-edit {
|
||||
display: flex;
|
||||
@@ -40,4 +42,20 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +168,10 @@
|
||||
background: #FF5630;
|
||||
}
|
||||
|
||||
.circular-label.local-rec {
|
||||
background: #FF5630;
|
||||
}
|
||||
|
||||
.circular-label.stream {
|
||||
background: #0065FF;
|
||||
}
|
||||
|
||||
32
css/third-party-branding/google.scss
Normal file
32
css/third-party-branding/google.scss
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
28
css/third-party-branding/microsoft.scss
Normal file
28
css/third-party-branding/microsoft.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
42
images/dropboxLogo.svg
Normal file
42
images/dropboxLogo.svg
Normal file
@@ -0,0 +1,42 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
1
images/microsoftLogo.svg
Normal file
1
images/microsoftLogo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 343 B |
@@ -48,10 +48,11 @@ var interfaceConfig = {
|
||||
'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
|
||||
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
|
||||
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
|
||||
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts'
|
||||
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
||||
'tileview'
|
||||
],
|
||||
|
||||
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile' ],
|
||||
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],
|
||||
|
||||
// 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
|
||||
@@ -172,6 +173,12 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,7 @@ PODS:
|
||||
- React/Core (= 0.55.4)
|
||||
- react-native-background-timer (2.0.0):
|
||||
- React
|
||||
- react-native-calendar-events (1.6.0):
|
||||
- react-native-calendar-events (1.6.2):
|
||||
- React
|
||||
- react-native-fast-image (4.0.14):
|
||||
- FLAnimatedImage
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
"mute": "Mute or unmute your microphone",
|
||||
"fullScreen": "View or exit full screen",
|
||||
"videoMute": "Start or stop your camera",
|
||||
"showSpeakerStats": "Show speaker stats"
|
||||
"showSpeakerStats": "Show speaker stats",
|
||||
"localRecording": "Show or hide local recording controls"
|
||||
},
|
||||
"welcomepage":{
|
||||
"accessibilityLabel": {
|
||||
@@ -56,6 +57,8 @@
|
||||
"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",
|
||||
@@ -87,6 +90,7 @@
|
||||
"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",
|
||||
@@ -102,6 +106,7 @@
|
||||
"shortcuts": "Toggle shortcuts",
|
||||
"speakerStats": "Toggle speaker statistics",
|
||||
"toggleCamera": "Toggle camera",
|
||||
"tileView": "Toggle tile view",
|
||||
"videomute": "Toggle mute video"
|
||||
},
|
||||
"addPeople": "Add people to your call",
|
||||
@@ -144,6 +149,7 @@
|
||||
"raiseHand": "Raise / Lower your hand",
|
||||
"shortcuts": "View shortcuts",
|
||||
"speakerStats": "Speaker stats",
|
||||
"tileViewToggle": "Toggle tile view",
|
||||
"invite": "Invite people"
|
||||
},
|
||||
"chat":{
|
||||
@@ -153,8 +159,14 @@
|
||||
},
|
||||
"messagebox": "Enter text..."
|
||||
},
|
||||
"settings":
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"title": "Settings",
|
||||
"update": "Update",
|
||||
"name": "Name",
|
||||
@@ -198,6 +210,7 @@
|
||||
"packetloss": "Packet loss:",
|
||||
"resolution": "Resolution:",
|
||||
"framerate": "Frame rate:",
|
||||
"e2e_rtt": "E2E RTT:",
|
||||
"less": "Show less",
|
||||
"more": "Show more",
|
||||
"address": "Address:",
|
||||
@@ -413,6 +426,11 @@
|
||||
],
|
||||
"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",
|
||||
@@ -445,7 +463,13 @@
|
||||
"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"
|
||||
@@ -617,13 +641,14 @@
|
||||
"startWithVideoMuted": "Start with video muted"
|
||||
},
|
||||
"calendarSync": {
|
||||
"later": "Later",
|
||||
"next": "Upcoming",
|
||||
"addMeetingURL": "Add a meeting link",
|
||||
"today": "Today",
|
||||
"nextMeeting": "next meeting",
|
||||
"now": "Now",
|
||||
"noEvents": "There are no upcoming events scheduled.",
|
||||
"ongoingMeeting": "ongoing meeting",
|
||||
"permissionButton": "Open settings",
|
||||
"permissionMessage": "The Calendar permission is required to see your meetings in the app."
|
||||
"permissionMessage": "The Calendar permission is required to see your meetings in the app.",
|
||||
"refresh": "Refresh calendar"
|
||||
},
|
||||
"recentList": {
|
||||
"joinPastMeeting": "Join A Past Meeting"
|
||||
@@ -665,5 +690,34 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ 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';
|
||||
|
||||
@@ -117,6 +118,31 @@ 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}.
|
||||
@@ -189,6 +215,10 @@ class FollowMe {
|
||||
this._sharedDocumentToggled
|
||||
.bind(this, this._UI.getSharedDocumentManager().isVisible());
|
||||
}
|
||||
|
||||
this._tileViewToggled.bind(
|
||||
this,
|
||||
APP.store.getState()['features/video-layout'].tileViewEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,6 +244,10 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,6 +261,8 @@ class FollowMe {
|
||||
this.sharedDocEventHandler);
|
||||
this._UI.removeListener(UIEvents.PINNED_ENDPOINT,
|
||||
this.pinnedEndpointEventHandler);
|
||||
this._UI.removeListener(UIEvents.TOGGLED_TILE_VIEW,
|
||||
this.tileViewEventHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,6 +302,18 @@ 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.
|
||||
*
|
||||
@@ -316,7 +364,8 @@ class FollowMe {
|
||||
attributes: {
|
||||
filmstripVisible: local.filmstripVisible,
|
||||
nextOnStage: local.nextOnStage,
|
||||
sharedDocumentVisible: local.sharedDocumentVisible
|
||||
sharedDocumentVisible: local.sharedDocumentVisible,
|
||||
tileViewEnabled: local.tileViewEnabled
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -355,6 +404,7 @@ class FollowMe {
|
||||
this._onFilmstripVisible(attributes.filmstripVisible);
|
||||
this._onNextOnStage(attributes.nextOnStage);
|
||||
this._onSharedDocumentVisible(attributes.sharedDocumentVisible);
|
||||
this._onTileViewEnabled(attributes.tileViewEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -434,6 +484,21 @@ 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.
|
||||
*
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/* global $ */
|
||||
/* global $, APP */
|
||||
import { shouldDisplayTileView } from '../../../react/features/video-layout';
|
||||
|
||||
import SmallVideo from '../videolayout/SmallVideo';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
@@ -64,7 +66,9 @@ SharedVideoThumb.prototype.createContainer = function(spanId) {
|
||||
* The thumb click handler.
|
||||
*/
|
||||
SharedVideoThumb.prototype.videoClick = function() {
|
||||
this._togglePin();
|
||||
if (!shouldDisplayTileView(APP.store.getState())) {
|
||||
this._togglePin();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
/* 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';
|
||||
@@ -233,6 +240,10 @@ 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;
|
||||
@@ -247,11 +258,10 @@ const Filmstrip = {
|
||||
* @returns {{availableWidth: number, availableHeight: number}}
|
||||
*/
|
||||
calculateAvailableSize() {
|
||||
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
|
||||
const thumbs = this.getThumbs(true);
|
||||
const numvids = thumbs.remoteThumbs.length;
|
||||
|
||||
const localVideoContainer = $('#localVideoContainer');
|
||||
const state = APP.store.getState();
|
||||
const currentLayout = getCurrentLayout(state);
|
||||
const isHorizontalFilmstripView
|
||||
= currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW;
|
||||
|
||||
/**
|
||||
* If the videoAreaAvailableWidth is set we use this one to calculate
|
||||
@@ -268,10 +278,15 @@ 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(
|
||||
@@ -289,10 +304,12 @@ const Filmstrip = {
|
||||
);
|
||||
}
|
||||
|
||||
// If the number of videos is 0 or undefined or we're in vertical
|
||||
// If the number of videos is 0 or undefined or we're not in horizontal
|
||||
// filmstrip mode we don't need to calculate further any adjustments
|
||||
// to width based on the number of videos present.
|
||||
if (numvids && !interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
const numvids = thumbs.remoteThumbs.length;
|
||||
|
||||
if (numvids && isHorizontalFilmstripView) {
|
||||
const remoteVideoContainer = thumbs.remoteThumbs.eq(0);
|
||||
|
||||
availableWidth = Math.floor(
|
||||
@@ -322,8 +339,10 @@ const Filmstrip = {
|
||||
availableHeight
|
||||
= Math.min(maxHeight, window.innerHeight - 18);
|
||||
|
||||
return { availableWidth,
|
||||
availableHeight };
|
||||
return {
|
||||
availableHeight,
|
||||
availableWidth
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -434,6 +453,51 @@ 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
|
||||
@@ -443,6 +507,28 @@ 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) {
|
||||
@@ -466,13 +552,15 @@ const Filmstrip = {
|
||||
});
|
||||
}
|
||||
|
||||
const currentLayout = getCurrentLayout(APP.store.getState());
|
||||
|
||||
// Let CSS take care of height in vertical filmstrip mode.
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
|
||||
$('#filmstripLocalVideo').css({
|
||||
// adds 4 px because of small video 2px border
|
||||
width: `${local.thumbWidth + 4}px`
|
||||
});
|
||||
} else {
|
||||
} else if (currentLayout === LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW) {
|
||||
this.filmstrip.css({
|
||||
// adds 4 px because of small video 2px border
|
||||
height: `${remote.thumbHeight + 4}px`
|
||||
|
||||
@@ -11,6 +11,7 @@ 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);
|
||||
@@ -26,7 +27,7 @@ function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
|
||||
this.streamEndedCallback = streamEndedCallback;
|
||||
this.container = this.createContainer();
|
||||
this.$container = $(this.container);
|
||||
$('#filmstripLocalVideoThumbnail').append(this.container);
|
||||
this.updateDOMLocation();
|
||||
|
||||
this.localVideoId = null;
|
||||
this.bindHoverHandler();
|
||||
@@ -109,16 +110,7 @@ LocalVideo.prototype.changeVideo = function(stream) {
|
||||
|
||||
this.localVideoId = `localVideo_${stream.getId()}`;
|
||||
|
||||
const localVideoContainer = document.getElementById('localVideoWrapper');
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<VideoTrack
|
||||
id = { this.localVideoId }
|
||||
videoTrack = {{ jitsiTrack: stream }} />
|
||||
</Provider>,
|
||||
localVideoContainer
|
||||
);
|
||||
this._updateVideoElement();
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
const isVideo = stream.videoType != 'desktop';
|
||||
@@ -128,12 +120,14 @@ 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 (this.videoStream.isEnded()) {
|
||||
if (localVideoContainer && this.videoStream.isEnded()) {
|
||||
ReactDOM.unmountComponentAtNode(localVideoContainer);
|
||||
}
|
||||
|
||||
@@ -235,6 +229,29 @@ 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
|
||||
@@ -258,7 +275,9 @@ LocalVideo.prototype._onContainerClick = function(event) {
|
||||
= $source.parents('.displayNameContainer').length > 0;
|
||||
const clickedOnPopover = $source.parents('.popover').length > 0
|
||||
|| classList.contains('popover');
|
||||
const ignoreClick = clickedOnDisplayName || clickedOnPopover;
|
||||
const ignoreClick = clickedOnDisplayName
|
||||
|| clickedOnPopover
|
||||
|| shouldDisplayTileView(APP.store.getState());
|
||||
|
||||
if (event.stopPropagation && !ignoreClick) {
|
||||
event.stopPropagation();
|
||||
@@ -269,4 +288,28 @@ 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;
|
||||
|
||||
@@ -20,6 +20,11 @@ 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);
|
||||
@@ -163,8 +168,17 @@ RemoteVideo.prototype._generatePopupContent = function() {
|
||||
const onVolumeChange = this._setAudioVolume;
|
||||
const { isModerator } = APP.conference;
|
||||
const participantID = this.id;
|
||||
const menuPosition = interfaceConfig.VERTICAL_FILMSTRIP
|
||||
? 'left bottom' : 'top center';
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
@@ -174,7 +188,7 @@ RemoteVideo.prototype._generatePopupContent = function() {
|
||||
initialVolumeValue = { initialVolumeValue }
|
||||
isAudioMuted = { this.isAudioMuted }
|
||||
isModerator = { isModerator }
|
||||
menuPosition = { menuPosition }
|
||||
menuPosition = { remoteMenuPosition }
|
||||
onMenuDisplay
|
||||
= {this._onRemoteVideoMenuDisplay.bind(this)}
|
||||
onRemoteControlToggle = { onRemoteControlToggle }
|
||||
@@ -613,7 +627,8 @@ RemoteVideo.prototype._onContainerClick = function(event) {
|
||||
const { classList } = event.target;
|
||||
|
||||
const ignoreClick = $source.parents('.popover').length > 0
|
||||
|| classList.contains('popover');
|
||||
|| classList.contains('popover')
|
||||
|| shouldDisplayTileView(APP.store.getState());
|
||||
|
||||
if (!ignoreClick) {
|
||||
this._togglePin();
|
||||
|
||||
@@ -27,6 +27,11 @@ 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);
|
||||
@@ -328,7 +333,21 @@ SmallVideo.prototype.setVideoMutedView = function(isMuted) {
|
||||
SmallVideo.prototype.updateStatusBar = function() {
|
||||
const statusBarContainer
|
||||
= this.container.querySelector('.videocontainer__toolbar');
|
||||
const tooltipPosition = interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top';
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
@@ -547,7 +566,8 @@ SmallVideo.prototype.isVideoPlayable = function() {
|
||||
*/
|
||||
SmallVideo.prototype.selectDisplayMode = function() {
|
||||
// Display name is always and only displayed when user is on the stage
|
||||
if (this.isCurrentlyOnLargeVideo()) {
|
||||
if (this.isCurrentlyOnLargeVideo()
|
||||
&& !shouldDisplayTileView(APP.store.getState())) {
|
||||
return this.isVideoPlayable() && !APP.conference.isAudioOnly()
|
||||
? DISPLAY_BLACKNESS_WITH_NAME : DISPLAY_AVATAR_WITH_NAME;
|
||||
} else if (this.isVideoPlayable()
|
||||
@@ -685,7 +705,10 @@ SmallVideo.prototype.showDominantSpeakerIndicator = function(show) {
|
||||
|
||||
this._showDominantSpeaker = show;
|
||||
|
||||
this.$container.toggleClass('active-speaker', this._showDominantSpeaker);
|
||||
|
||||
this.updateIndicators();
|
||||
this.updateView();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -765,6 +788,18 @@ 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
|
||||
@@ -784,7 +819,19 @@ SmallVideo.prototype.updateIndicators = function() {
|
||||
const iconSize = UIUtil.getIndicatorFontSize();
|
||||
const showConnectionIndicator = this.videoIsHovered
|
||||
|| !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
|
||||
const tooltipPosition = interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top';
|
||||
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';
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
@@ -799,7 +846,7 @@ SmallVideo.prototype.updateIndicators = function() {
|
||||
enableStatsDisplay
|
||||
= { !interfaceConfig.filmStripOnly }
|
||||
statsPopoverPosition
|
||||
= { this.statsPopoverLocation }
|
||||
= { statsPopoverPosition }
|
||||
userID = { this.id } />
|
||||
: null }
|
||||
{ this._showRaisedHand
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/* 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';
|
||||
@@ -9,6 +13,9 @@ 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';
|
||||
|
||||
@@ -594,12 +601,19 @@ 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 };
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1142,6 +1156,22 @@ 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.
|
||||
|
||||
54
package-lock.json
generated
54
package-lock.json
generated
@@ -3003,6 +3003,15 @@
|
||||
"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",
|
||||
@@ -6024,6 +6033,15 @@
|
||||
"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",
|
||||
@@ -6263,6 +6281,11 @@
|
||||
"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",
|
||||
@@ -6449,8 +6472,8 @@
|
||||
}
|
||||
},
|
||||
"eslint-config-jitsi": {
|
||||
"version": "github:jitsi/eslint-config-jitsi#3d193df6476a73f827582e137a67a8612130a455",
|
||||
"from": "github:jitsi/eslint-config-jitsi#v0.1.0",
|
||||
"version": "github:jitsi/eslint-config-jitsi#7474f6668515eb5852f1273dc5a50b940a550d3f",
|
||||
"from": "github:jitsi/eslint-config-jitsi#7474f6668515eb5852f1273dc5a50b940a550d3f",
|
||||
"dev": true
|
||||
},
|
||||
"eslint-import-resolver-node": {
|
||||
@@ -9643,6 +9666,11 @@
|
||||
"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",
|
||||
@@ -9719,8 +9747,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#2be752fc88ff71e454c6b9178b21a33b59c53f41",
|
||||
"from": "github:jitsi/lib-jitsi-meet#2be752fc88ff71e454c6b9178b21a33b59c53f41",
|
||||
"version": "github:jitsi/lib-jitsi-meet#095a4485f2e6749f5b4fe91da7088aed359ee728",
|
||||
"from": "github:jitsi/lib-jitsi-meet#095a4485f2e6749f5b4fe91da7088aed359ee728",
|
||||
"requires": {
|
||||
"@jitsi/sdp-interop": "0.1.13",
|
||||
"@jitsi/sdp-simulcast": "0.2.1",
|
||||
@@ -9736,6 +9764,10 @@
|
||||
"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",
|
||||
@@ -12712,8 +12744,8 @@
|
||||
"integrity": "sha512-vLNJIedXQZN4p3ChFsAgVHacnJqQMnLl+wBsnZuliRkmsjEHo8kQOA9fnLih/OoiDi1O3eHQvXC5L8f+RYiKgw=="
|
||||
},
|
||||
"react-native-calendar-events": {
|
||||
"version": "github:jitsi/react-native-calendar-events#cad37355f36d17587d84af72b0095e8cc5fd3df9",
|
||||
"from": "github:jitsi/react-native-calendar-events#cad37355f36d17587d84af72b0095e8cc5fd3df9"
|
||||
"version": "github:wmcmahan/react-native-calendar-events#cb2731db6684a49b4343e09de7f9c2fcc68bcd9b",
|
||||
"from": "github:wmcmahan/react-native-calendar-events#cb2731db6684a49b4343e09de7f9c2fcc68bcd9b"
|
||||
},
|
||||
"react-native-callstats": {
|
||||
"version": "3.52.0",
|
||||
@@ -12802,8 +12834,8 @@
|
||||
}
|
||||
},
|
||||
"react-native-webrtc": {
|
||||
"version": "github:jitsi/react-native-webrtc#6b0ea124414f6f5b7f234a7d5cec75d30f5f6312",
|
||||
"from": "github:jitsi/react-native-webrtc#6b0ea124414f6f5b7f234a7d5cec75d30f5f6312",
|
||||
"version": "github:jitsi/react-native-webrtc#bed49210a51cf53081954028589d720381e7cf40",
|
||||
"from": "github:jitsi/react-native-webrtc#bed49210a51cf53081954028589d720381e7cf40",
|
||||
"requires": {
|
||||
"base64-js": "^1.1.2",
|
||||
"event-target-shim": "^1.0.5",
|
||||
@@ -14298,9 +14330,9 @@
|
||||
}
|
||||
},
|
||||
"sdp": {
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.7.4.tgz",
|
||||
"integrity": "sha512-0+wTfgvUUEGcvvFoHIC0aiGbx6gzwAUm8FkKt5Oqqkjf9mEEDLgwnoDKX7MYTGXrNNwzikVbutJ+OVNAGmJBQw=="
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.8.0.tgz",
|
||||
"integrity": "sha512-wRSES07rAwKWAR7aev9UuClT7kdf9ZTdeUK5gTgHue9vlhs19Fbm3ccNEGJO4y2IitH4/JzS4sdzyPl6H2KQLw=="
|
||||
},
|
||||
"sdp-transform": {
|
||||
"version": "2.3.0",
|
||||
|
||||
12
package.json
12
package.json
@@ -34,8 +34,10 @@
|
||||
"@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",
|
||||
@@ -46,8 +48,10 @@
|
||||
"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#2be752fc88ff71e454c6b9178b21a33b59c53f41",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#095a4485f2e6749f5b4fe91da7088aed359ee728",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.4",
|
||||
"moment": "2.19.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
@@ -58,7 +62,7 @@
|
||||
"react-i18next": "4.8.0",
|
||||
"react-native": "0.55.4",
|
||||
"react-native-background-timer": "2.0.0",
|
||||
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#cad37355f36d17587d84af72b0095e8cc5fd3df9",
|
||||
"react-native-calendar-events": "github:wmcmahan/react-native-calendar-events#cb2731db6684a49b4343e09de7f9c2fcc68bcd9b",
|
||||
"react-native-callstats": "3.52.0",
|
||||
"react-native-fast-image": "4.0.14",
|
||||
"react-native-immersive": "1.1.0",
|
||||
@@ -69,7 +73,7 @@
|
||||
"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#6b0ea124414f6f5b7f234a7d5cec75d30f5f6312",
|
||||
"react-native-webrtc": "github:jitsi/react-native-webrtc#bed49210a51cf53081954028589d720381e7cf40",
|
||||
"react-redux": "5.0.7",
|
||||
"redux": "4.0.0",
|
||||
"redux-thunk": "2.2.0",
|
||||
@@ -87,7 +91,7 @@
|
||||
"clean-css": "3.4.25",
|
||||
"css-loader": "0.28.7",
|
||||
"eslint": "4.12.1",
|
||||
"eslint-config-jitsi": "github:jitsi/eslint-config-jitsi#v0.1.0",
|
||||
"eslint-config-jitsi": "github:jitsi/eslint-config-jitsi#7474f6668515eb5852f1273dc5a50b940a550d3f",
|
||||
"eslint-plugin-flowtype": "2.39.1",
|
||||
"eslint-plugin-import": "2.8.0",
|
||||
"eslint-plugin-jsdoc": "3.2.0",
|
||||
|
||||
@@ -124,6 +124,8 @@ 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 {
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
AVATAR_ID_COMMAND,
|
||||
AVATAR_URL_COMMAND,
|
||||
EMAIL_COMMAND,
|
||||
JITSI_CONFERENCE_URL_KEY
|
||||
JITSI_CONFERENCE_URL_KEY,
|
||||
VIDEO_QUALITY_LEVELS
|
||||
} from './constants';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
@@ -102,6 +103,38 @@ 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).
|
||||
|
||||
@@ -96,6 +96,7 @@ const WHITELISTED_KEYS = [
|
||||
'disableRtx',
|
||||
'disableSuspendVideo',
|
||||
'displayJids',
|
||||
'e2eping',
|
||||
'enableDisplayNameInStats',
|
||||
'enableLayerSuspension',
|
||||
'enableLipSync',
|
||||
|
||||
@@ -37,9 +37,6 @@ 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
|
||||
@@ -129,10 +126,8 @@ function _setConfig(state, { config }) {
|
||||
|
||||
const newState = _.merge(
|
||||
{},
|
||||
config, {
|
||||
error: undefined,
|
||||
locationURL: state.locationURL
|
||||
},
|
||||
config,
|
||||
{ error: undefined },
|
||||
|
||||
// The config of _getInitialState() is meant to override the config
|
||||
// downloaded from the Jitsi Meet deployment because the former contains
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
getCurrentConference
|
||||
} from '../conference';
|
||||
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
|
||||
import { parseStandardURIString } from '../util';
|
||||
import { parseURIString } from '../util';
|
||||
|
||||
import {
|
||||
CONNECTION_DISCONNECTED,
|
||||
@@ -276,18 +276,32 @@ function _connectionWillConnect(connection) {
|
||||
* {@code JitsiConnection}.
|
||||
*/
|
||||
function _constructOptions(state) {
|
||||
const defaultOptions = state['features/base/connection'].options;
|
||||
const options = _.merge(
|
||||
{},
|
||||
defaultOptions,
|
||||
// Deep clone the options to make sure we don't modify the object in the
|
||||
// redux store.
|
||||
const options = _.cloneDeep(state['features/base/config']);
|
||||
|
||||
// Lib-jitsi-meet wants the config passed in multiple places and here is
|
||||
// the latest one I have discovered.
|
||||
state['features/base/config'],
|
||||
);
|
||||
// Normalize the BOSH URL.
|
||||
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'];
|
||||
|
||||
@@ -296,16 +310,6 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { SET_ROOM } from '../conference';
|
||||
import { JitsiConnectionErrors } from '../lib-jitsi-meet';
|
||||
import { assign, ReducerRegistry } from '../redux';
|
||||
import { parseURIString } from '../util';
|
||||
import { assign, set, ReducerRegistry } from '../redux';
|
||||
|
||||
import {
|
||||
CONNECTION_DISCONNECTED,
|
||||
@@ -153,50 +152,6 @@ 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}.
|
||||
@@ -223,10 +178,7 @@ function _getCurrentConnection(baseConnectionState: Object): ?Object {
|
||||
function _setLocationURL(
|
||||
state: Object,
|
||||
{ locationURL }: { locationURL: ?URL }) {
|
||||
return assign(state, {
|
||||
locationURL,
|
||||
options: locationURL ? _constructOptions(locationURL) : undefined
|
||||
});
|
||||
return set(state, 'locationURL', locationURL);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -113,7 +113,7 @@ class Dialog extends AbstractDialog<Props, State> {
|
||||
[_TAG_KEY]: _SUBMIT_TEXT_TAG_VALUE
|
||||
};
|
||||
|
||||
let el: ?React$Element<*> = ( // eslint-disable-line no-extra-parens
|
||||
let el: ?React$Element<*> = (
|
||||
<Prompt
|
||||
cancelButtonTextStyle = { cancelButtonTextStyle }
|
||||
cancelText = { t(cancelTitleKey) }
|
||||
|
||||
@@ -212,7 +212,7 @@ class DialogWithTabs extends Component<Props, State> {
|
||||
const { onSubmit, tabs } = this.props;
|
||||
|
||||
tabs.forEach(({ submit }, idx) => {
|
||||
submit(this.state.tabStates[idx]);
|
||||
submit && submit(this.state.tabStates[idx]);
|
||||
});
|
||||
|
||||
onSubmit();
|
||||
|
||||
@@ -14,6 +14,7 @@ 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;
|
||||
|
||||
@@ -126,7 +126,6 @@ function _visitNode(node, callback) {
|
||||
//
|
||||
// Required by:
|
||||
// - jQuery
|
||||
// - lib-jitsi-meet/modules/RTC/adapter.screenshare.js
|
||||
// - Strophe
|
||||
if (typeof global.document === 'undefined') {
|
||||
const document
|
||||
@@ -151,14 +150,6 @@ function _visitNode(node, callback) {
|
||||
document.cookie = '';
|
||||
}
|
||||
|
||||
// document.implementation
|
||||
//
|
||||
// Required by:
|
||||
// - jQuery
|
||||
if (typeof document.implementation === 'undefined') {
|
||||
document.implementation = {};
|
||||
}
|
||||
|
||||
// document.implementation.createHTMLDocument
|
||||
//
|
||||
// Required by:
|
||||
@@ -362,26 +353,9 @@ 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 || '';
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -12,6 +12,11 @@ export type Item = {
|
||||
*/
|
||||
colorBase: string,
|
||||
|
||||
/**
|
||||
* An optional react element to append to the end of the Item.
|
||||
*/
|
||||
elementAfter?: ?ComponentType<any>,
|
||||
|
||||
/**
|
||||
* Item title
|
||||
*/
|
||||
|
||||
@@ -87,7 +87,7 @@ class NavigateSectionList extends Component<Props> {
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
renderListEmptyComponent = this._renderListEmptyComponent,
|
||||
renderListEmptyComponent = this._renderListEmptyComponent(),
|
||||
sections
|
||||
} = this.props;
|
||||
|
||||
@@ -128,11 +128,13 @@ class NavigateSectionList extends Component<Props> {
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onPress(url) {
|
||||
return () => {
|
||||
const { disabled, onPress } = this.props;
|
||||
const { disabled, onPress } = this.props;
|
||||
|
||||
!disabled && url && typeof onPress === 'function' && onPress(url);
|
||||
};
|
||||
if (!disabled && url && typeof onPress === 'function') {
|
||||
return () => onPress(url);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_onRefresh: () => void;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -244,7 +244,7 @@ class MultiSelectAutocomplete extends Component {
|
||||
if (!this.state.error) {
|
||||
return null;
|
||||
}
|
||||
const content = ( // eslint-disable-line no-extra-parens
|
||||
const content = (
|
||||
<div className = 'autocomplete-error'>
|
||||
<InlineDialogFailure
|
||||
onRetry = { this._onRetry } />
|
||||
|
||||
@@ -6,18 +6,22 @@ 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
|
||||
@@ -25,14 +29,16 @@ type Props = {
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class NavigateSectionListItem extends Component<Props> {
|
||||
export default class NavigateSectionListItem<P: Props>
|
||||
extends Component<P> {
|
||||
|
||||
/**
|
||||
* Renders the content of this component.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { lines, title } = this.props.item;
|
||||
const { elementAfter, lines, title } = this.props.item;
|
||||
const { onPress } = this.props;
|
||||
|
||||
/**
|
||||
@@ -52,22 +58,28 @@ export default class NavigateSectionListItem extends Component<Props> {
|
||||
duration = lines[1];
|
||||
}
|
||||
|
||||
const rootClassName = `navigate-section-list-tile ${
|
||||
onPress ? 'with-click-handler' : 'without-click-handler'}`;
|
||||
|
||||
return (
|
||||
<Container
|
||||
className = 'navigate-section-list-tile'
|
||||
className = { rootClassName }
|
||||
onClick = { onPress }>
|
||||
<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 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 }
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ 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.
|
||||
@@ -49,6 +54,7 @@ export default class SectionList extends Component<Props> {
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
ListEmptyComponent,
|
||||
renderSectionHeader,
|
||||
renderItem,
|
||||
sections,
|
||||
@@ -56,34 +62,34 @@ export default class SectionList extends Component<Props> {
|
||||
} = this.props;
|
||||
|
||||
/**
|
||||
* If there are no recent items we dont want to display anything
|
||||
* If there are no recent items we don't want to display anything
|
||||
*/
|
||||
if (sections) {
|
||||
return (
|
||||
/* eslint-disable no-extra-parens */
|
||||
<Container
|
||||
className = 'navigate-section-list'>
|
||||
{
|
||||
sections.map((section, sectionIndex) => (
|
||||
<Container
|
||||
key = { sectionIndex }>
|
||||
{ renderSectionHeader(section) }
|
||||
{ section.data
|
||||
.map((item, listIndex) => {
|
||||
const listItem = {
|
||||
item
|
||||
};
|
||||
sections.length === 0
|
||||
? ListEmptyComponent
|
||||
: 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 */
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ class Watermarks extends Component<*, *> {
|
||||
let reactElement = null;
|
||||
|
||||
if (this.state.showBrandWatermark) {
|
||||
reactElement = ( // eslint-disable-line no-extra-parens
|
||||
reactElement = (
|
||||
<div
|
||||
className = 'watermark rightwatermark'
|
||||
style = { _RIGHT_WATERMARK_STYLE } />
|
||||
@@ -114,7 +114,7 @@ class Watermarks extends Component<*, *> {
|
||||
const { brandWatermarkLink } = this.state;
|
||||
|
||||
if (brandWatermarkLink) {
|
||||
reactElement = ( // eslint-disable-line no-extra-parens
|
||||
reactElement = (
|
||||
<a
|
||||
href = { brandWatermarkLink }
|
||||
target = '_new'>
|
||||
@@ -144,7 +144,7 @@ class Watermarks extends Component<*, *> {
|
||||
const { jitsiWatermarkLink } = this.state;
|
||||
|
||||
if (jitsiWatermarkLink) {
|
||||
reactElement = ( // eslint-disable-line no-extra-parens
|
||||
reactElement = (
|
||||
<a
|
||||
href = { jitsiWatermarkLink }
|
||||
target = '_new'>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* {
|
||||
* type: SET_SESSION,
|
||||
* session: {
|
||||
* url: {string},
|
||||
* state: {string},
|
||||
* ...data
|
||||
* }
|
||||
* }
|
||||
* @public
|
||||
*/
|
||||
export const SET_SESSION = Symbol('SET_SESSION');
|
||||
@@ -1,16 +0,0 @@
|
||||
import { SET_SESSION } from './actionTypes';
|
||||
|
||||
/**
|
||||
* FIXME.
|
||||
*
|
||||
* @param {string} session - FIXME.
|
||||
* @returns {{
|
||||
* type: SET_SESSION
|
||||
* }}
|
||||
*/
|
||||
export function setSession(session) {
|
||||
return {
|
||||
type: SET_SESSION,
|
||||
session
|
||||
};
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
|
||||
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');
|
||||
@@ -1,36 +0,0 @@
|
||||
// @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));
|
||||
}
|
||||
@@ -1,349 +0,0 @@
|
||||
// @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;
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// @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;
|
||||
}
|
||||
@@ -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 = ( // eslint-disable-line no-extra-parens
|
||||
children = (
|
||||
<View style = { style }>
|
||||
{ children }
|
||||
<Text style = { styles && styles.labelStyle }>
|
||||
|
||||
@@ -35,7 +35,6 @@ 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() }
|
||||
@@ -47,7 +46,6 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
|
||||
);
|
||||
|
||||
if (useTooltip) {
|
||||
// eslint-disable-next-line no-extra-parens
|
||||
children = (
|
||||
<Tooltip
|
||||
content = { this.tooltip }
|
||||
|
||||
@@ -300,7 +300,15 @@ export function parseStandardURIString(str: string) {
|
||||
* references a Jitsi Meet resource (location).
|
||||
* @public
|
||||
* @returns {{
|
||||
* room: (string|undefined)
|
||||
* contextRoot: string,
|
||||
* hash: string,
|
||||
* host: string,
|
||||
* hostname: string,
|
||||
* pathname: string,
|
||||
* port: string,
|
||||
* protocol: string,
|
||||
* room: (string|undefined),
|
||||
* search: string
|
||||
* }}
|
||||
*/
|
||||
export function parseURIString(uri: ?string) {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
// @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.
|
||||
*
|
||||
@@ -32,3 +42,48 @@ 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');
|
||||
|
||||
@@ -1,10 +1,89 @@
|
||||
// @flow
|
||||
|
||||
import { loadGoogleAPI } from '../google-api';
|
||||
|
||||
import {
|
||||
CLEAR_CALENDAR_INTEGRATION,
|
||||
REFRESH_CALENDAR,
|
||||
SET_CALENDAR_AUTH_STATE,
|
||||
SET_CALENDAR_AUTHORIZATION,
|
||||
SET_CALENDAR_EVENTS
|
||||
SET_CALENDAR_EVENTS,
|
||||
SET_CALENDAR_INTEGRATION,
|
||||
SET_CALENDAR_PROFILE_EMAIL,
|
||||
SET_LOADING_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).
|
||||
@@ -28,6 +107,23 @@ 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}.
|
||||
@@ -61,3 +157,155 @@ 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));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// @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;
|
||||
@@ -0,0 +1,89 @@
|
||||
// @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));
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
// @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 { CALENDAR_ENABLED } from '../constants';
|
||||
import styles from './styles';
|
||||
|
||||
import { isCalendarEnabled } from '../functions';
|
||||
|
||||
import AddMeetingUrlButton from './AddMeetingUrlButton';
|
||||
|
||||
/**
|
||||
* The tyoe of the React {@code Component} props of {@link MeetingList}.
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link BaseCalendarList}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The current state of the calendar access permission.
|
||||
*/
|
||||
_authorization: ?string,
|
||||
|
||||
/**
|
||||
* The calendar event list.
|
||||
*/
|
||||
@@ -38,6 +34,11 @@ type Props = {
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
renderListEmptyComponent: Function,
|
||||
|
||||
/**
|
||||
* The translate function.
|
||||
*/
|
||||
@@ -45,9 +46,9 @@ type Props = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Component to display a list of events from the (mobile) user's calendar.
|
||||
* Component to display a list of events from a connected calendar.
|
||||
*/
|
||||
class MeetingList extends Component<Props> {
|
||||
class BaseCalendarList extends Component<Props> {
|
||||
/**
|
||||
* Default values for the component's props.
|
||||
*/
|
||||
@@ -75,7 +76,7 @@ class MeetingList extends Component<Props> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new {@code MeetingList} instance.
|
||||
* Initializes a new {@code BaseCalendarList} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
@@ -83,13 +84,12 @@ class MeetingList 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 MeetingList extends Component<Props> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { disabled } = this.props;
|
||||
const { disabled, renderListEmptyComponent } = this.props;
|
||||
|
||||
return (
|
||||
<NavigateSectionList
|
||||
@@ -106,46 +106,11 @@ class MeetingList extends Component<Props> {
|
||||
onPress = { this._onPress }
|
||||
onRefresh = { this._onRefresh }
|
||||
renderListEmptyComponent
|
||||
= { this._getRenderListEmptyComponent() }
|
||||
= { renderListEmptyComponent }
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -174,7 +139,7 @@ class MeetingList extends Component<Props> {
|
||||
_toDateString: Object => string;
|
||||
|
||||
/**
|
||||
* Generates a date (interval) string for a given event.
|
||||
* Generates a date string for a given event.
|
||||
*
|
||||
* @param {Object} event - The event.
|
||||
* @private
|
||||
@@ -182,11 +147,9 @@ class MeetingList extends Component<Props> {
|
||||
*/
|
||||
_toDateString(event) {
|
||||
const startDateTime
|
||||
= getLocalizedDateFormatter(event.startDate).format('lll');
|
||||
const endTime
|
||||
= getLocalizedDateFormatter(event.endDate).format('LT');
|
||||
= getLocalizedDateFormatter(event.startDate).format('MMM Do, YYYY');
|
||||
|
||||
return `${startDateTime} - ${endTime}`;
|
||||
return `${startDateTime}`;
|
||||
}
|
||||
|
||||
_toDisplayableItem: Object => Object;
|
||||
@@ -200,10 +163,15 @@ class MeetingList 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._toDateString(event)
|
||||
this._toTimeString(event)
|
||||
],
|
||||
title: event.title,
|
||||
url: event.url
|
||||
@@ -221,39 +189,60 @@ class MeetingList extends Component<Props> {
|
||||
_toDisplayableList() {
|
||||
const { _eventList, t } = this.props;
|
||||
|
||||
const now = Date.now();
|
||||
const now = new Date();
|
||||
|
||||
const { createSection } = NavigateSectionList;
|
||||
const nowSection = createSection(t('calendarSync.now'), 'now');
|
||||
const nextSection = createSection(t('calendarSync.next'), 'next');
|
||||
const laterSection = createSection(t('calendarSync.later'), 'later');
|
||||
const TODAY_SECTION = 'today';
|
||||
const sectionMap = new Map();
|
||||
|
||||
for (const event of _eventList) {
|
||||
const displayableEvent = this._toDisplayableItem(event);
|
||||
const startDate = new Date(event.startDate).getDate();
|
||||
|
||||
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);
|
||||
if (startDate === now.getDate()) {
|
||||
let todaySection = sectionMap.get(TODAY_SECTION);
|
||||
|
||||
if (!todaySection) {
|
||||
todaySection
|
||||
= createSection(t('calendarSync.today'), TODAY_SECTION);
|
||||
sectionMap.set(TODAY_SECTION, todaySection);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const sectionList = [];
|
||||
return Array.from(sectionMap.values());
|
||||
}
|
||||
|
||||
for (const section of [
|
||||
nowSection,
|
||||
nextSection,
|
||||
laterSection
|
||||
]) {
|
||||
section.data.length && sectionList.push(section);
|
||||
}
|
||||
_toTimeString: Object => string;
|
||||
|
||||
return sectionList;
|
||||
/**
|
||||
* 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}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,19 +251,15 @@ class MeetingList 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 {
|
||||
_authorization: authorization,
|
||||
_eventList: events
|
||||
_eventList: state['features/calendar-sync'].events
|
||||
};
|
||||
}
|
||||
|
||||
export default CALENDAR_ENABLED
|
||||
? translate(connect(_mapStateToProps)(MeetingList))
|
||||
export default isCalendarEnabled()
|
||||
? translate(connect(_mapStateToProps)(BaseCalendarList))
|
||||
: undefined;
|
||||
126
react/features/calendar-sync/components/CalendarList.native.js
Normal file
126
react/features/calendar-sync/components/CalendarList.native.js
Normal file
@@ -0,0 +1,126 @@
|
||||
// @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;
|
||||
194
react/features/calendar-sync/components/CalendarList.web.js
Normal file
194
react/features/calendar-sync/components/CalendarList.web.js
Normal file
@@ -0,0 +1,194 @@
|
||||
// @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;
|
||||
@@ -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 { CALENDAR_ENABLED } from '../constants';
|
||||
import { isCalendarEnabled } from '../functions';
|
||||
import styles from './styles';
|
||||
|
||||
const ALERT_MILLISECONDS = 5 * 60 * 1000;
|
||||
@@ -293,6 +293,6 @@ function _mapStateToProps(state: Object) {
|
||||
};
|
||||
}
|
||||
|
||||
export default CALENDAR_ENABLED
|
||||
export default isCalendarEnabled()
|
||||
? translate(connect(_mapStateToProps)(ConferenceNotification))
|
||||
: undefined;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// @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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as ConferenceNotification } from './ConferenceNotification';
|
||||
export { default as MeetingList } from './MeetingList';
|
||||
export { default as CalendarList } from './CalendarList';
|
||||
export { default as MicrosoftSignInButton } from './MicrosoftSignInButton';
|
||||
|
||||
@@ -4,7 +4,7 @@ const NOTIFICATION_SIZE = 55;
|
||||
|
||||
/**
|
||||
* The styles of the React {@code Component}s of the feature meeting-list i.e.
|
||||
* {@code MeetingList}.
|
||||
* {@code CalendarList}.
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
|
||||
|
||||
@@ -1,37 +1,26 @@
|
||||
// @flow
|
||||
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the calendar feature is enabled by the
|
||||
* app.
|
||||
* An enumeration of support calendar integration types.
|
||||
*
|
||||
* @type {boolean}
|
||||
* @enum {string}
|
||||
*/
|
||||
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: []
|
||||
export const CALENDAR_TYPE = {
|
||||
GOOGLE: 'google',
|
||||
MICROSOFT: 'microsoft'
|
||||
};
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
* The number of days to fetch.
|
||||
*/
|
||||
function _isCalendarEnabled() {
|
||||
const { calendarEnabled } = NativeModules.AppInfo;
|
||||
export const FETCH_END_DAYS = 10;
|
||||
|
||||
return typeof calendarEnabled === 'undefined' ? true : calendarEnabled;
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
|
||||
171
react/features/calendar-sync/functions.any.js
Normal file
171
react/features/calendar-sync/functions.any.js
Normal file
@@ -0,0 +1,171 @@
|
||||
// @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;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// @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;
|
||||
}
|
||||
99
react/features/calendar-sync/functions.native.js
Normal file
99
react/features/calendar-sync/functions.native.js
Normal file
@@ -0,0 +1,99 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
97
react/features/calendar-sync/functions.web.js
Normal file
97
react/features/calendar-sync/functions.web.js
Normal file
@@ -0,0 +1,97 @@
|
||||
// @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;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
export * from './actions';
|
||||
export * from './components';
|
||||
export * from './functions';
|
||||
export * from './constants';
|
||||
export { isCalendarEnabled } from './functions';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
|
||||
@@ -1,36 +1,15 @@
|
||||
// @flow
|
||||
|
||||
import md5 from 'js-md5';
|
||||
import RNCalendarEvents from 'react-native-calendar-events';
|
||||
|
||||
import { APP_WILL_MOUNT } from '../base/app';
|
||||
import { SET_CONFIG } from '../base/config';
|
||||
import { ADD_KNOWN_DOMAINS, addKnownDomains } from '../base/known-domains';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { APP_LINK_SCHEME, parseURIString } from '../base/util';
|
||||
import { APP_STATE_CHANGED } from '../mobile/background';
|
||||
import { equals, MiddlewareRegistry } from '../base/redux';
|
||||
import { APP_STATE_CHANGED } from '../mobile/background/actionTypes';
|
||||
|
||||
import { setCalendarAuthorization, setCalendarEvents } from './actions';
|
||||
import { setCalendarAuthorization } from './actions';
|
||||
import { REFRESH_CALENDAR } from './actionTypes';
|
||||
import { CALENDAR_ENABLED } from './constants';
|
||||
import { _fetchCalendarEntries, isCalendarEnabled } from './functions';
|
||||
|
||||
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
|
||||
isCalendarEnabled()
|
||||
&& MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case ADD_KNOWN_DOMAINS: {
|
||||
@@ -41,7 +20,8 @@ CALENDAR_ENABLED
|
||||
const result = next(action);
|
||||
const newValue = getState()['features/base/known-domains'];
|
||||
|
||||
oldValue === newValue || _fetchCalendarEntries(store, false, false);
|
||||
equals(oldValue, newValue)
|
||||
|| _fetchCalendarEntries(store, false, false);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -54,7 +34,9 @@ CALENDAR_ENABLED
|
||||
return result;
|
||||
}
|
||||
|
||||
case APP_WILL_MOUNT: {
|
||||
case SET_CONFIG: {
|
||||
const result = next(action);
|
||||
|
||||
// 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'];
|
||||
@@ -69,7 +51,7 @@ CALENDAR_ENABLED
|
||||
|
||||
_fetchCalendarEntries(store, false, false);
|
||||
|
||||
return next(action);
|
||||
return result;
|
||||
}
|
||||
|
||||
case REFRESH_CALENDAR: {
|
||||
@@ -85,121 +67,6 @@ CALENDAR_ENABLED
|
||||
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
|
||||
@@ -215,111 +82,3 @@ 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)));
|
||||
}
|
||||
|
||||
@@ -5,21 +5,37 @@ 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_EVENTS,
|
||||
SET_CALENDAR_INTEGRATION,
|
||||
SET_CALENDAR_PROFILE_EMAIL,
|
||||
SET_LOADING_CALENDAR_EVENTS
|
||||
} from './actionTypes';
|
||||
import { CALENDAR_ENABLED, DEFAULT_STATE } from './constants';
|
||||
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
|
||||
};
|
||||
|
||||
/**
|
||||
* Constant for the Redux subtree of the calendar feature.
|
||||
*
|
||||
* 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.
|
||||
* 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.
|
||||
*/
|
||||
const STORE_NAME = 'features/calendar-sync';
|
||||
|
||||
@@ -31,12 +47,14 @@ const STORE_NAME = 'features/calendar-sync';
|
||||
* runtime value to see if we need to re-request the calendar permission from
|
||||
* the user.
|
||||
*/
|
||||
CALENDAR_ENABLED
|
||||
isCalendarEnabled()
|
||||
&& PersistenceRegistry.register(STORE_NAME, {
|
||||
knownDomains: true
|
||||
integrationType: true,
|
||||
knownDomains: true,
|
||||
msAuthState: true
|
||||
});
|
||||
|
||||
CALENDAR_ENABLED
|
||||
isCalendarEnabled()
|
||||
&& ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
@@ -49,11 +67,39 @@ CALENDAR_ENABLED
|
||||
}
|
||||
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;
|
||||
|
||||
78
react/features/calendar-sync/web/googleCalendar.js
Normal file
78
react/features/calendar-sync/web/googleCalendar.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/* @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
|
||||
};
|
||||
593
react/features/calendar-sync/web/microsoftCalendar.js
Normal file
593
react/features/calendar-sync/web/microsoftCalendar.js
Normal file
@@ -0,0 +1,593 @@
|
||||
/* @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;
|
||||
}
|
||||
@@ -4,6 +4,8 @@ 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';
|
||||
@@ -13,6 +15,12 @@ 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,
|
||||
@@ -49,9 +57,10 @@ const FULL_SCREEN_EVENTS = [
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
const LAYOUT_CLASSES = {
|
||||
HORIZONTAL_FILMSTRIP: 'horizontal-filmstrip',
|
||||
VERTICAL_FILMSTRIP: 'vertical-filmstrip'
|
||||
const LAYOUT_CLASSNAMES = {
|
||||
[LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW]: 'horizontal-filmstrip',
|
||||
[LAYOUTS.TILE_VIEW]: 'tile-view',
|
||||
[LAYOUTS.VERTICAL_FILMSTRIP_VIEW]: 'vertical-filmstrip'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -68,13 +77,18 @@ type Props = {
|
||||
* The CSS class to apply to the root of {@link Conference} to modify the
|
||||
* application layout.
|
||||
*/
|
||||
_layoutModeClassName: string,
|
||||
_layoutClassName: string,
|
||||
|
||||
/**
|
||||
* Conference room name.
|
||||
*/
|
||||
_room: string,
|
||||
|
||||
/**
|
||||
* Whether or not the current UI layout should be in tile view.
|
||||
*/
|
||||
_shouldDisplayTileView: boolean,
|
||||
|
||||
dispatch: Function,
|
||||
t: Function
|
||||
}
|
||||
@@ -143,6 +157,25 @@ 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.
|
||||
@@ -180,7 +213,7 @@ class Conference extends Component<Props> {
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { this.props._layoutModeClassName }
|
||||
className = { this.props._layoutClassName }
|
||||
id = 'videoconference_page'
|
||||
onMouseMove = { this._onShowToolbar }>
|
||||
<Notice />
|
||||
@@ -257,29 +290,19 @@ class Conference extends Component<Props> {
|
||||
* @private
|
||||
* @returns {{
|
||||
* _iAmRecorder: boolean,
|
||||
* _room: ?string
|
||||
* _layoutClassName: string,
|
||||
* _room: ?string,
|
||||
* _shouldDisplayTileView: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { room } = state['features/base/conference'];
|
||||
const { iAmRecorder } = state['features/base/config'];
|
||||
const currentLayout = getCurrentLayout(state);
|
||||
|
||||
return {
|
||||
/**
|
||||
* 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
|
||||
_iAmRecorder: state['features/base/config'].iAmRecorder,
|
||||
_layoutClassName: LAYOUT_CLASSNAMES[currentLayout],
|
||||
_room: state['features/base/conference'].room,
|
||||
_shouldDisplayTileView: shouldDisplayTileView(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -324,6 +324,7 @@ 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 };
|
||||
@@ -337,7 +338,6 @@ class ConnectionIndicator extends Component {
|
||||
stats: newStats
|
||||
});
|
||||
|
||||
// Rely on React to batch setState actions.
|
||||
this._updateIndicatorAutoHide(newStats.percent);
|
||||
}
|
||||
|
||||
@@ -410,8 +410,10 @@ class ConnectionIndicator extends Component {
|
||||
const {
|
||||
bandwidth,
|
||||
bitrate,
|
||||
e2eRtt,
|
||||
framerate,
|
||||
packetLoss,
|
||||
region,
|
||||
resolution,
|
||||
transport
|
||||
} = this.state.stats;
|
||||
@@ -421,10 +423,12 @@ 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 } />
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import { JitsiConnectionQualityEvents } from '../base/lib-jitsi-meet';
|
||||
import {
|
||||
JitsiConnectionQualityEvents,
|
||||
JitsiE2ePingEvents
|
||||
} from '../base/lib-jitsi-meet';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
@@ -33,6 +36,17 @@ 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);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,7 +39,12 @@ class ConnectionStatsTable extends Component {
|
||||
connectionSummary: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Statistics related to framerates for each ssrc.
|
||||
* The end-to-end round-trip-time.
|
||||
*/
|
||||
e2eRtt: PropTypes.number,
|
||||
|
||||
/**
|
||||
* Statistics related to frame rates for each ssrc.
|
||||
* {{
|
||||
* [ ssrc ]: Number
|
||||
* }}
|
||||
@@ -47,7 +52,7 @@ class ConnectionStatsTable extends Component {
|
||||
framerate: PropTypes.object,
|
||||
|
||||
/**
|
||||
* Whether or not the statitics are for local video.
|
||||
* Whether or not the statistics are for local video.
|
||||
*/
|
||||
isLocalVideo: PropTypes.bool,
|
||||
|
||||
@@ -65,6 +70,11 @@ class ConnectionStatsTable extends Component {
|
||||
*/
|
||||
packetLoss: PropTypes.object,
|
||||
|
||||
/**
|
||||
* The region.
|
||||
*/
|
||||
region: PropTypes.string,
|
||||
|
||||
/**
|
||||
* Statistics related to display resolutions for each ssrc.
|
||||
* {{
|
||||
@@ -208,6 +218,31 @@ 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.
|
||||
@@ -245,7 +280,6 @@ class ConnectionStatsTable extends Component {
|
||||
if (packetLoss) {
|
||||
const { download, upload } = packetLoss;
|
||||
|
||||
// eslint-disable-next-line no-extra-parens
|
||||
packetLossTableData = (
|
||||
<td>
|
||||
<span className = 'connection-info__download'>
|
||||
@@ -330,12 +364,15 @@ 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>
|
||||
@@ -354,7 +391,6 @@ 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>
|
||||
|
||||
@@ -61,18 +61,16 @@ class DesktopPickerPane extends Component {
|
||||
const classNames
|
||||
= `desktop-picker-pane default-scrollbar source-type-${type}`;
|
||||
const previews
|
||||
= sources ? sources.map(
|
||||
source =>
|
||||
|
||||
// eslint-disable-next-line react/jsx-wrap-multilines
|
||||
= sources
|
||||
? sources.map(source => (
|
||||
<DesktopSourcePreview
|
||||
key = { source.id }
|
||||
onClick = { onClick }
|
||||
onDoubleClick = { onDoubleClick }
|
||||
selected = { source.id === selectedSourceId }
|
||||
source = { source }
|
||||
type = { type } />)
|
||||
: ( // eslint-disable-line no-extra-parens
|
||||
type = { type } />))
|
||||
: (
|
||||
<div className = 'desktop-picker-pane-spinner'>
|
||||
<Spinner
|
||||
isCompleting = { false }
|
||||
|
||||
11
react/features/dropbox/actionTypes.js
Normal file
11
react/features/dropbox/actionTypes.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// @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');
|
||||
77
react/features/dropbox/actions.js
Normal file
77
react/features/dropbox/actions.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// @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
|
||||
};
|
||||
}
|
||||
39
react/features/dropbox/functions.js
Normal file
39
react/features/dropbox/functions.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// @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;
|
||||
});
|
||||
}
|
||||
4
react/features/dropbox/index.js
Normal file
4
react/features/dropbox/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './actions';
|
||||
export * from './functions';
|
||||
|
||||
import './reducer';
|
||||
28
react/features/dropbox/reducer.js
Normal file
28
react/features/dropbox/reducer.js
Normal file
@@ -0,0 +1,28 @@
|
||||
// @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;
|
||||
}
|
||||
});
|
||||
@@ -121,17 +121,15 @@ 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
|
||||
|
||||
@@ -8,6 +8,7 @@ import { dockToolbox } from '../../../toolbox';
|
||||
|
||||
import { setFilmstripHovered } from '../../actions';
|
||||
import { shouldRemoteVideosBeVisible } from '../../functions';
|
||||
|
||||
import Toolbar from './Toolbar';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
@@ -185,9 +186,8 @@ function _mapStateToProps(state) {
|
||||
&& state['features/toolbox'].visible
|
||||
&& interfaceConfig.TOOLBAR_BUTTONS.length;
|
||||
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
|
||||
|
||||
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
|
||||
reduceHeight ? 'reduce-height' : ''}`;
|
||||
reduceHeight ? 'reduce-height' : ''}`.trim();
|
||||
|
||||
return {
|
||||
_className: className,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* @flow */
|
||||
import { getShareInfoText } from '../invite';
|
||||
|
||||
import {
|
||||
SET_GOOGLE_API_PROFILE,
|
||||
@@ -7,6 +8,21 @@ 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.
|
||||
*
|
||||
@@ -14,9 +30,16 @@ import googleApi from './googleApi';
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function loadGoogleAPI(clientId: string) {
|
||||
return (dispatch: Dispatch<*>) =>
|
||||
return (dispatch: Dispatch<*>, getState: Function) =>
|
||||
googleApi.get()
|
||||
.then(() => googleApi.initializeClient(clientId))
|
||||
.then(() => {
|
||||
if (getState()['features/google-api'].googleAPIState
|
||||
=== GOOGLE_API_STATES.NEEDS_LOADING) {
|
||||
return googleApi.initializeClient(clientId);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
})
|
||||
.then(() => dispatch({
|
||||
type: SET_GOOGLE_API_STATE,
|
||||
googleAPIState: GOOGLE_API_STATES.LOADED }))
|
||||
@@ -30,39 +53,6 @@ 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.
|
||||
@@ -137,3 +127,82 @@ 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));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link GoogleSignInButton}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
// The callback to invoke when {@code GoogleSignInButton} is clicked.
|
||||
onClick: Function,
|
||||
|
||||
// The text to display within {@code GoogleSignInButton}.
|
||||
text: string
|
||||
};
|
||||
|
||||
/**
|
||||
* A React Component showing a button to sign in with Google.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class GoogleSignInButton extends Component {
|
||||
/**
|
||||
* {@code GoogleSignInButton} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The callback to invoke when the button is clicked.
|
||||
*/
|
||||
onClick: PropTypes.func,
|
||||
|
||||
/**
|
||||
* The text to display in the button.
|
||||
*/
|
||||
text: PropTypes.string
|
||||
};
|
||||
|
||||
export default class GoogleSignInButton extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
1
react/features/google-api/components/index.js
Normal file
1
react/features/google-api/components/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default as GoogleSignInButton } from './GoogleSignInButton';
|
||||
@@ -1,14 +1,23 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* The Google API scopes to request access to for streaming.
|
||||
* The Google API scopes to request access for streaming and calendar.
|
||||
*
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
export const GOOGLE_API_SCOPES = [
|
||||
'https://www.googleapis.com/auth/youtube.readonly'
|
||||
'https://www.googleapis.com/auth/youtube.readonly',
|
||||
'https://www.googleapis.com/auth/calendar'
|
||||
];
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GOOGLE_API_SCOPES } from './constants';
|
||||
import { GOOGLE_API_SCOPES, DISCOVERY_DOCS } from './constants';
|
||||
|
||||
const GOOGLE_API_CLIENT_LIBRARY_URL = 'https://apis.google.com/js/api.js';
|
||||
|
||||
@@ -67,6 +67,7 @@ const googleApi = {
|
||||
setTimeout(() => {
|
||||
api.client.init({
|
||||
clientId,
|
||||
discoveryDocs: DISCOVERY_DOCS,
|
||||
scope: GOOGLE_API_SCOPES.join(' ')
|
||||
})
|
||||
.then(resolve)
|
||||
@@ -86,6 +87,7 @@ const googleApi = {
|
||||
.then(api => Boolean(api
|
||||
&& api.auth2
|
||||
&& api.auth2.getAuthInstance
|
||||
&& api.auth2.getAuthInstance()
|
||||
&& api.auth2.getAuthInstance().isSignedIn
|
||||
&& api.auth2.getAuthInstance().isSignedIn.get()));
|
||||
},
|
||||
@@ -183,6 +185,165 @@ 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.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export { GOOGLE_API_STATES } from './constants';
|
||||
export * from './googleApi';
|
||||
export { default as googleApi } from './googleApi';
|
||||
export * from './actions';
|
||||
export * from './components';
|
||||
|
||||
import './reducer';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user