Compare commits

..

35 Commits

Author SHA1 Message Date
virtuacoplenny
7c08116dc2 ref(toolbar): add accessibility labels for torture tests (#2685)
* ref(toolbar): add accessibility labels for torture tests

* squash: update propTypes
2018-04-03 12:32:00 -07:00
Leonard Kim
f8717a7135 fix(gum): add electron string for requesting gum permissions
Electron generally can bypass having to get permission for
audio and video. In the case it doesn't have it, and the
permission screen is displayed, a string should still display
prompting the user to click allow. Right now the string id
displays.
2018-04-02 16:13:57 -05:00
Leonard Kim
1b85442dba chore(deps): update lib-jitsi-meet to 6282e7a8
This update is explicitly for FF screenshare and
gum retry fixes.
2018-04-02 16:13:35 -05:00
George Politis
ea46cbc479 feat: enableTcc, enableRemb, minParticipants. 2018-04-02 15:37:53 -05:00
Leonard Kim
b76ab305e3 fix(welcome-page): prevent form submit to prevent page refresh 2018-03-30 15:16:22 -05:00
Leonard Kim
358ce0799e fix(toolbar): the conference obj is needed to submit feedback 2018-03-30 14:45:27 -05:00
Leonard Kim
e7223c49ef fix(feedback): let star label color be inherited from atlaskit 2018-03-30 14:23:01 -05:00
Leonard Kim
02a31746fb fix(toolbar): do not use toggle class for recording 2018-03-30 13:07:38 -05:00
Leonard Kim
40154b1feb fix(toolbar): tweak chat backgrounds to be darker 2018-03-29 13:38:42 -05:00
Leonard Kim
4c49e3bec0 fix(toolbar): use old toolbar logic for showing screenshare
I don't understand the old showDesktopSharingButton action
but I've tried my best to copy it over. There is an existing
issue where the keyboard shortcut gets registered when it
probably shouldn't because screensharing is disabled. It will
be fixed soon with refactoring of the entire logic determining
whether or not to show the screensharing button.
2018-03-29 13:38:42 -05:00
Leonard Kim
0a086fa3f7 fix(toolbar): do not show invite button if features not available
This is instead of showing the button with a tooltip about it
being disabled.
2018-03-29 13:38:42 -05:00
virtuacoplenny
b353b8fffb chore(deps): update lib-jitsi-meet to ef0e14b (#2679) 2018-03-29 13:42:42 +02:00
Zoltan Bettenbuk
a3c00021de [RN] Fix showing user avatar in sidebar 2018-03-29 11:47:32 +02:00
virtuacoplenny
1e0a3ceb74 fix(chat): polyfills for lib-jitsi-meet ChatRoom#onPresence (#2678)
The onPresence parsing was refactored to remove use of jQuery.
This exposed three methods not available in react-native:
ParentNode.children, ChildNode.remove, and
document.querySelectorAll. The querySelectorAll change could
be swapped for the already polyfilled querySelector, but
children and remove had to be added. The polyfills are based
on those supplied by MDN web docs, but modified to pass jitsi
linting.
2018-03-28 18:04:42 -07:00
Boris Grozev
8492aad7d6 npm: Updates lib-jitsi-meet to cab2fabd56e9591148997c78a82da433ecf28dec. 2018-03-28 16:08:54 -05:00
Leonard Kim
7ad9fa8392 fix(toolbar): exercise old video muting flow
The redux flows for video muting may not cover all cases
that the conference.js flows cover. Just exercise the old
flows to be safe.
2018-03-28 13:52:04 -05:00
Leonard Kim
6916252ce1 fix(toolbar): exercise conference audio toggling
The redux flows do not account for as many scenarios, such
as config.startWithAudioMuted being true.
2018-03-28 13:52:04 -05:00
Leonard Kim
b4eae56eed fix(toolbar): css hacks to raise notifications 2018-03-28 13:52:04 -05:00
Saúl Ibarra Corretgé
eb69fb69cb feat(conference): lower the redirect timeout after feedback submission (#2673) 2018-03-28 07:35:26 -07:00
virtuacoplenny
2b7cdbc6a8 ref(toolbar): use new toolbar by default (#2666) 2018-03-27 19:39:42 -05:00
jitsi-pootle
f3a90f048a New files added from translate.jitsi.org based on templates 2018-03-27 23:38:51 +00:00
virtuacoplenny
8bf69d30b7 fix(toolbar): make darker for better text contrast (#2667)
* fix(toolbar): make darker for better text contrast

* squash: borrow some atlaskit colors
2018-03-27 15:30:04 -07:00
Saúl Ibarra Corretgé
45078fe6b2 [RN] Don't auto-correct any field in settings 2018-03-27 12:58:56 -07:00
Saúl Ibarra Corretgé
4783b22018 [RN] Don't auto-capitalize email field in settings 2018-03-27 12:58:56 -07:00
Leonard Kim
d93782af8a feat(new-toolbars): initial implementation 2018-03-27 00:54:30 -05:00
Shuai Li
962df14382 [iOS] Fix launching the app after the introduction of Swift in sdk 2018-03-26 23:14:45 -05:00
virtuacoplenny
01db70fd3d Merge pull request #2636 from zbettenbuk/calendar-permission-fix
Reorganize calendar access request flow
2018-03-26 07:57:24 -07:00
Дамян Минков
6cc8800016 Update poltergeist's presence with identity information. (#2650) 2018-03-23 13:58:05 -07:00
virtuacoplenny
e5596c3cd5 fix(recording): let the google app for api calls be overridable (#2653) 2018-03-23 10:52:32 -07:00
virtuacoplenny
1b91e0bc2f improve invite error handling (#2649)
* fix(invite): do not send empty queries for people search

The endpoint might return an error if an empty query is sent.

* fix(invite): add error logging for failed people directory requests

The error currently being passed through from $.getJSON ended up
being an empty string plus was not getting logged. So switch to
fetch to move along with jquery killing and log the error.

* fix(dial-in): add error logging for failed requests

* ref(invite): create a fetch helper to remove duplicate logic
2018-03-23 09:37:04 -07:00
zbettenbuk
b258e0d397 Reorganize calendar access request flow 2018-03-23 07:53:36 +01:00
virtuacoplenny
83f47c2df1 feat(invite): add basic analytics for AddPeople dialog (#2641)
* feat(invite): add basic analytics for AddPeople dialog

Analytics for opening the dialog, closing the dialog, the
count of invites sent, and the count of invites errored.

* squash: fix typo, change default count init, remove extra analytics param
2018-03-22 17:53:16 -07:00
virtuacoplenny
a39da15c94 ref(invite): remove unused nuclear-js dep (#2642) 2018-03-22 16:22:26 -07:00
Daniel Ornelas
fd787abf85 Change deployment target of JitsiMeet iOS SDK to 10.0 2018-03-22 16:40:46 -04:00
Daniel Ornelas
7822155e5e Fix iPad rotation related issue when in PiP 2018-03-22 16:40:17 -04:00
155 changed files with 6738 additions and 1398 deletions

View File

@@ -43,7 +43,8 @@ import {
lockStateChanged,
onStartMutedPolicyChanged,
p2pStatusChanged,
sendLocalParticipant
sendLocalParticipant,
setDesktopSharingEnabled
} from './react/features/base/conference';
import { updateDeviceList } from './react/features/base/devices';
import {
@@ -104,6 +105,7 @@ import {
mediaPermissionPromptVisibilityChanged,
suspendDetected
} from './react/features/overlay';
import { setSharedVideoStatus } from './react/features/shared-video';
import {
isButtonEnabled,
showDesktopSharingButton
@@ -234,13 +236,14 @@ function maybeRedirectToWelcomePage(options) {
}));
}
// if Welcome page is enabled redirect to welcome page after 3 sec.
// if Welcome page is enabled redirect to welcome page after 3 sec, if
// there is a thank you message to be shown, 0.5s otherwise.
if (config.enableWelcomePage) {
setTimeout(
() => {
APP.store.dispatch(redirectWithStoredParams('/'));
},
3000);
options.showThankYou ? 3000 : 500);
}
}
@@ -505,16 +508,6 @@ export default {
*/
desktopSharingDisabledTooltip: null,
/*
* Whether the local "raisedHand" flag is on.
*/
isHandRaised: false,
/*
* Whether the local participant is the dominant speaker in the conference.
*/
isDominantSpeaker: false,
/**
* The local audio track (if any).
* FIXME tracks from redux store should be the single source of truth
@@ -773,6 +766,8 @@ export default {
JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
this.isDesktopSharingEnabled);
APP.store.dispatch(
setDesktopSharingEnabled(this.isDesktopSharingEnabled));
APP.store.dispatch(showDesktopSharingButton());
this._createRoom(tracks);
@@ -1896,19 +1891,6 @@ export default {
});
room.on(JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED, id => {
APP.store.dispatch(dominantSpeakerChanged(id));
if (this.isLocalId(id)) {
this.isDominantSpeaker = true;
this.setRaisedHand(false);
} else {
this.isDominantSpeaker = false;
const participant = room.getParticipantById(id);
if (participant) {
APP.UI.setRaisedHandStatus(participant, false);
}
}
APP.UI.markDominantSpeaker(id);
});
if (!interfaceConfig.filmStripOnly) {
@@ -2022,7 +2004,10 @@ export default {
(participant, name, oldValue, newValue) => {
switch (name) {
case 'raisedHand':
APP.UI.setRaisedHandStatus(participant, newValue);
APP.store.dispatch(participantUpdated({
id: participant.getId(),
raisedHand: newValue === 'true'
}));
break;
case 'remoteControlSessionStatus':
APP.UI.setRemoteControlActiveStatus(
@@ -2361,6 +2346,8 @@ export default {
}
});
}
APP.store.dispatch(setSharedVideoStatus(state));
});
room.addCommandListener(
this.commands.defaults.SHARED_VIDEO,
@@ -2623,30 +2610,6 @@ export default {
APP.API.notifyVideoAvailabilityChanged(available);
},
/**
* Toggles the local "raised hand" status.
*/
maybeToggleRaisedHand() {
this.setRaisedHand(!this.isHandRaised);
},
/**
* Sets the local "raised hand" status to a particular value.
*/
setRaisedHand(raisedHand) {
if (raisedHand !== this.isHandRaised) {
APP.UI.onLocalRaiseHandChanged(raisedHand);
this.isHandRaised = raisedHand;
// Advertise the updated status
room.setLocalParticipantProperty('raisedHand', raisedHand);
// Update the view
APP.UI.setLocalRaisedHandStatus(raisedHand);
}
},
/**
* Disconnect from the conference and optionally request user feedback.
* @param {boolean} [requestFeedback=false] if user feedback should be

View File

@@ -178,6 +178,23 @@ var config = {
// Disables or enables RTX (RFC 4588) (defaults to false).
// disableRtx: false,
// Disables or enables TCC (the default is in Jicofo and set to true)
// (draft-holmer-rmcat-transport-wide-cc-extensions-01). This setting
// affects congestion control, it practically enables send-side bandwidth
// estimations.
// enableTcc: true,
// Disables or enables REMB (the default is in Jicofo and set to false)
// (draft-alvestrand-rmcat-remb-03). This setting affects congestion
// control, it practically enables recv-side bandwidth estimations. When
// both TCC and REMB are enabled, TCC takes precedence. When both are
// disabled, then bandwidth estimations are disabled.
// enableRemb: false,
// Defines the minimum number of participants to start a call (the default
// is set in Jicofo and set to 2).
// minParticipants: 2,
// Use XEP-0215 to fetch STUN and TURN servers.
// useStunTurn: true,

View File

@@ -62,6 +62,36 @@
.localuser {
color: #087dba;
}
.use-new-toolbox {
.chatmessage {
color: white;
}
.localuser {
color: #4C9AFF;
}
.remoteuser {
color: #B8C7E0;
}
#usermsg {
color: white;
}
.chatmessage,
#smileysarea,
#smileysContainer,
#usermsg {
background-color: $newToolbarBackgroundColor;
}
.smileyContainer:hover {
background-color: $newToolbarButtonToggleColor;
border-radius: 5px;
cursor: pointer;
}
}
.errorMessage {
color: red;

View File

@@ -5,6 +5,20 @@
justify-content: flex-start;
}
.use-new-toolbox {
.filmstrip.reduce-height {
bottom: $newToolbarSize;
}
.filmstrip {
transition: bottom .3s;
}
.filmstrip__videos.hidden {
bottom: calc(-196px - #{$newToolbarSize});
}
}
.filmstrip {
position: absolute;
bottom: 0;

View File

@@ -180,3 +180,6 @@
.icon-gsm-bars:before {
content: "\e926";
}
.icon-open_in_new:before {
content: "\e89e";
}

View File

@@ -1,6 +1,37 @@
/**
* Toolbar side panel main container element.
*/
.use-new-toolbox #sideToolbarContainer {
background-color: $newToolbarBackgroundColor;
/**
* Make the sidebar flush with the top of the toolbar. Take the size of
* the toolbar, plus its padding, and subtract from 100%.
*/
height: calc(100% - #{$newToolbarSize} - 10px);
left: 0;
.side-toolbar-close {
background: gray;
border: 3px solid rgba(255, 255, 255, 0.1);
border-radius: 100%;
color: white;
cursor:pointer;
height: 10px;
line-height: 10px;
padding: 4px;
position: absolute;
right: 5px;
text-align: center;
top: 5px;
width: 10px;
z-index: 1;
}
#chatconversation {
top: 15px;
}
}
#sideToolbarContainer {
background-color: $sideToolbarContainerBg;
height: 100%;

View File

@@ -19,6 +19,17 @@
vertical-align: middle;
}
.use-new-toolbox {
.cxGWJB{
bottom: calc(#{$newToolbarSize} + 15px);
}
.gXSEsl:nth-child(n+2) {
transform: translateX(0) translateY(100%) translateY(calc(#{$newToolbarSize} + 25px));
-ms-transform: translateX(0) translateY(100%) translateY(calc(#{$newToolbarSize} + 25px));
-webkit-transform: translateX(0) translateY(100%) translateY(calc(#{$newToolbarSize} + 25px));
}
}
.toolbar-container {
display: block;
left:0;
@@ -261,6 +272,217 @@
}
}
/**
* TODO: when the old filmstrip has been removed, remove the "new-" prefix.
*/
.new-toolbox {
background-color: $newToolbarBackgroundColor;
bottom: calc((#{$newToolbarSize} * 2) * -1);
box-sizing: border-box;
display: flex;
justify-content: space-between;
padding: 5px 20px;
position: absolute;
transition: bottom .3s ease-in;
width: 100%;
z-index: $toolbarZ;
&.visible {
bottom: 0;
}
&.no-buttons {
display: none;
}
.button-group-center,
.button-group-left,
.button-group-right {
display: flex;
width: 33%;
}
.button-group-center {
justify-content: center;
}
.button-group-right {
justify-content: flex-end;
}
/**
* Overwrite font-awesome styling to match jitsi-icon styling.
*/
.fa {
font-size: 1.22em;
}
i {
border-radius: 5px;
cursor: pointer;
display: block;
height: 100%;
line-height: inherit;
width: 100%;
}
i:hover {
background-color: $newToolbarButtonHoverColor;
}
i.toggled {
background: $newToolbarButtonToggleColor;
}
i.toggled:hover {
background-color: $newToolbarButtonHoverColor;
}
i.disabled {
cursor: initial
}
i.disabled:hover {
background-color: initial;
}
.icon-hangup {
color: $hangupColor;
}
.overflow-menu {
font-size: 1.2em;
list-style-type: none;
/**
* Undo atlaskit padding by reducing margins.
*/
margin: -15px -24px;
padding: 0;
.overflow-menu-item {
align-items: center;
cursor: pointer;
display: flex;
padding: 5px 10px;
&:hover {
background: rgba(0255, 255, 255, 0.2);
}
&.unclickable {
cursor: default;
}
&.unclickable:hover {
background: inherit;
}
}
.overflow-menu-item-icon {
margin-right: 10px;
i {
display: inline;
}
i:hover {
background-color: initial;
}
img {
max-width: 18px;
max-height: 18px;
}
}
.profile-text {
max-width: 150px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
.toolbox-button {
color: $toolbarButtonColor;
cursor: pointer;
display: inline-block;
font-size: $newToolbarFontSize;
line-height: $newToolbarSize;
margin: 0 10px;
text-align: center;
}
.toolbar-button-with-badge {
position: relative;
.badge-round {
bottom: 9px;
position: absolute;
right: 9px;
}
}
.toolbox-button-wth-dialog {
display: inline-block;
}
.toolbox-icon {
height: $newToolbarSize;
width: $newToolbarSize;
}
}
.filmstrip-toolbox {
background-color: $newToolbarBackgroundColor;
box-sizing: border-box;
display: flex;
flex-direction: column;
z-index: $toolbarZ;
i {
cursor: pointer;
display: block;
font-size: $newToolbarFontSize;
height: 37px;
line-height: 37px;
width: 37px;
}
i:hover {
background-color: $newToolbarButtonHoverColor;
}
i.toggled {
background: $newToolbarButtonToggleColor;
}
i.toggled:hover {
background-color: $newToolbarButtonHoverColor;
}
.icon-hangup {
color: $hangupColor;
}
.toolbox-button {
color: $toolbarButtonColor;
cursor: pointer;
text-align: center;
}
border-radius: 3px;
.toolbox-button:first-child i {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.toolbox-button:last-child i {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
}
.filmstrip-only {
.toolbox,
.toolbox-toolbars {

View File

@@ -36,6 +36,11 @@ $alwaysOnTopToolbarFontSize: 1em;
$alwaysOnTopToolbarSize: 30px;
$defaultToolbarSize: 50px;
$defaultFilmStripOnlyToolbarSize: 37px;
$newToolbarBackgroundColor: rgba(22, 38, 55, 0.8);
$newToolbarButtonHoverColor: rgba(14, 20, 35, 0.6);
$newToolbarButtonToggleColor: rgba(14, 20, 35, 1);
$newToolbarFontSize: 1.9em;
$newToolbarSize: 50px;
$secToolbarFontSize: 1.9em;
$secToolbarLineHeight: 45px;
$toolbarAvatarPadding: 10px;

View File

@@ -19,6 +19,20 @@
text-align: left;
}
&.use-new-toolbox {
/**
* Adjust the height of the filmstrip as the toolbar is displayed.
*/
.filmstrip {
top: 0;
transition: height .3s ease-in;
&.reduce-height {
height: calc(100% - #{$newToolbarSize});
}
}
}
.filmstrip {
align-items: flex-end;
box-sizing: border-box;
@@ -32,14 +46,7 @@
* any parent is also fixed.
*/
position: fixed;
/**
* z-index adjusting is needed because the video state indicator has to
* display over the filmstrip when no videos are displayed but still be
* clickable but its inline dialogs must display over the video state
* indicator when videos are displayed.
*/
z-index: #{$tooltipsZ + 1};
z-index: $filmstripVideosZ;
/**
* Hide videos by making them slight to the right.

View File

@@ -77,7 +77,6 @@
text-align: center;
.star-label {
color: $rateStarLabelColor;
font-size: 14px;
height: 16px;
}

View File

@@ -135,6 +135,12 @@
}
}
.modal-dialog-form {
.video-quality-dialog-title {
display: none;
}
}
.video-state-indicator {
background: $videoStateIndicatorBackground;
cursor: default;
@@ -162,11 +168,11 @@
}
.centeredVideoLabel.moveToCorner {
z-index: $tooltipsZ;
z-index: $zindex3;
}
#videoResolutionLabel {
z-index: #{$tooltipsZ + 1};
z-index: $zindex3 + 1;
}
.centeredVideoLabel {

Binary file not shown.

View File

@@ -23,6 +23,7 @@
<glyph unicode="&#xe616;" glyph-name="event_note" d="M598 426v-84h-300v84h300zM810 214v468h-596v-468h596zM810 896c46 0 86-40 86-86v-596c0-46-40-86-86-86h-596c-48 0-86 40-86 86v596c0 46 38 86 86 86h42v86h86v-86h340v86h86v-86h42zM726 598v-86h-428v86h428z" />
<glyph unicode="&#xe61d;" glyph-name="phone-talk" d="M640 512c0 70-58 128-128 128v86c118 0 214-96 214-214h-86zM810 512c0 166-132 298-298 298v86c212 0 384-172 384-384h-86zM854 362c24 0 42-18 42-42v-150c0-24-18-42-42-42-400 0-726 326-726 726 0 24 18 42 42 42h150c24 0 42-18 42-42 0-54 8-104 24-152 4-14 2-32-10-44l-94-94c62-122 162-220 282-282l94 94c12 12 30 14 44 10 48-16 98-24 152-24z" />
<glyph unicode="&#xe80b;" glyph-name="public" d="M764 282c56 60 90 142 90 230 0 142-88 266-214 316v-18c0-46-40-84-86-84h-84v-86c0-24-20-42-44-42h-84v-86h256c24 0 42-18 42-42v-128h42c38 0 70-26 82-60zM470 174v82c-46 0-86 40-86 86v42l-204 204c-6-24-10-50-10-76 0-174 132-318 300-338zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
<glyph unicode="&#xe89e;" glyph-name="open_in_new" d="M598 896h298v-298h-86v152l-418-418-60 60 418 418h-152v86zM810 214v298h86v-298c0-46-40-86-86-86h-596c-48 0-86 40-86 86v596c0 46 38 86 86 86h298v-86h-298v-596h596z" />
<glyph unicode="&#xe8b3;" glyph-name="restore" d="M512 682h64v-180l150-90-32-52-182 110v212zM554 896c212 0 384-172 384-384s-172-384-384-384c-106 0-200 42-270 112l60 62c54-54 128-88 210-88 166 0 300 132 300 298s-134 298-300 298-298-132-298-298h128l-172-172-4 6-166 166h128c0 212 172 384 384 384z" />
<glyph unicode="&#xe901;" glyph-name="avatar" d="M512 204c106 0 200 56 256 138-2 84-172 132-256 132-86 0-254-48-256-132 56-82 150-138 256-138zM512 810c-70 0-128-58-128-128s58-128 128-128 128 58 128 128-58 128-128 128zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
<glyph unicode="&#xe902;" glyph-name="download" d="M726 470h-128v170h-172v-170h-128l214-214zM826 596c110-8 198-100 198-212 0-118-96-214-214-214h-554c-142 0-256 114-256 256 0 132 100 240 228 254 54 102 160 174 284 174 156 0 284-110 314-258z" />

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,10 @@ var interfaceConfig = {
'microphone', 'camera', 'desktop', 'fullscreen', 'fodeviceselection', 'hangup',
// extended toolbar
'profile', 'contacts', 'info', 'chat', 'recording', 'etherpad', 'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip' ],
'profile', 'contacts', 'info', 'chat', 'recording', 'etherpad',
'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip',
'invite', 'feedback', 'stats', 'shortcuts'
],
/**
* Main Toolbar Buttons
@@ -150,7 +153,19 @@ var interfaceConfig = {
*
* @type {boolean}
*/
VIDEO_QUALITY_LABEL_DISABLED: false
VIDEO_QUALITY_LABEL_DISABLED: false,
/**
* This is a temporary feature flag used to gate access to the toolbox so it
* can be developed through smaller changesets and set to false if bad bugs
* are found. This feature flag will be removed at some point, as well as
* the old toolbox. This new toolbox will be horizontal and the previous
* feature of supporting menu button ordering through interfaceConfig will
* be removed. Support for configuring which buttons display will remain.
*
* @type {boolean}
*/
_USE_NEW_TOOLBOX: true
/**
* Specify custom URL for downloading android mobile app.

View File

@@ -255,6 +255,7 @@
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDebug;
CODE_SIGN_ENTITLEMENTS = app.entitlements;
CURRENT_PROJECT_VERSION = 1;
@@ -286,6 +287,7 @@
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconRelease;
CODE_SIGN_ENTITLEMENTS = app.entitlements;
CURRENT_PROJECT_VERSION = 1;

View File

@@ -32,8 +32,8 @@ class ViewController: UIViewController {
@IBAction func openJitsiMeet(sender: Any?) {
let jitsiMeetCoordinator = JitsiMeetPresentationCoordinator()
self.jitsiMeetCoordinator = jitsiMeetCoordinator
jitsiMeetCoordinator.jitsiMeetView().welcomePageEnabled = true
jitsiMeetCoordinator.jitsiMeetView().load(nil)
jitsiMeetCoordinator.jitsiMeetView.welcomePageEnabled = true
jitsiMeetCoordinator.jitsiMeetView.load(nil)
jitsiMeetCoordinator.show()
}
}

View File

@@ -405,7 +405,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -458,7 +458,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";

View File

@@ -20,8 +20,20 @@ import Foundation
/// an external window that can be resized and dragged with custom PiP mode
open class JitsiMeetPresentationCoordinator: NSObject {
fileprivate let meetViewController: JitsiMeetViewController
fileprivate let meetWindow: PiPWindow
public let meetViewController: JitsiMeetViewController
public let meetWindow: PiPWindow
public var isInPiP: Bool {
get {
return meetWindow.isInPiP
}
}
public var jitsiMeetView: JitsiMeetView {
get {
return meetViewController.jitsiMeetView
}
}
public init(meetViewController: JitsiMeetViewController? = nil,
meetWindow: PiPWindow? = nil) {
@@ -34,14 +46,12 @@ open class JitsiMeetPresentationCoordinator: NSObject {
configureMeetViewController()
}
public func jitsiMeetView() -> JitsiMeetView {
return meetViewController.jitsiMeetView
}
/// Show window with jitsi meet and perform a completion closure
open func show(completion: CompletionAction? = nil) {
meetWindow.show(completion: completion)
}
/// Hide window with jitsi meet and perform a completion closure
open func hide(completion: CompletionAction? = nil) {
meetWindow.hide(completion: completion)
}
@@ -77,7 +87,7 @@ extension JitsiMeetPresentationCoordinator: JitsiMeetViewControllerDelegate {
switch to {
case .enterPictureInPicture:
meetWindow.enterPictureInPicture()
case .traitChange:
case .sizeChange:
// resize to full screen if rotation happens
if meetWindow.isInPiP {
meetWindow.exitPictureInPicture()

View File

@@ -19,8 +19,8 @@ public enum JitsiMeetPresentationUpdate {
/// The conference wants to enter Picture-in-Picture
case enterPictureInPicture
/// A system traitCollectionChange (usually screen rotation)
case traitChange
/// A screen size change (usually screen rotation)
case sizeChange
}
public protocol JitsiMeetViewControllerDelegate: class {
@@ -59,9 +59,10 @@ open class JitsiMeetViewController: UIViewController {
jitsiMeetView.delegate = self
}
open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
delegate?.performPresentationUpdate(to: .traitChange)
open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
delegate?.performPresentationUpdate(to: .sizeChange)
}
}

View File

@@ -32,7 +32,17 @@ open class PiPWindow: UIWindow {
}
/// The size ratio for root view controller view when in PiP mode
public var pipSizeRatio: CGFloat = 0.333
public var pipSizeRatio: CGFloat = {
let deviceIdiom = UIScreen.main.traitCollection.userInterfaceIdiom
switch (deviceIdiom) {
case .pad:
return 0.25
case .phone:
return 0.33
default:
return 0.25
}
}()
/// The PiP state of this contents of the window
private(set) var isInPiP: Bool = false

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

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

512
lang/main-ko.json Normal file
View File

@@ -0,0 +1,512 @@
{
"contactlist": "",
"contactlist_plural_undefined": "",
"passwordSetRemotely": "",
"poweredby": "",
"inviteUrlDefaultMsg": "",
"me": "",
"speaker": "",
"raisedHand": "",
"defaultNickname": "",
"defaultLink": "",
"audioDevices": {
"bluetooth": "",
"headphones": "",
"phone": "",
"speaker": ""
},
"audioOnly": {
"audioOnly": "",
"featureToggleDisabled": ""
},
"userMedia": {
"react-nativeGrantPermissions": "",
"chromeGrantPermissions": "",
"androidGrantPermissions": "",
"firefoxGrantPermissions": "",
"operaGrantPermissions": "",
"iexplorerGrantPermissions": "",
"safariGrantPermissions": "",
"nwjsGrantPermissions": "",
"edgeGrantPermissions": ""
},
"keyboardShortcuts": {
"keyboardShortcuts": "",
"raiseHand": "",
"pushToTalk": "",
"toggleScreensharing": "",
"toggleFilmstrip": "",
"toggleShortcuts": "",
"focusLocal": "",
"focusRemote": "",
"toggleChat": "",
"mute": "",
"fullScreen": "",
"videoMute": "",
"showSpeakerStats": ""
},
"welcomepage": {
"appDescription": "",
"audioVideoSwitch": {
"audio": "",
"video": ""
},
"calendar": "",
"go": "",
"join": "",
"privacy": "",
"roomname": "",
"roomnameHint": "",
"sendFeedback": "",
"terms": "",
"title": ""
},
"startupoverlay": {
"policyText": "",
"title": ""
},
"suspendedoverlay": {
"title": "",
"text": "",
"rejoinKeyTitle": ""
},
"toolbar": {
"addPeople": "",
"audioonly": "",
"mute": "",
"videomute": "",
"authenticate": "",
"lock": "",
"chat": "",
"etherpad": "",
"sharedvideo": "",
"sharescreen": "",
"fullscreen": "",
"sip": "",
"Settings": "",
"hangup": "",
"login": "",
"logout": "",
"dialpad": "",
"sharedVideoMutedPopup": "",
"micMutedPopup": "",
"talkWhileMutedPopup": "",
"unableToUnmutePopup": "",
"cameraDisabled": "",
"micDisabled": "",
"filmstrip": "",
"profile": "",
"raiseHand": ""
},
"unsupportedBrowser": {
"appNotInstalled": "",
"downloadApp": "",
"openApp": ""
},
"bottomtoolbar": {
"chat": "",
"filmstrip": "",
"contactlist": ""
},
"chat": {
"nickname": {
"title": "",
"popover": ""
},
"messagebox": ""
},
"settings": {
"title": "",
"update": "",
"name": "",
"startAudioMuted": "",
"startVideoMuted": "",
"selectCamera": "",
"selectMic": "",
"selectAudioOutput": "",
"followMe": "",
"noDevice": "",
"cameraAndMic": "",
"moderator": "",
"password": "",
"audioVideo": ""
},
"profile": {
"title": "",
"setDisplayNameLabel": "",
"setEmailLabel": "",
"setEmailInput": ""
},
"videothumbnail": {
"moderator": "",
"videomute": "",
"mute": "",
"kick": "",
"muted": "",
"domute": "",
"flip": "",
"remoteControl": ""
},
"connectionindicator": {
"header": "",
"bitrate": "",
"packetloss": "",
"resolution": "",
"framerate": "",
"less": "",
"more": "",
"address": "",
"remoteport": "",
"remoteport_plural_undefined": "",
"localport": "",
"localport_plural_undefined": "",
"localaddress": "",
"localaddress_plural_undefined": "",
"remoteaddress": "",
"remoteaddress_plural_undefined": "",
"transport": "",
"transport_plural_undefined": "",
"bandwidth": "",
"na": "",
"turn": "",
"quality": {
"good": "",
"inactive": "",
"lost": "",
"nonoptimal": "",
"poor": ""
},
"status": ""
},
"notify": {
"disconnected": "",
"moderator": "",
"connectedOneMember": "",
"connectedTwoMembers": "",
"connectedThreePlusMembers": "",
"somebody": "",
"me": "",
"focus": "",
"focusFail": "",
"grantedTo": "",
"grantedToUnknown": "",
"muted": "",
"mutedTitle": "",
"raisedHand": "",
"suboptimalExperienceTitle": "",
"suboptimalExperienceDescription": ""
},
"dialog": {
"allow": "",
"kickMessage": "",
"popupErrorTitle": "",
"popupError": "",
"passwordErrorTitle": "",
"passwordError": "",
"passwordError2": "",
"connectError": "",
"connectErrorWithMsg": "",
"incorrectPassword": "",
"connecting": "",
"copy": "",
"contactSupport": "",
"error": "",
"detectext": "",
"failedpermissions": "",
"conferenceReloadTitle": "",
"conferenceReloadMsg": "",
"conferenceDisconnectTitle": "",
"conferenceDisconnectMsg": "",
"dismiss": "",
"rejoinNow": "",
"maxUsersLimitReachedTitle": "",
"maxUsersLimitReached": "",
"lockTitle": "",
"lockMessage": "",
"warning": "",
"passwordNotSupportedTitle": "",
"passwordNotSupported": "",
"internalErrorTitle": "",
"internalError": "",
"unableToSwitch": "",
"SLDFailure": "",
"SRDFailure": "",
"oops": "",
"currentPassword": "",
"passwordLabel": "",
"defaultError": "",
"passwordRequired": "",
"Ok": "",
"done": "",
"Remove": "",
"removePassword": "",
"shareVideoTitle": "",
"shareVideoLinkError": "",
"removeSharedVideoTitle": "",
"removeSharedVideoMsg": "",
"alreadySharedVideoMsg": "",
"alreadySharedVideoTitle": "",
"WaitingForHost": "",
"WaitForHostMsg": "",
"IamHost": "",
"Cancel": "",
"Submit": "",
"retry": "",
"logoutTitle": "",
"logoutQuestion": "",
"sessTerminated": "",
"hungUp": "",
"joinAgain": "",
"Share": "",
"Save": "",
"recording": "",
"recordingToken": "",
"Back": "",
"serviceUnavailable": "",
"gracefulShutdown": "",
"Yes": "",
"reservationError": "",
"reservationErrorMsg": "",
"password": "",
"userPassword": "",
"token": "",
"tokenAuthFailedTitle": "",
"tokenAuthFailed": "",
"displayNameRequired": "",
"enterDisplayName": "",
"feedbackHelp": "",
"feedbackQuestion": "",
"thankYou": "",
"sorryFeedback": "",
"liveStreaming": "",
"streamKey": "",
"startLiveStreaming": "",
"stopStreamingWarning": "",
"stopRecordingWarning": "",
"stopLiveStreaming": "",
"stopRecording": "",
"doNotShowMessageAgain": "",
"permissionDenied": "",
"screenSharingFailedToInstall": "",
"screenSharingFailedToInstallTitle": "",
"screenSharingFirefoxPermissionDeniedError": "",
"screenSharingFirefoxPermissionDeniedTitle": "",
"screenSharingPermissionDeniedError": "",
"cameraUnsupportedResolutionError": "",
"cameraUnknownError": "",
"cameraPermissionDeniedError": "",
"cameraNotFoundError": "",
"cameraConstraintFailedError": "",
"micUnknownError": "",
"micPermissionDeniedError": "",
"micNotFoundError": "",
"micConstraintFailedError": "",
"micNotSendingDataTitle": "",
"micNotSendingData": "",
"cameraNotSendingDataTitle": "",
"cameraNotSendingData": "",
"goToStore": "",
"externalInstallationTitle": "",
"externalInstallationMsg": "",
"inlineInstallationMsg": "",
"inlineInstallExtension": "",
"muteParticipantTitle": "",
"muteParticipantBody": "",
"muteParticipantButton": "",
"remoteControlTitle": "",
"remoteControlRequestMessage": "",
"remoteControlShareScreenWarning": "",
"remoteControlDeniedMessage": "",
"remoteControlAllowedMessage": "",
"remoteControlErrorMessage": "",
"startRemoteControlErrorMessage": "",
"remoteControlStopMessage": "",
"close": "",
"shareYourScreen": "",
"yourEntireScreen": "",
"applicationWindow": ""
},
"email": {
"sharedKey": "",
"subject": "",
"body": "",
"and": ""
},
"connection": {
"ERROR": "",
"CONNECTING": "",
"RECONNECTING": "",
"CONNFAIL": "",
"AUTHENTICATING": "",
"AUTHFAIL": "",
"CONNECTED": "",
"DISCONNECTED": "",
"DISCONNECTING": "",
"ATTACHED": ""
},
"recording": {
"busy": "",
"busyTitle": "",
"buttonTooltip": "",
"error": "",
"failedToStart": "",
"off": "",
"on": "",
"pending": "",
"serviceName": "",
"unavailable": "",
"unavailableTitle": ""
},
"liveStreaming": {
"busy": "",
"busyTitle": "",
"buttonTooltip": "",
"changeSignIn": "",
"choose": "",
"chooseCTA": "",
"enterStreamKey": "",
"error": "",
"errorAPI": "",
"failedToStart": "",
"off": "",
"on": "",
"pending": "",
"serviceName": "",
"signIn": "",
"signInCTA": "",
"start": "",
"streamIdHelp": "",
"unavailableTitle": ""
},
"videoSIPGW": {
"busy": "",
"busyTitle": "",
"errorInvite": "",
"errorInviteTitle": "",
"errorAlreadyInvited": "",
"errorInviteFailedTitle": "",
"errorInviteFailed": "",
"pending": "",
"serviceName": "",
"unavailableTitle": ""
},
"speakerStats": {
"hours": "",
"minutes": "",
"name": "",
"seconds": "",
"speakerStats": "",
"speakerTime": ""
},
"deviceSelection": {
"deviceSettings": "",
"noPermission": "",
"previewUnavailable": "",
"selectADevice": "",
"testAudio": ""
},
"videoStatus": {
"callQuality": "",
"hd": "",
"highDefinition": "",
"labelTooltipVideo": "",
"labelTooltipAudioOnly": "",
"ld": "",
"lowDefinition": "",
"onlyAudioAvailable": "",
"onlyAudioSupported": "",
"p2pEnabled": "",
"p2pVideoQualityDescription": "",
"recHighDefinitionOnly": "",
"sd": "",
"standardDefinition": "",
"qualityButtonTip": ""
},
"dialOut": {
"statusMessage": ""
},
"addPeople": {
"add": "",
"countryNotSupported": "",
"countryReminder": "",
"disabled": "",
"invite": "",
"loading": "",
"loadingNumber": "",
"loadingPeople": "",
"noResults": "",
"noValidNumbers": "",
"searchNumbers": "",
"searchPeople": "",
"searchPeopleAndNumbers": "",
"telephone": "",
"title": "",
"failedToAdd": ""
},
"inlineDialogFailure": {
"msg": "",
"retry": "",
"support": "",
"supportMsg": ""
},
"deviceError": {
"cameraError": "",
"microphoneError": "",
"cameraPermission": "",
"microphonePermission": ""
},
"feedback": {
"average": "",
"bad": "",
"good": "",
"rateExperience": "",
"veryBad": "",
"veryGood": ""
},
"info": {
"addPassword": "",
"cancelPassword": "",
"conferenceURL": "",
"country": "",
"dialANumber": "",
"dialInNumber": "",
"dialInConferenceID": "",
"dialInNotSupported": "",
"genericError": "",
"invitePhone": "",
"invitePhoneAlternatives": "",
"inviteURL": "",
"moreNumbers": "",
"noNumbers": "",
"noPassword": "",
"noRoom": "",
"numbers": "",
"password": "",
"title": "",
"tooltip": ""
},
"settingsView": {
"alertOk": "",
"alertTitle": "",
"alertURLText": "",
"conferenceSection": "",
"displayName": "",
"email": "",
"header": "",
"profileSection": "",
"serverURL": "",
"startWithAudioMuted": "",
"startWithVideoMuted": ""
},
"calendarSync": {
"later": "",
"next": "",
"nextMeeting": "",
"now": ""
},
"recentList": {
"today": "",
"yesterday": "",
"earlier": ""
}
}

View File

@@ -23,6 +23,7 @@
"react-nativeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"androidGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"electronGrantPermissions": "Please grant permissions to use your camera and microphone",
"firefoxGrantPermissions": "Select <b><i>Share Selected Device</i></b> when your browser asks for permissions.",
"operaGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"iexplorerGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions.",
@@ -73,14 +74,22 @@
"toolbar": {
"addPeople": "Add people to your call",
"audioonly": "Enable / Disable audio only mode (saves bandwidth)",
"callQuality": "Manage call quality",
"enterFullScreen": "View full screen",
"exitFullScreen": "Exit full screen",
"feedback": "Leave feedback",
"moreActions": "More actions",
"mute": "Mute / Unmute",
"videomute": "Start / Stop camera",
"authenticate": "Authenticate",
"lock": "Lock / Unlock room",
"chat": "Open / Close chat",
"etherpad": "Open / Close shared document",
"documentOpen": "Open shared document",
"documentClose": "Close shared document",
"sharedvideo": "Share a YouTube video",
"sharescreen": "Start / Stop screen sharing",
"sharescreen": "Screen share",
"stopSharedVideo": "Stop YouTube video",
"fullscreen": "View / Exit full screen",
"sip": "Call SIP number",
"Settings": "Settings",
@@ -96,7 +105,9 @@
"micDisabled": "Microphone is not available",
"filmstrip": "Show / Hide videos",
"profile": "Edit your profile",
"raiseHand": "Raise / Lower your hand"
"raiseHand": "Raise / Lower your hand",
"shortcuts": "View shortcuts",
"speakerStats": "Speaker stats"
},
"unsupportedBrowser": {
"appNotInstalled": "Join this meeting with __app__ on your phone.",
@@ -285,6 +296,7 @@
"liveStreaming": "Live Streaming",
"streamKey": "Live stream key",
"startLiveStreaming": "Go live now",
"startRecording": "Start recording",
"stopStreamingWarning": "Are you sure you would like to stop the live streaming?",
"stopRecordingWarning": "Are you sure you would like to stop the recording?",
"stopLiveStreaming": "Stop live streaming",
@@ -473,6 +485,7 @@
"loadingPeople": "Searching for people to invite",
"noResults": "No matching search results",
"noValidNumbers": "Please enter a phone number",
"notAvailable": "You can't invite people.",
"searchNumbers": "Enter a phone number to invite",
"searchPeople": "Enter a name to invite",
"searchPeopleAndNumbers": "Enter a name or phone number to invite",
@@ -539,11 +552,16 @@
"later": "Later",
"next": "Upcoming",
"nextMeeting": "next meeting",
"now": "Now"
"now": "Now",
"permissionButton": "Open settings",
"permissionMessage": "Calendar permission is required to list your meetings in the app."
},
"recentList": {
"today": "Today",
"yesterday": "Yesterday",
"earlier": "Earlier"
},
"sectionList": {
"pullToRefresh": "Pull to refresh"
}
}

View File

@@ -30,6 +30,7 @@ import {
} from '../../react/features/base/participants';
import { destroyLocalTracks } from '../../react/features/base/tracks';
import { openDisplayNamePrompt } from '../../react/features/display-name';
import { setEtherpadHasInitialzied } from '../../react/features/etherpad';
import {
setNotificationsEnabled,
showWarningNotification
@@ -100,9 +101,6 @@ const UIListeners = new Map([
], [
UIEvents.SHARED_VIDEO_CLICKED,
() => sharedVideoManager && sharedVideoManager.toggleSharedVideo()
], [
UIEvents.TOGGLE_FULLSCREEN,
() => UI.toggleFullScreen()
], [
UIEvents.TOGGLE_CHAT,
() => UI.toggleChat()
@@ -135,14 +133,6 @@ const UIListeners = new Map([
]
]);
/**
* Toggles the application in and out of full screen mode
* (a.k.a. presentation mode in Chrome).
*/
UI.toggleFullScreen = function() {
UIUtil.isFullScreen() ? UIUtil.exitFullScreen() : UIUtil.enterFullScreen();
};
/**
* Indicates if we're currently in full screen mode.
*
@@ -255,12 +245,20 @@ UI.showLocalConnectionInterrupted = function(isInterrupted) {
/**
* Sets the "raised hand" status for a participant.
*
* @param {string} id - The id of the participant whose raised hand UI should
* be updated.
* @param {string} name - The name of the participant with the raised hand
* update.
* @param {boolean} raisedHandStatus - Whether the participant's hand is raised
* or not.
* @returns {void}
*/
UI.setRaisedHandStatus = (participant, raisedHandStatus) => {
VideoLayout.setRaisedHandStatus(participant.getId(), raisedHandStatus);
UI.setRaisedHandStatus = (id, name, raisedHandStatus) => {
VideoLayout.setRaisedHandStatus(id, raisedHandStatus);
if (raisedHandStatus) {
messageHandler.participantNotification(
participant.getDisplayName(),
name,
'notify.somebody',
'connected',
'notify.raisedHand');
@@ -374,6 +372,14 @@ UI.start = function() {
$('body').addClass('vertical-filmstrip');
}
// TODO: remove this class once the old toolbar has been removed. This class
// is set so that any CSS changes needed to adjust elements outside of the
// new toolbar can be scoped to just the app with the new toolbar enabled.
if (interfaceConfig._USE_NEW_TOOLBOX && !interfaceConfig.filmStripOnly) {
$('body').addClass('use-new-toolbox');
}
document.title = interfaceConfig.APP_NAME;
};
@@ -404,12 +410,7 @@ UI.bindEvents = () => {
// Resize and reposition videos in full screen mode.
$(document).on(
'webkitfullscreenchange mozfullscreenchange fullscreenchange',
() => {
eventEmitter.emit(
UIEvents.FULLSCREEN_TOGGLED,
UIUtil.isFullScreen());
onResize();
});
onResize);
$(window).resize(onResize);
};
@@ -474,6 +475,7 @@ UI.initEtherpad = name => {
etherpadManager
= new EtherpadManager(config.etherpad_base, name, eventEmitter);
APP.store.dispatch(setEtherpadHasInitialzied());
APP.store.dispatch(showEtherpadButton());
};

View File

@@ -1,4 +1,7 @@
/* global $, interfaceConfig */
/* global $, APP, interfaceConfig */
import { setDocumentEditingState } from '../../../react/features/etherpad';
import { getToolboxHeight } from '../../../react/features/toolbox';
import VideoLayout from '../videolayout/VideoLayout';
import LargeContainer from '../videolayout/LargeContainer';
@@ -126,7 +129,8 @@ class Etherpad extends LargeContainer {
let height, width;
if (interfaceConfig.VERTICAL_FILMSTRIP) {
height = containerHeight;
height = interfaceConfig._USE_NEW_TOOLBOX
? containerHeight - getToolboxHeight() : containerHeight;
width = containerWidth - Filmstrip.getFilmstripWidth();
} else {
height = containerHeight - Filmstrip.getFilmstripHeight();
@@ -242,5 +246,7 @@ export default class EtherpadManager {
this.eventEmitter
.emit(UIEvents.TOGGLED_SHARED_DOCUMENT, !isVisible);
APP.store.dispatch(setDocumentEditingState(!isVisible));
}
}

View File

@@ -35,6 +35,7 @@ import {
StartLiveStreamDialog,
StopLiveStreamDialog,
hideRecordingLabel,
setRecordingType,
updateRecordingState
} from '../../../react/features/recording';
@@ -202,6 +203,8 @@ const Recording = {
this.eventEmitter = eventEmitter;
this.recordingType = recordingType;
APP.store.dispatch(setRecordingType(recordingType));
this.updateRecordingState(APP.conference.getRecordingState());
if (recordingType === 'jibri') {
@@ -219,6 +222,9 @@ const Recording = {
'#toolbar_button_record',
ev => this._onToolbarButtonClick(ev));
this.eventEmitter.on(UIEvents.TOGGLE_RECORDING,
() => this._onToolbarButtonClick());
// If I am a recorder then I publish my recorder custom role to notify
// everyone.
if (config.iAmRecorder) {
@@ -287,6 +293,7 @@ const Recording = {
this.currentState = recordingState;
let labelDisplayConfiguration;
let isRecording = false;
switch (recordingState) {
case JitsiRecordingStatus.ON:
@@ -298,6 +305,7 @@ const Recording = {
};
this._setToolbarButtonToggled(true);
isRecording = true;
break;
}
@@ -362,6 +370,7 @@ const Recording = {
}
APP.store.dispatch(updateRecordingState({
isRecording,
labelDisplayConfiguration,
recordingState
}));

View File

@@ -18,7 +18,11 @@ import {
participantJoined,
participantLeft
} from '../../../react/features/base/participants';
import { dockToolbox, showToolbox } from '../../../react/features/toolbox';
import {
dockToolbox,
getToolboxHeight,
showToolbox
} from '../../../react/features/toolbox';
import SharedVideoThumb from './SharedVideoThumb';
@@ -695,7 +699,8 @@ class SharedVideoContainer extends LargeContainer {
let height, width;
if (interfaceConfig.VERTICAL_FILMSTRIP) {
height = containerHeight;
height = interfaceConfig._USE_NEW_TOOLBOX
? containerHeight - getToolboxHeight() : containerHeight;
width = containerWidth - Filmstrip.getFilmstripWidth();
} else {
height = containerHeight - Filmstrip.getFilmstripHeight();

View File

@@ -1,5 +1,6 @@
/* global $ */
/* global $, APP */
import UIEvents from '../../../service/UI/UIEvents';
import { setVisiblePanel } from '../../../react/features/side-panel';
/**
* Handles open and close of the extended toolbar side panel
@@ -57,6 +58,7 @@ const SideContainerToggler = {
if (isSelectorVisible) {
this.hide();
APP.store.dispatch(setVisiblePanel(null));
} else {
if (this.isVisible()) {
$('#sideToolbarContainer').children()
@@ -74,6 +76,7 @@ const SideContainerToggler = {
}
this.showInnerContainer(elementSelector);
APP.store.dispatch(setVisiblePanel(elementId));
}
},

View File

@@ -1,4 +1,4 @@
/* global APP, $ */
/* global APP, $, interfaceConfig */
import { processReplacements, linkify } from './Replacement';
import CommandsProcessor from './Commands';
@@ -9,7 +9,12 @@ import UIEvents from '../../../../service/UI/UIEvents';
import { smileys } from './smileys';
import { dockToolbox, setSubject } from '../../../../react/features/toolbox';
import { addMessage, markAllRead } from '../../../../react/features/chat';
import {
dockToolbox,
getToolboxHeight,
setSubject
} from '../../../../react/features/toolbox';
let unreadMessages = 0;
const sidePanelsContainerId = 'sideToolbarContainer';
@@ -163,6 +168,8 @@ function addSmileys() {
* Resizes the chat conversation.
*/
function resizeChatConversation() {
// FIXME: this function can all be done with CSS. If Chat is ever rewritten,
// do not copy over this logic.
const msgareaHeight = $('#usermsg').outerHeight();
const chatspace = $(`#${CHAT_CONTAINER_ID}`);
const width = chatspace.width();
@@ -173,7 +180,16 @@ function resizeChatConversation() {
$('#smileys').css('bottom', (msgareaHeight - 26) / 2);
$('#smileysContainer').css('bottom', msgareaHeight);
chat.width(width - 10);
chat.height(window.innerHeight - 15 - msgareaHeight);
if (interfaceConfig._USE_NEW_TOOLBOX) {
const maybeAMagicNumberForPaddingAndMargin = 100;
const offset = maybeAMagicNumberForPaddingAndMargin
+ msgareaHeight + getToolboxHeight();
chat.height(window.innerHeight - offset);
} else {
chat.height(window.innerHeight - 15 - msgareaHeight);
}
}
/**
@@ -249,6 +265,7 @@ const Chat = {
}
unreadMessages = 0;
APP.store.dispatch(markAllRead());
updateVisualNotification();
// Undock the toolbar when the chat is shown and if we're in a
@@ -274,9 +291,10 @@ const Chat = {
*/
// eslint-disable-next-line max-params
updateChatConversation(id, displayName, message, stamp) {
const isFromLocalParticipant = APP.conference.isLocalId(id);
let divClassName = '';
if (APP.conference.isLocalId(id)) {
if (isFromLocalParticipant) {
divClassName = 'localuser';
} else {
divClassName = 'remoteuser';
@@ -294,6 +312,7 @@ const Chat = {
.replace(/>/g, '&gt;')
.replace(/\n/g, '<br/>');
const escDisplayName = UIUtil.escapeHtml(displayName);
const timestamp = getCurrentTime(stamp);
// eslint-disable-next-line no-param-reassign
message = processReplacements(escMessage);
@@ -302,13 +321,18 @@ const Chat = {
= `${'<div class="chatmessage">'
+ '<img src="images/chatArrow.svg" class="chatArrow">'
+ '<div class="username '}${divClassName}">${escDisplayName
}</div><div class="timestamp">${getCurrentTime(stamp)
}</div><div class="timestamp">${timestamp
}</div><div class="usermessage">${message}</div>`
+ '</div>';
$('#chatconversation').append(messageContainer);
$('#chatconversation').animate(
{ scrollTop: $('#chatconversation')[0].scrollHeight }, 1000);
const markAsRead = Chat.isVisible() || isFromLocalParticipant;
APP.store.dispatch(addMessage(
escDisplayName, message, timestamp, markAsRead));
},
/**

View File

@@ -227,43 +227,10 @@ const UIUtil = {
* mode, {false} otherwise
*/
isFullScreen() {
return document.fullscreenElement
return Boolean(document.fullscreenElement
|| document.mozFullScreenElement
|| document.webkitFullscreenElement
|| document.msFullscreenElement;
},
/**
* Exits full screen mode.
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
*/
exitFullScreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
},
/**
* Enter full screen mode.
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
*/
enterFullScreen() {
if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen();
} else if (document.documentElement.msRequestFullscreen) {
document.documentElement.msRequestFullscreen();
} else if (document.documentElement.mozRequestFullScreen) {
document.documentElement.mozRequestFullScreen();
} else if (document.documentElement.webkitRequestFullscreen) {
document.documentElement
.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
}
|| document.msFullscreenElement);
},
/**

View File

@@ -90,6 +90,17 @@ const KeyboardShortcut = {
enabled = value;
},
/**
* Opens the {@KeyboardShortcutsDialog} dialog.
*
* @returns {void}
*/
openDialog() {
APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
shortcutDescriptions: _shortcutsHelp
}));
},
/**
* Registers a new shortcut.
*
@@ -177,9 +188,7 @@ const KeyboardShortcut = {
_initGlobalShortcuts() {
this.registerShortcut('?', null, () => {
sendAnalytics(createShortcutEvent('help'));
APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
shortcutDescriptions: _shortcutsHelp
}));
this.openDialog();
}, 'keyboardShortcuts.toggleShortcuts');
// register SPACE shortcut in two steps to insure visibility of help

15
package-lock.json generated
View File

@@ -6785,11 +6785,6 @@
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.2.tgz",
"integrity": "sha512-pH3vDzpczdsKHdZ9xxR3O46unSjisgVx0IImay7Zz2EdhRVbCkj+nthx9OuuWEhakx9FAO+fNVGrF0rZ2oMOvw=="
},
"immutable": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
"integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM="
},
"import-local": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-0.1.1.tgz",
@@ -7473,7 +7468,7 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#0503ec4d3f175b154b1c6fd7037520ce0768fa58",
"version": "github:jitsi/lib-jitsi-meet#6282e7a82e768dd6a5970de89602757a8f882ffb",
"requires": {
"async": "0.9.0",
"current-executing-script": "0.1.3",
@@ -8644,14 +8639,6 @@
"gauge": "1.2.7"
}
},
"nuclear-js": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/nuclear-js/-/nuclear-js-1.4.0.tgz",
"integrity": "sha1-bJwAGwZz8K6dj4sYjE2gTtaTp74=",
"requires": {
"immutable": "3.8.2"
}
},
"num2fraction": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",

View File

@@ -46,10 +46,9 @@
"jquery-i18next": "1.2.0",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#0503ec4d3f175b154b1c6fd7037520ce0768fa58",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#6282e7a82e768dd6a5970de89602757a8f882ffb",
"lodash": "4.17.4",
"moment": "2.19.4",
"nuclear-js": "1.4.0",
"postis": "2.2.0",
"prop-types": "15.6.0",
"react": "16.2.0",

View File

@@ -129,16 +129,21 @@ export function createFeedbackOpenEvent() {
}
/**
* Creates an event which indicates that the invite dialog was closed. This is
* not a TYPE_UI event, since it is not necessarily the result of a user
* interaction.
* Creates an event for an action regarding the AddPeopleDialog (invites).
*
* @param {string} action - The action that the event represents.
* @param {string} actionSubject - The subject that was acted upon.
* @param {boolean} attributes - Additional attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createInviteDialogClosedEvent() {
export function createInviteDialogEvent(
action, actionSubject, attributes = {}) {
return {
action: 'invite.dialog.closed'
action,
actionSubject,
attributes,
source: 'inviteDialog'
};
}

View File

@@ -96,6 +96,18 @@ export const P2P_STATUS_CHANGED = Symbol('P2P_STATUS_CHANGED');
*/
export const SET_AUDIO_ONLY = Symbol('SET_AUDIO_ONLY');
/**
* The type of (redux) action which sets the desktop sharing enabled flag for
* the current conference.
*
* {
* type: SET_DESKTOP_SHARING_ENABLED,
* desktopSharingEnabled: boolean
* }
*/
export const SET_DESKTOP_SHARING_ENABLED
= Symbol('SET_DESKTOP_SHARING_ENABLED');
/**
* The type of (redux) action which updates the current known status of the
* Follow Me feature.

View File

@@ -31,6 +31,7 @@ import {
LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED,
SET_AUDIO_ONLY,
SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME,
SET_LASTN,
SET_PASSWORD,
@@ -433,6 +434,22 @@ export function setAudioOnly(audioOnly: boolean) {
};
}
/**
* Sets the flag for indicating if desktop sharing is enabled.
*
* @param {boolean} desktopSharingEnabled - True if desktop sharing is enabled.
* @returns {{
* type: SET_DESKTOP_SHARING_ENABLED,
* desktopSharingEnabled: boolean
* }}
*/
export function setDesktopSharingEnabled(desktopSharingEnabled: boolean) {
return {
type: SET_DESKTOP_SHARING_ENABLED,
desktopSharingEnabled
};
}
/**
* Enables or disables the Follow Me feature.
*

View File

@@ -14,6 +14,7 @@ import {
LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED,
SET_AUDIO_ONLY,
SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME,
SET_PASSWORD,
SET_RECEIVE_VIDEO_QUALITY,
@@ -57,6 +58,9 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
case SET_AUDIO_ONLY:
return _setAudioOnly(state, action);
case SET_DESKTOP_SHARING_ENABLED:
return _setDesktopSharingEnabled(state, action);
case SET_FOLLOW_ME:
return {
...state,
@@ -329,6 +333,21 @@ function _setAudioOnly(state, action) {
return set(state, 'audioOnly', action.audioOnly);
}
/**
* Reduces a specific Redux action SET_DESKTOP_SHARING_ENABLED of the feature
* base/conference.
*
* @param {Object} state - The Redux state of the feature base/conference.
* @param {Action} action - The Redux action SET_DESKTOP_SHARING_ENABLED to
* reduce.
* @private
* @returns {Object} The new state of the feature base/conference after the
* reduction of the specified action.
*/
function _setDesktopSharingEnabled(state, action) {
return set(state, 'desktopSharingEnabled', action.desktopSharingEnabled);
}
/**
* Reduces a specific Redux action SET_PASSWORD of the feature base/conference.
*

View File

@@ -65,6 +65,7 @@ const WHITELISTED_KEYS = [
'firefox_fake_device',
'forceJVB121Ratio',
'gatherStats',
'googleApiApplicationClientID',
'hiddenDomain',
'hosts',
'iAmRecorder',

View File

@@ -1,4 +1,4 @@
/* @flow */
// @flow
import type { Dispatch } from 'redux';
@@ -54,10 +54,12 @@ export function connect() {
/**
* Closes connection.
*
* @param {boolean} [requestFeedback] - Whether or not to attempt showing a
* request for call feedback.
* @returns {Function}
*/
export function disconnect() {
export function disconnect(requestFeedback: boolean = false) {
// XXX For web based version we use conference hanging up logic from the old
// app.
return () => APP.conference.hangup();
return () => APP.conference.hangup(requestFeedback);
}

View File

@@ -39,6 +39,12 @@ type Props = {
*/
disableBlanketClickDismiss: boolean,
/**
* If true, the cancel button will not display but cancel actions, like
* clicking the blanket, will cancel.
*/
hideCancelButton: boolean,
/**
* Whether the dialog is modal. This means clicking on the blanket will
* leave the dialog open. No cancel button.
@@ -263,7 +269,9 @@ class StatelessDialog extends Component<Props> {
* not modal.
*/
_renderCancelButton(options = {}) {
if (options.cancelDisabled || options.isModal) {
if (options.cancelDisabled
|| options.isModal
|| options.hideCancelButton) {
return null;
}

File diff suppressed because it is too large Load Diff

View File

@@ -195,6 +195,18 @@ function _visitNode(node, callback) {
};
}
// Element.remove
//
// Required by:
// - lib-jitsi-meet ChatRoom#onPresence parsing
if (typeof elementPrototype.remove === 'undefined') {
elementPrototype.remove = function() {
if (this.parentNode !== null) {
this.parentNode.removeChild(this);
}
};
}
// Element.innerHTML
//
// Required by:
@@ -231,6 +243,31 @@ function _visitNode(node, callback) {
}
});
}
// Element.children
//
// Required by:
// - lib-jitsi-meet ChatRoom#onPresence parsing
if (!elementPrototype.hasOwnProperty('children')) {
Object.defineProperty(elementPrototype, 'children', {
get() {
const nodes = this.childNodes;
const children = [];
let i = 0;
let node = nodes[i];
while (node) {
if (node.nodeType === 1) {
children.push(node);
}
i += 1;
node = nodes[i];
}
return children;
}
});
}
}
// FIXME There is a weird infinite loop related to console.log and

View File

@@ -1,4 +1,4 @@
/* @flow */
// @flow
import UIEvents from '../../../../service/UI/UIEvents';
@@ -10,8 +10,12 @@ import {
import { MiddlewareRegistry } from '../redux';
import { playSound, registerSound, unregisterSound } from '../sounds';
import { localParticipantIdChanged } from './actions';
import {
localParticipantIdChanged,
participantUpdated
} from './actions';
import {
DOMINANT_SPEAKER_CHANGED,
KICK_PARTICIPANT,
MUTE_REMOTE_PARTICIPANT,
PARTICIPANT_DISPLAY_NAME_CHANGED,
@@ -27,6 +31,7 @@ import {
import {
getAvatarURLByParticipantId,
getLocalParticipant,
getParticipantById,
getParticipantCount
} from './functions';
import {
@@ -47,7 +52,7 @@ MiddlewareRegistry.register(store => next => action => {
const { conference } = store.getState()['features/base/conference'];
if (action.type === PARTICIPANT_JOINED
|| action.type === PARTICIPANT_LEFT) {
|| action.type === PARTICIPANT_LEFT) {
_maybePlaySounds(store, action);
}
@@ -66,6 +71,27 @@ MiddlewareRegistry.register(store => next => action => {
store.dispatch(localParticipantIdChanged(LOCAL_PARTICIPANT_DEFAULT_ID));
break;
case DOMINANT_SPEAKER_CHANGED: {
// Ensure the raised hand state is cleared for the dominant speaker.
const participant = getLocalParticipant(store.getState());
if (participant) {
const local = participant.id === action.participant.id;
store.dispatch(participantUpdated({
id: action.participant.id,
local,
raisedHand: false
}));
}
if (typeof APP === 'object') {
APP.UI.markDominantSpeaker(action.participant.id);
}
break;
}
case KICK_PARTICIPANT:
conference.kickParticipant(action.id);
break;
@@ -90,10 +116,37 @@ MiddlewareRegistry.register(store => next => action => {
case PARTICIPANT_JOINED:
case PARTICIPANT_UPDATED: {
if (typeof APP !== 'undefined') {
const participant = action.participant;
const { id, local } = participant;
const { participant } = action;
const { id, local, raisedHand } = participant;
// Send an external update of the local participant's raised hand state
// if a new raised hand state is defined in the action.
if (typeof raisedHand !== 'undefined') {
if (local) {
conference.setLocalParticipantProperty(
'raisedHand',
raisedHand);
}
if (typeof APP === 'object') {
if (local) {
APP.UI.onLocalRaiseHandChanged(raisedHand);
APP.UI.setLocalRaisedHandStatus(raisedHand);
} else {
const remoteParticipant
= getParticipantById(store.getState(), id);
remoteParticipant
&& APP.UI.setRaisedHandStatus(
remoteParticipant.id,
remoteParticipant.name,
raisedHand);
}
}
}
// Notify external listeners of potential avatarURL changes.
if (typeof APP === 'object') {
const preUpdateAvatarURL
= getAvatarURLByParticipantId(store.getState(), id);

View File

@@ -10,6 +10,10 @@ import {
import styles, { UNDERLAY_COLOR } from './styles';
import { translate } from '../../../i18n';
import { Icon } from '../../../font-icons';
type Props = {
/**
@@ -17,6 +21,11 @@ type Props = {
*/
disabled: boolean,
/**
* The translate function.
*/
t: Function,
/**
* Function to be invoked when an item is pressed. The item's URL is passed.
*/
@@ -27,6 +36,11 @@ type Props = {
*/
onRefresh: Function,
/**
* Function to override the rendered default empty list component.
*/
renderListEmptyComponent: Function,
/**
* Sections to be rendered in the following format:
*
@@ -53,7 +67,7 @@ type Props = {
* property and navigates to (probably) meetings, such as the recent list
* or the meeting list components.
*/
export default class NavigateSectionList extends Component<Props> {
class NavigateSectionList extends Component<Props> {
/**
* Constructor of the NavigateSectionList component.
*
@@ -69,6 +83,8 @@ export default class NavigateSectionList extends Component<Props> {
this._renderItem = this._renderItem.bind(this);
this._renderItemLine = this._renderItemLine.bind(this);
this._renderItemLines = this._renderItemLines.bind(this);
this._renderListEmptyComponent
= this._renderListEmptyComponent.bind(this);
this._renderSection = this._renderSection.bind(this);
}
@@ -80,12 +96,16 @@ export default class NavigateSectionList extends Component<Props> {
* @inheritdoc
*/
render() {
const { sections } = this.props;
const { renderListEmptyComponent, sections } = this.props;
return (
<SafeAreaView
style = { styles.container } >
<SectionList
ListEmptyComponent = {
renderListEmptyComponent
|| this._renderListEmptyComponent
}
keyExtractor = { this._getItemKey }
onRefresh = { this._onRefresh }
refreshing = { false }
@@ -274,6 +294,34 @@ export default class NavigateSectionList extends Component<Props> {
return null;
}
_renderListEmptyComponent: () => Object
/**
* Renders a component to display when the list is empty.
*
* @private
* @param {Object} section - The section being rendered.
* @returns {React$Node}
*/
_renderListEmptyComponent() {
const { t, onRefresh } = this.props;
if (typeof onRefresh === 'function') {
return (
<View style = { styles.pullToRefresh }>
<Text style = { styles.pullToRefreshText }>
{ t('sectionList.pullToRefresh') }
</Text>
<Icon
name = 'menu-down'
style = { styles.pullToRefreshIcon } />
</View>
);
}
return null;
}
_renderSection: Object => Object
/**
@@ -293,3 +341,5 @@ export default class NavigateSectionList extends Component<Props> {
);
}
}
export default translate(NavigateSectionList);

View File

@@ -180,6 +180,25 @@ const SECTION_LIST_STYLES = {
fontWeight: 'normal'
},
pullToRefresh: {
alignItems: 'center',
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
padding: 20
},
pullToRefreshIcon: {
backgroundColor: 'transparent',
color: OVERLAY_FONT_COLOR,
fontSize: 20
},
pullToRefreshText: {
backgroundColor: 'transparent',
color: OVERLAY_FONT_COLOR
},
touchableView: {
flexDirection: 'row'
}

View File

@@ -6,6 +6,8 @@ import React, { Component } from 'react';
import InlineDialogFailure from './InlineDialogFailure';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* A MultiSelect that is also auto-completing.
*/
@@ -290,7 +292,9 @@ class MultiSelectAutocomplete extends Component {
error: false
});
})
.catch(() => {
.catch(error => {
logger.error('MultiSelectAutocomplete error in query', error);
this.setState({
error: true,
loading: false,

View File

@@ -7,8 +7,16 @@ import type { Dispatch } from 'redux';
/**
* Size threshold for determining if we are in reduced UI mode or not.
*
* FIXME The logic to base {@code reducedUI} on a hardcoded width or height is
* very brittle because it's completely disconnected from the UI which wants to
* be rendered and, naturally, it broke on iPad where even the secondary Toolbar
* didn't fit in the height. We do need to measure the actual UI at runtime and
* determine whether and how to render it. I'm bumping from 240 to 300 because I
* don't have the time now to refactor {@code ReducedUIDetector} or rip it out
* completely.
*/
const REDUCED_UI_THRESHOLD = 240;
const REDUCED_UI_THRESHOLD = 300;
/**
* Sets the aspect ratio of the app's user interface based on specific width and

View File

@@ -1,3 +1,13 @@
/**
* The type of redux action dispatched to disable screensharing or to start the
* flow for enabling screenshare.
*
* {
* type: TOGGLE_SCREENSHARING
* }
*/
export const TOGGLE_SCREENSHARING = Symbol('TOGGLE_SCREENSHARING');
/**
* The type of redux action dispatched when a track has been (locally or
* remotely) added to the conference.

View File

@@ -12,6 +12,7 @@ import {
import { getLocalParticipant } from '../participants';
import {
TOGGLE_SCREENSHARING,
TRACK_ADDED,
TRACK_CREATE_CANCELED,
TRACK_CREATE_ERROR,
@@ -172,6 +173,20 @@ export function destroyLocalTracks() {
};
}
/**
* Signals that the local participant is ending screensharing or beginning the
* screensharing flow.
*
* @returns {{
* type: TOGGLE_SCREENSHARING,
* }}
*/
export function toggleScreensharing() {
return {
type: TOGGLE_SCREENSHARING
};
}
/**
* Replaces one track with another for one renegotiation instead of invoking
* two renegotiations with a separate removeTrack and addTrack. Disposes the

View File

@@ -1,4 +1,4 @@
/* @flow */
// @flow
import {
CAMERA_FACING_MODE,
@@ -10,9 +10,15 @@ import {
toggleCameraFacingMode
} from '../media';
import { MiddlewareRegistry } from '../redux';
import UIEvents from '../../../../service/UI/UIEvents';
import { createLocalTracksA } from './actions';
import { TRACK_ADDED, TRACK_REMOVED, TRACK_UPDATED } from './actionTypes';
import {
TOGGLE_SCREENSHARING,
TRACK_ADDED,
TRACK_REMOVED,
TRACK_UPDATED
} from './actionTypes';
import { getLocalTrack, setTrackMuted } from './functions';
declare var APP: Object;
@@ -81,6 +87,12 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case TOGGLE_SCREENSHARING:
if (typeof APP === 'object') {
APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);
}
break;
case TRACK_ADDED:
// TODO Remove this middleware case once all UI interested in new tracks
// being added are converted to react and listening for store changes.

View File

@@ -0,0 +1,28 @@
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Wrapper around fetch GET requests to handle json-ifying the response
* and logging errors.
*
* @param {string} url - The URL to perform a GET against.
* @returns {Promise<Object>} The response body, in JSON format, will be
* through the Promise.
*/
export function doGetJSON(url) {
return fetch(url)
.then(response => {
const jsonify = response.json();
if (response.ok) {
return jsonify;
}
return jsonify
.then(result => Promise.reject(result));
})
.catch(error => {
logger.error('Error performing get:', url, error);
return Promise.reject(error);
});
}

View File

@@ -1,4 +1,5 @@
export * from './helpers';
export * from './httpUtils';
export * from './loadScript';
export * from './randomUtil';
export * from './uri';

View File

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

View File

@@ -1,5 +1,12 @@
// @flow
/**
* Action to signal that calendar access has already been requested
* since the app started, so no new request should be done unless the
* user explicitly tries to refresh the calendar view.
*/
export const CALENDAR_ACCESS_REQUESTED = Symbol('CALENDAR_ACCESS_REQUESTED');
/**
* Action to update the current calendar entry list in the store.
*/

View File

@@ -1,10 +1,28 @@
// @flow
import {
CALENDAR_ACCESS_REQUESTED,
NEW_CALENDAR_ENTRY_LIST,
NEW_KNOWN_DOMAIN,
REFRESH_CALENDAR_ENTRY_LIST
} from './actionTypes';
/**
* Sends an action to signal that a calendar access has been requested. For
* more info see the {@link CALENDAR_ACCESS_REQUESTED}.
*
* @param {string | undefined} status - The result of the last calendar
* access request.
* @returns {{
* type: CALENDAR_ACCESS_REQUESTED
* }}
*/
export function updateCalendarAccessStatus(status: ?string) {
return {
status,
type: CALENDAR_ACCESS_REQUESTED
};
}
/**
* Sends an action to add a new known domain if not present yet.
*
@@ -24,12 +42,16 @@ export function maybeAddNewKnownDomain(domainName: string) {
/**
* Sends an action to refresh the entry list (fetches new data).
*
* @param {boolean|undefined} forcePermission - Whether to force to re-ask
* for the permission or not.
* @returns {{
* type: REFRESH_CALENDAR_ENTRY_LIST
* type: REFRESH_CALENDAR_ENTRY_LIST,
* forcePermission: boolean
* }}
*/
export function refreshCalendarEntryList() {
export function refreshCalendarEntryList(forcePermission: boolean = false) {
return {
forcePermission,
type: REFRESH_CALENDAR_ENTRY_LIST
};
}

View File

@@ -1,12 +1,16 @@
// @flow
import React, { Component } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { connect } from 'react-redux';
import styles from './styles';
import { refreshCalendarEntryList } from '../actions';
import { appNavigate } from '../../app';
import { getLocalizedDateFormatter, translate } from '../../base/i18n';
import { NavigateSectionList } from '../../base/react';
import { openSettings } from '../../mobile/permissions';
type Props = {
@@ -28,6 +32,11 @@ type Props = {
*/
displayed: boolean,
/**
* The current state of the calendar access permission.
*/
_calendarAccessStatus: string,
/**
* The calendar event list.
*/
@@ -43,8 +52,6 @@ type Props = {
* Component to display a list of events from the (mobile) user's calendar.
*/
class MeetingList extends Component<Props> {
_initialLoaded: boolean
/**
* Default values for the component's props.
*/
@@ -60,6 +67,14 @@ class MeetingList extends Component<Props> {
constructor(props) {
super(props);
const { dispatch, displayed } = props;
if (displayed) {
dispatch(refreshCalendarEntryList());
}
this._getRenderListEmptyComponent
= this._getRenderListEmptyComponent.bind(this);
this._onPress = this._onPress.bind(this);
this._onRefresh = this._onRefresh.bind(this);
this._toDisplayableItem = this._toDisplayableItem.bind(this);
@@ -73,16 +88,11 @@ class MeetingList extends Component<Props> {
* @inheritdoc
*/
componentWillReceiveProps(newProps) {
// This is a conditional logic to refresh the calendar entries (thus
// to request access to calendar) on component first receives a
// displayed=true prop - to avoid requesting calendar access on
// app start.
if (!this._initialLoaded
&& newProps.displayed
&& !this.props.displayed) {
const { displayed } = this.props;
if (newProps.displayed && !displayed) {
const { dispatch } = this.props;
this._initialLoaded = true;
dispatch(refreshCalendarEntryList());
}
}
@@ -100,10 +110,45 @@ class MeetingList extends Component<Props> {
disabled = { disabled }
onPress = { this._onPress }
onRefresh = { this._onRefresh }
renderListEmptyComponent = {
this._getRenderListEmptyComponent
}
sections = { this._toDisplayableList() } />
);
}
_getRenderListEmptyComponent: () => Object
/**
* Returns a list empty component if a custom one has to be rendered instead
* of the default one in the {@link NavigateSectionList}.
*
* @private
* @returns {Component}
*/
_getRenderListEmptyComponent() {
const { _calendarAccessStatus, t } = this.props;
if (_calendarAccessStatus === 'denied') {
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>
);
}
return null;
}
_onPress: string => Function
/**
@@ -130,7 +175,7 @@ class MeetingList extends Component<Props> {
_onRefresh() {
const { dispatch } = this.props;
dispatch(refreshCalendarEntryList());
dispatch(refreshCalendarEntryList(true));
}
_toDisplayableItem: Object => Object
@@ -219,12 +264,12 @@ class MeetingList extends Component<Props> {
* @returns {string}
*/
_toDateString(event) {
/* eslint-disable max-len */
const startDateTime = getLocalizedDateFormatter(event.startDate).format('lll');
const endTime = getLocalizedDateFormatter(event.endDate).format('LT');
const startDateTime
= getLocalizedDateFormatter(event.startDate).format('lll');
const endTime
= getLocalizedDateFormatter(event.endDate).format('LT');
return `${startDateTime} - ${endTime}`;
/* eslint-enable max-len */
}
}
@@ -237,8 +282,11 @@ class MeetingList extends Component<Props> {
* }}
*/
export function _mapStateToProps(state: Object) {
const calendarSyncState = state['features/calendar-sync'];
return {
_eventList: state['features/calendar-sync'].events
_calendarAccessStatus: calendarSyncState.calendarAccessStatus,
_eventList: calendarSyncState.events
};
}

View File

@@ -1,4 +1,4 @@
import { createStyleSheet } from '../../base/styles';
import { ColorPalette, createStyleSheet } from '../../base/styles';
const NOTIFICATION_SIZE = 55;
@@ -8,6 +8,46 @@ const NOTIFICATION_SIZE = 55;
*/
export default createStyleSheet({
/**
* Button style of the open settings button.
*/
noPermissionMessageButton: {
backgroundColor: ColorPalette.blue,
borderColor: ColorPalette.blue,
borderRadius: 4,
borderWidth: 1,
height: 30,
justifyContent: 'center',
margin: 15,
paddingHorizontal: 20
},
/**
* Text style of the open settings button.
*/
noPermissionMessageButtonText: {
color: ColorPalette.white
},
/**
* Text style of the no permission message.
*/
noPermissionMessageText: {
backgroundColor: 'transparent',
color: 'rgba(255, 255, 255, 0.6)'
},
/**
* Top level view of the no permission message.
*/
noPermissionMessageView: {
alignItems: 'center',
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
padding: 20
},
/**
* The top level container of the notification.
*/

View File

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

View File

@@ -2,13 +2,18 @@
import Logger from 'jitsi-meet-logger';
import RNCalendarEvents from 'react-native-calendar-events';
import { APP_WILL_MOUNT } from '../app';
import { SET_ROOM } from '../base/conference';
import { MiddlewareRegistry } from '../base/redux';
import { APP_LINK_SCHEME, parseURIString } from '../base/util';
import { APP_STATE_CHANGED } from '../mobile/background';
import { APP_WILL_MOUNT } from '../app';
import { maybeAddNewKnownDomain, updateCalendarEntryList } from './actions';
import {
maybeAddNewKnownDomain,
updateCalendarAccessStatus,
updateCalendarEntryList
} from './actions';
import { REFRESH_CALENDAR_ENTRY_LIST } from './actionTypes';
const FETCH_END_DAYS = 10;
@@ -20,12 +25,15 @@ MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) {
case APP_STATE_CHANGED:
_maybeClearAccessStatus(store, action);
break;
case APP_WILL_MOUNT:
_ensureDefaultServer(store);
_fetchCalendarEntries(store, false);
_fetchCalendarEntries(store, false, false);
break;
case REFRESH_CALENDAR_ENTRY_LIST:
_fetchCalendarEntries(store, true);
_fetchCalendarEntries(store, true, action.forcePermission);
break;
case SET_ROOM:
_parseAndAddDomain(store);
@@ -34,34 +42,53 @@ MiddlewareRegistry.register(store => next => action => {
return result;
});
/**
* 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 into the background and we need to try to request
* for a permission as often as possible, but not annoyingly often.
*
* @private
* @param {Object} store - The redux store.
* @param {Object} action - The Redux action.
* @returns {void}
*/
function _maybeClearAccessStatus(store, action) {
const { appState } = action;
if (appState === 'background') {
const { dispatch } = store;
dispatch(updateCalendarAccessStatus(undefined));
}
}
/**
* Ensures calendar access if possible and resolves the promise if it's granted.
*
* @private
* @param {boolean} promptForPermission - Flag to tell the app if it should
* prompt for a calendar permission if it wasn't granted yet.
* @param {Function} dispatch - The Redux dispatch function.
* @returns {Promise}
*/
function _ensureCalendarAccess(promptForPermission) {
function _ensureCalendarAccess(promptForPermission, dispatch) {
return new Promise((resolve, reject) => {
RNCalendarEvents.authorizationStatus()
.then(status => {
if (status === 'authorized') {
resolve();
resolve(true);
} else if (promptForPermission) {
RNCalendarEvents.authorizeEventStore()
.then(result => {
if (result === 'authorized') {
resolve();
} else {
reject(result);
}
dispatch(updateCalendarAccessStatus(result));
resolve(result === 'authorized');
})
.catch(error => {
reject(error);
});
} else {
reject(status);
resolve(false);
}
})
.catch(error => {
@@ -91,64 +118,49 @@ function _ensureDefaultServer(store) {
*
* @private
* @param {Object} store - The redux store.
* @param {boolean} promptForPermission - Flag to tell the app if it should
* @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.
* @returns {void}
*/
function _fetchCalendarEntries(store, promptForPermission) {
_ensureCalendarAccess(promptForPermission)
.then(() => {
const startDate = new Date();
const endDate = new Date();
function _fetchCalendarEntries(
store,
maybePromptForPermission,
forcePermission
) {
const { dispatch } = store;
const state = store.getState()['features/calendar-sync'];
const { calendarAccessStatus } = state;
const promptForPermission
= (maybePromptForPermission && !calendarAccessStatus)
|| forcePermission;
startDate.setDate(startDate.getDate() + FETCH_START_DAYS);
endDate.setDate(endDate.getDate() + FETCH_END_DAYS);
_ensureCalendarAccess(promptForPermission, dispatch)
.then(accessGranted => {
if (accessGranted) {
const startDate = new Date();
const endDate = new Date();
RNCalendarEvents.fetchAllEvents(
startDate.getTime(),
endDate.getTime(),
[]
)
.then(events => {
const { knownDomains } = store.getState()['features/calendar-sync'];
const eventList = [];
startDate.setDate(startDate.getDate() + FETCH_START_DAYS);
endDate.setDate(endDate.getDate() + FETCH_END_DAYS);
if (events && events.length) {
for (const event of events) {
const jitsiURL = _getURLFromEvent(event, knownDomains);
const now = Date.now();
RNCalendarEvents.fetchAllEvents(
startDate.getTime(),
endDate.getTime(),
[]
)
.then(events => {
const { knownDomains } = state;
if (jitsiURL) {
const eventStartDate = Date.parse(event.startDate);
const eventEndDate = Date.parse(event.endDate);
if (isNaN(eventStartDate) || isNaN(eventEndDate)) {
logger.warn(
'Skipping calendar event due to invalid dates',
event.title,
event.startDate,
event.endDate
);
} else if (eventEndDate > now) {
eventList.push({
endDate: eventEndDate,
id: event.id,
startDate: eventStartDate,
title: event.title,
url: jitsiURL
});
}
}
}
}
store.dispatch(updateCalendarEntryList(eventList.sort((a, b) =>
a.startDate - b.startDate
).slice(0, MAX_LIST_LENGTH)));
})
.catch(error => {
logger.error('Error fetching calendar.', error);
});
_updateCalendarEntries(events, knownDomains, dispatch);
})
.catch(error => {
logger.error('Error fetching calendar.', error);
});
} else {
logger.warn('Calendar access not granted.');
}
})
.catch(reason => {
logger.error('Error accessing calendar.', reason);
@@ -209,3 +221,70 @@ function _parseAndAddDomain(store) {
store.dispatch(maybeAddNewKnownDomain(locationURL.host));
}
/**
* Updates the calendar entries in Redux when new list is received.
*
* @private
* @param {Object} event - An event returned from the native calendar.
* @param {Array<string>} knownDomains - The known domain list.
* @returns {CalendarEntry}
*/
function _parseCalendarEntry(event, knownDomains) {
if (event) {
const jitsiURL = _getURLFromEvent(event, knownDomains);
if (jitsiURL) {
const eventStartDate = Date.parse(event.startDate);
const eventEndDate = Date.parse(event.endDate);
if (isNaN(eventStartDate) || isNaN(eventEndDate)) {
logger.warn(
'Skipping invalid calendar event',
event.title,
event.startDate,
event.endDate
);
} else {
return {
endDate: eventEndDate,
id: event.id,
startDate: eventStartDate,
title: event.title,
url: jitsiURL
};
}
}
}
return null;
}
/**
* Updates the calendar entries in Redux when new list is received.
*
* @private
* @param {Array<CalendarEntry>} events - The new event list.
* @param {Array<string>} knownDomains - The known domain list.
* @param {Function} dispatch - The Redux dispatch function.
* @returns {void}
*/
function _updateCalendarEntries(events, knownDomains, dispatch) {
if (events && events.length) {
const eventList = [];
for (const event of events) {
const calendarEntry
= _parseCalendarEntry(event, knownDomains);
const now = Date.now();
if (calendarEntry && calendarEntry.endDate > now) {
eventList.push(calendarEntry);
}
}
dispatch(updateCalendarEntryList(eventList.sort((a, b) =>
a.startDate - b.startDate
).slice(0, MAX_LIST_LENGTH)));
}
}

View File

@@ -3,13 +3,19 @@
import { ReducerRegistry } from '../base/redux';
import { PersistenceRegistry } from '../base/storage';
import { NEW_CALENDAR_ENTRY_LIST, NEW_KNOWN_DOMAIN } from './actionTypes';
import {
CALENDAR_ACCESS_REQUESTED,
NEW_CALENDAR_ENTRY_LIST,
NEW_KNOWN_DOMAIN
} from './actionTypes';
/**
* ZB: this is an object, as further data is to come here, like:
* - known domain list
*/
const DEFAULT_STATE = {
/**
* Note: If features/calendar-sync ever gets persisted, do not persist the
* calendarAccessStatus value as it's needed to remain a runtime value to
* see if we need to re-request the calendar permission from the user.
*/
calendarAccessStatus: undefined,
events: [],
knownDomains: []
};
@@ -26,6 +32,12 @@ ReducerRegistry.register(
STORE_NAME,
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case CALENDAR_ACCESS_REQUESTED:
return {
...state,
calendarAccessStatus: action.status
};
case NEW_CALENDAR_ENTRY_LIST:
return {
...state,

View File

@@ -0,0 +1,23 @@
/**
* The type of the action which signals to add a new chat message.
*
* {
* type: ADD_MESSAGE,
* hasRead: boolean,
* message: string,
* timestamp: string,
* userName: string
* }
*/
export const ADD_MESSAGE = Symbol('ADD_MESSAGE');
/**
* The type of the action which updates which is the most recent message that
* has been seen by the local participant.
*
* {
* type: SET_LAST_READ_MESSAGE,
* message: Object
* }
*/
export const SET_LAST_READ_MESSAGE = Symbol('SET_LAST_READ_MESSAGE');

View File

@@ -0,0 +1,63 @@
import { ADD_MESSAGE, SET_LAST_READ_MESSAGE } from './actionTypes';
/* eslint-disable max-params */
/**
* Adds a chat message to the collection of messages.
*
* @param {string} userName - The username to display of the participant that
* authored the message.
* @param {string} message - The received message to display.
* @param {string} timestamp - A timestamp to display for when the message was
* received.
* @param {boolean} hasRead - Whether or not to immediately mark the message as
* read.
* @returns {{
* type: ADD_MESSAGE,
* hasRead: boolean,
* message: string,
* timestamp: string,
* userName: string
* }}
*/
export function addMessage(userName, message, timestamp, hasRead) {
return {
type: ADD_MESSAGE,
hasRead,
message,
timestamp,
userName
};
}
/* eslint-enable max-params */
/**
* Sets the last read message cursor to the latest message.
*
* @returns {Function}
*/
export function markAllRead() {
return (dispatch, getState) => {
const { messages } = getState()['features/chat'];
dispatch(setLastReadMessage(messages[messages.length - 1]));
};
}
/**
* Updates the last read message cursor to be set at the passed in message. The
* assumption is that messages will be ordered chronologically.
*
* @param {Object} message - The message from the redux state.
* @returns {{
* type: SET_LAST_READ_MESSAGE,
* message: Object
* }}
*/
export function setLastReadMessage(message) {
return {
type: SET_LAST_READ_MESSAGE,
message
};
}

View File

@@ -0,0 +1,59 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getUnreadCount } from '../functions';
/**
* FIXME: Move this UI logic to a generic component that can be used for
* {@code ParticipantCounter} as well.
*/
/**
* Implements a React {@link Component} which displays a count of the number of
* unread chat messages.
*
* @extends Component
*/
class ChatCounter extends Component {
static propTypes = {
/**
* The number of unread chat messages in the conference.
*/
_count: PropTypes.number
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<span className = 'badge-round'>
<span>
{ this.props._count || null }
</span>
</span>
);
}
}
/**
* Maps (parts of) the Redux state to the associated {@code ChatCounter}'s
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _count: number
* }}
*/
function _mapStateToProps(state) {
return {
_count: getUnreadCount(state)
};
}
export default connect(_mapStateToProps)(ChatCounter);

View File

@@ -0,0 +1 @@
export ChatCounter from './ChatCounter';

View File

@@ -0,0 +1,20 @@
// @flow
/**
* Selector for calculating the number of unread chat messages.
*
* @param {Object} state - The redux state.
* @returns {number} The number of unread messages.
*/
export function getUnreadCount(state: Object) {
const { lastReadMessage, messages } = state['features/chat'];
const messagesCount = messages.length;
if (!messagesCount) {
return 0;
}
const lastReadIndex = messages.lastIndexOf(lastReadMessage);
return messagesCount - (lastReadIndex + 1);
}

View File

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

View File

@@ -0,0 +1,44 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import {
ADD_MESSAGE,
SET_LAST_READ_MESSAGE
} from './actionTypes';
const DEFAULT_STATE = {
open: false,
messages: [],
lastReadMessage: null
};
ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case ADD_MESSAGE: {
const newMessage = {
message: action.message,
timestamp: action.timestamp,
userName: action.userName
};
return {
...state,
lastReadMessage:
action.hasRead ? newMessage : state.lastReadMessage,
messages: [
...state.messages,
newMessage
]
};
}
case SET_LAST_READ_MESSAGE:
return {
...state,
lastReadMessage: action.message
};
}
return state;
});

View File

@@ -11,7 +11,14 @@ import { CalleeInfoContainer } from '../../base/jwt';
import { Filmstrip } from '../../filmstrip';
import { LargeVideo } from '../../large-video';
import { NotificationsContainer } from '../../notifications';
import { showToolbox, Toolbox } from '../../toolbox';
import { SidePanel } from '../../side-panel';
import {
Toolbox,
ToolboxV2,
fullScreenChanged,
setToolboxAlwaysVisible,
showToolbox
} from '../../toolbox';
import { HideNotificationBarStyle } from '../../unsupported-browser';
import { maybeShowSuboptimalExperienceNotification } from '../functions';
@@ -19,11 +26,29 @@ import { maybeShowSuboptimalExperienceNotification } from '../functions';
declare var APP: Object;
declare var interfaceConfig: Object;
/**
* DOM events for when full screen mode has changed. Different browsers need
* different vendor prefixes.
*
* @private
* @type {Array<string>}
*/
const FULL_SCREEN_EVENTS = [
'webkitfullscreenchange',
'mozfullscreenchange',
'fullscreenchange'
];
/**
* The type of the React {@code Component} props of {@link Conference}.
*/
type Props = {
/**
* Whether the toolbar should stay visible or be able to autohide.
*/
_alwaysVisibleToolbar: boolean,
/**
* Whether the local participant is recording the conference.
*/
@@ -37,6 +62,7 @@ type Props = {
* The conference page of the Web application.
*/
class Conference extends Component<Props> {
_onFullScreenChange: Function;
_onShowToolbar: Function;
_originalOnShowToolbar: Function;
@@ -59,6 +85,9 @@ class Conference extends Component<Props> {
leading: true,
trailing: false
});
// Bind event handler so it is only bound once for every instance.
this._onFullScreenChange = this._onFullScreenChange.bind(this);
}
/**
@@ -74,10 +103,16 @@ class Conference extends Component<Props> {
APP.UI.registerListeners();
APP.UI.bindEvents();
const { dispatch, t } = this.props;
FULL_SCREEN_EVENTS.forEach(name =>
document.addEventListener(name, this._onFullScreenChange));
const { _alwaysVisibleToolbar, dispatch, t } = this.props;
dispatch(connect());
maybeShowSuboptimalExperienceNotification(dispatch, t);
dispatch(setToolboxAlwaysVisible(
_alwaysVisibleToolbar || interfaceConfig.filmStripOnly));
}
/**
@@ -90,6 +125,9 @@ class Conference extends Component<Props> {
APP.UI.unregisterListeners();
APP.UI.unbindEvents();
FULL_SCREEN_EVENTS.forEach(name =>
document.removeEventListener(name, this._onFullScreenChange));
APP.conference.isJoined() && this.props.dispatch(disconnect());
}
@@ -100,12 +138,26 @@ class Conference extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { filmStripOnly, VIDEO_QUALITY_LABEL_DISABLED } = interfaceConfig;
const {
_USE_NEW_TOOLBOX,
VIDEO_QUALITY_LABEL_DISABLED,
filmStripOnly
} = interfaceConfig;
const hideVideoQualityLabel
= filmStripOnly
|| VIDEO_QUALITY_LABEL_DISABLED
|| this.props._iAmRecorder;
let ToolboxToUse;
if (filmStripOnly) {
ToolboxToUse = null;
} else if (interfaceConfig._USE_NEW_TOOLBOX) {
ToolboxToUse = ToolboxV2;
} else {
ToolboxToUse = Toolbox;
}
return (
<div
id = 'videoconference_page'
@@ -116,7 +168,10 @@ class Conference extends Component<Props> {
<Filmstrip filmstripOnly = { filmStripOnly } />
</div>
{ filmStripOnly ? null : <Toolbox /> }
{ ToolboxToUse && <ToolboxToUse /> }
{ _USE_NEW_TOOLBOX && !filmStripOnly
&& <SidePanel /> }
<DialogContainer />
<NotificationsContainer />
@@ -135,6 +190,17 @@ class Conference extends Component<Props> {
);
}
/**
* Updates the Redux state when full screen mode has been enabled or
* disabled.
*
* @private
* @returns {void}
*/
_onFullScreenChange() {
this.props.dispatch(fullScreenChanged(APP.UI.isFullScreen()));
}
/**
* Displays the toolbar.
*
@@ -153,17 +219,30 @@ class Conference extends Component<Props> {
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _alwaysVisibleToolbar: boolean,
* _iAmRecorder: boolean
* }}
*/
function _mapStateToProps(state) {
const {
alwaysVisibleToolbar,
iAmRecorder
} = state['features/base/config'];
return {
/**
* Whether the toolbar should stay visible or be able to autohide.
*
* @private
*/
_alwaysVisibleToolbar: alwaysVisibleToolbar,
/**
* Whether the local participant is recording the conference.
*
* @private
*/
_iAmRecorder: state['features/base/config'].iAmRecorder
_iAmRecorder: iAmRecorder
};
}

View File

@@ -0,0 +1,29 @@
/**
* The type of the action which signals document editing has been enabled.
*
* {
* type: ETHERPAD_INITIALIZED
* }
*/
export const ETHERPAD_INITIALIZED = Symbol('ETHERPAD_INITIALIZED');
/**
* The type of the action which signals document editing has stopped or started.
*
* {
* type: SET_DOCUMENT_EDITING_STATUS
* }
*/
export const SET_DOCUMENT_EDITING_STATUS
= Symbol('SET_DOCUMENT_EDITING_STATUS');
/**
* The type of the action which signals to start or stop editing a shared
* document.
*
* {
* type: TOGGLE_DOCUMENT_EDITING
* }
*/
export const TOGGLE_DOCUMENT_EDITING = Symbol('TOGGLE_DOCUMENT_EDITING');

View File

@@ -0,0 +1,50 @@
// @flow
import {
ETHERPAD_INITIALIZED,
SET_DOCUMENT_EDITING_STATUS,
TOGGLE_DOCUMENT_EDITING
} from './actionTypes';
/**
* Dispatches an action to set whether document editing has started or stopped.
*
* @param {boolean} editing - Whether or not a document is currently being
* edited.
* @returns {{
* type: SET_DOCUMENT_EDITING_STATUS,
* editing: boolean
* }}
*/
export function setDocumentEditingState(editing: boolean) {
return {
type: SET_DOCUMENT_EDITING_STATUS,
editing
};
}
/**
* Dispatches an action to set Etherpad as having been initialized.
*
* @returns {{
* type: ETHERPAD_INITIALIZED
* }}
*/
export function setEtherpadHasInitialzied() {
return {
type: ETHERPAD_INITIALIZED
};
}
/**
* Dispatches an action to show or hide Etherpad.
*
* @returns {{
* type: TOGGLE_DOCUMENT_EDITING
* }}
*/
export function toggleDocument() {
return {
type: TOGGLE_DOCUMENT_EDITING
};
}

View File

@@ -0,0 +1,5 @@
export * from './actions';
export * from './actionTypes';
import './middleware';
import './reducer';

View File

@@ -0,0 +1,30 @@
// @flow
import { MiddlewareRegistry } from '../base/redux';
import UIEvents from '../../../service/UI/UIEvents';
import { TOGGLE_DOCUMENT_EDITING } from './actionTypes';
declare var APP: Object;
/**
* Middleware that captures actions related to collaborative document editing
* and notifies components not hooked into redux.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
// eslint-disable-next-line no-unused-vars
MiddlewareRegistry.register(store => next => action => {
if (typeof APP === 'undefined') {
return next(action);
}
switch (action.type) {
case TOGGLE_DOCUMENT_EDITING:
APP.UI.emitEvent(UIEvents.ETHERPAD_CLICKED);
break;
}
return next(action);
});

View File

@@ -0,0 +1,30 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import {
ETHERPAD_INITIALIZED,
SET_DOCUMENT_EDITING_STATUS
} from './actionTypes';
/**
* Reduces the Redux actions of the feature features/etherpad.
*/
ReducerRegistry.register('features/etherpad', (state = {}, action) => {
switch (action.type) {
case ETHERPAD_INITIALIZED:
return {
...state,
initialized: true
};
case SET_DOCUMENT_EDITING_STATUS:
return {
...state,
editing: action.editing
};
default:
return state;
}
});

View File

@@ -7,11 +7,13 @@ import { connect } from 'react-redux';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../base/participants';
import { InviteButton } from '../../invite';
import { Toolbox } from '../../toolbox';
import { Toolbox, ToolboxFilmstrip, dockToolbox } from '../../toolbox';
import { setFilmstripHovered } from '../actions';
import { shouldRemoteVideosBeVisible } from '../functions';
declare var interfaceConfig: Object;
/**
* Implements a React {@link Component} which represents the filmstrip on
* Web/React.
@@ -62,6 +64,12 @@ class Filmstrip extends Component<*> {
*/
_remoteVideosVisible: PropTypes.bool,
/**
* Whether or not the toolbox is visible. The height of the vertical
* filmstrip needs to adjust to accommodate the horizontal toolbox.
*/
_toolboxVisible: PropTypes.bool,
/**
* Updates the redux store with filmstrip hover changes.
*/
@@ -111,6 +119,7 @@ class Filmstrip extends Component<*> {
_isAddToCallAvailable,
_isDialOutAvailable,
_remoteVideosVisible,
_toolboxVisible,
filmstripOnly
} = this.props;
@@ -122,13 +131,17 @@ class Filmstrip extends Component<*> {
* will get updated without replacing the DOM. If the known DOM gets
* modified, then the views will get blown away.
*/
const reduceHeight
= _toolboxVisible && interfaceConfig.TOOLBAR_BUTTONS.length;
const filmstripClassNames = `filmstrip ${_remoteVideosVisible
? '' : 'hide-videos'} ${reduceHeight ? 'reduce-height' : ''}`;
const filmstripClassNames = `filmstrip ${_remoteVideosVisible ? ''
: 'hide-videos'}`;
const ToolboxToUse = interfaceConfig._USE_NEW_TOOLBOX
? ToolboxFilmstrip : Toolbox;
return (
<div className = { filmstripClassNames }>
{ filmstripOnly ? <Toolbox /> : null }
{ filmstripOnly ? <ToolboxToUse /> : null }
<div
className = 'filmstrip__videos'
id = 'remoteVideos'>
@@ -172,6 +185,9 @@ class Filmstrip extends Component<*> {
*/
_notifyOfHoveredStateUpdate() {
if (this.props._hovered !== this._isHovered) {
if (interfaceConfig._USE_NEW_TOOLBOX) {
this.props.dispatch(dockToolbox(this._isHovered));
}
this.props.dispatch(setFilmstripHovered(this._isHovered));
}
}
@@ -211,7 +227,8 @@ class Filmstrip extends Component<*> {
* _hovered: boolean,
* _isAddToCallAvailable: boolean,
* _isDialOutAvailable: boolean,
* _remoteVideosVisible: boolean
* _remoteVideosVisible: boolean,
* _toolboxVisible: boolean
* }}
*/
function _mapStateToProps(state) {
@@ -231,11 +248,13 @@ function _mapStateToProps(state) {
return {
_hideInviteButton: iAmRecorder
|| (!isAddToCallAvailable && !isDialOutAvailable),
|| (!isAddToCallAvailable && !isDialOutAvailable)
|| interfaceConfig._USE_NEW_TOOLBOX,
_hovered: hovered,
_isAddToCallAvailable: isAddToCallAvailable,
_isDialOutAvailable: isDialOutAvailable,
_remoteVideosVisible: shouldRemoteVideosBeVisible(state)
_remoteVideosVisible: shouldRemoteVideosBeVisible(state),
_toolboxVisible: state['features/toolbox'].visible
};
}

View File

@@ -5,8 +5,7 @@ import {
UPDATE_DIAL_IN_NUMBERS_FAILED,
UPDATE_DIAL_IN_NUMBERS_SUCCESS
} from './actionTypes';
declare var $: Function;
import { getDialInConferenceID, getDialInNumbers } from './functions';
/**
* Opens the inline conference info dialog.
@@ -48,12 +47,10 @@ export function updateDialInNumbers() {
}
const { room } = state['features/base/conference'];
const conferenceIDURL
= `${dialInConfCodeUrl}?conference=${room}@${mucURL}`;
Promise.all([
$.getJSON(dialInNumbersUrl),
$.getJSON(conferenceIDURL)
getDialInNumbers(dialInNumbersUrl),
getDialInConferenceID(dialInConfCodeUrl, room, mucURL)
])
.then(([ dialInNumbers, { conference, id, message } ]) => {
if (!conference || !id) {

View File

@@ -2,11 +2,11 @@
import Avatar from '@atlaskit/avatar';
import InlineMessage from '@atlaskit/inline-message';
import { Immutable } from 'nuclear-js';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createInviteDialogEvent, sendAnalytics } from '../../analytics';
import { getInviteURL } from '../../base/connection';
import { Dialog, hideDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
@@ -114,16 +114,10 @@ class AddPeopleDialog extends Component<*, *> {
*/
addToCallInProgress: false,
// FIXME: Remove usage of Immutable. {@code MultiSelectAutocomplete}
// will default to having its internal implementation use a plain array
// if no {@link defaultValue} is passed in. As such is the case, this
// instance of Immutable.List gets overridden with an array on the first
// search.
/**
* The list of invite items.
*/
inviteItems: new Immutable.List()
inviteItems: []
};
/**
@@ -150,6 +144,17 @@ class AddPeopleDialog extends Component<*, *> {
};
}
/**
* Sends an analytics event to record the dialog has been shown.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
sendAnalytics(createInviteDialogEvent(
'invite.dialog.opened', 'dialog'));
}
/**
* React Component method that executes once component is updated.
*
@@ -169,6 +174,17 @@ class AddPeopleDialog extends Component<*, *> {
}
}
/**
* Sends an analytics event to record the dialog has been closed.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
sendAnalytics(createInviteDialogEvent(
'invite.dialog.closed', 'dialog'));
}
/**
* Renders the content of this component.
*
@@ -238,6 +254,32 @@ class AddPeopleDialog extends Component<*, *> {
return text.replace(/\D/g, '');
}
/**
* Helper for determining how many of each type of user is being invited.
* Used for logging and sending analytics related to invites.
*
* @param {Array} inviteItems - An array with the invite items, as created
* in {@link _parseQueryResults}.
* @private
* @returns {Object} An object with keys as user types and values as the
* number of invites for that type.
*/
_getInviteTypeCounts(inviteItems = []) {
const inviteTypeCounts = {};
inviteItems.forEach(i => {
const type = i.item.type;
if (!inviteTypeCounts[type]) {
inviteTypeCounts[type] = 0;
}
inviteTypeCounts[type]++;
});
return inviteTypeCounts;
}
_isAddDisabled: () => boolean;
/**
@@ -320,6 +362,15 @@ class AddPeopleDialog extends Component<*, *> {
* @returns {void}
*/
_onSubmit() {
const inviteTypeCounts
= this._getInviteTypeCounts(this.state.inviteItems);
sendAnalytics(createInviteDialogEvent(
'clicked', 'inviteButton', {
...inviteTypeCounts,
inviteAllowed: this._isAddDisabled()
}));
if (this._isAddDisabled()) {
return;
}
@@ -400,7 +451,16 @@ class AddPeopleDialog extends Component<*, *> {
// If any invites are left that means something failed to send
// so treat it as an error.
if (invitesLeftToSend.length) {
logger.error(`${invitesLeftToSend.length} invites failed`);
const erroredInviteTypeCounts
= this._getInviteTypeCounts(invitesLeftToSend);
logger.error(`${invitesLeftToSend.length} invites failed`,
erroredInviteTypeCounts);
sendAnalytics(createInviteDialogEvent(
'error', 'invite', {
...erroredInviteTypeCounts
}));
this.setState({
addToCallInProgress: false,
@@ -508,7 +568,7 @@ class AddPeopleDialog extends Component<*, *> {
let peopleSearchPromise;
if (this.props.enableAddPeople) {
if (this.props.enableAddPeople && text) {
peopleSearchPromise = searchDirectory(
_peopleSearchUrl,
_jwt,

View File

@@ -5,9 +5,15 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { ToolbarButton, TOOLTIP_TO_POPUP_POSITION } from '../../toolbox';
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { translate } from '../../base/i18n';
import {
ToolbarButton,
ToolbarButtonV2,
TOOLTIP_TO_POPUP_POSITION
} from '../../toolbox';
import { setInfoDialogVisibility } from '../actions';
import { setInfoDialogVisibility, updateDialInNumbers } from '../actions';
import { InfoDialog } from './info-dialog';
const { INITIAL_TOOLBAR_TIMEOUT } = interfaceConfig;
@@ -39,6 +45,15 @@ class InfoDialogButton extends Component {
* @static
*/
static propTypes = {
/**
* Phone numbers for dialing into the conference.
*/
_dialInNumbers: PropTypes.oneOfType([
PropTypes.object,
PropTypes.array
]),
/**
* Whether or not the {@code InfoDialog} should close by itself after a
* a timeout.
@@ -61,6 +76,11 @@ class InfoDialogButton extends Component {
*/
dispatch: PropTypes.func,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func,
/**
* From which side tooltips should display. Will be re-used for
* displaying the inline dialog for video quality adjustment.
@@ -100,6 +120,10 @@ class InfoDialogButton extends Component {
if (this.props._shouldAutoClose) {
this._setAutoCloseTimeout();
}
if (!this.props._dialInNumbers) {
this.props.dispatch(updateDialInNumbers());
}
}
/**
@@ -145,29 +169,9 @@ class InfoDialogButton extends Component {
* @returns {ReactElement}
*/
render() {
const { _showDialog, _toolboxVisible, tooltipPosition } = this.props;
const buttonConfiguration = {
...DEFAULT_BUTTON_CONFIGURATION,
classNames: [
...DEFAULT_BUTTON_CONFIGURATION.classNames,
_showDialog ? 'toggled button-active' : ''
]
};
return (
<InlineDialog
content = { <InfoDialog
onClose = { this._onDialogClose }
onMouseOver = { this._onDialogMouseOver } /> }
isOpen = { _toolboxVisible && _showDialog }
onClose = { this._onDialogClose }
position = { TOOLTIP_TO_POPUP_POSITION[tooltipPosition] }>
<ToolbarButton
button = { buttonConfiguration }
onClick = { this._onDialogToggle }
tooltipPosition = { tooltipPosition } />
</InlineDialog>
);
return interfaceConfig._USE_NEW_TOOLBOX
? this._renderNewToolbarButton()
: this._renderOldToolbarButton();
}
/**
@@ -208,9 +212,76 @@ class InfoDialogButton extends Component {
* @returns {void}
*/
_onDialogToggle() {
sendAnalytics(createToolbarEvent('info'));
this.props.dispatch(setInfoDialogVisibility(!this.props._showDialog));
}
/**
* Renders a React Element for the {@code InfoDialog} using legacy
* {@code ToolbarButton}.
*
* @private
* @returns {ReactElement}
*/
_renderOldToolbarButton() {
const { _showDialog, _toolboxVisible, tooltipPosition } = this.props;
const buttonConfiguration = {
...DEFAULT_BUTTON_CONFIGURATION,
classNames: [
...DEFAULT_BUTTON_CONFIGURATION.classNames,
_showDialog ? 'toggled button-active' : ''
]
};
return (
<InlineDialog
content = { <InfoDialog
autoUpdateNumbers = { false }
onClose = { this._onDialogClose }
onMouseOver = { this._onDialogMouseOver } /> }
isOpen = { _toolboxVisible && _showDialog }
onClose = { this._onDialogClose }
position = { TOOLTIP_TO_POPUP_POSITION[tooltipPosition] }>
<ToolbarButton
button = { buttonConfiguration }
onClick = { this._onDialogToggle }
tooltipPosition = { tooltipPosition } />
</InlineDialog>
);
}
/**
* Renders a React Element for the {@code InfoDialog} using the newer
* {@code ToolbarButtonV2}.
*
* @private
* @returns {ReactElement}
*/
_renderNewToolbarButton() {
const { _showDialog, _toolboxVisible, t } = this.props;
const iconClass = `icon-info ${_showDialog ? 'toggled' : ''}`;
return (
<div className = 'toolbox-button-wth-dialog'>
<InlineDialog
content = { <InfoDialog
autoUpdateNumbers = { false }
onClose = { this._onDialogClose }
onMouseOver = { this._onDialogMouseOver } /> }
isOpen = { _toolboxVisible && _showDialog }
onClose = { this._onDialogClose }
position = { 'top right' }>
<ToolbarButtonV2
accessibilityLabel = 'Info'
iconName = { iconClass }
onClick = { this._onDialogToggle }
tooltip = { t('info.tooltip') } />
</InlineDialog>
</div>
);
}
/**
* Set a timeout to automatically hide the {@code InfoDialog}.
*
@@ -235,6 +306,7 @@ class InfoDialogButton extends Component {
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _dialInNumbers: Array,
* _shouldAutoClose: boolean,
* _showDialog: boolean,
* _toolboxVisible: boolean
@@ -243,14 +315,16 @@ class InfoDialogButton extends Component {
function _mapStateToProps(state) {
const {
infoDialogVisible,
infoDialogWillAutoClose
infoDialogWillAutoClose,
numbers
} = state['features/invite'];
return {
_dialInNumbers: numbers,
_shouldAutoClose: infoDialogWillAutoClose,
_showDialog: infoDialogVisible,
_toolboxVisible: state['features/toolbox'].visible
};
}
export default connect(_mapStateToProps)(InfoDialogButton);
export default translate(connect(_mapStateToProps)(InfoDialogButton));

View File

@@ -24,6 +24,15 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
* @extends Component
*/
class InfoDialog extends Component {
/**
* Default values for {@code InfoDialog} component's properties.
*
* @static
*/
static defaultProps = {
autoUpdateNumbers: true
};
/**
* {@code InfoDialog} component's property types.
*
@@ -69,6 +78,13 @@ class InfoDialog extends Component {
*/
_password: PropTypes.string,
/**
* Whether or not this component should make a request for dial-in
* numbers. If false, this component will rely on an outside source
* updating and passing in numbers through the _dialIn prop.
*/
autoUpdateNumbers: PropTypes.bool,
/**
* Invoked to open a dialog for adding participants to the conference.
*/
@@ -148,7 +164,7 @@ class InfoDialog extends Component {
* @returns {void}
*/
componentDidMount() {
if (!this.state.phoneNumber) {
if (!this.state.phoneNumber && this.props.autoUpdateNumbers) {
this.props.dispatch(updateDialInNumbers());
}
}

View File

@@ -1,8 +1,44 @@
// @flow
import { doGetJSON } from '../base/util';
declare var $: Function;
declare var interfaceConfig: Object;
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Sends a GET request to obtain the conference ID necessary for identifying
* which conference to join after diaing the dial-in service.
*
* @param {string} baseUrl - The url for obtaining the conference ID (pin) for
* dialing into a conference.
* @param {string} roomName - The conference name to find the associated
* conference ID.
* @param {string} mucURL - In which MUC the conference exists.
* @returns {Promise} - The promise created by the request.
*/
export function getDialInConferenceID(
baseUrl: string,
roomName: string,
mucURL: string): Promise<Object> {
const conferenceIDURL = `${baseUrl}?conference=${roomName}@${mucURL}`;
return doGetJSON(conferenceIDURL);
}
/**
* Sends a GET request for phone numbers used to dial into a conference.
*
* @param {string} url - The service that returns confernce dial-in numbers.
* @returns {Promise} - The promise created by the request. The returned numbers
* may be an array of numbers or an object with countries as keys and arrays of
* phone number strings.
*/
export function getDialInNumbers(url: string): Promise<*> {
return doGetJSON(url);
}
/**
* Get the position of the invite option in the interfaceConfig.INVITE_OPTIONS
* list.
@@ -30,7 +66,7 @@ export function invitePeopleAndChatRooms( // eslint-disable-line max-params
inviteServiceUrl: string,
inviteUrl: string,
jwt: string,
inviteItems: Object): Promise<void> {
inviteItems: Array<Object>): Promise<void> {
if (!inviteItems || inviteItems.length === 0) {
return Promise.resolve();
}
@@ -78,13 +114,24 @@ export function searchDirectory( // eslint-disable-line max-params
): Promise<Array<Object>> {
const queryTypesString = JSON.stringify(queryTypes);
return new Promise((resolve, reject) => {
$.getJSON(
`${serviceUrl}?query=${encodeURIComponent(text)}&queryTypes=${
queryTypesString}&jwt=${jwt}`,
resolve)
.catch((jqxhr, textStatus, error) => reject(error));
});
return fetch(`${serviceUrl}?query=${encodeURIComponent(text)}&queryTypes=${
queryTypesString}&jwt=${jwt}`)
.then(response => {
const jsonify = response.json();
if (response.ok) {
return jsonify;
}
return jsonify
.then(result => Promise.reject(result));
})
.catch(error => {
logger.error(
'Error searching directory:', error);
return Promise.reject(error);
});
}
/**

View File

@@ -0,0 +1,10 @@
/**
* The type of the action which signals the keyboard shortcuts dialog should
* be displayed.
*
* {
* type: OPEN_KEYBOARD_SHORTCUTS_DIALOG
* }
*/
export const OPEN_KEYBOARD_SHORTCUTS_DIALOG
= Symbol('OPEN_KEYBOARD_SHORTCUTS_DIALOG');

View File

@@ -0,0 +1,14 @@
import { OPEN_KEYBOARD_SHORTCUTS_DIALOG } from './actionTypes';
/**
* Opens the dialog showing available keyboard shortcuts.
*
* @returns {{
* type: OPEN_KEYBOARD_SHORTCUTS_DIALOG
* }}
*/
export function openKeyboardShortcutsDialog() {
return {
type: OPEN_KEYBOARD_SHORTCUTS_DIALOG
};
}

View File

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

View File

@@ -0,0 +1,26 @@
// @flow
import { MiddlewareRegistry } from '../base/redux';
import { OPEN_KEYBOARD_SHORTCUTS_DIALOG } from './actionTypes';
declare var APP: Object;
/**
* Implements the middleware of the feature keyboard-shortcuts.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
// eslint-disable-next-line no-unused-vars
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case OPEN_KEYBOARD_SHORTCUTS_DIALOG:
if (typeof APP === 'object') {
APP.keyboardshortcut.openDialog();
}
break;
}
return next(action);
});

View File

@@ -0,0 +1,31 @@
// @flow
import { Alert, Linking, NativeModules } from 'react-native';
import { Platform } from '../../base/react';
/**
* Opens the settings panel for the current platform.
*
* @private
* @returns {void}
*/
export function openSettings() {
switch (Platform.OS) {
case 'android':
NativeModules.AndroidSettings.open().catch(() => {
Alert.alert(
'Error opening settings',
'Please open settings and grant the required permissions',
[
{ text: 'OK' }
]
);
});
break;
case 'ios':
Linking.openURL('app-settings:');
break;
}
}

View File

@@ -1 +1,3 @@
export * from './functions';
import './middleware';

View File

@@ -1,9 +1,10 @@
/* @flow */
import { Alert, Linking, NativeModules } from 'react-native';
import { Alert } from 'react-native';
import { openSettings } from './functions';
import { isRoomValid } from '../../base/conference';
import { Platform } from '../../base/react';
import { MiddlewareRegistry } from '../../base/redux';
import { TRACK_CREATE_ERROR } from '../../base/tracks';
@@ -64,35 +65,9 @@ function _alertPermissionErrorWithSettings(trackType) {
[
{ text: 'Cancel' },
{
onPress: _openSettings,
onPress: openSettings,
text: 'Settings'
}
],
{ cancelable: false });
}
/**
* Opens the settings panel for the current platform.
*
* @private
* @returns {void}
*/
function _openSettings() {
switch (Platform.OS) {
case 'android':
NativeModules.AndroidSettings.open().catch(() => {
Alert.alert(
'Error opening settings',
'Please open settings and grant the required permissions',
[
{ text: 'OK' }
]
);
});
break;
case 'ios':
Linking.openURL('app-settings:');
break;
}
}

View File

@@ -20,3 +20,25 @@ export const HIDE_RECORDING_LABEL = Symbol('HIDE_RECORDING_LABEL');
* @public
*/
export const RECORDING_STATE_UPDATED = Symbol('RECORDING_STATE_UPDATED');
/**
* The type of Redux action which updates the current known type of configured
* recording. For example, type "jibri" is used for live streaming.
*
* {
* type: RECORDING_STATE_UPDATED,
* recordingType: string
* }
* @public
*/
export const SET_RECORDING_TYPE = Symbol('SET_RECORDING_TYPE');
/**
* The type of Redux action triggers the flow to start or stop recording.
*
* {
* type: TOGGLE_RECORDING
* }
* @public
*/
export const TOGGLE_RECORDING = Symbol('TOGGLE_RECORDING');

View File

@@ -1,4 +1,9 @@
import { HIDE_RECORDING_LABEL, RECORDING_STATE_UPDATED } from './actionTypes';
import {
HIDE_RECORDING_LABEL,
RECORDING_STATE_UPDATED,
SET_RECORDING_TYPE,
TOGGLE_RECORDING
} from './actionTypes';
/**
* Hides any displayed recording label, regardless of current recording state.
@@ -13,6 +18,36 @@ export function hideRecordingLabel() {
};
}
/**
* Sets what type of recording service will be used.
*
* @param {string} recordingType - The type of recording service to be used.
* Should be one of the enumerated types in {@link RECORDING_TYPES}.
* @returns {{
* type: SET_RECORDING_TYPE,
* recordingType: string
* }}
*/
export function setRecordingType(recordingType) {
return {
type: SET_RECORDING_TYPE,
recordingType
};
}
/**
* Start or stop recording.
*
* @returns {{
* type: TOGGLE_RECORDING
* }}
*/
export function toggleRecording() {
return {
type: TOGGLE_RECORDING
};
}
/**
* Updates the redux state for the recording feature.
*

View File

@@ -0,0 +1,12 @@
// @flow
/**
* Expected supported recording types. JIBRI is known to support live streaming
* whereas JIRECON is for recording.
*
* @type {Object}
*/
export const RECORDING_TYPES = {
JIBRI: 'jibri',
JIRECON: 'jirecon'
};

View File

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

View File

@@ -0,0 +1,27 @@
// @flow
import { MiddlewareRegistry } from '../base/redux';
import UIEvents from '../../../service/UI/UIEvents';
import { TOGGLE_RECORDING } from './actionTypes';
declare var APP: Object;
/**
* Implements the middleware of the feature recording.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
// eslint-disable-next-line no-unused-vars
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case TOGGLE_RECORDING:
if (typeof APP === 'object') {
APP.UI.emitEvent(UIEvents.TOGGLE_RECORDING);
}
break;
}
return next(action);
});

View File

@@ -1,5 +1,9 @@
import { ReducerRegistry } from '../base/redux';
import { HIDE_RECORDING_LABEL, RECORDING_STATE_UPDATED } from './actionTypes';
import {
HIDE_RECORDING_LABEL,
RECORDING_STATE_UPDATED,
SET_RECORDING_TYPE
} from './actionTypes';
/**
* Reduces the Redux actions of the feature features/recording.
@@ -18,6 +22,12 @@ ReducerRegistry.register('features/recording', (state = {}, action) => {
...action.recordingState
};
case SET_RECORDING_TYPE:
return {
...state,
recordingType: action.recordingType
};
default:
return state;
}

View File

@@ -151,12 +151,15 @@ class SettingsView extends AbstractSettingsView {
fieldSeparator = { true }
i18nLabel = 'settingsView.displayName'>
<TextInput
autoCorrect = { false }
onChangeText = { this._onChangeDisplayName }
placeholder = 'John Doe'
value = { _profile.displayName } />
</FormRow>
<FormRow i18nLabel = 'settingsView.email'>
<TextInput
autoCapitalize = 'none'
autoCorrect = { false }
keyboardType = { 'email-address' }
onChangeText = { this._onChangeEmail }
placeholder = 'email@example.com'
@@ -169,6 +172,7 @@ class SettingsView extends AbstractSettingsView {
i18nLabel = 'settingsView.serverURL'>
<TextInput
autoCapitalize = 'none'
autoCorrect = { false }
onBlur = { this._onBlurServerURL }
onChangeText = { this._onChangeServerURL }
placeholder = { this.props._serverURL }

View File

@@ -0,0 +1,20 @@
/**
* The type of the action which signals to update the current known state of the
* shared YouTube video.
*
* {
* type: SET_SHARED_VIDEO_STATUS,
* status: string
* }
*/
export const SET_SHARED_VIDEO_STATUS = Symbol('SET_SHARED_VIDEO_STATUS');
/**
* The type of the action which signals to start the flow for starting or
* stopping a shared YouTube video.
*
* {
* type: TOGGLE_SHARED_VIDEO
* }
*/
export const TOGGLE_SHARED_VIDEO = Symbol('TOGGLE_SHARED_VIDEO');

View File

@@ -0,0 +1,31 @@
import { SET_SHARED_VIDEO_STATUS, TOGGLE_SHARED_VIDEO } from './actionTypes';
/**
* Updates the current known status of the shared YouTube video.
*
* @param {string} status - The current status of the YouTube video being
* shared.
* @returns {{
* type: SET_SHARED_VIDEO_STATUS,
* status: string
* }}
*/
export function setSharedVideoStatus(status) {
return {
type: SET_SHARED_VIDEO_STATUS,
status
};
}
/**
* Starts the flow for starting or stopping a shared YouTube video.
*
* @returns {{
* type: TOGGLE_SHARED_VIDEO
* }}
*/
export function toggleSharedVideo() {
return {
type: TOGGLE_SHARED_VIDEO
};
}

View File

@@ -0,0 +1,5 @@
export * from './actions';
export * from './actionTypes';
import './middleware';
import './reducer';

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