mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-04 05:42:28 +00:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c08116dc2 | ||
|
|
f8717a7135 | ||
|
|
1b85442dba | ||
|
|
ea46cbc479 | ||
|
|
b76ab305e3 | ||
|
|
358ce0799e | ||
|
|
e7223c49ef | ||
|
|
02a31746fb | ||
|
|
40154b1feb | ||
|
|
4c49e3bec0 | ||
|
|
0a086fa3f7 | ||
|
|
b353b8fffb | ||
|
|
a3c00021de | ||
|
|
1e0a3ceb74 | ||
|
|
8492aad7d6 | ||
|
|
7ad9fa8392 | ||
|
|
6916252ce1 | ||
|
|
b4eae56eed | ||
|
|
eb69fb69cb | ||
|
|
2b7cdbc6a8 | ||
|
|
f3a90f048a | ||
|
|
8bf69d30b7 | ||
|
|
45078fe6b2 | ||
|
|
4783b22018 | ||
|
|
d93782af8a | ||
|
|
962df14382 | ||
|
|
01db70fd3d | ||
|
|
6cc8800016 | ||
|
|
e5596c3cd5 | ||
|
|
1b91e0bc2f | ||
|
|
b258e0d397 | ||
|
|
83f47c2df1 | ||
|
|
a39da15c94 | ||
|
|
fd787abf85 | ||
|
|
7822155e5e |
@@ -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
|
||||
|
||||
17
config.js
17
config.js
@@ -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,
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -180,3 +180,6 @@
|
||||
.icon-gsm-bars:before {
|
||||
content: "\e926";
|
||||
}
|
||||
.icon-open_in_new:before {
|
||||
content: "\e89e";
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -77,7 +77,6 @@
|
||||
text-align: center;
|
||||
|
||||
.star-label {
|
||||
color: $rateStarLabelColor;
|
||||
font-size: 14px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
BIN
fonts/jitsi.eot
BIN
fonts/jitsi.eot
Binary file not shown.
@@ -23,6 +23,7 @@
|
||||
<glyph unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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 |
BIN
fonts/jitsi.ttf
BIN
fonts/jitsi.ttf
Binary file not shown.
BIN
fonts/jitsi.woff
BIN
fonts/jitsi.woff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
20
lang/languages-ko.json
Normal 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
512
lang/main-ko.json
Normal 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": ""
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
};
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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, '>')
|
||||
.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));
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
15
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -65,6 +65,7 @@ const WHITELISTED_KEYS = [
|
||||
'firefox_fake_device',
|
||||
'forceJVB121Ratio',
|
||||
'gatherStats',
|
||||
'googleApiApplicationClientID',
|
||||
'hiddenDomain',
|
||||
'hosts',
|
||||
'iAmRecorder',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
28
react/features/base/util/httpUtils.js
Normal file
28
react/features/base/util/httpUtils.js
Normal 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);
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './helpers';
|
||||
export * from './httpUtils';
|
||||
export * from './loadScript';
|
||||
export * from './randomUtil';
|
||||
export * from './uri';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './actions';
|
||||
export * from './components';
|
||||
|
||||
import './middleware';
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
23
react/features/chat/actionTypes.js
Normal file
23
react/features/chat/actionTypes.js
Normal 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');
|
||||
63
react/features/chat/actions.js
Normal file
63
react/features/chat/actions.js
Normal 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
|
||||
};
|
||||
}
|
||||
59
react/features/chat/components/ChatCounter.web.js
Normal file
59
react/features/chat/components/ChatCounter.web.js
Normal 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);
|
||||
1
react/features/chat/components/index.js
Normal file
1
react/features/chat/components/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export ChatCounter from './ChatCounter';
|
||||
20
react/features/chat/functions.js
Normal file
20
react/features/chat/functions.js
Normal 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);
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './components';
|
||||
export * from './constants';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
|
||||
44
react/features/chat/reducer.js
Normal file
44
react/features/chat/reducer.js
Normal 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;
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
29
react/features/etherpad/actionTypes.js
Normal file
29
react/features/etherpad/actionTypes.js
Normal 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');
|
||||
50
react/features/etherpad/actions.js
Normal file
50
react/features/etherpad/actions.js
Normal 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
|
||||
};
|
||||
}
|
||||
5
react/features/etherpad/index.js
Normal file
5
react/features/etherpad/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
30
react/features/etherpad/middleware.js
Normal file
30
react/features/etherpad/middleware.js
Normal 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);
|
||||
});
|
||||
30
react/features/etherpad/reducer.js
Normal file
30
react/features/etherpad/reducer.js
Normal 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;
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
10
react/features/keyboard-shortcuts/actionTypes.js
Normal file
10
react/features/keyboard-shortcuts/actionTypes.js
Normal 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');
|
||||
14
react/features/keyboard-shortcuts/actions.js
Normal file
14
react/features/keyboard-shortcuts/actions.js
Normal 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
|
||||
};
|
||||
}
|
||||
@@ -1 +1,4 @@
|
||||
export * from './actions';
|
||||
export * from './components';
|
||||
|
||||
import './middleware';
|
||||
|
||||
26
react/features/keyboard-shortcuts/middleware.js
Normal file
26
react/features/keyboard-shortcuts/middleware.js
Normal 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);
|
||||
});
|
||||
31
react/features/mobile/permissions/functions.js
Normal file
31
react/features/mobile/permissions/functions.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
export * from './functions';
|
||||
|
||||
import './middleware';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
12
react/features/recording/constants.js
Normal file
12
react/features/recording/constants.js
Normal 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'
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
export * from './actions';
|
||||
export * from './components';
|
||||
export * from './constants';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
|
||||
27
react/features/recording/middleware.js
Normal file
27
react/features/recording/middleware.js
Normal 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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
20
react/features/shared-video/actionTypes.js
Normal file
20
react/features/shared-video/actionTypes.js
Normal 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');
|
||||
31
react/features/shared-video/actions.js
Normal file
31
react/features/shared-video/actions.js
Normal 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
|
||||
};
|
||||
}
|
||||
5
react/features/shared-video/index.js
Normal file
5
react/features/shared-video/index.js
Normal 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
Reference in New Issue
Block a user