Selaa lähdekoodia

Merge branch 'master' into talk-muted

master
Lyubomir Marinov 8 vuotta sitten
vanhempi
commit
c95a8e058c
72 muutettua tiedostoa jossa 3519 lisäystä ja 1870 poistoa
  1. 1
    0
      .gitignore
  2. 11
    2
      app.js
  3. 8
    0
      authError.html
  4. 8
    0
      close.html
  5. 230
    31
      conference.js
  6. 2
    0
      config.js
  7. BIN
      css/.DS_Store
  8. 12
    15
      css/_base.scss
  9. 0
    5
      css/_chat.scss
  10. 4
    5
      css/_contact_list.scss
  11. 0
    6
      css/_font-awesome.scss
  12. 12
    7
      css/_font.scss
  13. 2
    1
      css/_jquery-impromptu.scss
  14. 13
    0
      css/_mixins.scss
  15. 12
    0
      css/_redirect_page.scss
  16. 8
    7
      css/_side_toolbar_container.scss
  17. 36
    8
      css/_toolbars.scss
  18. 32
    6
      css/_variables.scss
  19. 212
    105
      css/_videolayout_default.scss
  20. 4
    1
      css/main.scss
  21. 53
    0
      css/modals/_dialog.scss
  22. 44
    43
      css/modals/feedback/_feedback.scss
  23. 6
    1
      css/ringing/_ringing.scss
  24. BIN
      fonts/jitsi.eot
  25. 2
    0
      fonts/jitsi.svg
  26. BIN
      fonts/jitsi.ttf
  27. BIN
      fonts/jitsi.woff
  28. 52
    0
      fonts/selection.json
  29. 26
    22
      index.html
  30. 10
    1
      interface_config.js
  31. 2
    0
      lang/languages-de.json
  32. 15
    0
      lang/languages-pl.json
  33. 2
    0
      lang/languages-ptBR.json
  34. 2
    0
      lang/languages.json
  35. 57
    40
      lang/main-de.json
  36. 344
    0
      lang/main-pl.json
  37. 43
    26
      lang/main-ptBR.json
  38. 17
    15
      lang/main.json
  39. 1
    0
      modules/TokenData/TokenData.js
  40. 0
    321
      modules/UI/Feedback.js
  41. 88
    79
      modules/UI/UI.js
  42. 121
    216
      modules/UI/audio_levels/AudioLevels.js
  43. 0
    108
      modules/UI/audio_levels/CanvasUtils.js
  44. 30
    2
      modules/UI/authentication/RoomLocker.js
  45. 128
    0
      modules/UI/feedback/Feedback.js
  46. 193
    0
      modules/UI/feedback/FeedbackWindow.js
  47. 4
    1
      modules/UI/recording/Recording.js
  48. 57
    15
      modules/UI/ring_overlay/RingOverlay.js
  49. 1
    5
      modules/UI/shared_video/SharedVideo.js
  50. 40
    31
      modules/UI/side_pannels/chat/Chat.js
  51. 5
    6
      modules/UI/side_pannels/chat/Replacement.js
  52. 47
    0
      modules/UI/side_pannels/chat/smileys.js
  53. 0
    48
      modules/UI/side_pannels/chat/smileys.json
  54. 10
    18
      modules/UI/side_pannels/contactlist/ContactList.js
  55. 2
    6
      modules/UI/side_pannels/settings/SettingsMenu.js
  56. 67
    19
      modules/UI/toolbars/Toolbar.js
  57. 100
    6
      modules/UI/util/UIUtil.js
  58. 37
    12
      modules/UI/videolayout/ConnectionIndicator.js
  59. 138
    42
      modules/UI/videolayout/FilmStrip.js
  60. 501
    0
      modules/UI/videolayout/LargeVideoManager.js
  61. 25
    19
      modules/UI/videolayout/LocalVideo.js
  62. 171
    30
      modules/UI/videolayout/RemoteVideo.js
  63. 210
    136
      modules/UI/videolayout/SmallVideo.js
  64. 111
    325
      modules/UI/videolayout/VideoContainer.js
  65. 102
    49
      modules/UI/videolayout/VideoLayout.js
  66. 7
    5
      modules/UI/welcome_page/WelcomePage.js
  67. 1
    8
      modules/keyboardshortcut/keyboardshortcut.js
  68. 8
    4
      modules/settings/Settings.js
  69. 14
    6
      package.json
  70. 1
    1
      prosody-plugins/mod_token_verification.lua
  71. 11
    2
      service/UI/UIEvents.js
  72. 6
    3
      service/translation/languages.js

+ 1
- 0
.gitignore Näytä tiedosto

@@ -1,4 +1,5 @@
1 1
 node_modules
2
+.DS_Store
2 3
 *.swp
3 4
 .idea/
4 5
 *.iml

+ 11
- 2
app.js Näytä tiedosto

@@ -8,10 +8,14 @@ import "jquery-ui";
8 8
 import "strophe";
9 9
 import "strophe-disco";
10 10
 import "strophe-caps";
11
-import "tooltip";
12
-import "popover";
13 11
 import "jQuery-Impromptu";
14 12
 import "autosize";
13
+
14
+import 'aui';
15
+import 'aui-experimental';
16
+import 'aui-css';
17
+import 'aui-experimental-css';
18
+
15 19
 window.toastr = require("toastr");
16 20
 
17 21
 import URLProcessor from "./modules/config/URLProcessor";
@@ -106,6 +110,11 @@ function init() {
106 110
     var isUIReady = APP.UI.start();
107 111
     if (isUIReady) {
108 112
         APP.conference.init({roomName: buildRoomName()}).then(function () {
113
+            let server = APP.tokenData.server;
114
+            if(server) {
115
+                APP.conference.logEvent("server." + server, 1);
116
+            }
117
+
109 118
             APP.UI.initConference();
110 119
 
111 120
             APP.UI.addListener(UIEvents.LANG_CHANGED, function (language) {

+ 8
- 0
authError.html Näytä tiedosto

@@ -0,0 +1,8 @@
1
+<html>
2
+<head>
3
+    <link rel="stylesheet" href="css/all.css"/>
4
+</head>
5
+<body>
6
+    <div class="redirectPageMessage">Sorry! You are not allowed to be here :(</div>
7
+</body>
8
+</html>

+ 8
- 0
close.html Näytä tiedosto

@@ -0,0 +1,8 @@
1
+<html>
2
+<head>
3
+    <link rel="stylesheet" href="css/all.css"/>
4
+</head>
5
+<body>
6
+    <div class="redirectPageMessage">Thank you for your feedback!</div>
7
+</body>
8
+</html>

+ 230
- 31
conference.js Näytä tiedosto

@@ -40,7 +40,14 @@ let connectionIsInterrupted = false;
40 40
  */
41 41
 let DSExternalInstallationInProgress = false;
42 42
 
43
-import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo";
43
+/**
44
+ * Listens whether conference had been left from local user when we are trying
45
+ * to navigate away from current page.
46
+ * @type {ConferenceLeftListener}
47
+ */
48
+let conferenceLeftListener = null;
49
+
50
+import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/VideoContainer";
44 51
 
45 52
 /**
46 53
  * Known custom conference commands.
@@ -203,8 +210,28 @@ function muteLocalVideo (muted) {
203 210
 
204 211
 /**
205 212
  * Check if the welcome page is enabled and redirects to it.
213
+ * If requested show a thank you dialog before that.
214
+ * If we have a close page enabled, redirect to it without
215
+ * showing any other dialog.
216
+ * @param {boolean} showThankYou whether we should show a thank you dialog
206 217
  */
207
-function maybeRedirectToWelcomePage() {
218
+function maybeRedirectToWelcomePage(showThankYou) {
219
+
220
+    // if close page is enabled redirect to it, without further action
221
+    if (config.enableClosePage) {
222
+        window.location.pathname = "close.html";
223
+        return;
224
+    }
225
+
226
+    if (showThankYou) {
227
+        APP.UI.messageHandler.openMessageDialog(
228
+            null, null, null,
229
+            APP.translation.translateString(
230
+                "dialog.thankYou", {appName:interfaceConfig.APP_NAME}
231
+            )
232
+        );
233
+    }
234
+
208 235
     if (!config.enableWelcomePage) {
209 236
         return;
210 237
     }
@@ -236,7 +263,7 @@ function disconnectAndShowFeedback(requestFeedback) {
236 263
  * @param {boolean} [requestFeedback=false] if user feedback should be requested
237 264
  */
238 265
 function hangup (requestFeedback = false) {
239
-    const errCallback = (f, err) => {
266
+    const errCallback = (err) => {
240 267
 
241 268
         // If we want to break out the chain in our error handler, it needs
242 269
         // to return a rejected promise. In the case of feedback request
@@ -251,14 +278,69 @@ function hangup (requestFeedback = false) {
251 278
         }
252 279
     };
253 280
     const disconnect = disconnectAndShowFeedback.bind(null, requestFeedback);
281
+
282
+    if (!conferenceLeftListener)
283
+        conferenceLeftListener = new ConferenceLeftListener();
284
+
285
+    // Make sure that leave is resolved successfully and the set the handlers
286
+    // to be invoked once conference had been left
254 287
     APP.conference._room.leave()
255
-    .then(disconnect)
256
-    .catch(errCallback.bind(null, disconnect))
257
-    .then(maybeRedirectToWelcomePage)
258
-    .catch(function(err){
259
-            console.log(err);
260
-        });
288
+        .then(conferenceLeftListener.setHandler(disconnect, errCallback))
289
+        .catch(errCallback);
290
+}
291
+
292
+/**
293
+ * Listens for CONFERENCE_LEFT event so we can check whether it has finished.
294
+ * The handler will be called once the conference had been left or if it
295
+ * was already left when we are adding the handler.
296
+ */
297
+class ConferenceLeftListener {
298
+    /**
299
+     * Creates ConferenceLeftListener and start listening for conference
300
+     * failed event.
301
+     */
302
+    constructor() {
303
+        room.on(ConferenceEvents.CONFERENCE_LEFT,
304
+            this._handleConferenceLeft.bind(this));
305
+    }
261 306
 
307
+    /**
308
+     * Handles the conference left event, if we have a handler we invoke it.
309
+     * @private
310
+     */
311
+    _handleConferenceLeft() {
312
+        this.conferenceLeft = true;
313
+
314
+        if (this.handler)
315
+            this._handleLeave();
316
+    }
317
+
318
+    /**
319
+     * Sets the handlers. If we already left the conference invoke them.
320
+     * @param handler
321
+     * @param errCallback
322
+     */
323
+    setHandler (handler, errCallback) {
324
+        this.handler = handler;
325
+        this.errCallback = errCallback;
326
+
327
+        if (this.conferenceLeft)
328
+            this._handleLeave();
329
+    }
330
+
331
+    /**
332
+     * Invokes the handlers.
333
+     * @private
334
+     */
335
+    _handleLeave()
336
+    {
337
+        this.handler()
338
+            .catch(this.errCallback)
339
+            .then(maybeRedirectToWelcomePage)
340
+            .catch(function(err){
341
+                console.log(err);
342
+            });
343
+    }
262 344
 }
263 345
 
264 346
 /**
@@ -294,8 +376,13 @@ function createLocalTracks (options, checkForPermissionPrompt) {
294 376
             firefox_fake_device: config.firefox_fake_device,
295 377
             desktopSharingExtensionExternalInstallation:
296 378
                 options.desktopSharingExtensionExternalInstallation
297
-        }, checkForPermissionPrompt)
298
-        .catch(function (err) {
379
+        }, checkForPermissionPrompt).then( (tracks) => {
380
+            tracks.forEach((track) => {
381
+                track.on(TrackEvents.NO_DATA_FROM_SOURCE,
382
+                    APP.UI.showTrackNotWorkingDialog.bind(null, track));
383
+            });
384
+            return tracks;
385
+        }).catch(function (err) {
299 386
             console.error(
300 387
                 'failed to create local tracks', options.devices, err);
301 388
             return Promise.reject(err);
@@ -358,6 +445,14 @@ class ConferenceConnector {
358 445
         case ConferenceErrors.PASSWORD_REQUIRED:
359 446
             APP.UI.markRoomLocked(true);
360 447
             roomLocker.requirePassword().then(function () {
448
+                let pass = roomLocker.password;
449
+                // we received that password is required, but user is trying
450
+                // anyway to login without a password, mark room as not locked
451
+                // in case he succeeds (maybe someone removed the password
452
+                // meanwhile), if it is still locked another password required
453
+                // will be received and the room again will be marked as locked
454
+                if (!pass)
455
+                    APP.UI.markRoomLocked(false);
361 456
                 room.join(roomLocker.password);
362 457
             });
363 458
             break;
@@ -369,6 +464,13 @@ class ConferenceConnector {
369 464
             }
370 465
             break;
371 466
 
467
+        case ConferenceErrors.NOT_ALLOWED_ERROR:
468
+            {
469
+                // let's show some auth not allowed page
470
+                window.location.pathname = "authError.html";
471
+            }
472
+            break;
473
+
372 474
         case ConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE:
373 475
             APP.UI.notifyBridgeDown();
374 476
             break;
@@ -649,6 +751,61 @@ export default {
649 751
         return this._room
650 752
             && this._room.getConnectionState();
651 753
     },
754
+    /**
755
+     * Checks whether or not our connection is currently in interrupted and
756
+     * reconnect attempts are in progress.
757
+     *
758
+     * @returns {boolean} true if the connection is in interrupted state or
759
+     * false otherwise.
760
+     */
761
+    isConnectionInterrupted () {
762
+        return connectionIsInterrupted;
763
+    },
764
+    /**
765
+     * Finds JitsiParticipant for given id.
766
+     *
767
+     * @param {string} id participant's identifier(MUC nickname).
768
+     *
769
+     * @returns {JitsiParticipant|null} participant instance for given id or
770
+     * null if not found.
771
+     */
772
+    getParticipantById (id) {
773
+        return room ? room.getParticipantById(id) : null;
774
+    },
775
+    /**
776
+     * Checks whether the user identified by given id is currently connected.
777
+     *
778
+     * @param {string} id participant's identifier(MUC nickname)
779
+     *
780
+     * @returns {boolean|null} true if participant's connection is ok or false
781
+     * if the user is having connectivity issues.
782
+     */
783
+    isParticipantConnectionActive (id) {
784
+        let participant = this.getParticipantById(id);
785
+        return participant ? participant.isConnectionActive() : null;
786
+    },
787
+    /**
788
+     * Gets the display name foe the <tt>JitsiParticipant</tt> identified by
789
+     * the given <tt>id</tt>.
790
+     *
791
+     * @param id {string} the participant's id(MUC nickname/JVB endpoint id)
792
+     *
793
+     * @return {string} the participant's display name or the default string if
794
+     * absent.
795
+     */
796
+    getParticipantDisplayName (id) {
797
+        let displayName = getDisplayName(id);
798
+        if (displayName) {
799
+            return displayName;
800
+        } else {
801
+            if (APP.conference.isLocalId(id)) {
802
+                return APP.translation.generateTranslationHTML(
803
+                    interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
804
+            } else {
805
+                return interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
806
+            }
807
+        }
808
+    },
652 809
     getMyUserId () {
653 810
         return this._room
654 811
             && this._room.myUserId();
@@ -699,6 +856,30 @@ export default {
699 856
         return room.getLogs();
700 857
     },
701 858
 
859
+    /**
860
+     * Download logs, a function that can be called from console while
861
+     * debugging.
862
+     * @param filename (optional) specify target filename
863
+     */
864
+    saveLogs (filename = 'meetlog.json') {
865
+        // this can be called from console and will not have reference to this
866
+        // that's why we reference the global var
867
+        let logs = APP.conference.getLogs();
868
+        let data = encodeURIComponent(JSON.stringify(logs, null, '  '));
869
+
870
+        let elem = document.createElement('a');
871
+
872
+        elem.download = filename;
873
+        elem.href = 'data:application/json;charset=utf-8,\n' + data;
874
+        elem.dataset.downloadurl
875
+            = ['text/json', elem.download, elem.href].join(':');
876
+        elem.dispatchEvent(new MouseEvent('click', {
877
+            view: window,
878
+            bubbles: true,
879
+            cancelable: false
880
+        }));
881
+    },
882
+
702 883
     /**
703 884
      * Exposes a Command(s) API on this instance. It is necessitated by (1) the
704 885
      * desire to keep room private to this instance and (2) the need of other
@@ -858,8 +1039,6 @@ export default {
858 1039
 
859 1040
         return promise.then(function () {
860 1041
             if (stream) {
861
-                stream.on(TrackEvents.TRACK_AUDIO_NOT_WORKING,
862
-                    APP.UI.showAudioNotWorkingDialog);
863 1042
                 return room.addTrack(stream);
864 1043
             }
865 1044
         }).then(() => {
@@ -1021,7 +1200,7 @@ export default {
1021 1200
 
1022 1201
             console.log('USER %s connnected', id, user);
1023 1202
             APP.API.notifyUserJoined(id);
1024
-            APP.UI.addUser(id, user.getDisplayName());
1203
+            APP.UI.addUser(user);
1025 1204
 
1026 1205
             // check the roles for the new user and reflect them
1027 1206
             APP.UI.updateUserRole(user);
@@ -1037,8 +1216,10 @@ export default {
1037 1216
         room.on(ConferenceEvents.USER_ROLE_CHANGED, (id, role) => {
1038 1217
             if (this.isLocalId(id)) {
1039 1218
                 console.info(`My role changed, new role: ${role}`);
1040
-                this.isModerator = room.isModerator();
1041
-                APP.UI.updateLocalRole(room.isModerator());
1219
+                if (this.isModerator !== room.isModerator()) {
1220
+                    this.isModerator = room.isModerator();
1221
+                    APP.UI.updateLocalRole(room.isModerator());
1222
+                }
1042 1223
             } else {
1043 1224
                 let user = room.getParticipantById(id);
1044 1225
                 if (user) {
@@ -1115,6 +1296,11 @@ export default {
1115 1296
             ConferenceEvents.LAST_N_ENDPOINTS_CHANGED, (ids, enteringIds) => {
1116 1297
             APP.UI.handleLastNEndpoints(ids, enteringIds);
1117 1298
         });
1299
+        room.on(
1300
+            ConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
1301
+            (id, isActive) => {
1302
+                APP.UI.participantConnectionStatusChanged(id, isActive);
1303
+        });
1118 1304
         room.on(ConferenceEvents.DOMINANT_SPEAKER_CHANGED, (id) => {
1119 1305
             if (this.isLocalId(id)) {
1120 1306
                 this.isDominantSpeaker = true;
@@ -1146,10 +1332,12 @@ export default {
1146 1332
         room.on(ConferenceEvents.CONNECTION_INTERRUPTED, () => {
1147 1333
             connectionIsInterrupted = true;
1148 1334
             ConnectionQuality.updateLocalConnectionQuality(0);
1335
+            APP.UI.showLocalConnectionInterrupted(true);
1149 1336
         });
1150 1337
 
1151 1338
         room.on(ConferenceEvents.CONNECTION_RESTORED, () => {
1152 1339
             connectionIsInterrupted = false;
1340
+            APP.UI.showLocalConnectionInterrupted(false);
1153 1341
         });
1154 1342
 
1155 1343
         room.on(ConferenceEvents.DISPLAY_NAME_CHANGED, (id, displayName) => {
@@ -1173,6 +1361,7 @@ export default {
1173 1361
             console.log("Received channel password lock change: ", state,
1174 1362
                 error);
1175 1363
             APP.UI.markRoomLocked(state);
1364
+            roomLocker.lockedElsewhere = state;
1176 1365
         });
1177 1366
 
1178 1367
         room.on(ConferenceEvents.USER_STATUS_CHANGED, function (id, status) {
@@ -1300,15 +1489,6 @@ export default {
1300 1489
                 && APP.UI.notifyInitiallyMuted();
1301 1490
         });
1302 1491
 
1303
-        APP.UI.addListener(UIEvents.USER_INVITED, (roomUrl) => {
1304
-            APP.UI.inviteParticipants(
1305
-                roomUrl,
1306
-                APP.conference.roomName,
1307
-                roomLocker.password,
1308
-                APP.settings.getDisplayName()
1309
-            );
1310
-        });
1311
-
1312 1492
         room.on(
1313 1493
             ConferenceEvents.AVAILABLE_DEVICES_CHANGED, function (id, devices) {
1314 1494
                 APP.UI.updateDevicesAvailability(id, devices);
@@ -1395,6 +1575,8 @@ export default {
1395 1575
 
1396 1576
         APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (smallVideo, isPinned) => {
1397 1577
             var smallVideoId = smallVideo.getId();
1578
+            // FIXME why VIDEO_CONTAINER_TYPE instead of checking if
1579
+            // the participant is on the large video ?
1398 1580
             if (smallVideo.getVideoType() === VIDEO_CONTAINER_TYPE
1399 1581
                 && !APP.conference.isLocalId(smallVideoId)) {
1400 1582
 
@@ -1422,7 +1604,7 @@ export default {
1422 1604
                 .then(([stream]) => {
1423 1605
                     this.useVideoStream(stream);
1424 1606
                     console.log('switched local video device');
1425
-                    APP.settings.setCameraDeviceId(cameraDeviceId);
1607
+                    APP.settings.setCameraDeviceId(cameraDeviceId, true);
1426 1608
                 })
1427 1609
                 .catch((err) => {
1428 1610
                     APP.UI.showDeviceErrorDialog(null, err);
@@ -1444,7 +1626,7 @@ export default {
1444 1626
                 .then(([stream]) => {
1445 1627
                     this.useAudioStream(stream);
1446 1628
                     console.log('switched local audio device');
1447
-                    APP.settings.setMicDeviceId(micDeviceId);
1629
+                    APP.settings.setMicDeviceId(micDeviceId, true);
1448 1630
                 })
1449 1631
                 .catch((err) => {
1450 1632
                     APP.UI.showDeviceErrorDialog(err, null);
@@ -1539,13 +1721,13 @@ export default {
1539 1721
                 // storage and settings menu. This is a workaround until
1540 1722
                 // getConstraints() method will be implemented in browsers.
1541 1723
                 if (localAudio) {
1542
-                    localAudio._setRealDeviceIdFromDeviceList(devices);
1543
-                    APP.settings.setMicDeviceId(localAudio.getDeviceId());
1724
+                    APP.settings.setMicDeviceId(
1725
+                        localAudio.getDeviceId(), false);
1544 1726
                 }
1545 1727
 
1546 1728
                 if (localVideo) {
1547
-                    localVideo._setRealDeviceIdFromDeviceList(devices);
1548
-                    APP.settings.setCameraDeviceId(localVideo.getDeviceId());
1729
+                    APP.settings.setCameraDeviceId(
1730
+                        localVideo.getDeviceId(), false);
1549 1731
                 }
1550 1732
 
1551 1733
                 mediaDeviceHelper.setCurrentMediaDevices(devices);
@@ -1646,11 +1828,28 @@ export default {
1646 1828
     setRaisedHand(raisedHand) {
1647 1829
         if (raisedHand !== this.isHandRaised)
1648 1830
         {
1831
+            APP.UI.onLocalRaiseHandChanged(raisedHand);
1832
+
1649 1833
             this.isHandRaised = raisedHand;
1650 1834
             // Advertise the updated status
1651 1835
             room.setLocalParticipantProperty("raisedHand", raisedHand);
1652 1836
             // Update the view
1653 1837
             APP.UI.setLocalRaisedHandStatus(raisedHand);
1654 1838
         }
1839
+    },
1840
+    /**
1841
+     * Log event to callstats and analytics.
1842
+     * @param {string} name the event name
1843
+     * @param {int} value the value (it's int because google analytics supports
1844
+     * only int).
1845
+     * NOTE: Should be used after conference.init
1846
+     */
1847
+    logEvent(name, value) {
1848
+        if(JitsiMeetJS.analytics) {
1849
+            JitsiMeetJS.analytics.sendEvent(name, value);
1850
+        }
1851
+        if(room) {
1852
+            room.sendApplicationLog(JSON.stringify({name, value}));
1853
+        }
1655 1854
     }
1656 1855
 };

+ 2
- 0
config.js Näytä tiedosto

@@ -56,6 +56,8 @@ var config = {
56 56
     //disableAdaptiveSimulcast: false,
57 57
     enableRecording: false,
58 58
     enableWelcomePage: true,
59
+    //enableClosePage: false, // enabling the close page will ignore the welcome
60
+                              // page redirection when call is hangup
59 61
     disableSimulcast: false,
60 62
     logStats: false, // Enable logging of PeerConnection stats via the focus
61 63
 //    requireDisplayName: true, // Forces the participants that doesn't have display name to enter it when they enter the room.

BIN
css/.DS_Store Näytä tiedosto


+ 12
- 15
css/_base.scss Näytä tiedosto

@@ -81,24 +81,10 @@ form {
81 81
     display: block;
82 82
 }
83 83
 
84
-#downloadlog {
85
-    display: none;
86
-    position: absolute;
87
-    bottom: 5;
88
-    left: 5;
89
-    overflow: visible;
90
-    color: rgba(255,255,255,.50);
91
-}
92
-
93 84
 .active {
94 85
     background-color: #00ccff;
95 86
 }
96 87
 
97
-.glow
98
-{
99
-    text-shadow: 0px 0px 30px #06a5df, 0px 0px 10px #06a5df, 0px 0px 5px #06a5df,0px 0px 3px #06a5df;
100
-}
101
-
102 88
 .watermark {
103 89
     display: block;
104 90
     position: absolute;
@@ -175,4 +161,15 @@ form {
175 161
     display: -ms-flexbox !important;
176 162
     display: -webkit-flex !important;
177 163
     display: flex !important;
178
-}
164
+}
165
+
166
+.tipsy {
167
+    z-index: $tooltipsZ;
168
+    &-inner {
169
+        background-color: $tooltipBg;
170
+    }
171
+
172
+    &-arrow {
173
+        border-color: $tooltipBg;
174
+    }
175
+}

+ 0
- 5
css/_chat.scss Näytä tiedosto

@@ -104,11 +104,6 @@
104 104
     color: #a7a7a7;
105 105
 }
106 106
 
107
-#unreadMessages {
108
-    font-size: 8px;
109
-    position: absolute;
110
-}
111
-
112 107
 #chat_container .username {
113 108
     float: left;
114 109
     padding-left: 5px;

+ 4
- 5
css/_contact_list.scss Näytä tiedosto

@@ -2,12 +2,11 @@
2 2
     cursor: default;
3 3
 
4 4
     > ul#contacts {
5
-        position: absolute;
6
-        top: 31px;
5
+        font-size: 12px;
7 6
         bottom: 0px;
8
-        width: 100%;
9 7
         margin: 0px;
10 8
         padding: 0px;
9
+        width: 100%;
11 10
         overflow-y: scroll;
12 11
         overflow-x: hidden;
13 12
     }
@@ -20,13 +19,13 @@
20 19
 #contacts {
21 20
 
22 21
     >li {
22
+        color: $defaultSideBarFontColor;
23 23
         list-style-type: none;
24 24
         text-align: left;
25 25
         white-space: nowrap;
26 26
         color: #FFF;
27 27
         font-size: 10pt;
28
-        padding: 7px 10px;
29
-        margin: 2px;
28
+        padding: 6px 10%;
30 29
 
31 30
         &:hover,
32 31
         &:active {

+ 0
- 6
css/_font-awesome.scss Näytä tiedosto

@@ -255,9 +255,6 @@
255 255
 .fa-road:before {
256 256
   content: "\f018";
257 257
 }
258
-.fa-download:before {
259
-  content: "\f019";
260
-}
261 258
 .fa-arrow-circle-o-down:before {
262 259
   content: "\f01a";
263 260
 }
@@ -842,9 +839,6 @@
842 839
 .fa-exchange:before {
843 840
   content: "\f0ec";
844 841
 }
845
-.fa-cloud-download:before {
846
-  content: "\f0ed";
847
-}
848 842
 .fa-cloud-upload:before {
849 843
   content: "\f0ee";
850 844
 }

+ 12
- 7
css/_font.scss Näytä tiedosto

@@ -16,8 +16,9 @@
16 16
     font-weight: normal;
17 17
     font-variant: normal;
18 18
     text-transform: none;
19
-    line-height: 0.75em;
19
+    line-height: 1.22em;
20 20
     font-size: 1.22em;
21
+    cursor: default;
21 22
 
22 23
     /* Better Font Rendering =========== */
23 24
     -webkit-font-smoothing: antialiased;
@@ -42,9 +43,6 @@
42 43
 .icon-chat:before {
43 44
   content: "\e906";
44 45
 }
45
-.icon-download:before {
46
-  content: "\e902";
47
-}
48 46
 .icon-edit:before {
49 47
   content: "\e907";
50 48
 }
@@ -57,12 +55,21 @@
57 55
 .icon-kick:before {
58 56
   content: "\e904";
59 57
 }
58
+.icon-menu-up:before {
59
+  content: "\e91f";
60
+}
61
+.icon-menu-down:before {
62
+  content: "\e920";
63
+}
60 64
 .icon-full-screen:before {
61 65
   content: "\e90b";
62 66
 }
63 67
 .icon-exit-full-screen:before {
64 68
   content: "\e90c";
65 69
 }
70
+.icon-star:before {
71
+    content: "\e916";
72
+}
66 73
 .icon-star-full:before {
67 74
   content: "\e90a";
68 75
 }
@@ -99,9 +106,6 @@
99 106
 .icon-settings:before {
100 107
   content: "\e915";
101 108
 }
102
-.icon-star:before {
103
-  content: "\e916";
104
-}
105 109
 .icon-share-desktop:before {
106 110
   content: "\e917";
107 111
 }
@@ -126,6 +130,7 @@
126 130
 .icon-recEnable:before {
127 131
   content: "\e614";
128 132
 }
133
+// FIXME not used anymore - consider removing in the next font update
129 134
 .icon-presentation:before {
130 135
   content: "\e603";
131 136
 }

+ 2
- 1
css/_jquery-impromptu.scss Näytä tiedosto

@@ -9,7 +9,8 @@
9 9
 }
10 10
 div.jqi{ 
11 11
 	width: 400px;
12
-	position: absolute; 
12
+	position: absolute;
13
+    color: #3a3a3a;
13 14
 	background-color: #ffffff; 
14 15
 	font-size: 11px; 
15 16
 	text-align: left; 

+ 13
- 0
css/_mixins.scss Näytä tiedosto

@@ -36,6 +36,19 @@
36 36
   }
37 37
 }
38 38
 
39
+@mixin circle($diameter) {
40
+    width: $diameter;
41
+    height: $diameter;
42
+    border-radius: 50%;
43
+}
44
+
45
+@mixin absoluteAligning($sizeX, $sizeY) {
46
+    top: 50%;
47
+    left: 50%;
48
+    position: absolute;
49
+    @include transform(translate(-#{$sizeX / 2}, -#{$sizeY / 2}))
50
+}
51
+
39 52
 @mixin transform($func) {
40 53
     -moz-transform: $func;
41 54
     -ms-transform: $func;

+ 12
- 0
css/_redirect_page.scss Näytä tiedosto

@@ -0,0 +1,12 @@
1
+html, body {
2
+    width: 100%;
3
+    height:100%;
4
+    color: $defaultColor;
5
+    background: $defaultBackground;
6
+}
7
+
8
+.redirectPageMessage {
9
+    text-align: center;
10
+    font-size: 36px;
11
+    margin-top: 20%;
12
+}

css/_side_toolbar_container.css → css/_side_toolbar_container.scss Näytä tiedosto

@@ -12,12 +12,13 @@
12 12
     background-color: rgba(0,0,0,0.8);
13 13
     z-index: 800;
14 14
     overflow: hidden;
15
+    letter-spacing: 1px;
15 16
 
16 17
     /**
17 18
      * Labels inside the side panel.
18 19
      */
19 20
     label {
20
-        color: $defaultSemiDarkColor;
21
+        color: $defaultColor;
21 22
     }
22 23
 
23 24
     /**
@@ -70,16 +71,16 @@
70 71
          */
71 72
         > div.title,
72 73
           div.subTitle {
73
-            color: $defaultColor !important;
74 74
             text-align: left;
75 75
             margin: 10px 0px 10px 0px;
76
-            padding: 5px 10px 5px 10px;
77 76
         }
78 77
 
79 78
         /**
80 79
          * Main title size.
81 80
          */
82 81
         > div.title {
82
+            color: $defaultColor !important;
83
+            text-align: center;
83 84
             font-size: 16px;
84 85
         }
85 86
 
@@ -87,10 +88,10 @@
87 88
          * Subtitle specific properties.
88 89
          */
89 90
         > div.subTitle {
90
-            font-size: 12px;
91
-            background: $inputSemiBackground !important;
92
-            margin-top: 20px !important;
93
-            margin-bottom: 8px !important;
91
+            font-size: 11px;
92
+            font-weight: 500;
93
+            color: $defaultSideBarFontColor !important;
94
+            margin-left: 10%;
94 95
         }
95 96
 
96 97
         /**

+ 36
- 8
css/_toolbars.scss Näytä tiedosto

@@ -83,14 +83,6 @@
83 83
     display: none;
84 84
 }
85 85
 
86
-#numberOfParticipants {
87
-  position: absolute;
88
-  top: 5px;
89
-  line-height: 13px;
90
-  font-weight: bold;
91
-  font-size: 11px;
92
-}
93
-
94 86
 #mainToolbar a.button:last-child::after {
95 87
     content: none;
96 88
 }
@@ -118,6 +110,11 @@
118 110
     cursor: default;
119 111
 }
120 112
 
113
+.button.glow
114
+{
115
+    text-shadow: 0px 0px 5px $toolbarToggleBackground;
116
+}
117
+
121 118
 a.button.unclickable:hover,
122 119
 a.button.unclickable:active,
123 120
 a.button.unclickable.selected{
@@ -129,6 +126,7 @@ a.button:hover,
129 126
 a.button:active,
130 127
 a.button.selected {
131 128
     cursor: pointer;
129
+    text-decoration: none;
132 130
     // sum opacity with background layer should give us 0.8
133 131
     background: $toolbarSelectBackground;
134 132
 }
@@ -144,6 +142,36 @@ a.button>#avatar {
144 142
     margin-top: auto;
145 143
 }
146 144
 
145
+/**
146
+ * Round badge.
147
+ */
148
+.badge-round {
149
+    background-color: $toolbarBadgeBackground;
150
+    color: $toolbarBadgeColor;
151
+    font-size: 9px;
152
+    line-height: 13px;
153
+    font-weight: 700;
154
+    text-align: center;
155
+    border-radius: 50%;
156
+    min-width: 13px;
157
+    overflow: hidden;
158
+    text-overflow: ellipsis;
159
+    box-sizing: border-box;
160
+    vertical-align: middle;
161
+    // Do not inherit the font-family from the toolbar button, because it's an
162
+    // icon style.
163
+    font-family: $baseFontFamily;
164
+}
165
+
166
+/**
167
+ * Toolbar specific round badge.
168
+ */
169
+.toolbar .badge-round {
170
+    position: absolute;
171
+    right: 9px;
172
+    bottom: 9px;
173
+}
174
+
147 175
 /**
148 176
  * START of slide in animation for extended toolbar.
149 177
  */

+ 32
- 6
css/_variables.scss Näytä tiedosto

@@ -10,20 +10,49 @@ $hangupFontSize: 2em;
10 10
  */
11 11
 $defaultToolbarSize: 50px;
12 12
 
13
+// Video layout.
14
+$thumbnailIndicatorSize: 23px;
15
+$thumbnailIndicatorBorder: 0px;
16
+$thumbnailVideoMargin: 2px;
17
+$thumbnailToolbarHeight: 25px;
18
+
13 19
 /**
14 20
  * Color variables.
15 21
  */
16 22
 $defaultColor: #F1F1F1;
17
-$defaultSemiDarkColor: #ACACAC;
23
+$defaultSideBarFontColor: #44A5FF;
18 24
 $defaultDarkColor: #4F4F4F;
19 25
 $defaultBackground: #474747;
26
+$tooltipBg: rgba(0,0,0, 0.7);
27
+
28
+// Toolbar
20 29
 $toolbarSelectBackground: rgba(0, 0, 0, .6);
30
+
31
+$toolbarBadgeBackground: #165ECC;
32
+$toolbarBadgeColor: #FFFFFF;
33
+$toolbarToggleBackground: #165ECC;
34
+
35
+// Main controls
21 36
 $inputBackground: rgba(132, 132, 132, .5);
22 37
 $inputSemiBackground: rgba(132, 132, 132, .8);
23 38
 $inputLightBackground: #EBEBEB;
24 39
 $inputBorderColor: #EBEBEB;
25 40
 $buttonBackground: #44A5FF;
26 41
 
42
+// Video layout.
43
+$videoThumbnailHovered: #BFEBFF;
44
+$videoThumbnailSelected: #165ECC;
45
+$participantNameColor: #fff;
46
+$thumbnailPictogramColor: #fff;
47
+$dominantSpeakerBg: #165ecc;
48
+$raiseHandBg: #D6D61E;
49
+$audioLevelBg: #44A5FF;
50
+$audioLevelShadow: rgba(9, 36, 77, 0.9);
51
+
52
+$rateStarDefault: #ccc;
53
+$rateStarActivity: #165ecc;
54
+$rateStarLabelColor: #333;
55
+
27 56
 /**
28 57
  * Misc.
29 58
  */
@@ -33,9 +62,6 @@ $defaultWatermarkLink: '../images/watermark.png';
33 62
 /**
34 63
  * Z-indexes. TODO: Replace this by a function.
35 64
  */
65
+$tooltipsZ: 901;
36 66
 $toolbarZ: 900;
37
-$overlayZ: 800;
38
-
39
-$rateStarDefault: #ccc;
40
-$rateStarActivity: #f6c342;
41
-$rateStarLabelColor: #333;
67
+$overlayZ: 800;

+ 212
- 105
css/_videolayout_default.scss Näytä tiedosto

@@ -1,5 +1,10 @@
1
+#videoconference_page {
2
+    min-height: 100%;
3
+}
4
+
1 5
 #videospace {
2 6
     display: block;
7
+    min-height: 100%;
3 8
     position: absolute;
4 9
     top: 0px;
5 10
     left: 0px;
@@ -13,19 +18,19 @@
13 18
     display: -ms-flexbox;
14 19
     display: -webkit-flex;
15 20
     display: flex;
16
-    flex-direction: row;
21
+    flex-direction: row-reverse;
17 22
     flex-wrap: nowrap;
18
-    justify-content: flex-end;
23
+    justify-content: flex-start;
19 24
 
20 25
     position:absolute;
21 26
     text-align:right;
22 27
     height:196px;
23
-    padding: 18px;
28
+    padding: 10px 10px 10px 5px;
24 29
     bottom: 0;
25 30
     left: 0;
26
-    right: 20px;
31
+    right: 0;
27 32
     width:auto;
28
-    border:1px solid transparent;
33
+    border: 2px solid transparent;
29 34
     z-index: 5;
30 35
     transition: bottom 2s;
31 36
     overflow: visible !important;
@@ -43,33 +48,58 @@
43 48
 
44 49
 #remoteVideos .videocontainer {
45 50
     display: none;
51
+    position: relative;
46 52
     background-color: black;
47 53
     background-size: contain;
48 54
     border-radius:1px;
49
-    border: 1px solid #212425;
55
+    margin: 0 $thumbnailVideoMargin;
50 56
 }
51 57
 
52
-#remoteVideos .videocontainer.videoContainerFocused {
58
+/**
59
+ * The toolbar of the video thumbnail.
60
+ */
61
+.videocontainer__toolbar {
62
+    position: absolute;
63
+    bottom: 0;
64
+    left: 0;
65
+    z-index: 1;
66
+    width: 100%;
67
+    box-sizing: border-box; // Includes the padding in the 100% width.
68
+    height: $thumbnailToolbarHeight;
69
+    max-height: 100%;
70
+    background-color: rgba(0, 0, 0, 0.5);
71
+    padding: 0 5px 0 5px;
72
+}
73
+
74
+#remoteVideos .videocontainer.videoContainerFocused,
75
+#remoteVideos .videocontainer:hover {
53 76
     cursor: hand;
77
+    margin-right: $thumbnailVideoMargin - 2;
78
+    margin-left: $thumbnailVideoMargin - 2;
79
+    margin-top: -2px;
80
+}
81
+/**
82
+ * Focused video thumbnail.
83
+ */
84
+#remoteVideos .videocontainer.videoContainerFocused {
54 85
     transition-duration: 0.5s;
55 86
     -webkit-transition-duration: 0.5s;
56 87
     -webkit-animation-name: greyPulse;
57 88
     -webkit-animation-duration: 2s;
58 89
     -webkit-animation-iteration-count: 1;
90
+    border: 2px solid $videoThumbnailSelected !important;
91
+    box-shadow: inset 0 0 3px $videoThumbnailSelected,
92
+                0 0 3px $videoThumbnailSelected !important;
59 93
 }
60 94
 
95
+/**
96
+ * Hovered video thumbnail.
97
+ */
61 98
 #remoteVideos .videocontainer:hover {
62
-    border: 1px solid #c1c1c1;
63
-}
64
-
65
-#remoteVideos .videocontainer.videoContainerFocused {
66
-    box-shadow: inset 0 0 28px #006d91;
67
-    border: 1px solid #006d91;
68
-}
69
-
70
-#remoteVideos .videocontainer.videoContainerFocused:hover {
71
-    box-shadow: inset 0 0 5px #c1c1c1, 0 0 10px #c1c1c1, inset 0 0 60px #006d91;
72
-    border: 1px solid #c1c1c1;
99
+    cursor: hand;
100
+    border: 2px solid $videoThumbnailHovered;
101
+    box-shadow: inset 0 0 3px $videoThumbnailHovered,
102
+              0 0 3px $videoThumbnailHovered;
73 103
 }
74 104
 
75 105
 #localVideoWrapper {
@@ -113,7 +143,6 @@
113 143
     object-fit: cover;
114 144
 }
115 145
 
116
-#presentation,
117 146
 #sharedVideo,
118 147
 #etherpad,
119 148
 #localVideoWrapper>video,
@@ -132,8 +161,7 @@
132 161
     height: 100%;
133 162
 }
134 163
 
135
-#etherpad,
136
-#presentation {
164
+#etherpad {
137 165
     text-align: center;
138 166
 }
139 167
 
@@ -141,47 +169,36 @@
141 169
     z-index: 0;
142 170
 }
143 171
 
144
-#remoteVideos .videocontainer>span.focusindicator,
145
-#remoteVideos .videocontainer>div.remotevideomenu {
146
-    position: absolute;
147
-    color: #FFFFFF;
148
-    top: 0;
149
-    left: 0;
150
-    padding: 5px 0px;
151
-    width: 25px;
152
-    font-size: 11pt;
153
-    text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
154
-    border: 0px;
155
-    z-index: 2;
156
-    text-align: center;
157
-}
158
-
159
-#remoteVideos .videocontainer>span.focusindicator {
172
+/**
173
+ * Positions video thumbnail display name and editor.
174
+ */
175
+.videocontainer .displayname,
176
+.videocontainer .editdisplayname {
160 177
     display: inline-block;
161
-}
162
-
163
-#remoteVideos .videocontainer>div.remotevideomenu {
164
-    display: block;
165
-}
166
-
167
-.videocontainer>span.displayname,
168
-.videocontainer>input.displayname {
169
-    display: none;
170 178
     position: absolute;
171
-    color: #FFFFFF;
172
-    background: rgba(0,0,0,.7);
179
+    left: 30%;
180
+    width: 40%;
181
+    color: $participantNameColor;
173 182
     text-align: center;
174 183
     text-overflow: ellipsis;
175
-    width: 70%;
176
-    height: 20%;
177
-    left: 15%;
178
-    top: 40%;
179
-    padding: 5px;
180
-    font-size: 11pt;
184
+    font-size: 12px;
185
+    font-weight: 100;
186
+    letter-spacing: 1px;
181 187
     overflow: hidden;
182 188
     white-space: nowrap;
189
+    line-height: $thumbnailToolbarHeight;
183 190
     z-index: 2;
184
-    border-radius:3px;
191
+}
192
+
193
+/**
194
+ * Positions video thumbnail display name editor.
195
+ */
196
+.videocontainer .editdisplayname {
197
+    outline: none;
198
+    border: none;
199
+    background: none;
200
+    box-shadow: none;
201
+    padding: 0;
185 202
 }
186 203
 
187 204
 .videocontainer>span.status {
@@ -221,6 +238,12 @@
221 238
     overflow: hidden;
222 239
 }
223 240
 
241
+.connection.connection_lost
242
+{
243
+    color: #8B8B8B;
244
+    overflow: visible;
245
+}
246
+
224 247
 .connection.connection_full
225 248
 {
226 249
     color: #FFFFFF;/*#15A1ED*/
@@ -257,16 +280,16 @@
257 280
 }
258 281
 
259 282
 #localVideoContainer>span.status:hover,
260
-#localVideoContainer>span.displayname:hover {
283
+#localVideoContainer .displayname:hover {
261 284
     cursor: text;
262 285
 }
263 286
 
264 287
 .videocontainer>span.status,
265
-.videocontainer>span.displayname {
288
+.videocontainer .displayname {
266 289
     pointer-events: none;
267 290
 }
268 291
 
269
-.videocontainer>input.displayname {
292
+.videocontainer .editdisplayname {
270 293
     height: auto;
271 294
 }
272 295
 
@@ -287,53 +310,103 @@
287 310
     z-index: 2;
288 311
 }
289 312
 
290
-.videocontainer>span.audioMuted {
291
-    display: inline-block;
292
-    position: absolute;
293
-    color: #FFFFFF;
294
-    top: 0;
295
-    padding: 8px 5px;
296
-    width: 25px;
313
+/**
314
+ * Video thumbnail toolbar icon.
315
+ */
316
+.videocontainer .toolbar-icon {
297 317
     font-size: 8pt;
298
-    text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
299
-    border: 0px;
300
-    z-index: 3;
301 318
     text-align: center;
319
+    text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
320
+    color: #FFFFFF;
321
+    width: 12px;
322
+    line-height: $thumbnailToolbarHeight;
323
+    height: $thumbnailToolbarHeight;
324
+    padding: 0;
325
+    border: 0;
326
+    margin: 0px 5px 0px 0px;
327
+    float: left;
302 328
 }
303 329
 
304
-.videocontainer>span.videoMuted {
305
-    display: inline-block;
306
-    position: absolute;
307
-    color: #FFFFFF;
308
-    top: 0;
309
-    right: 0;
310
-    padding: 8px 5px;
311
-    width: 25px;
312
-    font-size: 8pt;
313
-    text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
314
-    border: 0px;
315
-    z-index: 3;
330
+/**
331
+ * Toolbar icon internal i elements (font icons).
332
+ */
333
+.toolbar-icon>i {
334
+    line-height: $thumbnailToolbarHeight;
335
+}
336
+
337
+/**
338
+ * Toolbar icons positioned on the right.
339
+ */
340
+.toolbar-icon.right {
341
+  float: right;
342
+  margin: 0px 0px 0px 5px;
316 343
 }
317 344
 
318 345
 .videocontainer>span.indicator {
319
-    bottom: 0px;
346
+    position: absolute;
347
+    top: 0px;
320 348
     left: 0px;
321
-    width: 25px;
322
-    height: 25px;
349
+    @include circle($thumbnailIndicatorSize);
350
+    box-sizing: border-box;
351
+    line-height: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
323 352
     z-index: 3;
324 353
     text-align: center;
325
-    border-radius: 50%;
326
-    background: #21B9FC;
327
-    margin: 5px;
354
+    background: $dominantSpeakerBg;
355
+    margin: 7px;
328 356
     display: inline-block;
357
+    color: $thumbnailPictogramColor;
358
+    font-size: 8pt;
359
+    border: $thumbnailIndicatorBorder solid $thumbnailPictogramColor;
360
+}
361
+
362
+.videocontainer>#raisehandindicator {
363
+    background: $raiseHandBg;
364
+}
365
+
366
+/**
367
+ * Audio indicator on video thumbnails.
368
+ */
369
+.videocontainer>span.audioindicator {
329 370
     position: absolute;
330
-    color: #FFFFFF;
331
-    font-size: 11pt;
332
-    border: 0px;
371
+    display: inline-block;
372
+    left: 6px;
373
+    top: 50%;
374
+    margin-top: -17px;
375
+    width: 6px;
376
+    height: 35px;
377
+    z-index: 2;
378
+    border: none;
379
+
380
+    .audiodot-top,
381
+    .audiodot-bottom,
382
+    .audiodot-middle {
383
+        opacity: 0;
384
+        display: inline-block;
385
+        @include circle(5px);
386
+        background: $audioLevelShadow;
387
+        margin: 1px 0 1px 0;
388
+        transition: opacity .25s ease-in-out;
389
+        -moz-transition: opacity .25s ease-in-out;
390
+    }
391
+
392
+    span.audiodot-top::after,
393
+    span.audiodot-bottom::after,
394
+    span.audiodot-middle::after {
395
+        content: "";
396
+        display: inline-block;
397
+        width: 5px;
398
+        height: 5px;
399
+        border-radius: 50%;
400
+        -webkit-filter: blur(0.5px);
401
+        filter: blur(0.5px);
402
+        background: $audioLevelBg;
403
+    }
333 404
 }
334 405
 
335 406
 #indicatoricon {
336
-    padding-top: 5px;
407
+    width: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
408
+    height: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
409
+    line-height: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
337 410
 }
338 411
 
339 412
 #reloadPresentation {
@@ -366,25 +439,20 @@
366 439
     width: 300px;
367 440
     height: 300px;
368 441
     margin: auto;
369
-    overflow: hidden;
370 442
     position: relative;
371 443
 }
372 444
 
373
-#dominantSpeakerAudioLevel {
374
-    position: absolute;
375
-    top: 0px;
376
-    left: 0px;
377
-    z-index: 2;
378
-    visibility: inherit;
379
-}
380
-
381 445
 #mixedstream {
382 446
     display:none !important;
383 447
 }
384 448
 
385
-#dominantSpeakerAvatar {
449
+#dominantSpeakerAvatar,
450
+.dynamic-shadow {
386 451
     width: 200px;
387 452
     height: 200px;
453
+}
454
+
455
+#dominantSpeakerAvatar {
388 456
     top: 50px;
389 457
     margin: auto;
390 458
     position: relative;
@@ -394,11 +462,18 @@
394 462
     background-color: #000000;
395 463
 }
396 464
 
397
-.userAvatar {
398
-    height: 100%;
465
+.dynamic-shadow {
466
+    border-radius: 50%;
399 467
     position: absolute;
400
-    left: 0;
401
-    border-radius: 2px;
468
+    top: 50%;
469
+    left: 50%;
470
+    margin: -100px 0 0 -100px;
471
+    transition: box-shadow 0.3s ease;
472
+}
473
+
474
+.userAvatar {
475
+    @include circle(60px);
476
+    @include absoluteAligning(60px, 60px);
402 477
 }
403 478
 
404 479
 .sharedVideoAvatar {
@@ -436,12 +511,44 @@
436 511
     filter: grayscale(.5) opacity(0.8);
437 512
 }
438 513
 
514
+.remoteVideoProblemFilter {
515
+    -webkit-filter: grayscale(100%);
516
+    filter: grayscale(100%);
517
+}
518
+
439 519
 .videoProblemFilter {
440 520
     -webkit-filter: blur(10px) grayscale(.5) opacity(0.8);
441 521
     filter: blur(10px) grayscale(.5) opacity(0.8);
442 522
 }
443 523
 
444
-#videoConnectionMessage {
524
+.videoThumbnailProblemFilter {
525
+    -webkit-filter: grayscale(100%);
526
+    filter: grayscale(100%);
527
+}
528
+
529
+#remoteConnectionMessage {
530
+    display: none;
531
+    position: absolute;
532
+    width: auto;
533
+    z-index: 1011;
534
+    font-weight: 600;
535
+    font-size: 14px;
536
+    text-align: center;
537
+    color: #FFF;
538
+    opacity: .80;
539
+    text-shadow:    0px 0px 1px rgba(0,0,0,0.3),
540
+                    0px 1px 1px rgba(0,0,0,0.3),
541
+                    1px 0px 1px rgba(0,0,0,0.3),
542
+                    0px 0px 1px rgba(0,0,0,0.3);
543
+
544
+    background: rgba(0,0,0,.5);
545
+    border-radius: 5px;
546
+    padding: 5px;
547
+    padding-left: 10px;
548
+    padding-right: 10px;
549
+}
550
+
551
+#localConnectionMessage {
445 552
     display: none;
446 553
     position: absolute;
447 554
     width: 100%;

+ 4
- 1
css/main.scss Näytä tiedosto

@@ -22,6 +22,8 @@
22 22
 @import 'toastr';
23 23
 @import 'base';
24 24
 @import 'overlay/overlay';
25
+@import 'modals/dialog';
26
+@import 'modals/feedback/feedback';
25 27
 @import 'videolayout_default';
26 28
 @import 'jquery-impromptu';
27 29
 @import 'modaldialog';
@@ -38,8 +40,9 @@
38 40
 @import 'toolbars';
39 41
 @import 'side_toolbar_container';
40 42
 @import 'device_settings_dialog';
41
-@import 'feedback';
42 43
 @import 'jquery.contextMenu';
43 44
 @import 'keyboard-shortcuts';
45
+@import 'redirect_page';
46
+
44 47
 
45 48
 /* Modules END */

+ 53
- 0
css/modals/_dialog.scss Näytä tiedosto

@@ -0,0 +1,53 @@
1
+.dialog{
2
+    visibility: visible;
3
+    height: auto;
4
+
5
+    p {
6
+        color: $defaultDarkColor;
7
+    }
8
+    textarea {
9
+        background: none;
10
+        border: 1px solid $inputBorderColor;
11
+    }
12
+    .aui-dialog2-content:last-child {
13
+        border-bottom-right-radius: 5px;
14
+        border-bottom-left-radius: 5px;
15
+    }
16
+    .aui-dialog2-content:first-child {
17
+        border-top-right-radius: 5px;
18
+        border-top-left-radius: 5px;
19
+    }
20
+    .aui-dialog2-footer{
21
+        border-top: 0;
22
+        border-radius: 0;
23
+        padding-top: 0;
24
+        background: none;
25
+        border: none;
26
+        height: auto;
27
+        margin-top: 10px;
28
+    }
29
+    .aui-button {
30
+        height: 28px;
31
+        font-size: 12px;
32
+        padding: 3px 6px 3px 6px;
33
+        border: none;
34
+        box-shadow: none;
35
+        outline: none;
36
+
37
+        &_close {
38
+            font-weight: 400 !important;
39
+            color: $buttonBackground;
40
+            background: none !important;
41
+
42
+            :hover {
43
+                text-decoration: underline;
44
+            }
45
+        }
46
+        &_submit {
47
+            font-weight: 700 !important;
48
+            color: $defaultColor;
49
+            background: $buttonBackground;
50
+            border-radius: 3px;
51
+        }
52
+    }
53
+}

css/_feedback.scss → css/modals/feedback/_feedback.scss Näytä tiedosto

@@ -33,6 +33,8 @@
33 33
 }
34 34
 
35 35
 .shake-rotate {
36
+    display: inline-block;
37
+
36 38
     -webkit-animation-duration: .4s;
37 39
     animation-duration: .4s;
38 40
     -webkit-animation-iteration-count: infinite;
@@ -43,65 +45,64 @@
43 45
     animation-timing-function: ease-in-out
44 46
 }
45 47
 
46
-.text-center {
47
-    text-align: center;
48
-}
49
-
50
-.feedbackDetails textarea {
51
-    resize: vertical;
52
-    min-height: 100px;
53
-}
54
-
55
-.feedback-rating {
56
-    line-height: 1.2;
57
-    padding: 20px 0;
58
-
48
+.feedback {
59 49
     h2 {
60 50
         font-weight: 400;
61 51
         font-size: 24px;
62 52
         line-height: 1.2;
63
-        padding: auto;
64
-        margin: auto;
65
-        border: none;
66 53
     }
67
-
68 54
     p {
69
-        margin-top: 10px;
70
-        margin-left: 0px;
71
-        margin-bottom: 0px;
72
-        margin-right: 0px;
55
+        font-weight: 400;
56
+        font-size: 14px;
73 57
     }
74 58
 
75
-    .star-label {
76
-        font-size: 16px;
77
-        color: $rateStarLabelColor;
59
+    &__content {
60
+        text-align: center;
61
+
62
+        textarea {
63
+            text-align: left;
64
+            min-height: 80px;
65
+            width: 100%;
66
+        }
78 67
     }
68
+    &__footer {
79 69
 
80
-    .star-btn {
81
-        color: $rateStarDefault;
82
-        font-size: 36px;
83
-        position: relative;
84
-        cursor: pointer;
85
-        outline: none;
86
-        text-decoration: none;
87
-        @include transition(all .2s ease);
88
-
89
-        &.starHover,
90
-        &.active,
91 70
         &:hover {
92
-            color: $rateStarActivity;
71
+            color: #287ade;
72
+            outline: 0;
73
+        }
74
+    }
75
+    &__rating {
76
+        line-height: 1.2;
77
+        padding: 20px 0;
93 78
 
94
-            .fa {
95
-                top: -6px;
96
-            }
97
-        };
79
+        p {
80
+            margin: 10px 0 0;
81
+        }
98 82
 
99
-        &.rated:hover .fa {
100
-            top: 0;
83
+        .star-label {
84
+            font-size: 16px;
85
+            color: $rateStarLabelColor;
101 86
         }
102 87
 
103
-        .fa {
88
+        .star-btn {
89
+            color: $rateStarDefault;
90
+            font-size: 36px;
104 91
             position: relative;
92
+            cursor: pointer;
93
+            outline: none;
94
+            text-decoration: none;
95
+            @include transition(all .2s ease);
96
+
97
+            &.starHover,
98
+            &.active,
99
+            &:hover {
100
+                color: $rateStarActivity;
101
+                > i:before {
102
+                    content: "\e90a";
103
+                }
104
+            };
105
+
105 106
         }
106 107
     }
107 108
 }

+ 6
- 1
css/ringing/_ringing.scss Näytä tiedosto

@@ -9,6 +9,11 @@
9 9
     background: linear-gradient(transparent, #000);
10 10
     opacity: 0.8;
11 11
 
12
+    &.solidBG {
13
+        background: $defaultBackground;
14
+        opacity: 1;
15
+    }
16
+
12 17
     &__content {
13 18
         position: absolute;
14 19
         width: 400px;
@@ -33,4 +38,4 @@
33 38
             color: #333;
34 39
         }
35 40
     }
36
-}
41
+}

BIN
fonts/jitsi.eot Näytä tiedosto


+ 2
- 0
fonts/jitsi.svg Näytä tiedosto

@@ -42,4 +42,6 @@
42 42
 <glyph unicode="&#xe91c;" glyph-name="toggle-filmstrip" d="M896 896h-768c-46.933 0-85.333-38.4-85.333-85.333v-597.333c0-46.933 38.4-85.333 85.333-85.333h768c46.933 0 85.333 38.4 85.333 85.333v597.333c0 46.933-38.4 85.333-85.333 85.333zM896 213.333h-768v128h768v-128z" />
43 43
 <glyph unicode="&#xe91d;" glyph-name="feedback" d="M42.667 128h170.667v512h-170.667v-512zM981.333 597.333c0 46.933-38.4 85.333-85.333 85.333h-269.227l40.533 194.987 1.28 13.653c0 17.493-7.253 33.707-18.773 45.227l-45.227 44.8-280.747-281.173c-15.787-15.36-25.173-36.693-25.173-60.16v-426.667c0-46.933 38.4-85.333 85.333-85.333h384c35.413 0 65.707 21.333 78.507 52.053l128.853 300.8c3.84 9.813 5.973 20.053 5.973 31.147v81.493l-0.427 0.427 0.427 3.413z" />
44 44
 <glyph unicode="&#xe91e;" glyph-name="raised-hand" d="M982 790v-620c0-94-78-170-172-170h-310c-46 0-90 18-122 50l-336 342s54 52 56 52c10 8 22 12 34 12 10 0 18-2 26-6 2 0 184-104 184-104v508c0 36 28 64 64 64s64-28 64-64v-300h42v406c0 36 28 64 64 64s64-28 64-64v-406h42v364c0 36 28 64 64 64s64-28 64-64v-364h44v236c0 36 28 64 64 64s64-28 64-64z" />
45
+<glyph unicode="&#xe91f;" glyph-name="menu-up" d="M512 682l256-256-60-60-196 196-196-196-60 60z" />
46
+<glyph unicode="&#xe920;" glyph-name="menu-down" d="M708 658l60-60-256-256-256 256 60 60 196-196z" />
45 47
 </font></defs></svg>

BIN
fonts/jitsi.ttf Näytä tiedosto


BIN
fonts/jitsi.woff Näytä tiedosto


+ 52
- 0
fonts/selection.json Näytä tiedosto

@@ -293,6 +293,58 @@
293 293
       "setId": 2,
294 294
       "iconIdx": 243
295 295
     },
296
+    {
297
+      "icon": {
298
+        "paths": [
299
+          "M512 342l256 256-60 60-196-196-196 196-60-60z"
300
+        ],
301
+        "isMulticolor": false,
302
+        "isMulticolor2": false,
303
+        "tags": [
304
+          "expand_less"
305
+        ],
306
+        "grid": 0,
307
+        "attrs": []
308
+      },
309
+      "attrs": [],
310
+      "properties": {
311
+        "id": 256,
312
+        "order": 106,
313
+        "ligatures": "expand_less",
314
+        "prevSize": 32,
315
+        "code": 59679,
316
+        "name": "menu-up"
317
+      },
318
+      "setIdx": 0,
319
+      "setId": 2,
320
+      "iconIdx": 257
321
+    },
322
+    {
323
+      "icon": {
324
+        "paths": [
325
+          "M708 366l60 60-256 256-256-256 60-60 196 196z"
326
+        ],
327
+        "isMulticolor": false,
328
+        "isMulticolor2": false,
329
+        "tags": [
330
+          "expand_more"
331
+        ],
332
+        "grid": 0,
333
+        "attrs": []
334
+      },
335
+      "attrs": [],
336
+      "properties": {
337
+        "id": 257,
338
+        "order": 107,
339
+        "ligatures": "expand_more",
340
+        "prevSize": 32,
341
+        "code": 59680,
342
+        "name": "menu-down"
343
+      },
344
+      "setIdx": 0,
345
+      "setId": 2,
346
+      "iconIdx": 258
347
+    },
296 348
     {
297 349
       "icon": {
298 350
         "paths": [

+ 26
- 22
index.html Näytä tiedosto

@@ -120,28 +120,32 @@
120 120
                     </li>
121 121
                 </ul>
122 122
             </span>
123
-            <a class="button icon-contactList" id="toolbar_contact_list" data-container="body" data-toggle="popover" data-placement="right" shortcut="contactlistpopover"  data-i18n="[content]bottomtoolbar.contactlist" content="Open / close contact list">
124
-                <span id="numberOfParticipants"></span>
123
+            <a class="button icon-contactList" id="toolbar_contact_list" shortcut="contactlistpopover">
124
+                <span class="badge-round">
125
+                    <span id="numberOfParticipants"></span>
126
+                </span>
125 127
             </a>
126
-            <!--a class="button icon-link" id="toolbar_button_link" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[content]toolbar.invite" content="Invite others"></a-->
127
-            <a class="button icon-chat" id="toolbar_button_chat" data-container="body" data-toggle="popover" shortcut="toggleChatPopover" data-placement="right" data-i18n="[content]toolbar.chat" content="Open / close chat">
128
-                <span id="unreadMessages"></span>
128
+            <!--a class="button icon-link" id="toolbar_button_link"></a-->
129
+            <a class="button icon-chat" id="toolbar_button_chat" shortcut="toggleChatPopover">
130
+                <span class="badge-round">
131
+                    <span id="unreadMessages"></span>
132
+                </span>
129 133
             </a>
130
-            <a class="button" id="toolbar_button_record" data-container="body" data-toggle="popover" data-placement="right" style="display: none"></a>
131
-            <a class="button icon-security" id="toolbar_button_security" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[content]toolbar.lock" content="Lock / unlock room"></a>
132
-            <a class="button icon-share-doc" id="toolbar_button_etherpad" data-container="body" data-toggle="popover" data-placement="right" content="Shared document" data-i18n="[content]toolbar.etherpad"></a>
133
-            <a class="button icon-shared-video" id="toolbar_button_sharedvideo" data-container="body" data-toggle="popover" data-placement="right" content="Share a YouTube video" data-i18n="[content]toolbar.sharedvideo" style="display: none">
134
+            <a class="button" id="toolbar_button_record" style="display: none"></a>
135
+            <a class="button icon-security" id="toolbar_button_security"></a>
136
+            <a class="button icon-share-doc" id="toolbar_button_etherpad"></a>
137
+            <a class="button icon-shared-video" id="toolbar_button_sharedvideo" style="display: none">
134 138
                 <ul id="sharedVideoMutedPopup" class="loginmenu extendedToolbarPopup">
135 139
                     <li data-i18n="[html]toolbar.sharedVideoMutedPopup"></li>
136 140
                 </ul>
137 141
             </a>
138
-            <a class="button icon-telephone" id="toolbar_button_sip" data-container="body" data-toggle="popover" data-placement="right" content="Call SIP number" data-i18n="[content]toolbar.sip" style="display: none"></a>
139
-            <a class="button icon-dialpad" id="toolbar_button_dialpad" data-container="body" data-toggle="popover" data-placement="right" content="Open dialpad" data-i18n="[content]toolbar.dialpad" style="display: none"></a>
140
-            <a class="button icon-settings" id="toolbar_button_settings" data-container="body" data-toggle="popover" data-placement="right" content="Settings" data-i18n="[content]toolbar.Settings"></a>
141
-            <a class="button icon-raised-hand" id="toolbar_button_raisehand" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[content]toolbar.raiseHand" content="Raise Hand" shortcut="raiseHandPopover"></a>
142
-            <a class="button icon-full-screen" id="toolbar_button_fullScreen" data-container="body" data-toggle="popover" data-placement="right" shortcut="toggleFullscreenPopover" data-i18n="[content]toolbar.fullscreen" content="Enter / Exit Full Screen"></a>
143
-            <a class="button icon-toggle-filmstrip" id="toolbar_film_strip" data-container="body" data-toggle="popover" shortcut="filmstripPopover" data-placement="right" data-i18n="[content]toolbar.filmstrip" content="Show / hide videos"></a>
144
-            <a class="button icon-feedback" id="feedbackButton" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[content]feedback"></a>
142
+            <a class="button icon-telephone" id="toolbar_button_sip" style="display: none"></a>
143
+            <a class="button icon-dialpad" id="toolbar_button_dialpad" style="display: none"></a>
144
+            <a class="button icon-settings" id="toolbar_button_settings"></a>
145
+            <a class="button icon-raised-hand" id="toolbar_button_raisehand" shortcut="raiseHandPopover"></a>
146
+            <a class="button icon-full-screen" id="toolbar_button_fullScreen" shortcut="toggleFullscreenPopover"></a>
147
+            <a class="button icon-toggle-filmstrip" id="toolbar_film_strip" data-container="body" shortcut="filmstripPopover"></a>
148
+            <a class="button icon-feedback" id="feedbackButton"></a>
145 149
             <div id="sideToolbarContainer">
146 150
                 <div id="profile_container" class="sideToolbarContainer__inner">
147 151
                     <div class="title" data-i18n="profile.title"></div>
@@ -208,13 +212,11 @@
208 212
                         <input type="checkbox" id="followMeCheckBox">
209 213
                         <label class="followMeLabel" for="followMeCheckBox" data-i18n="settings.followMe"></label>
210 214
                     </div>
211
-                    <a id="downloadlog" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[data-content]downloadlogs" ><i class="icon-download"></i></a>
212 215
                 </div>
213 216
             </div>
214 217
         </div>
215 218
         <div id="videospace">
216 219
             <div id="largeVideoContainer" class="videocontainer">
217
-                <div id="presentation"></div>
218 220
                 <div id="sharedVideo"><div id="sharedVideoIFrame"></div></div>
219 221
                 <div id="etherpad"></div>
220 222
                 <a target="_new"><div class="watermark leftwatermark"></div></a>
@@ -223,13 +225,14 @@
223 225
                     <span data-i18n="poweredby"></span> jitsi.org
224 226
                 </a>
225 227
                 <div id="dominantSpeaker">
228
+                    <div class="dynamic-shadow"></div>
226 229
                     <img id="dominantSpeakerAvatar" src=""/>
227
-                    <canvas id="dominantSpeakerAudioLevel"></canvas>
228 230
                 </div>
231
+                <span id="remoteConnectionMessage"></span>
229 232
                 <div id="largeVideoWrapper">
230 233
                     <video id="largeVideo" muted="true" autoplay></video>
231 234
                 </div>
232
-                <span id="videoConnectionMessage"></span>
235
+                <span id="localConnectionMessage"></span>
233 236
                 <span id="videoResolutionLabel">HD</span>
234 237
                 <span id="recordingLabel" class="centeredVideoLabel">
235 238
                     <span id="recordingLabelText"></span>
@@ -238,12 +241,12 @@
238 241
             </div>
239 242
 
240 243
             <div id="remoteVideos">
241
-                <span id="localVideoContainer" class="videocontainer">
244
+                <span id="localVideoContainer" class="videocontainer videocontainer_small">
242 245
                     <span id="localVideoWrapper">
243 246
                         <!--<video id="localVideo" autoplay muted></video> - is now per stream generated -->
244 247
                     </span>
245 248
                     <audio id="localAudio" autoplay muted></audio>
246
-                    <span class="focusindicator"></span>
249
+                    <div class="videocontainer__toolbar"></div>
247 250
                 </span>
248 251
                 <audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
249 252
                 <audio id="userLeft" src="sounds/left.wav" preload="auto"></audio>
@@ -257,5 +260,6 @@
257 260
             </ul>
258 261
         </div>
259 262
     </div>
263
+    <div id="aui-feedback-dialog" class="dialog feedback aui-layer aui-dialog2 aui-dialog2-medium" style="display: none;"></div>
260 264
   </body>
261 265
 </html>

+ 10
- 1
interface_config.js Näytä tiedosto

@@ -34,5 +34,14 @@ var interfaceConfig = {
34 34
     filmStripOnly: false,
35 35
     RANDOM_AVATAR_URL_PREFIX: false,
36 36
     RANDOM_AVATAR_URL_SUFFIX: false,
37
-    FILM_STRIP_MAX_HEIGHT: 120
37
+    FILM_STRIP_MAX_HEIGHT: 120,
38
+    LOCAL_THUMBNAIL_RATIO_WIDTH: 16,
39
+    LOCAL_THUMBNAIL_RATIO_HEIGHT: 9,
40
+    REMOTE_THUMBNAIL_RATIO_WIDTH: 1,
41
+    REMOTE_THUMBNAIL_RATIO_HEIGHT: 1,
42
+    // Enables feedback star animation.
43
+    ENABLE_FEEDBACK_ANIMATION: false,
44
+    DISABLE_FOCUS_INDICATOR: false,
45
+    AUDIO_LEVEL_PRIMARY_COLOR: "rgba(255,255,255,0.7)",
46
+    AUDIO_LEVEL_SECONDARY_COLOR: "rgba(255,255,255,0.4)"
38 47
 };

+ 2
- 0
lang/languages-de.json Näytä tiedosto

@@ -7,7 +7,9 @@
7 7
     "hy": "Armenisch",
8 8
     "it": "Italienisch",
9 9
     "oc": "Okzitanisch",
10
+    "pl": "Polnisch",
10 11
     "ptBR": "Portugiesisch (Brasilien)",
12
+    "ru": "Russisch",
11 13
     "sk": "Slowakisch",
12 14
     "sl": "Slowenisch",
13 15
     "sv": "Schwedisch",

+ 15
- 0
lang/languages-pl.json Näytä tiedosto

@@ -0,0 +1,15 @@
1
+{
2
+    "en": "Angielski",
3
+    "bg": "Bułgarski",
4
+    "de": "Niemiecki",
5
+    "es": "Hiszpański",
6
+    "fr": "Francuski",
7
+    "hy": "Ormiański",
8
+    "it": "Włoski",
9
+    "oc": "Prowansalski",
10
+    "ptBR": "portugalski (brazylijski)",
11
+    "sk": "Słowacki",
12
+    "sl": "Słoweński",
13
+    "sv": "Szwedzki",
14
+    "tr": "Turecki"
15
+}

+ 2
- 0
lang/languages-ptBR.json Näytä tiedosto

@@ -7,7 +7,9 @@
7 7
     "hy": "Armênio",
8 8
     "it": "Italiano",
9 9
     "oc": "Provençal",
10
+    "pl": "Polonês",
10 11
     "ptBR": "Português (Brasil)",
12
+    "ru": "Russo",
11 13
     "sk": "Eslovaco",
12 14
     "sl": "Esloveno",
13 15
     "sv": "Sueco",

+ 2
- 0
lang/languages.json Näytä tiedosto

@@ -8,7 +8,9 @@
8 8
     "hy": "Armenian",
9 9
     "it": "Italian",
10 10
     "oc": "Occitan",
11
+    "pl": "Polish",
11 12
     "ptBR": "Portuguese (Brazil)",
13
+    "ru": "Russian",
12 14
     "sk": "Slovak",
13 15
     "sl": "Slovenian",
14 16
     "sv": "Swedish",

+ 57
- 40
lang/main-de.json Näytä tiedosto

@@ -1,17 +1,15 @@
1 1
 {
2
-    "contactlist": "Kontaktliste",
2
+    "contactlist": "Im Gespräch",
3 3
     "connectionsettings": "Verbindungseinstellungen",
4 4
     "poweredby": "Betrieben von",
5
-    "downloadlogs": "Log herunterladen",
6 5
     "feedback": "Wir freuen uns auf Ihr Feedback!",
7 6
     "roomUrlDefaultMsg": "Die Konferenz wird erstellt...",
8
-    "participant": "Teilnehmer",
9 7
     "me": "ich",
10 8
     "speaker": "Sprecher",
11 9
     "raisedHand": "Möchte sprechen",
12 10
     "defaultNickname": "Bsp: Heidi Blau",
13 11
     "defaultLink": "Bsp.: __url__",
14
-    "calling": "Rufe __name__ an...",
12
+    "callingName": "__name__",
15 13
     "userMedia": {
16 14
         "react-nativeGrantPermissions": "Bitte Berechtigungen zur Verwendung der Kamera und des Mikrofons durch anwählen von <b><i>Erlauben</i></b>",
17 15
         "chromeGrantPermissions": "Bitte Berechtigungen zur Verwendung der Kamera und des Mikrofons durch anwählen von <b><i>Erlauben</i></b>",
@@ -27,7 +25,7 @@
27 25
         "raiseHand": "Heben Sie Ihre Hand.",
28 26
         "pushToTalk": "Drücken um zu sprechen.",
29 27
         "toggleScreensharing": "Zwischen Kamera und Bildschirmfreigabe wechseln.",
30
-        "toggleFilmstrip": "Videovorschau anzeigen oder verstecken.",
28
+        "toggleFilmstrip": "Videos anzeigen oder verbergen.",
31 29
         "toggleShortcuts": "Hilfe-Menu anzeigen oder verdecken.",
32 30
         "focusLocal": "Lokales Video fokussieren.",
33 31
         "focusRemote": "Andere Videos fokussieren.",
@@ -37,7 +35,7 @@
37 35
     },
38 36
     "welcomepage": {
39 37
         "go": "Los",
40
-        "roomname": "Raumnamen eingeben",
38
+        "roomname": "Konferenzname eingeben",
41 39
         "disable": "Diesen Hinweis nicht mehr anzeigen",
42 40
         "feature1": {
43 41
             "title": "Einfach zu benutzen",
@@ -49,7 +47,7 @@
49 47
         },
50 48
         "feature3": {
51 49
             "title": "Open Source",
52
-            "content": "__app__ steht unter der Apache Lizenz. Es steht ihnen frei __app__ gemäß dieser Lizenz herunterzuladen, zu verändern oder zu verbreiten."
50
+            "content": "__app__ steht unter der Apache Lizenz. Es steht ihnen frei __app__ gemäss dieser Lizenz herunterzuladen, zu verändern oder zu verbreiten."
53 51
         },
54 52
         "feature4": {
55 53
             "title": "Unbegrenzte Anzahl Benutzer",
@@ -76,16 +74,16 @@
76 74
         "mute": "Stummschaltung aktivieren / deaktivieren",
77 75
         "videomute": "Kamera starten / stoppen",
78 76
         "authenticate": "Anmelden",
79
-        "lock": "Raum schützen / Schutz aufheben",
77
+        "lock": "Konferenz schützen / Schutz aufheben",
80 78
         "invite": "Andere einladen",
81
-        "chat": "Chat öffnen / schließen",
82
-        "etherpad": "Geteiltes Dokument",
83
-        "sharedvideo": "Ein YouTube-Video teilen",
79
+        "chat": "Chat öffnen / schliessen",
80
+        "etherpad": "Dokument teilen",
81
+        "sharedvideo": "YouTube-Video teilen",
84 82
         "sharescreen": "Bildschirm freigeben",
85 83
         "fullscreen": "Vollbildmodus aktivieren / deaktivieren",
86 84
         "sip": "SIP Nummer anrufen",
87 85
         "Settings": "Einstellungen",
88
-        "hangup": "Auflegen",
86
+        "hangup": "Konferenz verlassen",
89 87
         "login": "Anmelden",
90 88
         "logout": "Abmelden",
91 89
         "dialpad": "Tastenblock anzeigen",
@@ -93,17 +91,19 @@
93 91
         "micMutedPopup": "Ihr Mikrofon wurde stumm geschaltet damit das<br/>geteilte Video genossen werden kann.",
94 92
         "unableToUnmutePopup": "Die Stummschaltung kann nicht aufgehoben werden während das geteilte Video abgespielt wird.",
95 93
         "cameraDisabled": "Keine Kamera verfügbar",
96
-        "micDisabled": "Kein Mikrofon verfügbar"
94
+        "micDisabled": "Kein Mikrofon verfügbar",
95
+        "filmstrip": "Videos anzeigen / verbergen",
96
+        "raiseHand": "Hand erheben um zu sprechen"
97 97
     },
98 98
     "bottomtoolbar": {
99
-        "chat": "Chat öffnen / schließen",
100
-        "filmstrip": "Videovorschau anzeigen / verstecken",
101
-        "contactlist": "Kontaktliste öffnen / schließen"
99
+        "chat": "Chat öffnen / schliessen",
100
+        "filmstrip": "Videos anzeigen / verbergen",
101
+        "contactlist": "Kontaktliste öffnen / schliessen"
102 102
     },
103 103
     "chat": {
104 104
         "nickname": {
105
-            "title": "Nickname im Eingabefeld eingeben",
106
-            "popover": "Einen Namen auswählen"
105
+            "title": "Name eingeben",
106
+            "popover": "Name"
107 107
         },
108 108
         "messagebox": "Text eingeben..."
109 109
     },
@@ -111,20 +111,29 @@
111 111
         "title": "Einstellungen",
112 112
         "update": "Aktualisieren",
113 113
         "name": "Name",
114
-        "startAudioMuted": "Stumm beitreten",
115
-        "startVideoMuted": "Ohne Video beitreten",
116
-        "selectCamera": "Kamera auswählen",
117
-        "selectMic": "Mikrofon auswählen",
118
-        "selectAudioOutput": "Audio-Ausgabe auswählen",
119
-        "followMe": "Follow-me aktivieren",
114
+        "startAudioMuted": "Alle Teilnehmer treten stumm geschaltet bei",
115
+        "startVideoMuted": "Alle Teilnehmer treten ohne Video bei",
116
+        "selectCamera": "Kamera",
117
+        "selectMic": "Mikrofon",
118
+        "selectAudioOutput": "Audioausgabe",
119
+        "followMe": "Follow-me für alle Teilnehmer",
120 120
         "noDevice": "Kein",
121 121
         "noPermission": "Keine Berechtigung um das Gerät zu verwenden",
122
-        "avatarUrl": "Avatar URL"
122
+        "cameraAndMic": "Kamera und Mikrofon",
123
+        "moderator": "MODERATOR",
124
+        "password": "PASSWORT SETZEN",
125
+        "audioVideo": "AUDIO UND VIDEO",
126
+        "setPasswordLabel": "Konferenz mit einem Passwort schützen."
127
+    },
128
+    "profile": {
129
+        "title": "PROFIL",
130
+        "setDisplayNameLabel": "Anzeigename festlegen",
131
+        "setEmailLabel": "E-Mail Adresse für Gravatar"
123 132
     },
124 133
     "videothumbnail": {
125 134
         "editnickname": "Klicken, um den Anzeigenamen zu bearbeiten",
126 135
         "moderator": "Besitzer dieser Konferenz",
127
-        "videomute": "Teilnehmer hat die Kamera pausiert.",
136
+        "videomute": "Teilnehmer hat die Kamera pausiert",
128 137
         "mute": "Teilnehmer ist stumm geschaltet",
129 138
         "kick": "Hinauswerfen",
130 139
         "muted": "Stummgeschaltet",
@@ -172,6 +181,7 @@
172 181
         "connectError": "Oh! Es hat etwas nicht geklappt und der Konferenz konnte nicht beigetreten werden.",
173 182
         "connectErrorWithMsg": "Oh! Es hat etwas nicht geklappt und der Konferenz konnte nicht beigetreten werden: __msg__",
174 183
         "connecting": "Verbindung wird hergestellt",
184
+        "copy": "Kopieren",
175 185
         "error": "Fehler",
176 186
         "detectext": "Fehler bei der Erkennung der Bildschirmfreigabeerweiterung.",
177 187
         "failtoinstall": "Die Bildschirmfreigabeerweiterung konnte nicht installiert werden.",
@@ -183,8 +193,8 @@
183 193
         "lockMessage": "Die Konferenz konnte nicht gesperrt werden.",
184 194
         "warning": "Warnung",
185 195
         "passwordNotSupported": "Passwörter für Räume werden nicht unterstützt.",
186
-        "sorry": "Entschuldigung",
187
-        "internalError": "Interner Anwendungsfehler [setRemoteDescription]",
196
+        "internalErrorTitle": "Interner Fehler",
197
+        "internalError": "Ups! Es ist etwas schiefgegangen. Der Fehler [setRemoteDescription] ist aufgetreten.",
188 198
         "unableToSwitch": "Der Videodatenstrom kann nicht gewechselt werden.",
189 199
         "SLDFailure": "Oh! Die Stummschaltung konnte nicht aktiviert werden. (SLD Fehler)",
190 200
         "SRDFailure": "Oh! Das Video konnte nicht gestoppt werden. (SRD Fehler)",
@@ -206,7 +216,7 @@
206 216
         "logoutTitle": "Abmelden",
207 217
         "logoutQuestion": "Sind Sie sicher, dass Sie sich abmelden und die Konferenz verlassen möchten?",
208 218
         "sessTerminated": "Sitzung beendet",
209
-        "hungUp": "Anruf beendet",
219
+        "hungUp": "Konferenz beendet",
210 220
         "joinAgain": "Erneut beitreten",
211 221
         "Share": "Teilen",
212 222
         "Save": "Speichern",
@@ -215,27 +225,29 @@
215 225
         "Dial": "Wählen",
216 226
         "sipMsg": "Geben Sie eine SIP Nummer ein",
217 227
         "passwordCheck": "Sind Sie sicher, dass Sie das Passwort entfernen möchten?",
218
-        "passwordMsg": "Passwort setzen, um den Raum zu schützen",
219
-        "Invite": "Einladen",
220
-        "shareLink": "Teilen Sie diesen Link mit jedem den Sie einladen möchten",
228
+        "passwordMsg": "Passwort setzen um die Konferenz zu schützen",
229
+        "shareLink": "Diesen Link kopieren und teilen",
221 230
         "settings1": "Konferenz einrichten",
222 231
         "settings2": "Teilnehmer treten stummgeschaltet bei",
223
-        "settings3": "Nickname erforderlich<br/><br/>Setzen Sie ein Passwort, um den Raum zu schützen:",
224
-        "yourPassword": "Ihr Passwort",
232
+        "settings3": "Name erforderlich<br/><br/>Setzen Sie ein Passwort, um die Konferenz zu schützen:",
233
+        "yourPassword": "Neues Passwort eingeben",
225 234
         "Back": "Zurück",
226 235
         "serviceUnavailable": "Dienst nicht verfügbar",
227 236
         "gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
228 237
         "Yes": "Ja",
229 238
         "reservationError": "Fehler im Reservationssystem",
230 239
         "reservationErrorMsg": "Fehler, Nummer: __code__, Nachricht: __msg__",
231
-        "password": "Passwort",
240
+        "password": "Passwort eingeben",
232 241
         "userPassword": "Benutzerpasswort",
233 242
         "token": "Token",
234
-        "tokenAuthFailed": "Anmeldung am XMPP-Server fehlgeschlagen: ungültiges Token",
243
+        "tokenAuthFailedTitle": "Authentifizierungsfehler",
244
+        "tokenAuthFailed": "Sie sind nicht berechtigt dieser Konferenz beizutreten.",
235 245
         "displayNameRequired": "Geben Sie Ihren Anzeigenamen ein",
236 246
         "extensionRequired": "Erweiterung erforderlich:",
237 247
         "firefoxExtensionPrompt": "Um die Bildschirmfreigabe nutzen zu können, muss eine Firefox-Erweiterung installiert werden. Bitte versuchen Sie es erneut nachdem die <a href='__url__'>Erweiterung installiert</a> wurde.",
238
-        "feedbackQuestion": "Wie war der Anruf?",
248
+        "rateExperience": "Bitte bewerten Sie diese Konferenz.",
249
+        "feedbackHelp": "Ihr Feedback hilft uns die Qualität der Konferenzen zu verbessern.",
250
+        "feedbackQuestion": "Anmerkungen zur Konferenz.",
239 251
         "thankYou": "Danke für die Verwendung von __appName__!",
240 252
         "sorryFeedback": "Tut uns leid. Möchten Sie uns mehr mitteilen?",
241 253
         "liveStreaming": "Live-Streaming",
@@ -253,12 +265,17 @@
253 265
         "cameraUnsupportedResolutionError": "Die Kamera unterstützt die erforderliche Auflösung nicht.",
254 266
         "cameraUnknownError": "Die Kamera kann aus einem unbekannten Grund nicht verwendet werden.",
255 267
         "cameraPermissionDeniedError": "Die Berechtigung zur Verwendung der Kamera wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht sehen. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste um die Berechtigungen zu erteilen.",
256
-        "cameraNotFoundError": "Die Berechtigung zur Verwendung der Kamera wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht sehen. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste um die Berechtigungen zu erteilen.",
268
+        "cameraNotFoundError": "Kamera nicht gefunden.",
257 269
         "cameraConstraintFailedError": "Ihre Kamera erfüllt die notwendigen Anforderungen nicht.",
258 270
         "micUnknownError": "Das Mikrofon kann aus einem unbekannten Grund nicht verwendet werden.",
259 271
         "micPermissionDeniedError": "Die Berechtigung zur Verwendung des Mikrofons wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht hören. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste um die Berechtigungen zu erteilen.",
260
-        "micNotFoundError": "Das angeforderte Mikrofon konnte nicht gefunden werden.",
261
-        "micConstraintFailedError": "Ihr Mikrofon erfüllt die notwendigen Anforderungen nicht."
272
+        "micNotFoundError": "Mikrofon nicht gefunden.",
273
+        "micConstraintFailedError": "Ihr Mikrofon erfüllt die notwendigen Anforderungen nicht.",
274
+        "micNotSendingData": "Das Mikrofon kann nicht verwendet werden. Bitte wählen Sie ein anderes Mikrofon in den Einstellungen oder laden Sie die Konferenz neu.",
275
+        "cameraNotSendingData": "Die Kamera kann nicht verwendet werden. Bitte wählen Sie eine andere Kamera in den Einstellungen oder laden Sie die Konferenz neu.",
276
+        "goToStore": "Zum Store",
277
+        "externalInstallationTitle": "Erweiterung erforderlich",
278
+        "externalInstallationMsg": "Die Bildschirmfreigabeerweiterung muss installiert werden."
262 279
     },
263 280
     "\u0005dialog": {},
264 281
     "email": {

+ 344
- 0
lang/main-pl.json Näytä tiedosto

@@ -0,0 +1,344 @@
1
+{
2
+    "contactlist": "w trakcie rozmowy",
3
+    "connectionsettings": "ustawienia połączenia",
4
+    "poweredby": "Uruchomiono",
5
+    "feedback": "jaka jest twoja opinia ?",
6
+    "roomUrlDefaultMsg": "otwarto twoją konferencję",
7
+    "me": "to ja",
8
+    "speaker": "głośnik",
9
+    "raisedHand": "Chcesz się odezwać ?",
10
+    "defaultNickname": "np. Ziutek Kowalski",
11
+    "defaultLink": "np. _url_",
12
+    "callingName": "_nazwa_",
13
+    "userMedia": {
14
+        "react-nativeGrantPermissions": "",
15
+        "chromeGrantPermissions": "",
16
+        "androidGrantPermissions": "",
17
+        "firefoxGrantPermissions": "wyraź zgodę na użycie kamery i mikrofonu naciskając <b><i>Share Selected Device</i></b> przycisk",
18
+        "operaGrantPermissions": "wyraź zgodę na użycie kamery i mikrofonu naciskając <b><i>Allow</i></b> przycisk",
19
+        "iexplorerGrantPermissions": "",
20
+        "safariGrantPermissions": "wyraź zgodę na użycie kamery i mikrofonu naciskając <b><i>OK</i></b> przycisk",
21
+        "nwjsGrantPermissions": "wyraź zgodę na użycie kamery i mikrofonu"
22
+    },
23
+    "keyboardShortcuts": {
24
+        "keyboardShortcuts": "Skróty klawiaturowe:",
25
+        "raiseHand": "Unieś rękę.",
26
+        "pushToTalk": "naciśnij i mów",
27
+        "toggleScreensharing": "Przełączanie pomiędzy kamerą i wspóldzieleniem ekranu",
28
+        "toggleFilmstrip": "Pokaż lub ukryj klipy wideo",
29
+        "toggleShortcuts": "Pokaż lub ukryj pasek pomocy.",
30
+        "focusLocal": "Przełącz na lokalne wideo.",
31
+        "focusRemote": "Przełącz na któreś ze zdalnych wideo.",
32
+        "toggleChat": "Otwórz lub zamknij panel czat.",
33
+        "mute": "Wyłącz lub włącz mikrofon.",
34
+        "videoMute": "Start lub stop lokalne wideo."
35
+    },
36
+    "welcomepage": {
37
+        "go": "IDŹ",
38
+        "roomname": "Podaj nazwę sali konferencyjnej",
39
+        "disable": "nie pokazuj ponownie",
40
+        "feature1": {
41
+            "title": "użyj",
42
+            "content": "nie musisz nic pobierać. _app_ jest gotowa do użycia bezpośrednio w przeglądarce. Zaproś innych do udziału w konferencji podając adres URL"
43
+        },
44
+        "feature2": {
45
+            "title": "za mała przepustowość",
46
+            "content": "Dla konferencji video potrzeba nie więcej niż 128 kbit/sek. Konferencje dzielenia ekranu lub tylko audio są możliwe przy mniejszej przepustowości. "
47
+        },
48
+        "feature3": {
49
+            "title": "Open source",
50
+            "content": "_app_ oparta jest na Apache License. Możesz swobodnie pobierać ją, używać, modyfikować i dzielić się nią."
51
+        },
52
+        "feature4": {
53
+            "title": "Nieograniczona liczba użytkowników",
54
+            "content": "Liczba użytkowników czy uczestników konferencji nie jest ograniczona.  Determinuje ją moc serwera i dostępna przepustowość lącza."
55
+        },
56
+        "feature5": {
57
+            "title": "Współdzielenie ekranu",
58
+            "content": "Z łatwością podzielisz się ekranem z innymi. _app_ jest idealnym narzędziem do prezentacji, nauczania i udzielania zdalnej pomocy technicznej."
59
+        },
60
+        "feature6": {
61
+            "title": "Sale bezpieczne.",
62
+            "content": "Potrzebujesz prywatności? _app_ sale konferencyjne mogą być zabezpieczone hasłami niedopuszczającymi niezaproszonych uczestników czy też osoby chcące zakłócić konferencję."
63
+        },
64
+        "feature7": {
65
+            "title": "Współdzielenie uwag.",
66
+            "content": "_app_ zawiera Etherpad, współdzielony edytor tekstu doskonały dla redakcji zespołowych artykułów czy komentarzy."
67
+        },
68
+        "feature8": {
69
+            "title": "Statystyki użycia.",
70
+            "content": "Analizuj uczestników konferencji z łatwościa integrując dane z Piwik i Google Analitics i innymi systemami monitorującymi."
71
+        }
72
+    },
73
+    "toolbar": {
74
+        "mute": "Wycisz / Pogłośnij",
75
+        "videomute": "Kamera start / stop ",
76
+        "authenticate": "Uwierzytelnianie",
77
+        "lock": "Zamknij / Otwórz salę",
78
+        "invite": "Zaproś innych",
79
+        "chat": "",
80
+        "etherpad": "Udostępniaj dokument",
81
+        "sharedvideo": "Udostępniaj wideo w Youtube",
82
+        "sharescreen": "Udostępnij ekran",
83
+        "fullscreen": "Otwórz / Zamknij pełny ekran",
84
+        "sip": "Wykręć numer SIP",
85
+        "Settings": "",
86
+        "hangup": "Rozłącz",
87
+        "login": "Zaloguj",
88
+        "logout": "",
89
+        "dialpad": "Wyświetl panel wybierania",
90
+        "sharedVideoMutedPopup": "Współdzielone wideo zostało wyciszone i <br/> możesz zacząć rozmawiać z innymi.",
91
+        "micMutedPopup": "Mikrofon został wyłączony i <br/> możesz spokojnie konsumować współdzielone wideo",
92
+        "unableToUnmutePopup": "Nie możesz pogłośnić audio podczas współużytkowania wideo",
93
+        "cameraDisabled": "Kamera nie jest dostępna",
94
+        "micDisabled": "Mikrofon nie jest dostępny",
95
+        "filmstrip": "",
96
+        "raiseHand": "Podnieś rękę chcąc zabrać głos"
97
+    },
98
+    "bottomtoolbar": {
99
+        "chat": "Otwórz / Zamknij Czat",
100
+        "filmstrip": "Pokaż / Ukryj klipy wideo",
101
+        "contactlist": "Otwórz / Zamknij spis kontaktów"
102
+    },
103
+    "chat": {
104
+        "nickname": {
105
+            "title": "Podaj swój nick poniżej",
106
+            "popover": "Wybierz swój nick"
107
+        },
108
+        "messagebox": "Umieść tekst...."
109
+    },
110
+    "settings": {
111
+        "title": "Ustawienia",
112
+        "update": "Aktualizacja",
113
+        "name": "Nazwa",
114
+        "startAudioMuted": "Wszyscy się wyciszyli",
115
+        "startVideoMuted": "Wszyscy się ukryli",
116
+        "selectCamera": "Kamera",
117
+        "selectMic": "Mikrofon",
118
+        "selectAudioOutput": "Wyjście audio",
119
+        "followMe": "Wszyscy za mną",
120
+        "noDevice": "Brak",
121
+        "noPermission": "Nie ma zgody na użycie urządzenia",
122
+        "cameraAndMic": "Kamera i Mikrofon",
123
+        "moderator": "MODERATOR",
124
+        "password": "USTAW HASŁO",
125
+        "audioVideo": "AUDIO I WIDEO",
126
+        "setPasswordLabel": "Zamknij salę konferencyjną z hasłem"
127
+    },
128
+    "profile": {
129
+        "title": "PROFIL",
130
+        "setDisplayNameLabel": "Podaj swoją wyświetlaną nazwę",
131
+        "setEmailLabel": "Ustaw email swojego gravatara"
132
+    },
133
+    "videothumbnail": {
134
+        "editnickname": "Kliknij <br/>celem edycji swojej nazwy",
135
+        "moderator": "Gospodarz <br/>tej konferencji",
136
+        "videomute": "Uczestnik <br/>wyłączyl kamerę",
137
+        "mute": "Uczestnik ma wyciszone audio",
138
+        "kick": "Kick out",
139
+        "muted": "Wyciszony",
140
+        "domute": "Wyciszenie",
141
+        "flip": "Odwrócenie"
142
+    },
143
+    "connectionindicator": {
144
+        "bitrate": "Szybkość transmisji:",
145
+        "packetloss": "Strata pakietów:",
146
+        "resolution": "Rozdzielczość:",
147
+        "less": "Pokaż mniej",
148
+        "more": "Pokaż więcej",
149
+        "address": "Adres:",
150
+        "remoteport": "Zdalny port:Zdalne porty:",
151
+        "remoteport_plural_2": "",
152
+        "remoteport_plural_5": "",
153
+        "localport": "Lokalny port:Lokalne porty:",
154
+        "localport_plural_2": "",
155
+        "localport_plural_5": "",
156
+        "localaddress": "Lokalny adres:Lokalne Adresy:",
157
+        "localaddress_plural_2": "",
158
+        "localaddress_plural_5": "",
159
+        "remoteaddress": "Zdalny adres:Zdalne adresy:",
160
+        "remoteaddress_plural_2": "",
161
+        "remoteaddress_plural_5": "",
162
+        "transport": "Przekazywanie:",
163
+        "bandwidth": "Zakładana przepustowość:",
164
+        "na": "Po informację o połączeniu wróć gdy wystartuje konferencja"
165
+    },
166
+    "notify": {
167
+        "disconnected": "rozłączone",
168
+        "moderator": "Prawa moderatora przydzielone!",
169
+        "connected": "połączono",
170
+        "somebody": "Ktoś",
171
+        "me": "To ja",
172
+        "focus": "Fokus konferencji",
173
+        "focusFail": "_składnik_nie dostępny - zastosuj w _ms_sek",
174
+        "grantedTo": "Prawa moderatora przyznane _to_!",
175
+        "grantedToUnknown": "Prawa Moderatora przyznane $t(somebody)!",
176
+        "muted": "Masz wyciszony mikrofon",
177
+        "mutedTitle": "Jesteś wyciszony!",
178
+        "raisedHand": "Możesz mówić."
179
+    },
180
+    "dialog": {
181
+        "kickMessage": "Ocho! Zostałeś wyproszony z konferencji!",
182
+        "popupError": "Twoja przeglądarka blokuje wyskakujące okienka z tej witryny. Proszę, zmień w ustawieniach przeglądarki.",
183
+        "passwordError": "Ta konwersacja aktualnie jest zabezpieczona hasłem. Tylko gospodarz konferencji może zakładać hasło.",
184
+        "passwordError2": "Ta rozmowa nie jest zabezpieczona hasłem. Tylko gospodarz konferencji może ustanowić hasło zabezpieczające.",
185
+        "connectError": "Ocho! Cos poszło nie tak, nie można podłaczyć się do tej konferencji.",
186
+        "connectErrorWithMsg": "Ocho! Coś poszło nie tak i nie można podłączyć się do tej konferencji:_msg_",
187
+        "connecting": "",
188
+        "copy": "Kopiuj",
189
+        "error": "",
190
+        "detectext": "Błąd podczas rozpoznania rozszerzenia wspóldzielenia ekranu.",
191
+        "failtoinstall": "Instalacja współdzielenia ekranu nie powiodła się.",
192
+        "failedpermissions": "Brak akceptacji dla użycia kamery i mikrofonu",
193
+        "bridgeUnavailable": "Jitsi Videobridge aktualnie jest niedostępne. Proszę, spróbuj później!",
194
+        "jicofoUnavailable": "Jicofo jest aktualnie niedostępne. Proszę, spróbuj później!",
195
+        "maxUsersLimitReached": "Osiągnięto max liczbę uczestników konferencji. Proszę spróbuj później! ",
196
+        "lockTitle": "Nie powiodło się zabezpieczenie konferencji",
197
+        "lockMessage": "Zabezpieczenie konferencji nie powiodło się.",
198
+        "warning": "Uwaga",
199
+        "passwordNotSupported": "Hasła sali konferencyjnych są aktualnie niedostępne.",
200
+        "internalErrorTitle": "Błąd wewnętrzny",
201
+        "internalError": "Ocho! coś poszło nie tak. Wystąpił błąd:  [setRemoteDescription]",
202
+        "unableToSwitch": "Nie można przełaczyć na strumień wideo",
203
+        "SLDFailure": "Ocho! Coś poszło nie tak i nie można wyciszyć! (SLD Failure)",
204
+        "SRDFailure": "Ocho! Coś poszło nie tak i nie można zatrzymać wideo! (SRD Failure)",
205
+        "oops": "Ups",
206
+        "defaultError": "Wystąpił jakiś błąd",
207
+        "passwordRequired": "Wymagane hasło",
208
+        "Ok": "Ok",
209
+        "Remove": "Usuń",
210
+        "shareVideoTitle": "Współdziel wideo",
211
+        "shareVideoLinkError": "Podaj proszę prawidłowy link youtube.",
212
+        "removeSharedVideoTitle": "Usuń wideo współdzielone",
213
+        "removeSharedVideoMsg": "Na pewno chcesz usunąć współdzielone wideo?",
214
+        "alreadySharedVideoMsg": "Inny uczestnik aktualnie współdzieli wideo. W tej konferencji tylko jedno wideo może być współdzielone.",
215
+        "WaitingForHost": "Oczekiwanie na komputer",
216
+        "WaitForHostMsg": "Konferencja <b>_room_</b> jeszcze nie wystartowała. Jeśli jesteś gospodarzem podaj dane autentykacji. Jeśli nie czekaj na gospodarza.",
217
+        "IamHost": "Jestem gospodarzem",
218
+        "Cancel": "Anuluj",
219
+        "retry": "Ponów",
220
+        "logoutTitle": "Wyloguj",
221
+        "logoutQuestion": "Na pewno chcesz się wylogować i zakończyć konferencję?",
222
+        "sessTerminated": "Sesja zakończona",
223
+        "hungUp": "Przerwałeś połączenie",
224
+        "joinAgain": "Ponownie przystąp",
225
+        "Share": "Współdziel",
226
+        "Save": "Zapisz",
227
+        "recording": "",
228
+        "recordingToken": "Proszę podać token nagrywania",
229
+        "Dial": "Dzwoń",
230
+        "sipMsg": "Podaj numer SIP",
231
+        "passwordCheck": "Czy na pewno chcesz usunąć swoje hasło ?",
232
+        "passwordMsg": "Podaj hasło aby zabezpieczyć salę konferencyjną",
233
+        "shareLink": "Skopiuj i udostępnij ten link",
234
+        "settings1": "Skonfiguruj swoją konferencję",
235
+        "settings2": "Wyciszenie współuczestników",
236
+        "settings3": "Wymagane nicki <br/><br/>Wprowadź hasło dla zabezpieczenia sali konferencyjnej:",
237
+        "yourPassword": "Proszę wprowadzić nowe hasło",
238
+        "Back": "Wstecz",
239
+        "serviceUnavailable": "Usługa jest niedostępna",
240
+        "gracefulShutdown": "Aktualnie serwis jest konserwowany. Prosze spróbować później.",
241
+        "Yes": "Tak",
242
+        "reservationError": "Błąd systemu rezerwacji",
243
+        "reservationErrorMsg": "Kod błędu: _code_, treść: _msg_",
244
+        "password": "Podaj hasło",
245
+        "userPassword": "hasło użytkownika",
246
+        "token": "token",
247
+        "tokenAuthFailedTitle": "Problem uwierzytelnienia",
248
+        "tokenAuthFailed": "Przepraszam, ale nie jesteś upoważniony do uczestnictwa w tym połączeniu",
249
+        "displayNameRequired": "Wprowadź swoją nazwę użytkownika",
250
+        "extensionRequired": "Wymagane jest rozszerzenie:",
251
+        "firefoxExtensionPrompt": "Potrzebujesz zainstalować rozszerzenie firefox aby móc współdzielić ekran. Spróbuj ponownie później <a href='__url__'>weź z</a>!",
252
+        "rateExperience": "Oceń proszę swoje doświadczenia z konferencji.",
253
+        "feedbackHelp": "Twoja opinia będzie pomocna w usprawnieniu naszego serwisu.",
254
+        "feedbackQuestion": "Powiedz nam o twoim połączeniu!",
255
+        "thankYou": "Dziękujemy Ci za używanie _appName_!",
256
+        "sorryFeedback": "Przykro nam to słyszeć. Czy możesz powiedzieć więcej na ten temat?",
257
+        "liveStreaming": "",
258
+        "streamKey": "Nazwa strumienia/klucz",
259
+        "startLiveStreaming": "Uruchom strumień live",
260
+        "stopStreamingWarning": "Czy jesteś pewny, że chcesz zatrzymać ten strumień live?",
261
+        "stopRecordingWarning": "Naprawdę chcesz zatrzymać nagrywanie?",
262
+        "stopLiveStreaming": "Zatrzymaj transmisję live",
263
+        "stopRecording": "Zatrzymaj nagrywanie",
264
+        "doNotShowWarningAgain": "Nie pokazuj tego ostrzeżenia ponownie",
265
+        "permissionDenied": "Brak uprawnień",
266
+        "screenSharingPermissionDeniedError": "Nie posiadasz uprawnień do współdzielenia ekranu.",
267
+        "micErrorPresent": "Wystąpił błąd w dostępie do mikrofonu.",
268
+        "cameraErrorPresent": "Wystąpił błąd w dostępie do twojej kamery.",
269
+        "cameraUnsupportedResolutionError": "Twoja kamera nie obsługuje wymaganej rozdzielczości.",
270
+        "cameraUnknownError": "Z nieznanej przyczyny nie można użyć kamery ",
271
+        "cameraPermissionDeniedError": "Nie udzieliłeś pozwolenia na użycie twojej kamery. Nadal możesz włączyć się do konferencji ale inni nie będą cię widzieli. Naciśnij przycisk kamera w pasku menu aby użyć właściwą kamerę. ",
272
+        "cameraNotFoundError": "Kamera nie znaleziona.",
273
+        "cameraConstraintFailedError": "Twoja kamera nie spełnia wymagań.",
274
+        "micUnknownError": "Z przyczyn nieznanych nie można użyć mikrofonu. ",
275
+        "micPermissionDeniedError": "Nie udzieliłeś pozwolenia na użycie twojego mikrofonu. Nadal możesz uczestniczyc w konferencji ale inni nie będą cię słyszeli. Użyj przycisku kamera aby to naprawić.",
276
+        "micNotFoundError": "Mikrofon nie jest odnaleziony.",
277
+        "micConstraintFailedError": "Twój mikrofon nie obsługuje wymaganych parametrów.",
278
+        "micNotSendingData": "Nie możemy mieć dostępu do twojego mikrofonu. Proszę, wskaż inne urządzenie lub przeładuj aplikację.",
279
+        "cameraNotSendingData": "Nie możemy mieć dostępu do twojej kamery. Sprawdź czy inna aplikacja nie używa twojej kamery, wybierz inne urządzenie lub ponownie uruchom aplikację.",
280
+        "goToStore": "Idź do sklepu",
281
+        "externalInstallationTitle": "Wymagane rozszerzenie",
282
+        "externalInstallationMsg": "Zainstaluj rozszerzenie naszego współdzielenia ekranu."
283
+    },
284
+    "email": {
285
+        "sharedKey": [
286
+            "Ta konferencja jest zabezpieczona hasłem. Aby się podłączyć proszę zastosuj następujący pin:",
287
+            "",
288
+            "",
289
+            "_sharedKey_",
290
+            "",
291
+            " "
292
+        ],
293
+        "subject": "Zaproszenie do a_appName_(_conferenceName_)",
294
+        "body": [
295
+            "Witaj, I%27 zaprasza cię do udziału w konferencji_appName_.",
296
+            "",
297
+            "",
298
+            "Kliknij na poniższy link aby uczestniczyć w konferencji.",
299
+            "",
300
+            "",
301
+            "_roomUrl_",
302
+            "",
303
+            "",
304
+            "_sharedKeyTex_",
305
+            "Zauważ, że -appName_ możesz używać tylko przy pomocy _supportedBrowsers_.",
306
+            "",
307
+            "",
308
+            "Polączymy się błyskawicznie! "
309
+        ],
310
+        "and": "i"
311
+    },
312
+    "connection": {
313
+        "ERROR": "Błąd",
314
+        "CONNECTING": "Nawiązywanie połączenia",
315
+        "RECONNECTING": "Wystąpił problem w sieci. Ponowienie połaczenia....",
316
+        "CONNFAIL": "Połączenie się nie powiodło",
317
+        "AUTHENTICATING": "Uwierzytelnianie",
318
+        "AUTHFAIL": "Uwierzytelnianie nie powiodło się",
319
+        "CONNECTED": "Połączono",
320
+        "DISCONNECTED": "Rozłączony",
321
+        "DISCONNECTING": "Rozłączanie",
322
+        "ATTACHED": "Załącznik"
323
+    },
324
+    "recording": {
325
+        "pending": "Nagrywanie oczekiwanie na uczestników konferencji.....",
326
+        "on": "Nagrywanie",
327
+        "off": "Nagrywanie zatrzymane",
328
+        "failedToStart": "Nagrywanie nie jest możliwe",
329
+        "buttonTooltip": "Nagrywanie start / stop",
330
+        "error": "Nagranie się nie powiodło. Proszę, spróbuj ponownie.",
331
+        "unavailable": "Serwis nagrywania jest aktualnie niedostępny. Proszę, spróbować później."
332
+    },
333
+    "liveStreaming": {
334
+        "pending": "Start strumieniowania live...",
335
+        "on": "Strumień live",
336
+        "off": "Strumieniowanie live zastopowane",
337
+        "unavailable": "Strumieniowanie live aktualnie jest niedostepne. Proszę spróbować później.",
338
+        "failedToStart": "Strumieniowanie live nie powiodło się",
339
+        "buttonTooltip": "Strumieniowanie live start / stop",
340
+        "streamIdRequired": "Proszę podaj id strumieniowania aby uruchomić live.",
341
+        "error": "Strumieniowanie live nie powiodło się. Spróbuj później.",
342
+        "busy": "Wszystkie nagrywarki są zajęte. Proszę, sprawdź ponownie później."
343
+    }
344
+}

+ 43
- 26
lang/main-ptBR.json Näytä tiedosto

@@ -1,17 +1,15 @@
1 1
 {
2
-    "contactlist": "LISTA DE CONTATO",
2
+    "contactlist": "Na chamada",
3 3
     "connectionsettings": "Configurações de conexão",
4 4
     "poweredby": "distribuído por",
5
-    "downloadlogs": "Baixar registros",
6 5
     "feedback": "Dê seus comentários",
7 6
     "roomUrlDefaultMsg": "Sua conferência está sendo criado...",
8
-    "participant": "Participante",
9 7
     "me": "eu",
10 8
     "speaker": "Orador",
11 9
     "raisedHand": "Gostaria de falar",
12 10
     "defaultNickname": "ex. João Pedro",
13 11
     "defaultLink": "i.e. __url__",
14
-    "calling": "Chamando __name__ ...",
12
+    "callingName": "__name__",
15 13
     "userMedia": {
16 14
         "react-nativeGrantPermissions": "Dê as permissões para usar sua câmera e microfone pressionando o botão <b> <i>Permitir</i> </b>",
17 15
         "chromeGrantPermissions": "Dê as permissões para usar sua câmera e microfone pressionando o botão <b> <i>Permitir</i> </b>",
@@ -27,7 +25,7 @@
27 25
         "raiseHand": "Erguer sua mão.",
28 26
         "pushToTalk": "Pressione para falar.",
29 27
         "toggleScreensharing": "Trocar entre câmera e compartilhamento de tela.",
30
-        "toggleFilmstrip": "Mostrar ou ocultar a tira de filme.",
28
+        "toggleFilmstrip": "Mostrar ou ocultar os vídeos.",
31 29
         "toggleShortcuts": "Mostrar ou ocultar este menu de ajuda.",
32 30
         "focusLocal": "Foco no vídeo local.",
33 31
         "focusRemote": "Foco em um dos vídeos remotos.",
@@ -93,11 +91,13 @@
93 91
         "micMutedPopup": "Seu microfone está mudo assim que você<br/>pode curtir plenamente seu vídeo compartilhado.",
94 92
         "unableToUnmutePopup": "Você não pode sair do mudo enquanto seu vídeo compartilhado está ativo.",
95 93
         "cameraDisabled": "A câmera não está disponível",
96
-        "micDisabled": "O microfone não está disponível"
94
+        "micDisabled": "O microfone não está disponível",
95
+        "filmstrip": "",
96
+        "raiseHand": "Levantar a mão para falar"
97 97
     },
98 98
     "bottomtoolbar": {
99 99
         "chat": "Abrir / fechar bate-papo",
100
-        "filmstrip": "Mostrar / ocultar a tira de usuários",
100
+        "filmstrip": "Mostrar/ocultar vídeos",
101 101
         "contactlist": "Abrir / fechar a lista de contatos"
102 102
     },
103 103
     "chat": {
@@ -108,18 +108,27 @@
108 108
         "messagebox": "Digite um texto..."
109 109
     },
110 110
     "settings": {
111
-        "title": "CONFIGURAÇÕES",
111
+        "title": "Configurações",
112 112
         "update": "Atualizar",
113 113
         "name": "Nome",
114
-        "startAudioMuted": "Iniciar sem áudio",
115
-        "startVideoMuted": "Iniciar sem vídeo",
116
-        "selectCamera": "Selecione a câmera",
117
-        "selectMic": "Selecionar o microfone",
118
-        "selectAudioOutput": "Selecionar a saída de áudio",
119
-        "followMe": "Habilitar o siga-me",
114
+        "startAudioMuted": "Todos iniciam mudos",
115
+        "startVideoMuted": "Todos iniciam ocultos",
116
+        "selectCamera": "Câmera",
117
+        "selectMic": "Microfone",
118
+        "selectAudioOutput": "Saída de áudio",
119
+        "followMe": "Todos me seguem",
120 120
         "noDevice": "Nenhum",
121 121
         "noPermission": "Permissão para usar o dispositivo não concedida",
122
-        "avatarUrl": "URL do Avatar"
122
+        "cameraAndMic": "Câmera e microfone",
123
+        "moderator": "MODERADOR",
124
+        "password": "DEFINIR SENHA",
125
+        "audioVideo": "ÁUDIO E VÍDEO",
126
+        "setPasswordLabel": "Trancar sua sala com uma senha."
127
+    },
128
+    "profile": {
129
+        "title": "PERFIL",
130
+        "setDisplayNameLabel": "Definir seu nome de exibição",
131
+        "setEmailLabel": "Definir seu email de gravatar"
123 132
     },
124 133
     "videothumbnail": {
125 134
         "editnickname": "Clique para editar o seu <br/>nome de exibição",
@@ -172,6 +181,7 @@
172 181
         "connectError": "Oops! Alguma coisa está errada e nós não pudemos conectar à conferência.",
173 182
         "connectErrorWithMsg": "Oops! Alguma coisa está errada e não podemos conectar à conferência: __msg__",
174 183
         "connecting": "Conectando",
184
+        "copy": "Copiar",
175 185
         "error": "Erro",
176 186
         "detectext": "Erro enquanto tenta detectar a extensão de compartilhamento de tela.",
177 187
         "failtoinstall": "Falhou a instalação da extensão de compartilhamento de tela",
@@ -183,8 +193,8 @@
183 193
         "lockMessage": "Falha ao travar a conferência.",
184 194
         "warning": "Atenção",
185 195
         "passwordNotSupported": "Senhas de salas não são suportadas atualmente.",
186
-        "sorry": "Desculpe",
187
-        "internalError": "Erro interno de aplicação [setRemoteDescription]",
196
+        "internalErrorTitle": "Erro interno",
197
+        "internalError": "Ops! Alguma coisa está errada. Ocorreu o seguinte erro: [setRemoteDescriptio]",
188 198
         "unableToSwitch": "Impossível trocar o fluxo de vídeo.",
189 199
         "SLDFailure": "Oops! Alguma coisa está errada e nós falhamos em silenciar! (Falha do SLD)",
190 200
         "SRDFailure": "Oops! Alguma coisa está errada e nós falhamos em parar o vídeo! (Falha do SRD)",
@@ -216,26 +226,28 @@
216 226
         "sipMsg": "Digite o número SIP",
217 227
         "passwordCheck": "Você tem certeza que deseja remover sua senha?",
218 228
         "passwordMsg": "Definir uma senha para trancar sua sala",
219
-        "Invite": "Convidar",
220
-        "shareLink": "Compartilhar este link com quem você espera convidar",
229
+        "shareLink": "Copiar e compartilhar este link",
221 230
         "settings1": "Configure sua conferência",
222 231
         "settings2": "Participantes entram mudos",
223 232
         "settings3": "Requer apelidos<br/><br/>Defina uma senha para trancar sua sala:",
224
-        "yourPassword": "sua Senha",
233
+        "yourPassword": "Digite a nova senha",
225 234
         "Back": "Voltar",
226 235
         "serviceUnavailable": "Serviço indisponível",
227 236
         "gracefulShutdown": "Nosso serviço está desligado para manutenção. Por favor, tente mais tarde.",
228 237
         "Yes": "Sim",
229 238
         "reservationError": "Erro de sistema de reserva",
230 239
         "reservationErrorMsg": "Código do erro: __code__, mensagem: __msg__",
231
-        "password": "senha",
240
+        "password": "Insira a senha",
232 241
         "userPassword": "senha do usuário",
233 242
         "token": "token",
234
-        "tokenAuthFailed": "Falha em autenticar com o servidor XMPP: token inválido",
243
+        "tokenAuthFailedTitle": "Problema na autenticação",
244
+        "tokenAuthFailed": "Desculpe, você não está autorizado a entrar nesta chamada.",
235 245
         "displayNameRequired": "Digite seu nome de exibição",
236 246
         "extensionRequired": "Extensão requerida:",
237 247
         "firefoxExtensionPrompt": "Você precisa instalar uma extensão do Firefox para compartilhar a tela. Tente novamente depois que você <a href='__url__'>pegá-lo aqui</a>!",
238
-        "feedbackQuestion": "Como foi a chamada?",
248
+        "rateExperience": "Por favor, avalie sua experiência na reunião.",
249
+        "feedbackHelp": "Seu retorno nos ajudará a melhorar nossa experiência de vídeo.",
250
+        "feedbackQuestion": "Nos conte sobre sua chamada!",
239 251
         "thankYou": "Obrigado por usar o __appName__!",
240 252
         "sorryFeedback": "Lamentamos escutar isso. Gostaria de nos contar mais?",
241 253
         "liveStreaming": "Live Streaming",
@@ -253,12 +265,17 @@
253 265
         "cameraUnsupportedResolutionError": "Sua câmera não suporta a resolução de vídeo requerida.",
254 266
         "cameraUnknownError": "Não pode usar a câmera por uma razão desconhecida.",
255 267
         "cameraPermissionDeniedError": "Você não tem permissão para usar sua câmera. Você ainda pode entrar na conferência, mas os outros não verão você. Use o botão da câmera na barra de endereço para fixar isto.",
256
-        "cameraNotFoundError": "Câmera solicitada não foi encontrada.",
268
+        "cameraNotFoundError": "A câmera não foi encontrada.",
257 269
         "cameraConstraintFailedError": "Sua câmera não satisfaz algumas condições requeridas.",
258 270
         "micUnknownError": "Não pode usar o microfone por uma razão desconhecida.",
259 271
         "micPermissionDeniedError": "Você não tem permissão para usar seu microfone. Você ainda pode entrar na conferência, mas os outros não ouvirão você. Use o botão da câmera na barra de endereço para fixar isto.",
260
-        "micNotFoundError": "O microfone solicitado não foi encontrado.",
261
-        "micConstraintFailedError": "Seu microfone não satisfaz algumas condições requeridas."
272
+        "micNotFoundError": "O microfone não foi encontrado.",
273
+        "micConstraintFailedError": "Seu microfone não satisfaz algumas condições requeridas.",
274
+        "micNotSendingData": "Seu microfone está inacessível. Selecione outro dispositivo do menu de configurações ou tente reiniciar a aplicação.",
275
+        "cameraNotSendingData": "Sua câmera está inacessível. Verifique se outra aplicação está usando este dispositivo, selecione outro dispositivo do menu de configurações ou tente reiniciar a aplicação.",
276
+        "goToStore": "Vá para a loja virtual",
277
+        "externalInstallationTitle": "Extensão requerida",
278
+        "externalInstallationMsg": "Você precisa instalar nossa extensão de compartilhamento de tela."
262 279
     },
263 280
     "email": {
264 281
         "sharedKey": [

+ 17
- 15
lang/main.json Näytä tiedosto

@@ -1,11 +1,9 @@
1 1
 {
2
-    "contactlist": "ON CALL (__participants__)",
2
+    "contactlist": "On Call",
3 3
     "connectionsettings": "Connection Settings",
4 4
     "poweredby": "powered by",
5
-    "downloadlogs": "Download logs",
6 5
     "feedback": "Give us your feedback",
7 6
     "roomUrlDefaultMsg": "Your conference is currently being created...",
8
-    "participant": "Participant",
9 7
     "me": "me",
10 8
     "speaker": "Speaker",
11 9
     "raisedHand": "Would like to speak",
@@ -33,6 +31,7 @@
33 31
         "focusRemote": "Focus on one of the remote videos.",
34 32
         "toggleChat": "Open or close the chat panel.",
35 33
         "mute": "Mute or unmute the microphone.",
34
+        "fullScreen": "Enter or exit full screen mode.",
36 35
         "videoMute": "Stop or start the local video."
37 36
     },
38 37
     "welcomepage":{
@@ -112,21 +111,21 @@
112 111
     },
113 112
     "settings":
114 113
     {
115
-        "title": "SETTINGS",
114
+        "title": "Settings",
116 115
         "update": "Update",
117 116
         "name": "Name",
118 117
         "startAudioMuted": "Everyone starts muted",
119 118
         "startVideoMuted": "Everyone starts hidden",
120
-        "selectCamera": "Select camera",
121
-        "selectMic": "Select microphone",
122
-        "selectAudioOutput": "Select audio output",
119
+        "selectCamera": "Camera",
120
+        "selectMic": "Microphone",
121
+        "selectAudioOutput": "Audio output",
123 122
         "followMe": "Everyone follows me",
124 123
         "noDevice": "None",
125 124
         "noPermission": "Permission to use device is not granted",
126 125
         "cameraAndMic": "Camera and microphone",
127 126
         "moderator": "MODERATOR",
128 127
         "password": "SET PASSWORD",
129
-        "audioVideo": "AUDIO / VIDEO",
128
+        "audioVideo": "AUDIO AND VIDEO",
130 129
         "setPasswordLabel": "Lock your room with a password."
131 130
     },
132 131
     "profile": {
@@ -138,7 +137,7 @@
138 137
     {
139 138
         "editnickname": "Click to edit your<br/>display name",
140 139
         "moderator": "The owner of<br/>this conference",
141
-        "videomute": "Participant has<br/>stopped the camera.",
140
+        "videomute": "Participant has<br/>stopped the camera",
142 141
         "mute": "Participant is muted",
143 142
         "kick": "Kick out",
144 143
         "muted": "Muted",
@@ -188,6 +187,7 @@
188 187
         "connectError": "Oops! Something went wrong and we couldn't connect to the conference.",
189 188
         "connectErrorWithMsg": "Oops! Something went wrong and we couldn't connect to the conference: __msg__",
190 189
         "connecting": "Connecting",
190
+        "copy": "Copy",
191 191
         "error": "Error",
192 192
         "detectext": "Error when trying to detect desktopsharing extension.",
193 193
         "failtoinstall": "Failed to install desktop sharing extension",
@@ -199,8 +199,8 @@
199 199
         "lockMessage": "Failed to lock the conference.",
200 200
         "warning": "Warning",
201 201
         "passwordNotSupported": "Room passwords are currently not supported.",
202
-        "sorry": "Sorry",
203
-        "internalError": "Internal application error [setRemoteDescription]",
202
+        "internalErrorTitle": "Internal error",
203
+        "internalError": "Oups! Something went wrong. The following error occurred: [setRemoteDescription]",
204 204
         "unableToSwitch": "Unable to switch video stream.",
205 205
         "SLDFailure": "Oops! Something went wrong and we failed to mute! (SLD Failure)",
206 206
         "SRDFailure": "Oops! Something went wrong and we failed to stop video! (SRD Failure)",
@@ -233,7 +233,6 @@
233 233
         "passwordCheck": "Are you sure you would like to remove your password?",
234 234
         "Remove": "Remove",
235 235
         "passwordMsg": "Set a password to lock your room",
236
-        "Invite": "Invite",
237 236
         "shareLink": "Copy and share this link",
238 237
         "settings1": "Configure your conference",
239 238
         "settings2": "Participants join muted",
@@ -248,13 +247,14 @@
248 247
         "password": "Enter password",
249 248
         "userPassword": "user password",
250 249
         "token": "token",
251
-        "tokenAuthFailed": "Failed to authenticate with XMPP server: invalid token",
250
+        "tokenAuthFailedTitle": "Authentication problem",
251
+        "tokenAuthFailed": "Sorry, you're not allowed to join this call.",
252 252
         "displayNameRequired": "Please enter your display name",
253 253
         "extensionRequired": "Extension required:",
254 254
         "firefoxExtensionPrompt": "You need to install a Firefox extension in order to use screen sharing. Please try again after you <a href='__url__'>get it from here</a>!",
255 255
         "rateExperience": "Please rate your meeting experience.",
256 256
         "feedbackHelp": "Your feedback will help us to improve our video experience.",
257
-        "feedbackQuestion": "How was your call?",
257
+        "feedbackQuestion": "Tell us about your call!",
258 258
         "thankYou": "Thank you for using __appName__!",
259 259
         "sorryFeedback": "We're sorry to hear that. Would you like to tell us more?",
260 260
         "liveStreaming": "Live Streaming",
@@ -279,6 +279,7 @@
279 279
         "micNotFoundError": "Microphone was not found.",
280 280
         "micConstraintFailedError": "Yor microphone does not satisfy some of required constraints.",
281 281
         "micNotSendingData": "We are unable to access your microphone. Please select another device from the settings menu or try to restart the application.",
282
+        "cameraNotSendingData": "We are unable to access your camera. Please check if another application is using this device, select another device from the settings menu or try to restart the application.",
282 283
         "goToStore": "Go to the webstore",
283 284
         "externalInstallationTitle": "Extension required",
284 285
         "externalInstallationMsg": "You need to install our desktop sharing extension."
@@ -325,7 +326,8 @@
325 326
         "ATTACHED": "Attached",
326 327
         "FETCH_SESSION_ID": "Obtaining session-id...",
327 328
         "GOT_SESSION_ID": "Obtaining session-id... Done",
328
-        "GET_SESSION_ID_ERROR": "Get session-id error: "
329
+        "GET_SESSION_ID_ERROR": "Get session-id error: ",
330
+        "USER_CONNECTION_INTERRUPTED": "__displayName__ is having connectivity issues..."
329 331
     },
330 332
     "recording":
331 333
     {

+ 1
- 0
modules/TokenData/TokenData.js Näytä tiedosto

@@ -97,6 +97,7 @@ class TokenData{
97 97
         this.payload = this.decodedJWT.payload;
98 98
         if(!this.payload.context)
99 99
             return;
100
+        this.server = this.payload.context.server;
100 101
         let callerData = this.payload.context.user;
101 102
         let calleeData = this.payload.context.callee;
102 103
         if(callerData)

+ 0
- 321
modules/UI/Feedback.js Näytä tiedosto

@@ -1,321 +0,0 @@
1
-/* global $, APP, config, interfaceConfig, JitsiMeetJS */
2
-import UIEvents from "../../service/UI/UIEvents";
3
-
4
-/**
5
- * Constructs the html for the overall feedback window.
6
- *
7
- * @returns {string} the constructed html string
8
- */
9
-var constructOverallFeedbackHtml = function() {
10
-    var feedbackQuestion = (Feedback.feedbackScore < 0)
11
-        ? '<br/><br/>' + APP.translation
12
-        .translateString("dialog.feedbackQuestion")
13
-        : '';
14
-
15
-    var message = '<div class="feedback"><div>' +
16
-        '<div class="feedbackTitle">' +
17
-        APP.translation.translateString("dialog.thankYou",
18
-                                        {appName:interfaceConfig.APP_NAME}) +
19
-        '</div>' +
20
-        feedbackQuestion +
21
-        '</div><br/><br/>' +
22
-        '<div id="stars">' +
23
-        '<a><i class="icon-star icon-star-full"></i></a>' +
24
-        '<a><i class="icon-star icon-star-full"></i></a>' +
25
-        '<a><i class="icon-star icon-star-full"></i></a>' +
26
-        '<a><i class="icon-star icon-star-full"></i></a>' +
27
-        '<a><i class="icon-star icon-star-full"></i></a>' +
28
-        '</div></div>';
29
-
30
-    return message;
31
-};
32
-
33
-/**
34
- * Constructs the html for the detailed feedback window.
35
- *
36
- * @returns {string} the contructed html string
37
- */
38
-var constructDetailedFeedbackHtml = function() {
39
-    // Construct the html, which will be served as a dialog message.
40
-    var message = '<div class="feedback">' +
41
-        '<div class="feedbackTitle">' +
42
-        APP.translation.translateString("dialog.sorryFeedback") +
43
-        '</div><br/><br/>' +
44
-        '<div class="feedbackDetails">' +
45
-        '<textarea id="feedbackTextArea" rows="10" cols="50" autofocus>' +
46
-        '</textarea>' +
47
-        '</div></div>';
48
-
49
-    return message;
50
-};
51
-
52
-var createRateFeedbackHTML = function () {
53
-    var rate = APP.translation.translateString('dialog.rateExperience'),
54
-        help = APP.translation.translateString('dialog.feedbackHelp');
55
-
56
-    return `
57
-        <div class="feedback-rating text-center">
58
-            <h2>${ rate }</h2>
59
-            <p class="star-label">&nbsp;</p>
60
-            <div id="stars" class="feedback-stars">
61
-                <a class="star-btn">
62
-                    <i class="fa fa-star shake-rotate"></i>
63
-                </a>
64
-                <a class="star-btn">
65
-                    <i class="fa fa-star shake-rotate"></i>
66
-                </a>
67
-                <a class="star-btn">
68
-                    <i class="fa fa-star shake-rotate"></i>
69
-                </a>
70
-                <a class="star-btn">
71
-                    <i class="fa fa-star shake-rotate"></i>
72
-                </a>
73
-                <a class="star-btn">
74
-                    <i class="fa fa-star shake-rotate"></i>
75
-                </a>
76
-            </div>
77
-            <p>&nbsp;</p>
78
-            <p>${ help }</p>
79
-        </div>
80
-    `;
81
-};
82
-
83
-/**
84
- * The callback function corresponding to the openFeedbackWindow parameter.
85
- *
86
- * @type {function}
87
- */
88
-var feedbackWindowCallback = null;
89
-
90
-/**
91
- * Shows / hides the feedback button.
92
- * @private
93
- */
94
-function _toggleFeedbackIcon() {
95
-    $('#feedbackButtonDiv').toggleClass("hidden");
96
-}
97
-
98
-/**
99
- * Shows / hides the feedback button.
100
- * @param {show} set to {true} to show the feedback button or to  {false}
101
- * to hide it
102
- * @private
103
- */
104
-function _showFeedbackButton (show) {
105
-    var feedbackButton = $("#feedbackButtonDiv");
106
-
107
-    if (show)
108
-        feedbackButton.css("display", "block");
109
-    else
110
-        feedbackButton.css("display", "none");
111
-}
112
-
113
-/**
114
- * Defines all methods in connection to the Feedback window.
115
- *
116
- * @type {{feedbackScore: number, openFeedbackWindow: Function,
117
- * toggleStars: Function, hoverStars: Function, unhoverStars: Function}}
118
- */
119
-var Feedback = {
120
-    /**
121
-     * The feedback score. -1 indicates no score has been given for now.
122
-     */
123
-    feedbackScore: -1,
124
-
125
-    /**
126
-     * Initialise the Feedback functionality.
127
-     * @param emitter the EventEmitter to associate with the Feedback.
128
-     */
129
-    init: function (emitter) {
130
-        // CallStats is the way we send feedback, so we don't have to initialise
131
-        // if callstats isn't enabled.
132
-        if (!APP.conference.isCallstatsEnabled())
133
-            return;
134
-
135
-        // If enabled property is still undefined, i.e. it hasn't been set from
136
-        // some other module already, we set it to true by default.
137
-        if (typeof this.enabled == "undefined")
138
-            this.enabled = true;
139
-
140
-        _showFeedbackButton(this.enabled);
141
-
142
-        $("#feedbackButton").click(function (event) {
143
-            Feedback.openFeedbackWindow();
144
-        });
145
-
146
-        // Show / hide the feedback button whenever the film strip is
147
-        // shown / hidden.
148
-        emitter.addListener(UIEvents.TOGGLE_FILM_STRIP, function () {
149
-            _toggleFeedbackIcon();
150
-        });
151
-    },
152
-    /**
153
-     * Enables/ disabled the feedback feature.
154
-     */
155
-    enableFeedback: function (enable) {
156
-        if (this.enabled !== enable)
157
-            _showFeedbackButton(enable);
158
-        this.enabled = enable;
159
-    },
160
-
161
-    /**
162
-     * Indicates if the feedback functionality is enabled.
163
-     *
164
-     * @return true if the feedback functionality is enabled, false otherwise.
165
-     */
166
-    isEnabled: function() {
167
-        return this.enabled && APP.conference.isCallstatsEnabled();
168
-    },
169
-
170
-    /**
171
-     * Returns true if the feedback window is currently visible and false
172
-     * otherwise.
173
-     * @return {boolean} true if the feedback window is visible, false
174
-     * otherwise
175
-     */
176
-    isVisible: function() {
177
-        return $(".feedback").is(":visible");
178
-    },
179
-
180
-    /**
181
-     * Opens the feedback window.
182
-     */
183
-    openFeedbackWindow: function (callback) {
184
-        feedbackWindowCallback = callback;
185
-        // Add all mouse and click listeners.
186
-        var onLoadFunction = function (event) {
187
-            $('#stars >a').each(function(index) {
188
-                // On star mouse over.
189
-                $(this).get(0).onmouseover = function(){
190
-                    Feedback.hoverStars(index);
191
-                };
192
-                // On star mouse leave.
193
-                $(this).get(0).onmouseleave = function(){
194
-                    Feedback.unhoverStars(index);
195
-                };
196
-                // On star click.
197
-                $(this).get(0).onclick = function(){
198
-                    Feedback.toggleStars(index);
199
-                    Feedback.feedbackScore = index+1;
200
-
201
-                    // If the feedback is less than 3 stars we're going to
202
-                    // ask the user for more information.
203
-                    if (Feedback.feedbackScore > 3) {
204
-                        APP.conference.sendFeedback(Feedback.feedbackScore, "");
205
-                        if (feedbackWindowCallback)
206
-                            feedbackWindowCallback();
207
-                        else
208
-                            APP.UI.messageHandler.closeDialog();
209
-                    }
210
-                    else {
211
-                        feedbackDialog.goToState('detailed_feedback');
212
-                    }
213
-                };
214
-                // Init stars to correspond to previously entered feedback.
215
-                if (Feedback.feedbackScore > 0
216
-                    && index < Feedback.feedbackScore) {
217
-                    Feedback.hoverStars(index);
218
-                    Feedback.toggleStars(index);
219
-                }
220
-            });
221
-        };
222
-
223
-        // Defines the different states of the feedback window.
224
-        var states = {
225
-            overall_feedback: {
226
-                html: createRateFeedbackHTML(),
227
-                persistent: false,
228
-                buttons: {},
229
-                closeText: '',
230
-                focus: "div[id='stars']",
231
-                position: {width: 500}
232
-            },
233
-            detailed_feedback: {
234
-                html: constructDetailedFeedbackHtml(),
235
-                buttons: {"Submit": true, "Cancel": false},
236
-                closeText: '',
237
-                focus: "textarea[id='feedbackTextArea']",
238
-                position: {width: 500},
239
-                submit: function(e,v,m,f) {
240
-                    e.preventDefault();
241
-                    if (v) {
242
-                        var feedbackDetails
243
-                            = document.getElementById("feedbackTextArea").value;
244
-
245
-                        if (feedbackDetails && feedbackDetails.length > 0) {
246
-                            APP.conference.sendFeedback( Feedback.feedbackScore,
247
-                                                    feedbackDetails);
248
-                        }
249
-
250
-                        if (feedbackWindowCallback)
251
-                            feedbackWindowCallback();
252
-                        else
253
-                            APP.UI.messageHandler.closeDialog();
254
-                    } else {
255
-                        // User cancelled
256
-                        if (feedbackWindowCallback)
257
-                            feedbackWindowCallback();
258
-                        else
259
-                            APP.UI.messageHandler.closeDialog();
260
-                    }
261
-                }
262
-            }
263
-        };
264
-
265
-        // Create the feedback dialog.
266
-        var feedbackDialog
267
-            = APP.UI.messageHandler.openDialogWithStates(
268
-                states,
269
-                {   persistent: false,
270
-                    buttons: {},
271
-                    closeText: '',
272
-                    loaded: onLoadFunction,
273
-                    position: {width: 500}}, null);
274
-        JitsiMeetJS.analytics.sendEvent('feedback.open');
275
-    },
276
-    /**
277
-     * Toggles the appropriate css class for the given number of stars, to
278
-     * indicate that those stars have been clicked/selected.
279
-     *
280
-     * @param starCount the number of stars, for which to toggle the css class
281
-     */
282
-    toggleStars: function (starCount)
283
-    {
284
-        $('#stars >a >i').each(function(index) {
285
-            if (index <= starCount) {
286
-                $(this).removeClass("icon-star");
287
-            }
288
-            else
289
-                $(this).addClass("icon-star");
290
-        });
291
-    },
292
-    /**
293
-     * Toggles the appropriate css class for the given number of stars, to
294
-     * indicate that those stars have been hovered.
295
-     *
296
-     * @param starCount the number of stars, for which to toggle the css class
297
-     */
298
-    hoverStars: function (starCount)
299
-    {
300
-        $('#stars >a >i').each(function(index) {
301
-            if (index <= starCount)
302
-                $(this).addClass("starHover");
303
-        });
304
-    },
305
-    /**
306
-     * Toggles the appropriate css class for the given number of stars, to
307
-     * indicate that those stars have been un-hovered.
308
-     *
309
-     * @param starCount the number of stars, for which to toggle the css class
310
-     */
311
-    unhoverStars: function (starCount)
312
-    {
313
-        $('#stars >a >i').each(function(index) {
314
-            if (index <= starCount && $(this).hasClass("icon-star"))
315
-                $(this).removeClass("starHover");
316
-        });
317
-    }
318
-};
319
-
320
-// Exports the Feedback class.
321
-module.exports = Feedback;

+ 88
- 79
modules/UI/UI.js Näytä tiedosto

@@ -29,7 +29,7 @@ var EventEmitter = require("events");
29 29
 UI.messageHandler = require("./util/MessageHandler");
30 30
 var messageHandler = UI.messageHandler;
31 31
 var JitsiPopover = require("./util/JitsiPopover");
32
-var Feedback = require("./Feedback");
32
+var Feedback = require("./feedback/Feedback");
33 33
 
34 34
 import FollowMe from "../FollowMe";
35 35
 
@@ -60,6 +60,8 @@ JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[TrackErrors.NOT_FOUND]
60 60
     = "dialog.cameraNotFoundError";
61 61
 JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[TrackErrors.CONSTRAINT_FAILED]
62 62
     = "dialog.cameraConstraintFailedError";
63
+JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[TrackErrors.NO_DATA_FROM_SOURCE]
64
+    = "dialog.cameraNotSendingData";
63 65
 JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.GENERAL]
64 66
     = "dialog.micUnknownError";
65 67
 JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.PERMISSION_DENIED]
@@ -68,6 +70,8 @@ JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.NOT_FOUND]
68 70
     = "dialog.micNotFoundError";
69 71
 JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.CONSTRAINT_FAILED]
70 72
     = "dialog.micConstraintFailedError";
73
+JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.NO_DATA_FROM_SOURCE]
74
+    = "dialog.micNotSendingData";
71 75
 
72 76
 /**
73 77
  * Prompt user for nickname.
@@ -257,6 +261,17 @@ UI.changeDisplayName = function (id, displayName) {
257 261
     }
258 262
 };
259 263
 
264
+/**
265
+ * Shows/hides the indication about local connection being interrupted.
266
+ *
267
+ * @param {boolean} isInterrupted <tt>true</tt> if local connection is
268
+ * currently in the interrupted state or <tt>false</tt> if the connection
269
+ * is fine.
270
+ */
271
+UI.showLocalConnectionInterrupted = function (isInterrupted) {
272
+    VideoLayout.showLocalConnectionInterrupted(isInterrupted);
273
+};
274
+
260 275
 /**
261 276
  * Sets the "raised hand" status for a participant.
262 277
  */
@@ -292,11 +307,11 @@ UI.initConference = function () {
292 307
     }
293 308
 
294 309
     // Add myself to the contact list.
295
-    ContactList.addContact(id);
310
+    ContactList.addContact(id, true);
296 311
 
297
-    //update default button states before showing the toolbar
298
-    //if local role changes buttons state will be again updated
299
-    UI.updateLocalRole(false);
312
+    // Update default button states before showing the toolbar
313
+    // if local role changes buttons state will be again updated.
314
+    UI.updateLocalRole(APP.conference.isModerator);
300 315
 
301 316
     UI.showToolbar();
302 317
 
@@ -325,6 +340,8 @@ UI.initConference = function () {
325 340
     // to the UI (depending on the moderator role of the local participant) and
326 341
     // (2) APP.conference as means of communication between the participants.
327 342
     followMeHandler = new FollowMe(APP.conference, UI);
343
+
344
+    UIUtil.activateTooltips();
328 345
 };
329 346
 
330 347
 UI.mucJoined = function () {
@@ -339,6 +356,22 @@ UI.handleToggleFilmStrip = () => {
339 356
     VideoLayout.resizeVideoArea(true, false);
340 357
 };
341 358
 
359
+/**
360
+ * Sets tooltip defaults.
361
+ *
362
+ * @private
363
+ */
364
+function _setTooltipDefaults() {
365
+    $.fn.tooltip.defaults = {
366
+        opacity: 1, //defaults to 1
367
+        offset: 1,
368
+        delayIn: 0, //defaults to 500
369
+        hoverable: true,
370
+        hideOnClick: true,
371
+        aria: true
372
+    };
373
+}
374
+
342 375
 /**
343 376
  * Setup some UI event listeners.
344 377
  */
@@ -431,6 +464,9 @@ UI.start = function () {
431 464
     // Set the defaults for prompt dialogs.
432 465
     $.prompt.setDefaults({persistent: false});
433 466
 
467
+    // Set the defaults for tooltips.
468
+    _setTooltipDefaults();
469
+
434 470
     registerListeners();
435 471
 
436 472
     ToolbarToggler.init();
@@ -463,20 +499,10 @@ UI.start = function () {
463 499
             $('#noticeText').text(config.noticeMessage);
464 500
             $('#notice').css({display: 'block'});
465 501
         }
466
-        $("#downloadlog").click(function (event) {
467
-            let logs = APP.conference.getLogs();
468
-            let data = encodeURIComponent(JSON.stringify(logs, null, '  '));
469
-
470
-            let elem = event.target.parentNode;
471
-            elem.download = 'meetlog.json';
472
-            elem.href = 'data:application/json;charset=utf-8,\n' + data;
473
-        });
474 502
     } else {
475 503
         $("#mainToolbarContainer").css("display", "none");
476
-        $("#downloadlog").css("display", "none");
477 504
         FilmStrip.setupFilmStripOnly();
478 505
         messageHandler.enableNotifications(false);
479
-        $('body').popover("disable");
480 506
         JitsiPopover.enabled = false;
481 507
     }
482 508
 
@@ -589,10 +615,11 @@ UI.getSharedDocumentManager = function () {
589 615
 
590 616
 /**
591 617
  * Show user on UI.
592
- * @param {string} id user id
593
- * @param {string} displayName user nickname
618
+ * @param {JitsiParticipant} user
594 619
  */
595
-UI.addUser = function (id, displayName) {
620
+UI.addUser = function (user) {
621
+    var id = user.getId();
622
+    var displayName = user.getDisplayName();
596 623
     UI.hideRingOverLay();
597 624
     ContactList.addContact(id);
598 625
 
@@ -605,7 +632,7 @@ UI.addUser = function (id, displayName) {
605 632
         UIUtil.playSoundNotification('userJoined');
606 633
 
607 634
     // Add Peer's container
608
-    VideoLayout.addParticipantContainer(id);
635
+    VideoLayout.addParticipantContainer(user);
609 636
 
610 637
     // Configure avatar
611 638
     UI.setUserEmail(id);
@@ -662,7 +689,9 @@ UI.updateLocalRole = function (isModerator) {
662 689
     SettingsMenu.showFollowMeOptions(isModerator);
663 690
 
664 691
     if (isModerator) {
665
-        messageHandler.notify(null, "notify.me", 'connected', "notify.moderator");
692
+        if (!interfaceConfig.DISABLE_FOCUS_INDICATOR)
693
+            messageHandler
694
+                .notify(null, "notify.me", 'connected', "notify.moderator");
666 695
 
667 696
         Recording.checkAutoRecord();
668 697
     }
@@ -676,7 +705,9 @@ UI.updateLocalRole = function (isModerator) {
676 705
 UI.updateUserRole = function (user) {
677 706
     VideoLayout.showModeratorIndicator();
678 707
 
679
-    if (!user.isModerator()) {
708
+    // We don't need to show moderator notifications when the focus (moderator)
709
+    // indicator is disabled.
710
+    if (!user.isModerator() || interfaceConfig.DISABLE_FOCUS_INDICATOR) {
680 711
         return;
681 712
     }
682 713
 
@@ -970,6 +1001,17 @@ UI.handleLastNEndpoints = function (ids, enteringIds) {
970 1001
     VideoLayout.onLastNEndpointsChanged(ids, enteringIds);
971 1002
 };
972 1003
 
1004
+/**
1005
+ * Will handle notification about participant's connectivity status change.
1006
+ *
1007
+ * @param {string} id the id of remote participant(MUC jid)
1008
+ * @param {boolean} isActive true if the connection is ok or false if the user
1009
+ * is having connectivity issues.
1010
+ */
1011
+UI.participantConnectionStatusChanged = function (id, isActive) {
1012
+    VideoLayout.onParticipantConnectionStatusChanged(id, isActive);
1013
+};
1014
+
973 1015
 /**
974 1016
  * Update audio level visualization for specified user.
975 1017
  * @param {string} id user id
@@ -1052,50 +1094,6 @@ UI.updateDTMFSupport = function (isDTMFSupported) {
1052 1094
     //Toolbar.showDialPadButton(dtmfSupport);
1053 1095
 };
1054 1096
 
1055
-/**
1056
- * Invite participants to conference.
1057
- * @param {string} roomUrl
1058
- * @param {string} conferenceName
1059
- * @param {string} key
1060
- * @param {string} nick
1061
- */
1062
-UI.inviteParticipants = function (roomUrl, conferenceName, key, nick) {
1063
-    let keyText = "";
1064
-    if (key) {
1065
-        keyText = APP.translation.translateString(
1066
-            "email.sharedKey", {sharedKey: key}
1067
-        );
1068
-    }
1069
-
1070
-    let and = APP.translation.translateString("email.and");
1071
-    let supportedBrowsers = `Chromium, Google Chrome, Firefox ${and} Opera`;
1072
-
1073
-    let subject = APP.translation.translateString(
1074
-        "email.subject", {appName:interfaceConfig.APP_NAME, conferenceName}
1075
-    );
1076
-
1077
-    let body = APP.translation.translateString(
1078
-        "email.body", {
1079
-            appName:interfaceConfig.APP_NAME,
1080
-            sharedKeyText: keyText,
1081
-            roomUrl,
1082
-            supportedBrowsers
1083
-        }
1084
-    );
1085
-
1086
-    body = body.replace(/\n/g, "%0D%0A");
1087
-
1088
-    if (nick) {
1089
-        body += "%0D%0A%0D%0A" + UIUtil.escapeHtml(nick);
1090
-    }
1091
-
1092
-    if (interfaceConfig.INVITATION_POWERED_BY) {
1093
-        body += "%0D%0A%0D%0A--%0D%0Apowered by jitsi.org";
1094
-    }
1095
-
1096
-    window.open(`mailto:?subject=${subject}&body=${body}`, '_blank');
1097
-};
1098
-
1099 1097
 /**
1100 1098
  * Show user feedback dialog if its required or just show "thank you" dialog.
1101 1099
  * @returns {Promise} when dialog is closed.
@@ -1103,12 +1101,15 @@ UI.inviteParticipants = function (roomUrl, conferenceName, key, nick) {
1103 1101
 UI.requestFeedback = function () {
1104 1102
     if (Feedback.isVisible())
1105 1103
         return Promise.reject(UIErrors.FEEDBACK_REQUEST_IN_PROGRESS);
1104
+    // Feedback has been submitted already.
1105
+    else if (Feedback.isEnabled() && Feedback.isSubmitted())
1106
+        return Promise.resolve();
1106 1107
     else
1107 1108
         return new Promise(function (resolve, reject) {
1108 1109
             if (Feedback.isEnabled()) {
1109 1110
                 // If the user has already entered feedback, we'll show the
1110 1111
                 // window and immidiately start the conference dispose timeout.
1111
-                if (Feedback.feedbackScore > 0) {
1112
+                if (Feedback.getFeedbackScore() > 0) {
1112 1113
                     Feedback.openFeedbackWindow();
1113 1114
                     resolve();
1114 1115
 
@@ -1117,14 +1118,9 @@ UI.requestFeedback = function () {
1117 1118
                 }
1118 1119
             } else {
1119 1120
                 // If the feedback functionality isn't enabled we show a thank
1120
-                // you dialog.
1121
-                messageHandler.openMessageDialog(
1122
-                    null, null, null,
1123
-                    APP.translation.translateString(
1124
-                        "dialog.thankYou", {appName:interfaceConfig.APP_NAME}
1125
-                    )
1126
-                );
1127
-                resolve();
1121
+                // you dialog. Signaling it (true), so the caller
1122
+                // of requestFeedback can act on it
1123
+                resolve(true);
1128 1124
             }
1129 1125
         });
1130 1126
 };
@@ -1134,11 +1130,13 @@ UI.updateRecordingState = function (state) {
1134 1130
 };
1135 1131
 
1136 1132
 UI.notifyTokenAuthFailed = function () {
1137
-    messageHandler.showError("dialog.error", "dialog.tokenAuthFailed");
1133
+    messageHandler.showError(   "dialog.tokenAuthFailedTitle",
1134
+                                "dialog.tokenAuthFailed");
1138 1135
 };
1139 1136
 
1140 1137
 UI.notifyInternalError = function () {
1141
-    messageHandler.showError("dialog.sorry", "dialog.internalError");
1138
+    messageHandler.showError(   "dialog.internalErrorTitle",
1139
+                                "dialog.internalError");
1142 1140
 };
1143 1141
 
1144 1142
 UI.notifyFocusDisconnected = function (focus, retrySec) {
@@ -1193,6 +1191,16 @@ UI.onStartMutedChanged = function (startAudioMuted, startVideoMuted) {
1193 1191
     SettingsMenu.updateStartMutedBox(startAudioMuted, startVideoMuted);
1194 1192
 };
1195 1193
 
1194
+/**
1195
+ * Notifies interested listeners that the raise hand property has changed.
1196
+ *
1197
+ * @param {boolean} isRaisedHand indicates the current state of the
1198
+ * "raised hand"
1199
+ */
1200
+UI.onLocalRaiseHandChanged = function (isRaisedHand) {
1201
+    eventEmitter.emit(UIEvents.LOCAL_RAISE_HAND_CHANGED, isRaisedHand);
1202
+};
1203
+
1196 1204
 /**
1197 1205
  * Update list of available physical devices.
1198 1206
  * @param {object[]} devices new list of available devices
@@ -1415,12 +1423,13 @@ UI.showDeviceErrorDialog = function (micError, cameraError) {
1415 1423
 
1416 1424
 /**
1417 1425
  * Shows error dialog that informs the user that no data is received from the
1418
- * microphone.
1426
+ * device.
1419 1427
  */
1420
-UI.showAudioNotWorkingDialog = function () {
1428
+UI.showTrackNotWorkingDialog = function (stream) {
1421 1429
     messageHandler.openMessageDialog(
1422 1430
         "dialog.error",
1423
-        "dialog.micNotSendingData",
1431
+        stream.isAudioTrack()? "dialog.micNotSendingData" :
1432
+            "dialog.cameraNotSendingData",
1424 1433
         null,
1425 1434
         null);
1426 1435
 };

+ 121
- 216
modules/UI/audio_levels/AudioLevels.js Näytä tiedosto

@@ -1,260 +1,165 @@
1
-/* global APP, interfaceConfig, $ */
2
-/* jshint -W101 */
1
+/* global interfaceConfig */
3 2
 
4
-import CanvasUtil from './CanvasUtils';
5
-import FilmStrip from '../videolayout/FilmStrip';
6
-
7
-const LOCAL_LEVEL = 'local';
8
-
9
-let ASDrawContext = null;
10
-let audioLevelCanvasCache = {};
11
-let dominantSpeakerAudioElement = null;
12
-
13
-function initDominantSpeakerAudioLevels(dominantSpeakerAvatarSize) {
14
-    let ASRadius = dominantSpeakerAvatarSize / 2;
15
-    let ASCenter = (dominantSpeakerAvatarSize + ASRadius) / 2;
16
-
17
-    // Draw a circle.
18
-    ASDrawContext.beginPath();
19
-    ASDrawContext.arc(ASCenter, ASCenter, ASRadius, 0, 2 * Math.PI);
20
-    ASDrawContext.closePath();
21
-
22
-    // Add a shadow around the circle
23
-    ASDrawContext.shadowColor = interfaceConfig.SHADOW_COLOR;
24
-    ASDrawContext.shadowOffsetX = 0;
25
-    ASDrawContext.shadowOffsetY = 0;
26
-}
3
+import UIUtil from "../util/UIUtil";
27 4
 
28 5
 /**
29
- * Resizes the given audio level canvas to match the given thumbnail size.
6
+ * Responsible for drawing audio levels.
30 7
  */
31
-function resizeAudioLevelCanvas(audioLevelCanvas, thumbnailWidth, thumbnailHeight) {
32
-    audioLevelCanvas.width = thumbnailWidth + interfaceConfig.CANVAS_EXTRA;
33
-    audioLevelCanvas.height = thumbnailHeight + interfaceConfig.CANVAS_EXTRA;
34
-}
35
-
36
-/**
37
- * Draws the audio level canvas into the cached canvas object.
38
- *
39
- * @param id of the user for whom we draw the audio level
40
- * @param audioLevel the newAudio level to render
41
- */
42
-function drawAudioLevelCanvas(id, audioLevel) {
43
-    if (!audioLevelCanvasCache[id]) {
44
-
45
-        let videoSpanId = getVideoSpanId(id);
46
-
47
-        let audioLevelCanvasOrig = $(`#${videoSpanId}>canvas`).get(0);
48
-
49
-        /*
50
-         * FIXME Testing has shown that audioLevelCanvasOrig may not exist.
51
-         * In such a case, the method CanvasUtil.cloneCanvas may throw an
52
-         * error. Since audio levels are frequently updated, the errors have
53
-         * been observed to pile into the console, strain the CPU.
54
-         */
55
-        if (audioLevelCanvasOrig) {
56
-            audioLevelCanvasCache[id]
57
-                = CanvasUtil.cloneCanvas(audioLevelCanvasOrig);
58
-        }
59
-    }
60
-
61
-    let canvas = audioLevelCanvasCache[id];
62
-
63
-    if (!canvas) {
64
-        return;
65
-    }
66
-
67
-    let drawContext = canvas.getContext('2d');
68
-
69
-    drawContext.clearRect(0, 0, canvas.width, canvas.height);
70
-
71
-    let shadowLevel = getShadowLevel(audioLevel);
72
-
73
-    if (shadowLevel > 0) {
74
-        // drawContext, x, y, w, h, r, shadowColor, shadowLevel
75
-        CanvasUtil.drawRoundRectGlow(
76
-            drawContext,
77
-            interfaceConfig.CANVAS_EXTRA / 2, interfaceConfig.CANVAS_EXTRA / 2,
78
-            canvas.width - interfaceConfig.CANVAS_EXTRA,
79
-            canvas.height - interfaceConfig.CANVAS_EXTRA,
80
-            interfaceConfig.CANVAS_RADIUS,
81
-            interfaceConfig.SHADOW_COLOR,
82
-            shadowLevel);
83
-    }
84
-}
85
-
86
-/**
87
- * Returns the shadow/glow level for the given audio level.
88
- *
89
- * @param audioLevel the audio level from which we determine the shadow
90
- * level
91
- */
92
-function getShadowLevel (audioLevel) {
93
-    let shadowLevel = 0;
94
-
95
-    if (audioLevel <= 0.3) {
96
-        shadowLevel = Math.round(
97
-            interfaceConfig.CANVAS_EXTRA/2*(audioLevel/0.3));
98
-    } else if (audioLevel <= 0.6) {
99
-        shadowLevel = Math.round(
100
-            interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
101
-    } else {
102
-        shadowLevel = Math.round(
103
-            interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
104
-    }
105
-
106
-    return shadowLevel;
107
-}
108
-
109
-/**
110
- * Returns the video span id corresponding to the given user id
111
- */
112
-function getVideoSpanId(id) {
113
-    let videoSpanId = null;
8
+const AudioLevels = {
114 9
 
115
-    if (id === LOCAL_LEVEL || APP.conference.isLocalId(id)) {
116
-        videoSpanId = 'localVideoContainer';
117
-    } else {
118
-        videoSpanId = `participant_${id}`;
119
-    }
10
+    /**
11
+     * The number of dots.
12
+     *
13
+     * IMPORTANT: functions below assume that this is an odd number.
14
+     */
15
+    _AUDIO_LEVEL_DOTS: 5,
120 16
 
121
-    return videoSpanId;
122
-}
17
+    /**
18
+     * Creates the audio level indicator span element.
19
+     *
20
+     * IMPORTANT: This function assumes that the number of dots is an
21
+     * odd number.
22
+     *
23
+     * @return {Element} the document element representing audio levels
24
+     */
25
+    createThumbnailAudioLevelIndicator() {
123 26
 
124
-/**
125
- * The audio Levels plugin.
126
- */
127
-const AudioLevels = {
27
+        let audioSpan = document.createElement('span');
28
+        audioSpan.className = 'audioindicator';
128 29
 
129
-    init () {
130
-        dominantSpeakerAudioElement =  $('#dominantSpeakerAudioLevel')[0];
131
-        ASDrawContext = dominantSpeakerAudioElement.getContext('2d');
30
+        this.sideDotsCount = Math.floor(this._AUDIO_LEVEL_DOTS/2);
132 31
 
133
-        let parentContainer = $("#dominantSpeaker");
134
-        let dominantSpeakerWidth = parentContainer.width();
135
-        let dominantSpeakerHeight = parentContainer.height();
32
+        for (let i = 0; i < this._AUDIO_LEVEL_DOTS; i++) {
33
+            let audioDot = document.createElement('span');
136 34
 
137
-        dominantSpeakerAudioElement.width = dominantSpeakerWidth;
138
-        dominantSpeakerAudioElement.height = dominantSpeakerHeight;
35
+            // The median index will be equal to the number of dots on each
36
+            // side.
37
+            if (i === this.sideDotsCount)
38
+                audioDot.className = "audiodot-middle";
39
+            else
40
+                audioDot.className = (i < this.sideDotsCount)
41
+                                    ? "audiodot-top"
42
+                                    : "audiodot-bottom";
139 43
 
140
-        let dominantSpeakerAvatar = $("#dominantSpeakerAvatar");
141
-        initDominantSpeakerAudioLevels(dominantSpeakerAvatar.width());
44
+            audioSpan.appendChild(audioDot);
45
+        }
46
+        return audioSpan;
142 47
     },
143 48
 
144 49
     /**
145
-     * Updates the audio level canvas for the given id. If the canvas
146
-     * didn't exist we create it.
50
+     * Updates the audio level UI for the given id.
51
+     *
52
+     * @param {string} id id of the user for whom we draw the audio level
53
+     * @param {number} audioLevel the newAudio level to render
147 54
      */
148
-    updateAudioLevelCanvas (id, thumbWidth, thumbHeight) {
149
-        let videoSpanId = 'localVideoContainer';
150
-        if (id) {
151
-            videoSpanId = `participant_${id}`;
152
-        }
55
+    updateThumbnailAudioLevel (id, audioLevel) {
153 56
 
154
-        let videoSpan = document.getElementById(videoSpanId);
57
+        // First make sure we are sensitive enough.
58
+        audioLevel *= 1.2;
59
+        audioLevel = Math.min(audioLevel, 1);
155 60
 
156
-        if (!videoSpan) {
157
-            if (id) {
158
-                console.error("No video element for id", id);
159
-            } else {
160
-                console.error("No video element for local video.");
161
-            }
162
-            return;
163
-        }
164
-
165
-        let audioLevelCanvas = $(`#${videoSpanId}>canvas`);
166
-
167
-        if (!audioLevelCanvas || audioLevelCanvas.length === 0) {
61
+        // Let's now stretch the audio level over the number of dots we have.
62
+        let stretchedAudioLevel = (this.sideDotsCount + 1) * audioLevel;
63
+        let dotLevel = 0.0;
168 64
 
169
-            audioLevelCanvas = document.createElement('canvas');
170
-            audioLevelCanvas.className = "audiolevel";
171
-            audioLevelCanvas.style.bottom
172
-                = `-${interfaceConfig.CANVAS_EXTRA/2}px`;
173
-            audioLevelCanvas.style.left
174
-                = `-${interfaceConfig.CANVAS_EXTRA/2}px`;
175
-            resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
65
+        for (let i = 0; i < (this.sideDotsCount + 1); i++) {
176 66
 
177
-            videoSpan.appendChild(audioLevelCanvas);
178
-        } else {
179
-            audioLevelCanvas = audioLevelCanvas.get(0);
180
-
181
-            resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
67
+            dotLevel = Math.min(1, Math.max(0, (stretchedAudioLevel - i)));
68
+            this._setDotLevel(id, i, dotLevel);
182 69
         }
183 70
     },
184 71
 
185 72
     /**
186
-     * Updates the audio level UI for the given id.
73
+     * Fills the dot(s) with the specified "index", with as much opacity as
74
+     * indicated by "opacity".
187 75
      *
188
-     * @param id id of the user for whom we draw the audio level
189
-     * @param audioLevel the newAudio level to render
76
+     * @param {string} elementID the parent audio indicator span element
77
+     * @param {number} index the index of the dots to fill, where 0 indicates
78
+     * the middle dot and the following increments point toward the
79
+     * corresponding pair of dots.
80
+     * @param {number} opacity the opacity to set for the specified dot.
190 81
      */
191
-    updateAudioLevel (id, audioLevel, largeVideoId) {
192
-        drawAudioLevelCanvas(id, audioLevel);
82
+    _setDotLevel(elementID, index, opacity) {
193 83
 
194
-        let videoSpanId = getVideoSpanId(id);
84
+        let audioSpan = document.getElementById(elementID)
85
+            .getElementsByClassName("audioindicator");
195 86
 
196
-        let audioLevelCanvas = $(`#${videoSpanId}>canvas`).get(0);
197
-
198
-        if (!audioLevelCanvas) {
87
+        // Make sure the audio span is still around.
88
+        if (audioSpan && audioSpan.length > 0)
89
+            audioSpan = audioSpan[0];
90
+        else
199 91
             return;
200
-        }
201 92
 
202
-        let drawContext = audioLevelCanvas.getContext('2d');
93
+        let audioTopDots
94
+            = audioSpan.getElementsByClassName("audiodot-top");
95
+        let audioDotMiddle
96
+            = audioSpan.getElementsByClassName("audiodot-middle");
97
+        let audioBottomDots
98
+            = audioSpan.getElementsByClassName("audiodot-bottom");
203 99
 
204
-        let canvasCache = audioLevelCanvasCache[id];
205
-
206
-        drawContext.clearRect(
207
-            0, 0, audioLevelCanvas.width, audioLevelCanvas.height
208
-        );
209
-        drawContext.drawImage(canvasCache, 0, 0);
210
-
211
-        if (id === LOCAL_LEVEL) {
212
-            id = APP.conference.getMyUserId();
213
-            if (!id) {
214
-                return;
215
-            }
100
+        // First take care of the middle dot case.
101
+        if (index === 0){
102
+            audioDotMiddle[0].style.opacity = opacity;
103
+            return;
216 104
         }
217 105
 
218
-        if(id === largeVideoId) {
219
-            window.requestAnimationFrame(function () {
220
-                AudioLevels.updateDominantSpeakerAudioLevel(audioLevel);
221
-            });
222
-        }
106
+        // Index > 0 : we are setting non-middle dots.
107
+        index--;
108
+        audioBottomDots[index].style.opacity = opacity;
109
+        audioTopDots[this.sideDotsCount - index - 1].style.opacity = opacity;
223 110
     },
224 111
 
225
-    updateDominantSpeakerAudioLevel (audioLevel) {
226
-        if($("#dominantSpeaker").css("visibility") == "hidden"
227
-            || ASDrawContext === null) {
228
-            return;
229
-        }
230
-
231
-        ASDrawContext.clearRect(0, 0,
232
-            dominantSpeakerAudioElement.width,
233
-            dominantSpeakerAudioElement.height);
112
+    /**
113
+     * Updates the audio level of the large video.
114
+     *
115
+     * @param audioLevel the new audio level to set.
116
+     */
117
+    updateLargeVideoAudioLevel(elementId, audioLevel) {
118
+        let element = document.getElementById(elementId);
234 119
 
235
-        if (!audioLevel) {
120
+        if(!UIUtil.isVisible(element))
236 121
             return;
237
-        }
238 122
 
239
-        ASDrawContext.shadowBlur = getShadowLevel(audioLevel);
123
+        let level = parseFloat(audioLevel);
240 124
 
241
-        // Fill the shape.
242
-        ASDrawContext.fill();
243
-    },
125
+        level = isNaN(level) ? 0 : level;
244 126
 
245
-    updateCanvasSize (thumbWidth, thumbHeight) {
246
-        let canvasWidth = thumbWidth + interfaceConfig.CANVAS_EXTRA;
247
-        let canvasHeight = thumbHeight + interfaceConfig.CANVAS_EXTRA;
127
+        let shadowElement = element.getElementsByClassName("dynamic-shadow");
248 128
 
249
-        FilmStrip.getThumbs().children('canvas').each(function () {
250
-            $(this).attr('width', canvasWidth);
251
-            $(this).attr('height', canvasHeight);
252
-        });
129
+        if (shadowElement && shadowElement.length > 0)
130
+            shadowElement = shadowElement[0];
253 131
 
254
-        Object.keys(audioLevelCanvasCache).forEach(function (id) {
255
-            audioLevelCanvasCache[id].width = canvasWidth;
256
-            audioLevelCanvasCache[id].height = canvasHeight;
257
-        });
132
+        shadowElement.style.boxShadow = this._updateLargeVideoShadow(level);
133
+    },
134
+
135
+    /**
136
+     * Updates the large video shadow effect.
137
+     */
138
+    _updateLargeVideoShadow (level) {
139
+        var scale = 2,
140
+
141
+        // Internal circle audio level.
142
+        int = {
143
+            level: level > 0.15 ? 20 : 0,
144
+            color: interfaceConfig.AUDIO_LEVEL_PRIMARY_COLOR
145
+        },
146
+
147
+        // External circle audio level.
148
+        ext = {
149
+            level: (int.level * scale * level + int.level).toFixed(0),
150
+            color: interfaceConfig.AUDIO_LEVEL_SECONDARY_COLOR
151
+        };
152
+
153
+        // Internal blur.
154
+        int.blur = int.level ? 2 : 0;
155
+
156
+        // External blur.
157
+        ext.blur = ext.level ? 6 : 0;
158
+
159
+        return [
160
+            `0 0 ${ int.blur }px ${ int.level }px ${ int.color }`,
161
+            `0 0 ${ ext.blur }px ${ ext.level }px ${ ext.color }`
162
+        ].join(', ');
258 163
     }
259 164
 };
260 165
 

+ 0
- 108
modules/UI/audio_levels/CanvasUtils.js Näytä tiedosto

@@ -1,108 +0,0 @@
1
-/**
2
- * Utility class for drawing canvas shapes.
3
- */
4
-const CanvasUtil = {
5
-
6
-    /**
7
-     * Draws a round rectangle with a glow. The glowWidth indicates the depth
8
-     * of the glow.
9
-     *
10
-     * @param drawContext the context of the canvas to draw to
11
-     * @param x the x coordinate of the round rectangle
12
-     * @param y the y coordinate of the round rectangle
13
-     * @param w the width of the round rectangle
14
-     * @param h the height of the round rectangle
15
-     * @param glowColor the color of the glow
16
-     * @param glowWidth the width of the glow
17
-     */
18
-    drawRoundRectGlow (drawContext, x, y, w, h, r, glowColor, glowWidth) {
19
-
20
-        // Save the previous state of the context.
21
-        drawContext.save();
22
-
23
-        if (w < 2 * r) r = w / 2;
24
-        if (h < 2 * r) r = h / 2;
25
-
26
-        // Draw a round rectangle.
27
-        drawContext.beginPath();
28
-        drawContext.moveTo(x+r, y);
29
-        drawContext.arcTo(x+w, y,   x+w, y+h, r);
30
-        drawContext.arcTo(x+w, y+h, x,   y+h, r);
31
-        drawContext.arcTo(x,   y+h, x,   y,   r);
32
-        drawContext.arcTo(x,   y,   x+w, y,   r);
33
-        drawContext.closePath();
34
-
35
-        // Add a shadow around the rectangle
36
-        drawContext.shadowColor = glowColor;
37
-        drawContext.shadowBlur = glowWidth;
38
-        drawContext.shadowOffsetX = 0;
39
-        drawContext.shadowOffsetY = 0;
40
-
41
-        // Fill the shape.
42
-        drawContext.fill();
43
-
44
-        drawContext.save();
45
-
46
-        drawContext.restore();
47
-
48
-//      1) Uncomment this line to use Composite Operation, which is doing the
49
-//      same as the clip function below and is also antialiasing the round
50
-//      border, but is said to be less fast performance wise.
51
-
52
-//      drawContext.globalCompositeOperation='destination-out';
53
-
54
-        drawContext.beginPath();
55
-        drawContext.moveTo(x+r, y);
56
-        drawContext.arcTo(x+w, y,   x+w, y+h, r);
57
-        drawContext.arcTo(x+w, y+h, x,   y+h, r);
58
-        drawContext.arcTo(x,   y+h, x,   y,   r);
59
-        drawContext.arcTo(x,   y,   x+w, y,   r);
60
-        drawContext.closePath();
61
-
62
-//      2) Uncomment this line to use Composite Operation, which is doing the
63
-//      same as the clip function below and is also antialiasing the round
64
-//      border, but is said to be less fast performance wise.
65
-
66
-//      drawContext.fill();
67
-
68
-        // Comment these two lines if choosing to do the same with composite
69
-        // operation above 1 and 2.
70
-        drawContext.clip();
71
-        drawContext.clearRect(0, 0, 277, 200);
72
-
73
-        // Restore the previous context state.
74
-        drawContext.restore();
75
-    },
76
-
77
-    /**
78
-     * Clones the given canvas.
79
-     *
80
-     * @return the new cloned canvas.
81
-     */
82
-    cloneCanvas (oldCanvas) {
83
-        /*
84
-         * FIXME Testing has shown that oldCanvas may not exist. In such a case,
85
-         * the method CanvasUtil.cloneCanvas may throw an error. Since audio
86
-         * levels are frequently updated, the errors have been observed to pile
87
-         * into the console, strain the CPU.
88
-         */
89
-        if (!oldCanvas)
90
-            return oldCanvas;
91
-
92
-        //create a new canvas
93
-        var newCanvas = document.createElement('canvas');
94
-        var context = newCanvas.getContext('2d');
95
-
96
-        //set dimensions
97
-        newCanvas.width = oldCanvas.width;
98
-        newCanvas.height = oldCanvas.height;
99
-
100
-        //apply the old canvas to the new one
101
-        context.drawImage(oldCanvas, 0, 0);
102
-
103
-        //return the new canvas
104
-        return newCanvas;
105
-    }
106
-};
107
-
108
-export default CanvasUtil;

+ 30
- 2
modules/UI/authentication/RoomLocker.js Näytä tiedosto

@@ -116,6 +116,13 @@ export default function createRoomLocker (room) {
116 116
     let password;
117 117
     let dialog = null;
118 118
 
119
+    /**
120
+     * If the room was locked from someone other than us, we indicate it with
121
+     * this property in order to have correct roomLocker state of isLocked.
122
+     * @type {boolean} whether room is locked, but not from us.
123
+     */
124
+    let lockedElsewhere = false;
125
+
119 126
     function lock (newPass) {
120 127
         return room.lock(newPass).then(function () {
121 128
             password = newPass;
@@ -135,13 +142,30 @@ export default function createRoomLocker (room) {
135 142
      */
136 143
     return {
137 144
         get isLocked () {
138
-            return !!password;
145
+            return !!password || lockedElsewhere;
139 146
         },
140 147
 
141 148
         get password () {
142 149
             return password;
143 150
         },
144 151
 
152
+        /**
153
+         * Sets that the room is locked from another user, not us.
154
+         * @param {boolean} value locked/unlocked state
155
+         */
156
+        set lockedElsewhere (value) {
157
+            lockedElsewhere = value;
158
+        },
159
+
160
+        /**
161
+         * Whether room is locked from someone else.
162
+         * @returns {boolean} whether room is not locked locally,
163
+         * but it is still locked.
164
+         */
165
+        get lockedElsewhere () {
166
+            return lockedElsewhere;
167
+        },
168
+
145 169
         /**
146 170
          * Allows to remove password from the conference (asks user first).
147 171
          * @returns {Promise}
@@ -185,6 +209,10 @@ export default function createRoomLocker (room) {
185 209
                 newPass => { password = newPass; }
186 210
             ).catch(
187 211
                 reason => {
212
+                    // user canceled, no pass was entered.
213
+                    // clear, as if we use the same instance several times
214
+                    // pass stays between attempts
215
+                    password = null;
188 216
                     if (reason !== APP.UI.messageHandler.CANCEL)
189 217
                         console.error(reason);
190 218
                 }
@@ -202,7 +230,7 @@ export default function createRoomLocker (room) {
202 230
                 dialog = null;
203 231
             };
204 232
 
205
-            if (password) {
233
+            if (this.isLocked) {
206 234
                 dialog = APP.UI.messageHandler
207 235
                     .openMessageDialog(null, "dialog.passwordError",
208 236
                         null, null, closeCallback);

+ 128
- 0
modules/UI/feedback/Feedback.js Näytä tiedosto

@@ -0,0 +1,128 @@
1
+/* global $, APP, config, interfaceConfig, JitsiMeetJS */
2
+import UIEvents from "../../../service/UI/UIEvents";
3
+import FeedabckWindow from "./FeedbackWindow";
4
+
5
+/**
6
+ * Shows / hides the feedback button.
7
+ * @private
8
+ */
9
+function _toggleFeedbackIcon() {
10
+    $('#feedbackButtonDiv').toggleClass("hidden");
11
+}
12
+
13
+/**
14
+ * Shows / hides the feedback button.
15
+ * @param {show} set to {true} to show the feedback button or to  {false}
16
+ * to hide it
17
+ * @private
18
+ */
19
+function _showFeedbackButton (show) {
20
+    var feedbackButton = $("#feedbackButtonDiv");
21
+
22
+    if (show)
23
+        feedbackButton.css("display", "block");
24
+    else
25
+        feedbackButton.css("display", "none");
26
+}
27
+
28
+/**
29
+ * Defines all methods in connection to the Feedback window.
30
+ *
31
+ * @type {{openFeedbackWindow: Function}}
32
+ */
33
+var Feedback = {
34
+
35
+    /**
36
+     * Initialise the Feedback functionality.
37
+     * @param emitter the EventEmitter to associate with the Feedback.
38
+     */
39
+    init: function (emitter) {
40
+        // CallStats is the way we send feedback, so we don't have to initialise
41
+        // if callstats isn't enabled.
42
+        if (!APP.conference.isCallstatsEnabled())
43
+            return;
44
+
45
+        // If enabled property is still undefined, i.e. it hasn't been set from
46
+        // some other module already, we set it to true by default.
47
+        if (typeof this.enabled == "undefined")
48
+            this.enabled = true;
49
+
50
+        _showFeedbackButton(this.enabled);
51
+
52
+        this.window = new FeedabckWindow({});
53
+
54
+        $("#feedbackButton").click(Feedback.openFeedbackWindow);
55
+
56
+        // Show / hide the feedback button whenever the film strip is
57
+        // shown / hidden.
58
+        emitter.addListener(UIEvents.TOGGLE_FILM_STRIP, function () {
59
+            _toggleFeedbackIcon();
60
+        });
61
+    },
62
+    /**
63
+     * Enables/ disabled the feedback feature.
64
+     */
65
+    enableFeedback: function (enable) {
66
+        if (this.enabled !== enable)
67
+            _showFeedbackButton(enable);
68
+        this.enabled = enable;
69
+    },
70
+
71
+    /**
72
+     * Indicates if the feedback functionality is enabled.
73
+     *
74
+     * @return true if the feedback functionality is enabled, false otherwise.
75
+     */
76
+    isEnabled: function() {
77
+        return this.enabled && APP.conference.isCallstatsEnabled();
78
+    },
79
+
80
+    /**
81
+     * Returns true if the feedback window is currently visible and false
82
+     * otherwise.
83
+     * @return {boolean} true if the feedback window is visible, false
84
+     * otherwise
85
+     */
86
+    isVisible: function() {
87
+        return $(".feedback").is(":visible");
88
+    },
89
+
90
+    /**
91
+     * Indicates if the feedback is submitted.
92
+     *
93
+     * @return {boolean} {true} to indicate if the feedback is submitted,
94
+     * {false} - otherwise
95
+     */
96
+    isSubmitted: function() {
97
+        return Feedback.window.submitted;
98
+    },
99
+
100
+    /**
101
+     * Opens the feedback window.
102
+     */
103
+    openFeedbackWindow: function (callback) {
104
+        Feedback.window.show(callback);
105
+
106
+        JitsiMeetJS.analytics.sendEvent('feedback.open');
107
+    },
108
+
109
+    /**
110
+     * Returns the feedback score.
111
+     *
112
+     * @returns {*}
113
+     */
114
+    getFeedbackScore: function() {
115
+        return Feedback.window.feedbackScore;
116
+    },
117
+
118
+    /**
119
+     * Returns the feedback free text.
120
+     *
121
+     * @returns {null|*|message}
122
+     */
123
+    getFeedbackText: function() {
124
+        return Feedback.window.feedbackText;
125
+    }
126
+};
127
+
128
+module.exports = Feedback;

+ 193
- 0
modules/UI/feedback/FeedbackWindow.js Näytä tiedosto

@@ -0,0 +1,193 @@
1
+/* global $, APP, interfaceConfig, AJS */
2
+/* jshint -W101 */
3
+
4
+const selector = '#aui-feedback-dialog';
5
+
6
+/**
7
+ * Toggles the appropriate css class for the given number of stars, to
8
+ * indicate that those stars have been clicked/selected.
9
+ *
10
+ * @param starCount the number of stars, for which to toggle the css class
11
+ */
12
+let toggleStars = function(starCount) {
13
+    $('#stars > a').each(function(index, el) {
14
+        if (index <= starCount) {
15
+            el.classList.add("starHover");
16
+        } else
17
+            el.classList.remove("starHover");
18
+    });
19
+};
20
+
21
+/**
22
+ * Constructs the html for the rated feedback window.
23
+ *
24
+ * @returns {string} the contructed html string
25
+ */
26
+let createRateFeedbackHTML = function (Feedback) {
27
+    let rateExperience
28
+            = APP.translation.translateString('dialog.rateExperience'),
29
+        feedbackHelp = APP.translation.translateString('dialog.feedbackHelp');
30
+
31
+    let starClassName = (interfaceConfig.ENABLE_FEEDBACK_ANIMATION)
32
+                            ? "icon-star shake-rotate"
33
+                            : "icon-star";
34
+
35
+    return `
36
+        <div class="aui-dialog2-content feedback__content">
37
+            <form action="javascript:false;" onsubmit="return false;">
38
+                <div class="feedback__rating">
39
+                    <h2>${ rateExperience }</h2>
40
+                    <p class="star-label">&nbsp;</p>
41
+                    <div id="stars" class="feedback-stars">
42
+                        <a class="star-btn">
43
+                            <i class=${ starClassName }></i>
44
+                        </a>
45
+                        <a class="star-btn">
46
+                            <i class=${ starClassName }></i>
47
+                        </a>
48
+                        <a class="star-btn">
49
+                            <i class=${ starClassName }></i>
50
+                        </a>
51
+                        <a class="star-btn">
52
+                            <i class=${ starClassName }></i>
53
+                        </a>
54
+                        <a class="star-btn">
55
+                            <i class=${ starClassName }></i>
56
+                        </a>
57
+                    </div>
58
+                    <p>&nbsp;</p>
59
+                    <p>${ feedbackHelp }</p>
60
+                </div>
61
+                <textarea id="feedbackTextArea" rows="10" cols="40" autofocus></textarea>
62
+            </form>
63
+            <footer class="aui-dialog2-footer feedback__footer">
64
+                <div class="aui-dialog2-footer-actions">
65
+                    <button id="dialog-close-button" class="aui-button aui-button_close">Close</button>
66
+                    <button id="dialog-submit-button" class="aui-button aui-button_submit">Submit</button>
67
+                </div>
68
+            </footer>
69
+        </div>
70
+`;
71
+};
72
+
73
+/**
74
+ * Callback for Rate Feedback
75
+ *
76
+ * @param Feedback
77
+ */
78
+let onLoadRateFunction = function (Feedback) {
79
+    $('#stars > a').each((index, el) => {
80
+        el.onmouseover = function(){
81
+            toggleStars(index);
82
+        };
83
+        el.onmouseleave = function(){
84
+            toggleStars(Feedback.feedbackScore - 1);
85
+        };
86
+        el.onclick = function(){
87
+            Feedback.feedbackScore = index + 1;
88
+        };
89
+    });
90
+
91
+    // Init stars to correspond to previously entered feedback.
92
+    if (Feedback.feedbackScore > 0) {
93
+        toggleStars(Feedback.feedbackScore - 1);
94
+    }
95
+
96
+    if (Feedback.feedbackText && Feedback.feedbackText.length > 0)
97
+        $('#feedbackTextArea').text(Feedback.feedbackText);
98
+
99
+    let submitBtn = Feedback.$el.find('#dialog-submit-button');
100
+    let closeBtn = Feedback.$el.find('#dialog-close-button');
101
+
102
+    if (submitBtn && submitBtn.length) {
103
+        submitBtn.on('click', (e) => {
104
+            e.preventDefault();
105
+            Feedback.onFeedbackSubmitted();
106
+        });
107
+    }
108
+    if (closeBtn && closeBtn.length) {
109
+        closeBtn.on('click', (e) => {
110
+            e.preventDefault();
111
+            Feedback.hide();
112
+        });
113
+    }
114
+
115
+    $('#feedbackTextArea').focus();
116
+};
117
+
118
+/**
119
+ * @class Dialog
120
+ *
121
+ */
122
+export default class Dialog {
123
+
124
+    constructor(options) {
125
+        this.feedbackScore = -1;
126
+        this.feedbackText = null;
127
+        this.submitted = false;
128
+        this.onCloseCallback = null;
129
+
130
+        this.states = {
131
+            rate_feedback: {
132
+                getHtml: createRateFeedbackHTML,
133
+                onLoad: onLoadRateFunction
134
+            }
135
+        };
136
+        this.state = options.state || 'rate_feedback';
137
+
138
+        this.window = AJS.dialog2(selector, {
139
+            closeOnOutsideClick: true
140
+        });
141
+        this.$el = this.window.$el;
142
+
143
+        AJS.dialog2(selector).on("hide", function() {
144
+            if (this.onCloseCallback) {
145
+                this.onCloseCallback();
146
+                this.onCloseCallback = null;
147
+            }
148
+        }.bind(this));
149
+
150
+        this.setState();
151
+    }
152
+
153
+    setState(state) {
154
+        let newState = state || this.state;
155
+
156
+        let htmlStr = this.states[newState].getHtml(this);
157
+
158
+        this.$el.html(htmlStr);
159
+
160
+        this.states[newState].onLoad(this);
161
+    }
162
+
163
+    show(cb) {
164
+        this.setState('rate_feedback');
165
+        if (typeof cb == 'function') {
166
+            this.onCloseCallback = cb;
167
+        }
168
+
169
+        this.window.show();
170
+
171
+    }
172
+
173
+    hide() {
174
+        this.window.hide();
175
+    }
176
+
177
+    onFeedbackSubmitted() {
178
+        let message = this.$el.find('textarea').val();
179
+        let self = this;
180
+
181
+        if (message && message.length > 0) {
182
+            self.feedbackText = message;
183
+        }
184
+
185
+        APP.conference.sendFeedback(self.feedbackScore,
186
+                                    self.feedbackText);
187
+
188
+        // TO DO: make sendFeedback return true or false.
189
+        self.submitted = true;
190
+
191
+        this.hide();
192
+    }
193
+}

+ 4
- 1
modules/UI/recording/Recording.js Näytä tiedosto

@@ -17,7 +17,7 @@
17 17
 import UIEvents from "../../../service/UI/UIEvents";
18 18
 import UIUtil from '../util/UIUtil';
19 19
 import VideoLayout from '../videolayout/VideoLayout';
20
-import Feedback from '../Feedback.js';
20
+import Feedback from '../feedback/Feedback.js';
21 21
 import Toolbar from '../toolbars/Toolbar';
22 22
 
23 23
 /**
@@ -270,6 +270,9 @@ var Recording = {
270 270
     initRecordingButton(recordingType) {
271 271
         let selector = $('#toolbar_button_record');
272 272
 
273
+        let button = selector.get(0);
274
+        UIUtil.setTooltip(button, 'liveStreaming.buttonTooltip', 'right');
275
+
273 276
         if (recordingType === 'jibri') {
274 277
             this.baseClass = "fa fa-play-circle";
275 278
             this.recordingTitle = "dialog.liveStreaming";

+ 57
- 15
modules/UI/ring_overlay/RingOverlay.js Näytä tiedosto

@@ -1,5 +1,21 @@
1
-/* global $ */
1
+/* global $, APP */
2 2
 /* jshint -W101 */
3
+import UIEvents from "../../../service/UI/UIEvents";
4
+
5
+/**
6
+ * Store the current ring overlay instance.
7
+ * Note: We want to have only 1 instance at a time.
8
+ */
9
+let overlay = null;
10
+
11
+/**
12
+ * Handler for UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED event.
13
+ * @param {boolean} shown indicates whether the avatar on the large video is
14
+ *  currently displayed or not.
15
+ */
16
+function onAvatarDisplayed(shown) {
17
+    overlay._changeBackground(shown);
18
+}
3 19
 
4 20
 /**
5 21
  * Shows ring overlay
@@ -11,28 +27,49 @@ class RingOverlay {
11 27
     constructor(callee) {
12 28
         this._containerId = 'ringOverlay';
13 29
         this._audioContainerId = 'ringOverlayRinging';
14
-
30
+        this.isRinging = true;
15 31
         this.callee = callee;
16 32
         this.render();
17 33
         this.audio = document.getElementById(this._audioContainerId);
18 34
         this.audio.play();
19 35
         this._setAudioTimeout();
36
+        this._timeout = setTimeout(() => {
37
+            this.destroy();
38
+            this.render();
39
+        }, 30000);
40
+    }
41
+
42
+    /**
43
+     * Chagnes the background of the ring overlay.
44
+     * @param {boolean} solid - if true the new background will be the solid
45
+     * one, otherwise the background will be default one.
46
+     * NOTE: The method just toggles solidBG css class.
47
+     */
48
+    _changeBackground(solid) {
49
+        const container = $("#" + this._containerId);
50
+        if(solid) {
51
+            container.addClass("solidBG");
52
+        } else {
53
+            container.removeClass("solidBG");
54
+        }
20 55
     }
21 56
 
22 57
     /**
23 58
      * Builds and appends the ring overlay to the html document
24 59
      */
25 60
     _getHtmlStr(callee) {
61
+        let callingLabel = this.isRinging? "<p>Calling...</p>" : "";
62
+        let callerStateLabel =  this.isRinging? "" : " isn't available";
26 63
         return `
27 64
             <div id="${this._containerId}" class='ringing' >
28 65
                 <div class='ringing__content'>
29
-                    <p>Calling...</p>
66
+                    ${callingLabel}
30 67
                     <img class='ringing__avatar' src="${callee.getAvatarUrl()}" />
31 68
                     <div class="ringing__caller-info">
32
-                        <p>${callee.getName()}</p>
69
+                        <p>${callee.getName()}${callerStateLabel}</p>
33 70
                     </div>
34 71
                 </div>
35
-                <audio id="${this._audioContainerId}" src="/sounds/ring.ogg" />
72
+                <audio id="${this._audioContainerId}" src="./sounds/ring.ogg" />
36 73
             </div>`;
37 74
     }
38 75
 
@@ -49,10 +86,7 @@ class RingOverlay {
49 86
      * related to the ring overlay.
50 87
      */
51 88
     destroy() {
52
-        if (this.interval) {
53
-            clearInterval(this.interval);
54
-        }
55
-
89
+        this._stopAudio();
56 90
         this._detach();
57 91
     }
58 92
 
@@ -64,6 +98,16 @@ class RingOverlay {
64 98
         $(`#${this._containerId}`).remove();
65 99
     }
66 100
 
101
+    _stopAudio() {
102
+        this.isRinging = false;
103
+        if (this.interval) {
104
+            clearInterval(this.interval);
105
+        }
106
+        if(this._timeout) {
107
+            clearTimeout(this._timeout);
108
+        }
109
+    }
110
+
67 111
     /**
68 112
      * Sets the interval that is going to play the ringing sound.
69 113
      */
@@ -74,12 +118,6 @@ class RingOverlay {
74 118
     }
75 119
 }
76 120
 
77
-/**
78
- * Store the current ring overlay instance.
79
- * Note: We want to have only 1 instance at a time.
80
- */
81
-let overlay = null;
82
-
83 121
 export default {
84 122
     /**
85 123
      * Shows the ring overlay for the passed callee.
@@ -92,6 +130,8 @@ export default {
92 130
         }
93 131
 
94 132
         overlay = new RingOverlay(callee);
133
+        APP.UI.addListener(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED,
134
+            onAvatarDisplayed);
95 135
     },
96 136
 
97 137
     /**
@@ -104,6 +144,8 @@ export default {
104 144
         }
105 145
         overlay.destroy();
106 146
         overlay = null;
147
+        APP.UI.removeListener(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED,
148
+            onAvatarDisplayed);
107 149
         return true;
108 150
     },
109 151
 

+ 1
- 5
modules/UI/shared_video/SharedVideo.js Näytä tiedosto

@@ -243,7 +243,7 @@ export default class SharedVideoManager {
243 243
 
244 244
             let thumb = new SharedVideoThumb(self.url);
245 245
             thumb.setDisplayName(player.getVideoData().title);
246
-            VideoLayout.addParticipantContainer(self.url, thumb);
246
+            VideoLayout.addRemoteVideoContainer(self.url, thumb);
247 247
 
248 248
             let iframe = player.getIframe();
249 249
             self.sharedVideo = new SharedVideoContainer(
@@ -567,10 +567,6 @@ class SharedVideoContainer extends LargeContainer {
567 567
         this.player = player;
568 568
     }
569 569
 
570
-    get $video () {
571
-        return this.$iframe;
572
-    }
573
-
574 570
     show () {
575 571
         let self = this;
576 572
         return new Promise(resolve => {

+ 40
- 31
modules/UI/side_pannels/chat/Chat.js Näytä tiedosto

@@ -3,24 +3,26 @@
3 3
 import {processReplacements, linkify} from './Replacement';
4 4
 import CommandsProcessor from './Commands';
5 5
 import ToolbarToggler from '../../toolbars/ToolbarToggler';
6
+import VideoLayout from "../../videolayout/VideoLayout";
6 7
 
7 8
 import UIUtil from '../../util/UIUtil';
8 9
 import UIEvents from '../../../../service/UI/UIEvents';
9 10
 
10
-var smileys = require("./smileys.json").smileys;
11
+import { smileys } from './smileys';
11 12
 
12
-var notificationInterval = false;
13 13
 var unreadMessages = 0;
14 14
 
15
+/**
16
+ * The container id, which is and the element id.
17
+ */
18
+var CHAT_CONTAINER_ID = "chat_container";
15 19
 
16 20
 /**
17
- * Shows/hides a visual notification, indicating that a message has arrived.
21
+ *  Updates visual notification, indicating that a message has arrived.
18 22
  */
19
-function setVisualNotification(show) {
23
+function updateVisualNotification() {
20 24
     var unreadMsgElement = document.getElementById('unreadMessages');
21 25
 
22
-    var glower = $('#toolbar_button_chat');
23
-
24 26
     if (unreadMessages) {
25 27
         unreadMsgElement.innerHTML = unreadMessages.toString();
26 28
 
@@ -37,28 +39,12 @@ function setVisualNotification(show) {
37 39
             'style',
38 40
                 'top:' + topIndent +
39 41
                 '; left:' + leftIndent + ';');
40
-
41
-        if (!glower.hasClass('icon-chat-simple')) {
42
-            glower.removeClass('icon-chat');
43
-            glower.addClass('icon-chat-simple');
44
-        }
45 42
     }
46 43
     else {
47 44
         unreadMsgElement.innerHTML = '';
48
-        glower.removeClass('icon-chat-simple');
49
-        glower.addClass('icon-chat');
50 45
     }
51 46
 
52
-    if (show && !notificationInterval) {
53
-        notificationInterval = window.setInterval(function () {
54
-            glower.toggleClass('active');
55
-        }, 800);
56
-    }
57
-    else if (!show && notificationInterval) {
58
-        window.clearInterval(notificationInterval);
59
-        notificationInterval = false;
60
-        glower.removeClass('active');
61
-    }
47
+    $(unreadMsgElement).parent()[unreadMessages > 0 ? 'show' : 'hide']();
62 48
 }
63 49
 
64 50
 
@@ -131,7 +117,7 @@ function addSmileys() {
131 117
  */
132 118
 function resizeChatConversation() {
133 119
     var msgareaHeight = $('#usermsg').outerHeight();
134
-    var chatspace = $('#chat_container');
120
+    var chatspace = $('#' + CHAT_CONTAINER_ID);
135 121
     var width = chatspace.width();
136 122
     var chat = $('#chatconversation');
137 123
     var smileys = $('#smileysarea');
@@ -187,13 +173,30 @@ var Chat = {
187 173
         };
188 174
         usermsg.autosize({callback: onTextAreaResize});
189 175
 
190
-        $("#chat_container").bind("shown",
191
-            function () {
176
+        eventEmitter.on(UIEvents.SIDE_TOOLBAR_CONTAINER_TOGGLED,
177
+            function(containerId, isVisible) {
178
+                if (containerId !== CHAT_CONTAINER_ID || !isVisible)
179
+                    return;
180
+
192 181
                 unreadMessages = 0;
193
-                setVisualNotification(false);
182
+                updateVisualNotification();
183
+
184
+                // Undock the toolbar when the chat is shown and if we're in a
185
+                // video mode.
186
+                if (VideoLayout.isLargeVideoVisible()) {
187
+                    ToolbarToggler.dockToolbar(false);
188
+                }
189
+
190
+                // if we are in conversation mode focus on the text input
191
+                // if we are not, focus on the display name input
192
+                if (APP.settings.getDisplayName())
193
+                    $('#usermsg').focus();
194
+                else
195
+                    $('#nickinput').focus();
194 196
             });
195 197
 
196 198
         addSmileys();
199
+        updateVisualNotification();
197 200
     },
198 201
 
199 202
     /**
@@ -210,7 +213,7 @@ var Chat = {
210 213
             if (!Chat.isVisible()) {
211 214
                 unreadMessages++;
212 215
                 UIUtil.playSoundNotification('chatNotification');
213
-                setVisualNotification(true);
216
+                updateVisualNotification();
214 217
             }
215 218
         }
216 219
 
@@ -271,12 +274,18 @@ var Chat = {
271 274
 
272 275
     /**
273 276
      * Sets the chat conversation mode.
277
+     * Conversation mode is the normal chat mode, non conversation mode is
278
+     * where we ask user to input its display name.
274 279
      * @param {boolean} isConversationMode if chat should be in
275 280
      * conversation mode or not.
276 281
      */
277 282
     setChatConversationMode (isConversationMode) {
278
-        $('#chat_container')
283
+        $('#' + CHAT_CONTAINER_ID)
279 284
             .toggleClass('is-conversation-mode', isConversationMode);
285
+
286
+        // this is needed when we transition from no conversation mode to
287
+        // conversation mode. When user enters his nickname and hits enter,
288
+        // to focus on the write area.
280 289
         if (isConversationMode) {
281 290
             $('#usermsg').focus();
282 291
         }
@@ -286,7 +295,7 @@ var Chat = {
286 295
      * Resizes the chat area.
287 296
      */
288 297
     resizeChat (width, height) {
289
-        $('#chat_container').width(width).height(height);
298
+        $('#' + CHAT_CONTAINER_ID).width(width).height(height);
290 299
 
291 300
         resizeChatConversation();
292 301
     },
@@ -296,7 +305,7 @@ var Chat = {
296 305
      */
297 306
     isVisible () {
298 307
         return UIUtil.isVisible(
299
-            document.getElementById("chat_container"));
308
+            document.getElementById(CHAT_CONTAINER_ID));
300 309
     },
301 310
     /**
302 311
      * Shows and hides the window with the smileys

+ 5
- 6
modules/UI/side_pannels/chat/Replacement.js Näytä tiedosto

@@ -1,5 +1,5 @@
1 1
 /* jshint -W101 */
2
-var Smileys = require("./smileys.json");
2
+import { regexes } from './smileys';
3 3
 
4 4
 /**
5 5
  * Processes links and smileys in "body"
@@ -29,7 +29,7 @@ export function linkify(inputText) {
29 29
     replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
30 30
     replacedText = replacedText.replace(replacePattern2, '$1<a href="http://$2" target="_blank">$2</a>');
31 31
 
32
-    //Change email addresses to mailto:: links.
32
+    //Change email addresses to mailto: links.
33 33
     replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
34 34
     replacedText = replacedText.replace(replacePattern3, '<a href="mailto:$1">$1</a>');
35 35
 
@@ -44,10 +44,9 @@ function smilify(body) {
44 44
         return body;
45 45
     }
46 46
 
47
-    var regexs = Smileys.regexs;
48
-    for(var smiley in regexs) {
49
-        if(regexs.hasOwnProperty(smiley)) {
50
-            body = body.replace(regexs[smiley],
47
+    for(var smiley in regexes) {
48
+        if(regexes.hasOwnProperty(smiley)) {
49
+            body = body.replace(regexes[smiley],
51 50
                     '<img class="smiley" src="images/smileys/' + smiley + '.svg">');
52 51
         }
53 52
     }

+ 47
- 0
modules/UI/side_pannels/chat/smileys.js Näytä tiedosto

@@ -0,0 +1,47 @@
1
+export const smileys = {
2
+    smiley1: ":)",
3
+    smiley2: ":(",
4
+    smiley3: ":D",
5
+    smiley4: "(y)",
6
+    smiley5: " :P",
7
+    smiley6: "(wave)",
8
+    smiley7: "(blush)",
9
+    smiley8: "(chuckle)",
10
+    smiley9: "(shocked)",
11
+    smiley10: ":*",
12
+    smiley11: "(n)",
13
+    smiley12: "(search)",
14
+    smiley13: " <3",
15
+    smiley14: "(oops)",
16
+    smiley15: "(angry)",
17
+    smiley16: "(angel)",
18
+    smiley17: "(sick)",
19
+    smiley18: ";(",
20
+    smiley19: "(bomb)",
21
+    smiley20: "(clap)",
22
+    smiley21: " ;)"
23
+};
24
+
25
+export const regexes = {
26
+    smiley2: /(:-\(\(|:-\(|:\(\(|:\(|\(sad\))/gi,
27
+    smiley3: /(:-\)\)|:\)\)|\(lol\)|:-D|:D)/gi,
28
+    smiley1: /(:-\)|:\))/gi,
29
+    smiley4: /(\(y\)|\(Y\)|\(ok\))/gi,
30
+    smiley5: /(:-P|:P|:-p|:p)/gi,
31
+    smiley6: /(\(wave\))/gi,
32
+    smiley7: /(\(blush\))/gi,
33
+    smiley8: /(\(chuckle\))/gi,
34
+    smiley9: /(:-0|\(shocked\))/gi,
35
+    smiley10: /(:-\*|:\*|\(kiss\))/gi,
36
+    smiley11: /(\(n\))/gi,
37
+    smiley12: /(\(search\))/g,
38
+    smiley13: /(<3|&lt;3|&amp;lt;3|\(L\)|\(l\)|\(H\)|\(h\))/gi,
39
+    smiley14: /(\(oops\))/gi,
40
+    smiley15: /(\(angry\))/gi,
41
+    smiley16: /(\(angel\))/gi,
42
+    smiley17: /(\(sick\))/gi,
43
+    smiley18: /(;-\(\(|;\(\(|;-\(|;\(|:"\(|:"-\(|:~-\(|:~\(|\(upset\))/gi,
44
+    smiley19: /(\(bomb\))/gi,
45
+    smiley20: /(\(clap\))/gi,
46
+    smiley21: /(;-\)|;\)|;-\)\)|;\)\)|;-D|;D|\(wink\))/gi
47
+};

+ 0
- 48
modules/UI/side_pannels/chat/smileys.json Näytä tiedosto

@@ -1,48 +0,0 @@
1
-{
2
-    "smileys": {
3
-        "smiley1": ":)",
4
-        "smiley2": ":(",
5
-        "smiley3": ":D",
6
-        "smiley4": "(y)",
7
-        "smiley5": " :P",
8
-        "smiley6": "(wave)",
9
-        "smiley7": "(blush)",
10
-        "smiley8": "(chuckle)",
11
-        "smiley9": "(shocked)",
12
-        "smiley10": ":*",
13
-        "smiley11": "(n)",
14
-        "smiley12": "(search)",
15
-        "smiley13": " <3",
16
-        "smiley14": "(oops)",
17
-        "smiley15": "(angry)",
18
-        "smiley16": "(angel)",
19
-        "smiley17": "(sick)",
20
-        "smiley18": ";(",
21
-        "smiley19": "(bomb)",
22
-        "smiley20": "(clap)",
23
-        "smiley21": " ;)"
24
-    },
25
-    "regexs": {
26
-        "smiley2": /(:-\(\(|:-\(|:\(\(|:\(|\(sad\))/gi,
27
-        "smiley3": /(:-\)\)|:\)\)|\(lol\)|:-D|:D)/gi,
28
-        "smiley1": /(:-\)|:\))/gi,
29
-        "smiley4": /(\(y\)|\(Y\)|\(ok\))/gi,
30
-        "smiley5": /(:-P|:P|:-p|:p)/gi,
31
-        "smiley6": /(\(wave\))/gi,
32
-        "smiley7": /(\(blush\))/gi,
33
-        "smiley8": /(\(chuckle\))/gi,
34
-        "smiley9": /(:-0|\(shocked\))/gi,
35
-        "smiley10": /(:-\*|:\*|\(kiss\))/gi,
36
-        "smiley11": /(\(n\))/gi,
37
-        "smiley12": /(\(search\))/g,
38
-        "smiley13": /(<3|&lt;3|&amp;lt;3|\(L\)|\(l\)|\(H\)|\(h\))/gi,
39
-        "smiley14": /(\(oops\))/gi,
40
-        "smiley15": /(\(angry\))/gi,
41
-        "smiley16": /(\(angel\))/gi,
42
-        "smiley17": /(\(sick\))/gi,
43
-        "smiley18": /(;-\(\(|;\(\(|;-\(|;\(|:"\(|:"-\(|:~-\(|:~\(|\(upset\))/gi,
44
-        "smiley19": /(\(bomb\))/gi,
45
-        "smiley20": /(\(clap\))/gi,
46
-        "smiley21": /(;-\)|;\)|;-\)\)|;\)\)|;-D|;D|\(wink\))/gi
47
-    }
48
-}

+ 10
- 18
modules/UI/side_pannels/contactlist/ContactList.js Näytä tiedosto

@@ -20,13 +20,11 @@ function updateNumberOfParticipants(delta) {
20 20
         return;
21 21
     }
22 22
 
23
-    let buttonIndicatorText = (numberOfContacts === 1) ? '' : numberOfContacts;
24
-    $("#numberOfParticipants").text(buttonIndicatorText);
23
+    $("#numberOfParticipants").text(numberOfContacts);
25 24
 
26 25
     $("#contacts_container>div.title").text(
27
-        APP.translation.translateString(
28
-            "contactlist", {participants: numberOfContacts}
29
-        ));
26
+        APP.translation.translateString("contactlist")
27
+            + ' (' + numberOfContacts + ')');
30 28
 }
31 29
 
32 30
 /**
@@ -59,16 +57,6 @@ function createDisplayNameParagraph(key, displayName) {
59 57
     return p;
60 58
 }
61 59
 
62
-
63
-function stopGlowing(glower) {
64
-    window.clearInterval(notificationInterval);
65
-    notificationInterval = false;
66
-    glower.removeClass('glowing');
67
-    if (!ContactList.isVisible()) {
68
-        glower.removeClass('active');
69
-    }
70
-}
71
-
72 60
 function getContactEl (id) {
73 61
     return $(`#contacts>li[id="${id}"]`);
74 62
 }
@@ -96,9 +84,9 @@ var ContactList = {
96 84
 
97 85
     /**
98 86
      * Adds a contact for the given id.
99
-     *
87
+     * @param isLocal is an id for the local user.
100 88
      */
101
-    addContact (id) {
89
+    addContact (id, isLocal) {
102 90
         let contactlist = $('#contacts');
103 91
 
104 92
         let newContact = document.createElement('li');
@@ -112,7 +100,11 @@ var ContactList = {
112 100
 
113 101
         if (interfaceConfig.SHOW_CONTACTLIST_AVATARS)
114 102
             newContact.appendChild(createAvatar(id));
115
-        newContact.appendChild(createDisplayNameParagraph("participant"));
103
+
104
+        newContact.appendChild(
105
+            createDisplayNameParagraph(
106
+                isLocal ? interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME : null,
107
+                isLocal ? null : interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME));
116 108
 
117 109
         if (APP.conference.isLocalId(id)) {
118 110
             contactlist.prepend(newContact);

+ 2
- 6
modules/UI/side_pannels/settings/SettingsMenu.js Näytä tiedosto

@@ -74,10 +74,7 @@ export default {
74 74
                     }
75 75
                 });
76 76
 
77
-            // Only show the subtitle if this isn't the only setting section.
78
-            if (interfaceConfig.SETTINGS_SECTIONS.length > 1)
79
-                UIUtil.showElement("deviceOptionsTitle");
80
-
77
+            UIUtil.showElement("deviceOptionsTitle");
81 78
             UIUtil.showElement("devicesOptions");
82 79
         }
83 80
 
@@ -150,8 +147,7 @@ export default {
150 147
     showStartMutedOptions (show) {
151 148
         if (show && UIUtil.isSettingEnabled('moderator')) {
152 149
             // Only show the subtitle if this isn't the only setting section.
153
-            if (!$("#moderatorOptionsTitle").is(":visible")
154
-                && interfaceConfig.SETTINGS_SECTIONS.length > 1)
150
+            if (!$("#moderatorOptionsTitle").is(":visible"))
155 151
                 UIUtil.showElement("moderatorOptionsTitle");
156 152
 
157 153
             UIUtil.showElement("startMutedOptions");

+ 67
- 19
modules/UI/toolbars/Toolbar.js Näytä tiedosto

@@ -7,7 +7,6 @@ import SideContainerToggler from "../side_pannels/SideContainerToggler";
7 7
 let roomUrl = null;
8 8
 let emitter = null;
9 9
 
10
-
11 10
 /**
12 11
  * Opens the invite link dialog.
13 12
  */
@@ -21,36 +20,46 @@ function openLinkDialog () {
21 20
         inviteAttributes = "value=\"" + encodeURI(roomUrl) + "\"";
22 21
     }
23 22
 
23
+    let inviteLinkId = "inviteLinkRef";
24
+    let focusInviteLink = function() {
25
+        $('#' + inviteLinkId).focus();
26
+        $('#' + inviteLinkId).select();
27
+    };
28
+
24 29
     let title = APP.translation.generateTranslationHTML("dialog.shareLink");
25 30
     APP.UI.messageHandler.openTwoButtonDialog(
26
-        null, null, null,
27
-        '<h2>' + title + '</h2>'
28
-        + '<input id="inviteLinkRef" type="text" '
29
-        + inviteAttributes + ' onclick="this.select();" readonly>',
30
-        false, "dialog.Invite",
31
+        null, title, null,
32
+        '<input id="' + inviteLinkId + '" type="text" '
33
+            + inviteAttributes + ' readonly/>',
34
+        false, "dialog.copy",
31 35
         function (e, v) {
32 36
             if (v && roomUrl) {
33 37
                 JitsiMeetJS.analytics.sendEvent('toolbar.invite.button');
34
-                emitter.emit(UIEvents.USER_INVITED, roomUrl);
38
+
39
+                focusInviteLink();
40
+
41
+                document.execCommand('copy');
35 42
             }
36 43
             else {
37 44
                 JitsiMeetJS.analytics.sendEvent('toolbar.invite.cancel');
38 45
             }
39 46
         },
40 47
         function (event) {
41
-            if (roomUrl) {
42
-                document.getElementById('inviteLinkRef').select();
43
-            } else {
48
+            if (!roomUrl) {
44 49
                 if (event && event.target) {
45 50
                     $(event.target).find('button[value=true]')
46 51
                         .prop('disabled', true);
47 52
                 }
48 53
             }
54
+            else {
55
+                focusInviteLink();
56
+            }
49 57
         },
50 58
         function (e, v, m, f) {
51 59
             if(!v && !m && !f)
52 60
                 JitsiMeetJS.analytics.sendEvent('toolbar.invite.close');
53
-        }
61
+        },
62
+        'Copy' // Focus Copy button.
54 63
     );
55 64
 }
56 65
 
@@ -181,6 +190,7 @@ const buttonHandlers = {
181 190
 const defaultToolbarButtons = {
182 191
     'microphone': {
183 192
         id: 'toolbar_button_mute',
193
+        tooltipKey: 'toolbar.mute',
184 194
         className: "button icon-microphone",
185 195
         shortcut: 'M',
186 196
         shortcutAttr: 'mutePopover',
@@ -211,6 +221,7 @@ const defaultToolbarButtons = {
211 221
     },
212 222
     'camera': {
213 223
         id: 'toolbar_button_camera',
224
+        tooltipKey: 'toolbar.videomute',
214 225
         className: "button icon-camera",
215 226
         shortcut: 'V',
216 227
         shortcutAttr: 'toggleVideoPopover',
@@ -224,6 +235,7 @@ const defaultToolbarButtons = {
224 235
     },
225 236
     'desktop': {
226 237
         id: 'toolbar_button_desktopsharing',
238
+        tooltipKey: 'toolbar.sharescreen',
227 239
         className: 'button icon-share-desktop',
228 240
         shortcut: 'D',
229 241
         shortcutAttr: 'toggleDesktopSharingPopover',
@@ -236,16 +248,19 @@ const defaultToolbarButtons = {
236 248
         i18n: '[content]toolbar.sharescreen'
237 249
     },
238 250
     'security': {
239
-        id: 'toolbar_button_security'
251
+        id: 'toolbar_button_security',
252
+        tooltipKey: 'toolbar.lock'
240 253
     },
241 254
     'invite': {
242 255
         id: 'toolbar_button_link',
256
+        tooltipKey: 'toolbar.invite',
243 257
         className: 'button icon-link',
244 258
         content: 'Invite others',
245 259
         i18n: '[content]toolbar.invite'
246 260
     },
247 261
     'chat': {
248 262
         id: 'toolbar_button_chat',
263
+        tooltipKey: 'toolbar.chat',
249 264
         shortcut: 'C',
250 265
         shortcutAttr: 'toggleChatPopover',
251 266
         shortcutFunc: function() {
@@ -257,40 +272,47 @@ const defaultToolbarButtons = {
257 272
     },
258 273
     'contacts': {
259 274
         id: 'toolbar_contact_list',
275
+        tooltipKey: 'bottomtoolbar.contactlist',
260 276
         sideContainerId: 'contacts_container'
261 277
     },
262 278
     'profile': {
263 279
         id: 'toolbar_button_profile',
280
+        tooltipKey: 'profile.setDisplayNameLabel',
264 281
         sideContainerId: 'profile_container'
265 282
     },
266 283
     'etherpad': {
267
-        id: 'toolbar_button_etherpad'
284
+        id: 'toolbar_button_etherpad',
285
+        tooltipKey: 'toolbar.etherpad',
268 286
     },
269 287
     'fullscreen': {
270 288
         id: 'toolbar_button_fullScreen',
289
+        tooltipKey: 'toolbar.fullscreen',
271 290
         className: "button icon-full-screen",
272
-        shortcut: 'F',
291
+        shortcut: 'S',
273 292
         shortcutAttr: 'toggleFullscreenPopover',
274 293
         shortcutFunc: function() {
275 294
             JitsiMeetJS.analytics.sendEvent('shortcut.fullscreen.toggled');
276 295
             APP.UI.toggleFullScreen();
277 296
         },
278
-        shortcutDescription: "keyboardShortcuts.toggleChat",
297
+        shortcutDescription: "keyboardShortcuts.fullScreen",
279 298
         content: "Enter / Exit Full Screen",
280 299
         i18n: "[content]toolbar.fullscreen"
281 300
     },
282 301
     'settings': {
283 302
         id: 'toolbar_button_settings',
303
+        tooltipKey: 'toolbar.Settings',
284 304
         sideContainerId: "settings_container"
285 305
     },
286 306
     'hangup': {
287 307
         id: 'toolbar_button_hangup',
308
+        tooltipKey: 'toolbar.hangup',
288 309
         className: "button icon-hangup",
289 310
         content: "Hang Up",
290 311
         i18n: "[content]toolbar.hangup"
291 312
     },
292 313
     'filmstrip': {
293 314
         id: 'toolbar_film_strip',
315
+        tooltipKey: 'toolbar.filmstrip',
294 316
         shortcut: "F",
295 317
         shortcutAttr: "filmstripPopover",
296 318
         shortcutFunc: function() {
@@ -301,6 +323,7 @@ const defaultToolbarButtons = {
301 323
     },
302 324
     'raisehand': {
303 325
         id: "toolbar_button_raisehand",
326
+        tooltipKey: 'toolbar.raiseHand',
304 327
         className: "button icon-raised-hand",
305 328
         shortcut: "R",
306 329
         shortcutAttr: "raiseHandPopover",
@@ -357,7 +380,17 @@ const Toolbar = {
357 380
         Object.keys(defaultToolbarButtons).forEach(
358 381
             id => {
359 382
                 if (UIUtil.isButtonEnabled(id)) {
360
-                    var button = defaultToolbarButtons[id];
383
+                    let button = defaultToolbarButtons[id];
384
+                    let buttonElement = document.getElementById(button.id);
385
+
386
+                    let tooltipPosition
387
+                        = (interfaceConfig.MAIN_TOOLBAR_BUTTONS
388
+                                .indexOf(id) > -1)
389
+                            ? "bottom" : "right";
390
+
391
+                    UIUtil.setTooltip(  buttonElement,
392
+                                        button.tooltipKey,
393
+                                        tooltipPosition);
361 394
 
362 395
                     if (button.shortcut)
363 396
                         APP.keyboardshortcut.registerShortcut(
@@ -382,8 +415,15 @@ const Toolbar = {
382 415
                                                             isVisible);
383 416
             });
384 417
 
418
+        APP.UI.addListener(UIEvents.LOCAL_RAISE_HAND_CHANGED,
419
+            function(isRaisedHand) {
420
+                Toolbar._toggleRaiseHand(isRaisedHand);
421
+            });
422
+
385 423
         if(!APP.tokenData.isGuest) {
386 424
             $("#toolbar_button_profile").addClass("unclickable");
425
+            UIUtil.removeTooltip(
426
+                document.getElementById('toolbar_button_profile'));
387 427
         }
388 428
     },
389 429
     /**
@@ -458,9 +498,11 @@ const Toolbar = {
458 498
 
459 499
     // Shows or hides the 'shared video' button.
460 500
     showSharedVideoButton () {
501
+        let $element = $('#toolbar_button_sharedvideo');
461 502
         if (UIUtil.isButtonEnabled('sharedvideo')
462 503
                 && config.disableThirdPartyRequests !== true) {
463
-            $('#toolbar_button_sharedvideo').css({display: "inline-block"});
504
+            $element.css({display: "inline-block"});
505
+            UIUtil.setTooltip($element.get(0), 'toolbar.sharedvideo', 'right');
464 506
         } else {
465 507
             $('#toolbar_button_sharedvideo').css({display: "none"});
466 508
         }
@@ -545,6 +587,13 @@ const Toolbar = {
545 587
         }
546 588
     },
547 589
 
590
+    /**
591
+     * Toggles / untoggles the view for raised hand.
592
+     */
593
+    _toggleRaiseHand(isRaisedHand) {
594
+        $('#toolbar_button_raisehand').toggleClass("glow", isRaisedHand);
595
+    },
596
+
548 597
     /**
549 598
      * Marks video icon as muted or not.
550 599
      * @param {boolean} muted if icon should look like muted or not
@@ -750,7 +799,6 @@ const Toolbar = {
750 799
             buttonElement.setAttribute("data-i18n", button.i18n);
751 800
 
752 801
         buttonElement.setAttribute("data-container", "body");
753
-        buttonElement.setAttribute("data-toggle", "popover");
754 802
         buttonElement.setAttribute("data-placement", "bottom");
755 803
         this._addPopups(buttonElement, button.popups);
756 804
 
@@ -771,4 +819,4 @@ const Toolbar = {
771 819
     }
772 820
 };
773 821
 
774
-export default Toolbar;
822
+export default Toolbar;

+ 100
- 6
modules/UI/util/UIUtil.js Näytä tiedosto

@@ -1,4 +1,22 @@
1
-/* global $, config, interfaceConfig */
1
+/* global $, APP, config, AJS, interfaceConfig */
2
+
3
+import KeyboardShortcut from '../../keyboardshortcut/keyboardshortcut';
4
+
5
+/**
6
+ * Associates tooltip element position (in the terms of
7
+ * {@link UIUtil#setTooltip} which do not look like CSS <tt>position</tt>) with
8
+ * AUI tooltip <tt>gravity</tt>.
9
+ */
10
+const TOOLTIP_POSITIONS = {
11
+    'bottom': 'n',
12
+    'bottom-left': 'ne',
13
+    'bottom-right': 'nw',
14
+    'left': 'e',
15
+    'right': 'w',
16
+    'top': 's',
17
+    'top-left': 'se',
18
+    'top-right': 'sw'
19
+};
2 20
 
3 21
 /**
4 22
  * Created by hristo on 12/22/14.
@@ -82,12 +100,71 @@
82 100
         context.putImageData(imgData, 0, 0);
83 101
     },
84 102
 
103
+    /**
104
+     * Sets a global handler for all tooltips. Once invoked, create a new
105
+     * tooltip by merely updating a DOM node with the appropriate class (e.g.
106
+     * <tt>tooltip-n</tt>) and the attribute <tt>content</tt>.
107
+     */
108
+    activateTooltips() {
109
+        AJS.$('[data-tooltip]').tooltip({
110
+            gravity() {
111
+                return this.getAttribute('data-tooltip');
112
+            },
113
+
114
+            title() {
115
+                return this.getAttribute('content');
116
+            },
117
+
118
+            html: true, // Handle multiline tooltips.
119
+
120
+            // The following two prevent tooltips from being stuck:
121
+            hoverable: false, // Make custom tooltips behave like native ones.
122
+            live: true // Attach listener to document element.
123
+        });
124
+    },
125
+
126
+    /**
127
+     * Sets the tooltip to the given element.
128
+     *
129
+     * @param element the element to set the tooltip to
130
+     * @param key the tooltip data-i18n key
131
+     * @param position the position of the tooltip in relation to the element
132
+     */
85 133
     setTooltip: function (element, key, position) {
86
-        element.setAttribute("data-i18n", "[data-content]" + key);
87
-        element.setAttribute("data-toggle", "popover");
88
-        element.setAttribute("data-placement", position);
89
-        element.setAttribute("data-html", true);
90
-        element.setAttribute("data-container", "body");
134
+        element.setAttribute('data-tooltip', TOOLTIP_POSITIONS[position]);
135
+        element.setAttribute('data-i18n', '[content]' + key);
136
+
137
+        APP.translation.translateElement($(element));
138
+    },
139
+
140
+    /**
141
+     * Removes the tooltip to the given element.
142
+     *
143
+     * @param element the element to remove the tooltip from
144
+     */
145
+    removeTooltip: function (element) {
146
+        AJS.$(element).tooltip('destroy');
147
+        element.setAttribute('data-tooltip', '');
148
+        element.setAttribute('data-i18n','');
149
+        element.setAttribute('content','');
150
+        element.setAttribute('shortcut','');
151
+    },
152
+
153
+    /**
154
+     * Internal util function for generating tooltip title.
155
+     *
156
+     * @param element
157
+     * @returns {string|*}
158
+     * @private
159
+     */
160
+    _getTooltipText: function (element) {
161
+        let title = element.getAttribute('content');
162
+        let shortcut = element.getAttribute('shortcut');
163
+        if(shortcut) {
164
+            let shortcutString = KeyboardShortcut.getShortcutTooltip(shortcut);
165
+            title += ` ${shortcutString}`;
166
+        }
167
+        return title;
91 168
     },
92 169
 
93 170
     /**
@@ -233,6 +310,23 @@
233 310
      */
234 311
     parseCssInt(cssValue) {
235 312
         return parseInt(cssValue) || 0;
313
+    },
314
+
315
+    /**
316
+     * Adds href value to 'a' link jquery object. If link value is null,
317
+     * undefined or empty string, disables the link.
318
+     * @param {object} aLinkElement the jquery object
319
+     * @param {string} link the link value
320
+     */
321
+    setLinkHref(aLinkElement, link) {
322
+        if (link) {
323
+            aLinkElement.attr('href', link);
324
+        } else {
325
+            aLinkElement.css({
326
+                "pointer-events": "none",
327
+                "cursor": "default"
328
+            });
329
+        }
236 330
     }
237 331
 };
238 332
 

+ 37
- 12
modules/UI/videolayout/ConnectionIndicator.js Näytä tiedosto

@@ -245,13 +245,13 @@ ConnectionIndicator.prototype.showMore = function () {
245 245
 };
246 246
 
247 247
 
248
-function createIcon(classes) {
248
+function createIcon(classes, iconClass) {
249 249
     var icon = document.createElement("span");
250 250
     for(var i in classes) {
251 251
         icon.classList.add(classes[i]);
252 252
     }
253 253
     icon.appendChild(
254
-        document.createElement("i")).classList.add("icon-connection");
254
+        document.createElement("i")).classList.add(iconClass);
255 255
     return icon;
256 256
 }
257 257
 
@@ -282,9 +282,12 @@ ConnectionIndicator.prototype.create = function () {
282 282
     }.bind(this);
283 283
 
284 284
     this.emptyIcon = this.connectionIndicatorContainer.appendChild(
285
-        createIcon(["connection", "connection_empty"]));
285
+        createIcon(["connection", "connection_empty"], "icon-connection"));
286 286
     this.fullIcon = this.connectionIndicatorContainer.appendChild(
287
-        createIcon(["connection", "connection_full"]));
287
+        createIcon(["connection", "connection_full"], "icon-connection"));
288
+    this.interruptedIndicator = this.connectionIndicatorContainer.appendChild(
289
+        createIcon(["connection", "connection_lost"],"icon-connection-lost"));
290
+    $(this.interruptedIndicator).hide();
288 291
 };
289 292
 
290 293
 /**
@@ -298,6 +301,27 @@ ConnectionIndicator.prototype.remove = function() {
298 301
     this.popover.forceHide();
299 302
 };
300 303
 
304
+/**
305
+ * Updates the UI which displays warning about user's connectivity problems.
306
+ *
307
+ * @param {boolean} isActive true if the connection is working fine or false if
308
+ * the user is having connectivity issues.
309
+ */
310
+ConnectionIndicator.prototype.updateConnectionStatusIndicator
311
+= function (isActive) {
312
+    this.isConnectionActive = isActive;
313
+    if (this.isConnectionActive) {
314
+        $(this.interruptedIndicator).hide();
315
+        $(this.emptyIcon).show();
316
+        $(this.fullIcon).show();
317
+    } else {
318
+        $(this.interruptedIndicator).show();
319
+        $(this.emptyIcon).hide();
320
+        $(this.fullIcon).hide();
321
+        this.updateConnectionQuality(0 /* zero bars */);
322
+    }
323
+};
324
+
301 325
 /**
302 326
  * Updates the data of the indicator
303 327
  * @param percent the percent of connection quality
@@ -312,15 +336,16 @@ ConnectionIndicator.prototype.updateConnectionQuality =
312 336
     } else {
313 337
         if(this.connectionIndicatorContainer.style.display == "none") {
314 338
             this.connectionIndicatorContainer.style.display = "block";
315
-            this.videoContainer.updateIconPositions();
316 339
         }
317 340
     }
318
-    this.bandwidth = object.bandwidth;
319
-    this.bitrate = object.bitrate;
320
-    this.packetLoss = object.packetLoss;
321
-    this.transport = object.transport;
322
-    if (object.resolution) {
323
-        this.resolution = object.resolution;
341
+    if (object) {
342
+        this.bandwidth = object.bandwidth;
343
+        this.bitrate = object.bitrate;
344
+        this.packetLoss = object.packetLoss;
345
+        this.transport = object.transport;
346
+        if (object.resolution) {
347
+            this.resolution = object.resolution;
348
+        }
324 349
     }
325 350
     for (var quality in ConnectionIndicator.connectionQualityValues) {
326 351
         if (percent >= quality) {
@@ -328,7 +353,7 @@ ConnectionIndicator.prototype.updateConnectionQuality =
328 353
                 ConnectionIndicator.connectionQualityValues[quality];
329 354
         }
330 355
     }
331
-    if (object.isResolutionHD) {
356
+    if (object && typeof object.isResolutionHD === 'boolean') {
332 357
         this.isResolutionHD = object.isResolutionHD;
333 358
     }
334 359
     this.updateResolutionIndicator();

+ 138
- 42
modules/UI/videolayout/FilmStrip.js Näytä tiedosto

@@ -3,8 +3,6 @@
3 3
 import UIEvents from "../../../service/UI/UIEvents";
4 4
 import UIUtil from "../util/UIUtil";
5 5
 
6
-const thumbAspectRatio = 1 / 1;
7
-
8 6
 const FilmStrip = {
9 7
     /**
10 8
      *
@@ -26,7 +24,7 @@ const FilmStrip = {
26 24
      */
27 25
     toggleFilmStrip (visible) {
28 26
         if (typeof visible === 'boolean'
29
-                && this.isFilmStripVisible() == visible) {
27
+            && this.isFilmStripVisible() == visible) {
30 28
             return;
31 29
         }
32 30
 
@@ -36,8 +34,8 @@ const FilmStrip = {
36 34
         var eventEmitter = this.eventEmitter;
37 35
         if (eventEmitter) {
38 36
             eventEmitter.emit(
39
-                    UIEvents.TOGGLED_FILM_STRIP,
40
-                    this.isFilmStripVisible());
37
+                UIEvents.TOGGLED_FILM_STRIP,
38
+                this.isFilmStripVisible());
41 39
         }
42 40
     },
43 41
 
@@ -66,13 +64,52 @@ const FilmStrip = {
66 64
             - parseInt(this.filmStrip.css('paddingRight'), 10);
67 65
     },
68 66
 
67
+    calculateThumbnailSize() {
68
+        let availableSizes = this.calculateAvailableSize();
69
+        let width = availableSizes.availableWidth;
70
+        let height = availableSizes.availableHeight;
71
+
72
+        return this.calculateThumbnailSizeFromAvailable(width, height);
73
+    },
74
+
69 75
     /**
70
-     * Calculates the thumbnail size.
76
+     * Normalizes local and remote thumbnail ratios
71 77
      */
72
-     calculateThumbnailSize () {
73
-        let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
78
+    normalizeThumbnailRatio () {
79
+        let remoteHeightRatio = interfaceConfig.REMOTE_THUMBNAIL_RATIO_HEIGHT;
80
+        let remoteWidthRatio = interfaceConfig.REMOTE_THUMBNAIL_RATIO_WIDTH;
81
+
82
+        let localHeightRatio = interfaceConfig.LOCAL_THUMBNAIL_RATIO_HEIGHT;
83
+        let localWidthRatio = interfaceConfig.LOCAL_THUMBNAIL_RATIO_WIDTH;
84
+
85
+        let commonHeightRatio = remoteHeightRatio * localHeightRatio;
74 86
 
75
-        let numvids = this.getThumbs(true).length;
87
+        let localRatioCoefficient = localWidthRatio / localHeightRatio;
88
+        let remoteRatioCoefficient = remoteWidthRatio / remoteHeightRatio;
89
+
90
+        remoteWidthRatio = commonHeightRatio * remoteRatioCoefficient;
91
+        remoteHeightRatio = commonHeightRatio;
92
+
93
+        localWidthRatio = commonHeightRatio * localRatioCoefficient;
94
+        localHeightRatio = commonHeightRatio;
95
+
96
+        let localRatio = {
97
+            widthRatio: localWidthRatio,
98
+            heightRatio: localHeightRatio
99
+        };
100
+
101
+        let remoteRatio = {
102
+            widthRatio: remoteWidthRatio,
103
+            heightRatio: remoteHeightRatio
104
+        };
105
+
106
+        return { localRatio, remoteRatio };
107
+    },
108
+
109
+    calculateAvailableSize() {
110
+        let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
111
+        let thumbs = this.getThumbs(true);
112
+        let numvids = thumbs.remoteThumbs.length;
76 113
 
77 114
         let localVideoContainer = $("#localVideoContainer");
78 115
 
@@ -83,20 +120,19 @@ const FilmStrip = {
83 120
          */
84 121
         let videoAreaAvailableWidth
85 122
             = UIUtil.getAvailableVideoWidth()
86
-                - UIUtil.parseCssInt(this.filmStrip.css('right'), 10)
87
-                - UIUtil.parseCssInt(this.filmStrip.css('paddingLeft'), 10)
88
-                - UIUtil.parseCssInt(this.filmStrip.css('paddingRight'), 10)
89
-                - UIUtil.parseCssInt(this.filmStrip.css('borderLeftWidth'), 10)
90
-                - UIUtil.parseCssInt(this.filmStrip.css('borderRightWidth'), 10)
123
+            - UIUtil.parseCssInt(this.filmStrip.css('right'), 10)
124
+            - UIUtil.parseCssInt(this.filmStrip.css('paddingLeft'), 10)
125
+            - UIUtil.parseCssInt(this.filmStrip.css('paddingRight'), 10)
126
+            - UIUtil.parseCssInt(this.filmStrip.css('borderLeftWidth'), 10)
127
+            - UIUtil.parseCssInt(this.filmStrip.css('borderRightWidth'), 10)
91 128
             - 5;
92 129
 
93 130
         let availableWidth = videoAreaAvailableWidth;
94 131
 
95
-        // If the number of videos is 0 or undefined we don't need to calculate
96
-        // further.
97
-        if (numvids)
132
+        // If local thumb is not hidden
133
+        if(thumbs.localThumb) {
98 134
             availableWidth = Math.floor(
99
-                (videoAreaAvailableWidth - numvids * (
135
+                (videoAreaAvailableWidth - (
100 136
                 UIUtil.parseCssInt(
101 137
                     localVideoContainer.css('borderLeftWidth'), 10)
102 138
                 + UIUtil.parseCssInt(
@@ -109,45 +145,99 @@ const FilmStrip = {
109 145
                     localVideoContainer.css('marginLeft'), 10)
110 146
                 + UIUtil.parseCssInt(
111 147
                     localVideoContainer.css('marginRight'), 10)))
112
-                / numvids);
148
+            );
149
+        }
150
+
151
+        // If the number of videos is 0 or undefined we don't need to calculate
152
+        // further.
153
+        if (numvids) {
154
+            let remoteVideoContainer = thumbs.remoteThumbs.eq(0);
155
+            availableWidth = Math.floor(
156
+                (videoAreaAvailableWidth - numvids * (
157
+                UIUtil.parseCssInt(
158
+                    remoteVideoContainer.css('borderLeftWidth'), 10)
159
+                + UIUtil.parseCssInt(
160
+                    remoteVideoContainer.css('borderRightWidth'), 10)
161
+                + UIUtil.parseCssInt(
162
+                    remoteVideoContainer.css('paddingLeft'), 10)
163
+                + UIUtil.parseCssInt(
164
+                    remoteVideoContainer.css('paddingRight'), 10)
165
+                + UIUtil.parseCssInt(
166
+                    remoteVideoContainer.css('marginLeft'), 10)
167
+                + UIUtil.parseCssInt(
168
+                    remoteVideoContainer.css('marginRight'), 10)))
169
+            );
170
+        }
113 171
 
114 172
         let maxHeight
115 173
             // If the MAX_HEIGHT property hasn't been specified
116 174
             // we have the static value.
117
-            = Math.min( interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120,
118
-                        availableHeight);
175
+            = Math.min(interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120,
176
+            availableHeight);
119 177
 
120 178
         availableHeight
121
-            = Math.min( maxHeight, window.innerHeight - 18);
179
+            = Math.min(maxHeight, window.innerHeight - 18);
180
+
181
+        return { availableWidth, availableHeight };
182
+    },
183
+
184
+    calculateThumbnailSizeFromAvailable(availableWidth, availableHeight) {
185
+        let { localRatio, remoteRatio } = this.normalizeThumbnailRatio();
186
+        let { remoteThumbs } = this.getThumbs(true);
187
+        let remoteProportion = remoteRatio.widthRatio * remoteThumbs.length;
188
+        let widthProportion = remoteProportion + localRatio.widthRatio;
189
+
190
+        let heightUnit = availableHeight / localRatio.heightRatio;
191
+        let widthUnit = availableWidth / widthProportion;
122 192
 
123
-        if (availableHeight < availableWidth) {
124
-            availableWidth = availableHeight;
193
+        if (heightUnit < widthUnit) {
194
+            widthUnit = heightUnit;
125 195
         }
126 196
         else
127
-            availableHeight = availableWidth;
197
+            heightUnit = widthUnit;
198
+
199
+        let localVideo = {
200
+            thumbWidth: widthUnit * localRatio.widthRatio,
201
+            thumbHeight: heightUnit * localRatio.heightRatio
202
+        };
203
+        let remoteVideo = {
204
+            thumbWidth: widthUnit * remoteRatio.widthRatio,
205
+            thumbHeight: widthUnit * remoteRatio.heightRatio
206
+        };
128 207
 
129 208
         return {
130
-            thumbWidth: availableWidth,
131
-            thumbHeight: availableHeight
209
+            localVideo,
210
+            remoteVideo
132 211
         };
133 212
     },
134 213
 
135
-    resizeThumbnails (thumbWidth, thumbHeight,
214
+    resizeThumbnails (local, remote,
136 215
                       animate = false, forceUpdate = false) {
137 216
 
138 217
         return new Promise(resolve => {
139
-            this.getThumbs(!forceUpdate).animate({
140
-                height: thumbHeight,
141
-                width: thumbWidth
142
-            }, {
143
-                queue: false,
144
-                duration: animate ? 500 : 0,
145
-                complete:  resolve
146
-            });
218
+            let thumbs = this.getThumbs(!forceUpdate);
219
+            if(thumbs.localThumb)
220
+                thumbs.localThumb.animate({
221
+                    height: local.thumbHeight,
222
+                    width: local.thumbWidth
223
+                }, {
224
+                    queue: false,
225
+                    duration: animate ? 500 : 0,
226
+                    complete:  resolve
227
+                });
228
+            if(thumbs.remoteThumbs)
229
+                thumbs.remoteThumbs.animate({
230
+                    height: remote.thumbHeight,
231
+                    width: remote.thumbWidth
232
+                }, {
233
+                    queue: false,
234
+                    duration: animate ? 500 : 0,
235
+                    complete:  resolve
236
+                });
147 237
 
148 238
             this.filmStrip.animate({
149 239
                 // adds 2 px because of small video 1px border
150
-                height: thumbHeight + 2
240
+                height: remote.thumbHeight + 2
151 241
             }, {
152 242
                 queue: false,
153 243
                 duration: animate ? 500 : 0
@@ -165,13 +255,19 @@ const FilmStrip = {
165 255
             selector += ':visible';
166 256
         }
167 257
 
258
+        let localThumb = $("#localVideoContainer");
259
+        let remoteThumbs = this.filmStrip.children(selector)
260
+            .not("#localVideoContainer");
261
+
168 262
         // Exclude the local video container if it has been hidden.
169
-        if ($("#localVideoContainer").hasClass("hidden"))
170
-            return this.filmStrip.children(selector)
171
-                    .not("#localVideoContainer");
172
-        else
173
-            return this.filmStrip.children(selector);
263
+        if (localThumb.hasClass("hidden")) {
264
+            return { remoteThumbs };
265
+        } else {
266
+            return { remoteThumbs, localThumb };
267
+        }
268
+
174 269
     }
270
+
175 271
 };
176 272
 
177 273
 export default FilmStrip;

+ 501
- 0
modules/UI/videolayout/LargeVideoManager.js Näytä tiedosto

@@ -0,0 +1,501 @@
1
+/* global $, APP, interfaceConfig */
2
+/* jshint -W101 */
3
+
4
+import Avatar from "../avatar/Avatar";
5
+import {createDeferred} from '../../util/helpers';
6
+import UIUtil from "../util/UIUtil";
7
+import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer";
8
+
9
+import LargeContainer from "./LargeContainer";
10
+
11
+import AudioLevels from "../audio_levels/AudioLevels";
12
+
13
+/**
14
+ * Manager for all Large containers.
15
+ */
16
+export default class LargeVideoManager {
17
+    constructor (emitter) {
18
+        /**
19
+         * The map of <tt>LargeContainer</tt>s where the key is the video
20
+         * container type.
21
+         * @type {Object.<string, LargeContainer>}
22
+         */
23
+        this.containers = {};
24
+
25
+        this.state = VIDEO_CONTAINER_TYPE;
26
+        this.videoContainer = new VideoContainer(
27
+            () => this.resizeContainer(VIDEO_CONTAINER_TYPE), emitter);
28
+        this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
29
+
30
+        // use the same video container to handle and desktop tracks
31
+        this.addContainer("desktop", this.videoContainer);
32
+
33
+        this.width = 0;
34
+        this.height = 0;
35
+
36
+        this.$container = $('#largeVideoContainer');
37
+
38
+        this.$container.css({
39
+            display: 'inline-block'
40
+        });
41
+
42
+        if (interfaceConfig.SHOW_JITSI_WATERMARK) {
43
+            let leftWatermarkDiv
44
+                = this.$container.find("div.watermark.leftwatermark");
45
+
46
+            leftWatermarkDiv.css({display: 'block'});
47
+
48
+            UIUtil.setLinkHref(
49
+                leftWatermarkDiv.parent(),
50
+                interfaceConfig.JITSI_WATERMARK_LINK);
51
+        }
52
+
53
+        if (interfaceConfig.SHOW_BRAND_WATERMARK) {
54
+            let rightWatermarkDiv
55
+                = this.$container.find("div.watermark.rightwatermark");
56
+
57
+            rightWatermarkDiv.css({
58
+                display: 'block',
59
+                backgroundImage: 'url(images/rightwatermark.png)'
60
+            });
61
+
62
+            UIUtil.setLinkHref(
63
+                rightWatermarkDiv.parent(),
64
+                interfaceConfig.BRAND_WATERMARK_LINK);
65
+        }
66
+
67
+        if (interfaceConfig.SHOW_POWERED_BY) {
68
+            this.$container.children("a.poweredby").css({display: 'block'});
69
+        }
70
+
71
+        this.$container.hover(
72
+            e => this.onHoverIn(e),
73
+            e => this.onHoverOut(e)
74
+        );
75
+    }
76
+
77
+    onHoverIn (e) {
78
+        if (!this.state) {
79
+            return;
80
+        }
81
+        let container = this.getContainer(this.state);
82
+        container.onHoverIn(e);
83
+    }
84
+
85
+    onHoverOut (e) {
86
+        if (!this.state) {
87
+            return;
88
+        }
89
+        let container = this.getContainer(this.state);
90
+        container.onHoverOut(e);
91
+    }
92
+
93
+    /**
94
+     * Called when the media connection has been interrupted.
95
+     */
96
+    onVideoInterrupted () {
97
+        this.enableLocalConnectionProblemFilter(true);
98
+        this._setLocalConnectionMessage("connection.RECONNECTING");
99
+        // Show the message only if the video is currently being displayed
100
+        this.showLocalConnectionMessage(this.state === VIDEO_CONTAINER_TYPE);
101
+    }
102
+
103
+    /**
104
+     * Called when the media connection has been restored.
105
+     */
106
+    onVideoRestored () {
107
+        this.enableLocalConnectionProblemFilter(false);
108
+        this.showLocalConnectionMessage(false);
109
+    }
110
+
111
+    get id () {
112
+        let container = this.getContainer(this.state);
113
+        return container.id;
114
+    }
115
+
116
+    scheduleLargeVideoUpdate () {
117
+        if (this.updateInProcess || !this.newStreamData) {
118
+            return;
119
+        }
120
+
121
+        this.updateInProcess = true;
122
+
123
+        let container = this.getContainer(this.state);
124
+
125
+        // Include hide()/fadeOut only if we're switching between users
126
+        let preUpdate;
127
+        let isUserSwitch = this.newStreamData.id != this.id;
128
+        if (isUserSwitch) {
129
+            preUpdate = container.hide();
130
+        } else {
131
+            preUpdate = Promise.resolve();
132
+        }
133
+
134
+        preUpdate.then(() => {
135
+            let {id, stream, videoType, resolve} = this.newStreamData;
136
+            this.newStreamData = null;
137
+
138
+            console.info("hover in %s", id);
139
+            this.state = videoType;
140
+            let container = this.getContainer(this.state);
141
+            container.setStream(stream, videoType);
142
+
143
+            // change the avatar url on large
144
+            this.updateAvatar(Avatar.getAvatarUrl(id));
145
+
146
+            // FIXME that does not really make sense, because the videoType
147
+            // (camera or desktop) is a completely different thing than
148
+            // the video container type (Etherpad, SharedVideo, VideoContainer).
149
+            // ----------------------------------------------------------------
150
+            // If we the continer is VIDEO_CONTAINER_TYPE, we need to check
151
+            // its stream whether exist and is muted to set isVideoMuted
152
+            // in rest of the cases it is false
153
+            let showAvatar = false;
154
+            if (videoType == VIDEO_CONTAINER_TYPE)
155
+                showAvatar = stream ? stream.isMuted() : true;
156
+
157
+            // If the user's connection is disrupted then the avatar will be
158
+            // displayed in case we have no video image cached. That is if
159
+            // there was a user switch(image is lost on stream detach) or if
160
+            // the video was not rendered, before the connection has failed.
161
+            let isHavingConnectivityIssues
162
+                = APP.conference.isParticipantConnectionActive(id) === false;
163
+            if (isHavingConnectivityIssues
164
+                    && (isUserSwitch | !container.wasVideoRendered)) {
165
+                showAvatar = true;
166
+            }
167
+
168
+            let promise;
169
+
170
+            // do not show stream if video is muted
171
+            // but we still should show watermark
172
+            if (showAvatar) {
173
+                this.showWatermark(true);
174
+                // If the intention of this switch is to show the avatar
175
+                // we need to make sure that the video is hidden
176
+                promise = container.hide();
177
+            } else {
178
+                promise = container.show();
179
+            }
180
+
181
+            // show the avatar on large if needed
182
+            container.showAvatar(showAvatar);
183
+
184
+            // Make sure no notification about remote failure is shown as
185
+            // it's UI conflicts with the one for local connection interrupted.
186
+            if (APP.conference.isConnectionInterrupted()) {
187
+                this.updateParticipantConnStatusIndication(id, true);
188
+            } else {
189
+                this.updateParticipantConnStatusIndication(
190
+                    id, !isHavingConnectivityIssues);
191
+            }
192
+
193
+            // resolve updateLargeVideo promise after everything is done
194
+            promise.then(resolve);
195
+
196
+            return promise;
197
+        }).then(() => {
198
+            // after everything is done check again if there are any pending
199
+            // new streams.
200
+            this.updateInProcess = false;
201
+            this.scheduleLargeVideoUpdate();
202
+        });
203
+    }
204
+
205
+    /**
206
+     * Shows/hides notification about participant's connectivity issues to be
207
+     * shown on the large video area.
208
+     *
209
+     * @param {string} id the id of remote participant(MUC nickname)
210
+     * @param {boolean} isConnected true if the connection is active or false
211
+     * when the user is having connectivity issues.
212
+     *
213
+     * @private
214
+     */
215
+    updateParticipantConnStatusIndication (id, isConnected) {
216
+
217
+        // Apply grey filter on the large video
218
+        this.videoContainer.showRemoteConnectionProblemIndicator(!isConnected);
219
+
220
+        if (isConnected) {
221
+            // Hide the message
222
+            this.showRemoteConnectionMessage(false);
223
+        } else {
224
+            // Get user's display name
225
+            let displayName
226
+                = APP.conference.getParticipantDisplayName(id);
227
+            this._setRemoteConnectionMessage(
228
+                "connection.USER_CONNECTION_INTERRUPTED",
229
+                { displayName: displayName });
230
+
231
+            // Show it now only if the VideoContainer is on top
232
+            this.showRemoteConnectionMessage(
233
+                this.state === VIDEO_CONTAINER_TYPE);
234
+        }
235
+    }
236
+
237
+    /**
238
+     * Update large video.
239
+     * Switches to large video even if previously other container was visible.
240
+     * @param userID the userID of the participant associated with the stream
241
+     * @param {JitsiTrack?} stream new stream
242
+     * @param {string?} videoType new video type
243
+     * @returns {Promise}
244
+     */
245
+    updateLargeVideo (userID, stream, videoType) {
246
+        if (this.newStreamData) {
247
+            this.newStreamData.reject();
248
+        }
249
+
250
+        this.newStreamData = createDeferred();
251
+        this.newStreamData.id = userID;
252
+        this.newStreamData.stream = stream;
253
+        this.newStreamData.videoType = videoType;
254
+
255
+        this.scheduleLargeVideoUpdate();
256
+
257
+        return this.newStreamData.promise;
258
+    }
259
+
260
+    /**
261
+     * Update container size.
262
+     */
263
+    updateContainerSize () {
264
+        this.width = UIUtil.getAvailableVideoWidth();
265
+        this.height = window.innerHeight;
266
+    }
267
+
268
+    /**
269
+     * Resize Large container of specified type.
270
+     * @param {string} type type of container which should be resized.
271
+     * @param {boolean} [animate=false] if resize process should be animated.
272
+     */
273
+    resizeContainer (type, animate = false) {
274
+        let container = this.getContainer(type);
275
+        container.resize(this.width, this.height, animate);
276
+    }
277
+
278
+    /**
279
+     * Resize all Large containers.
280
+     * @param {boolean} animate if resize process should be animated.
281
+     */
282
+    resize (animate) {
283
+        // resize all containers
284
+        Object.keys(this.containers)
285
+            .forEach(type => this.resizeContainer(type, animate));
286
+
287
+        this.$container.animate({
288
+            width: this.width,
289
+            height: this.height
290
+        }, {
291
+            queue: false,
292
+            duration: animate ? 500 : 0
293
+        });
294
+    }
295
+
296
+    /**
297
+     * Enables/disables the filter indicating a video problem to the user caused
298
+     * by the problems with local media connection.
299
+     *
300
+     * @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
301
+     */
302
+    enableLocalConnectionProblemFilter (enable) {
303
+        this.videoContainer.enableLocalConnectionProblemFilter(enable);
304
+    }
305
+
306
+    /**
307
+     * Updates the src of the dominant speaker avatar
308
+     */
309
+    updateAvatar (avatarUrl) {
310
+        $("#dominantSpeakerAvatar").attr('src', avatarUrl);
311
+    }
312
+
313
+    /**
314
+     * Updates the audio level indicator of the large video.
315
+     *
316
+     * @param lvl the new audio level to set
317
+     */
318
+    updateLargeVideoAudioLevel (lvl) {
319
+        AudioLevels.updateLargeVideoAudioLevel("dominantSpeaker", lvl);
320
+    }
321
+
322
+    /**
323
+     * Show or hide watermark.
324
+     * @param {boolean} show
325
+     */
326
+    showWatermark (show) {
327
+        $('.watermark').css('visibility', show ? 'visible' : 'hidden');
328
+    }
329
+
330
+    /**
331
+     * Shows/hides the message indicating problems with local media connection.
332
+     * @param {boolean|null} show(optional) tells whether the message is to be
333
+     * displayed or not. If missing the condition will be based on the value
334
+     * obtained from {@link APP.conference.isConnectionInterrupted}.
335
+     */
336
+    showLocalConnectionMessage (show) {
337
+        if (typeof show !== 'boolean') {
338
+            show = APP.conference.isConnectionInterrupted();
339
+        }
340
+
341
+        if (show) {
342
+            $('#localConnectionMessage').css({display: "block"});
343
+            // Avatar message conflicts with 'videoConnectionMessage',
344
+            // so it must be hidden
345
+            this.showRemoteConnectionMessage(false);
346
+        } else {
347
+            $('#localConnectionMessage').css({display: "none"});
348
+        }
349
+    }
350
+
351
+    /**
352
+     * Shows hides the "avatar" message which is to be displayed either in
353
+     * the middle of the screen or below the avatar image.
354
+     *
355
+     * @param {null|boolean} show (optional) <tt>true</tt> to show the avatar
356
+     * message or <tt>false</tt> to hide it. If not provided then the connection
357
+     * status of the user currently on the large video will be obtained form
358
+     * "APP.conference" and the message will be displayed if the user's
359
+     * connection is interrupted.
360
+     */
361
+    showRemoteConnectionMessage (show) {
362
+        if (typeof show !== 'boolean') {
363
+            show = APP.conference.isParticipantConnectionActive(this.id);
364
+        }
365
+
366
+        if (show) {
367
+            $('#remoteConnectionMessage').css({display: "block"});
368
+            // 'videoConnectionMessage' message conflicts with 'avatarMessage',
369
+            // so it must be hidden
370
+            this.showLocalConnectionMessage(false);
371
+        } else {
372
+            $('#remoteConnectionMessage').hide();
373
+        }
374
+    }
375
+
376
+    /**
377
+     * Updates the text which describes that the remote user is having
378
+     * connectivity issues.
379
+     *
380
+     * @param {string} msgKey the translation key which will be used to get
381
+     * the message text.
382
+     * @param {object} msgOptions translation options object.
383
+     *
384
+     * @private
385
+     */
386
+    _setRemoteConnectionMessage (msgKey, msgOptions) {
387
+        if (msgKey) {
388
+            let text = APP.translation.translateString(msgKey, msgOptions);
389
+            $('#remoteConnectionMessage')
390
+                .attr("data-i18n", msgKey).text(text);
391
+        }
392
+
393
+        this.videoContainer.positionRemoteConnectionMessage();
394
+    }
395
+
396
+    /**
397
+     * Updated the text which is to be shown on the top of large video, when
398
+     * local media connection is interrupted.
399
+     *
400
+     * @param {string} msgKey the translation key which will be used to get
401
+     * the message text to be displayed on the large video.
402
+     * @param {object} msgOptions translation options object
403
+     *
404
+     * @private
405
+     */
406
+    _setLocalConnectionMessage (msgKey, msgOptions) {
407
+        $('#localConnectionMessage')
408
+            .attr("data-i18n", msgKey)
409
+            .text(APP.translation.translateString(msgKey, msgOptions));
410
+    }
411
+
412
+    /**
413
+     * Add container of specified type.
414
+     * @param {string} type container type
415
+     * @param {LargeContainer} container container to add.
416
+     */
417
+    addContainer (type, container) {
418
+        if (this.containers[type]) {
419
+            throw new Error(`container of type ${type} already exist`);
420
+        }
421
+
422
+        this.containers[type] = container;
423
+        this.resizeContainer(type);
424
+    }
425
+
426
+    /**
427
+     * Get Large container of specified type.
428
+     * @param {string} type container type.
429
+     * @returns {LargeContainer}
430
+     */
431
+    getContainer (type) {
432
+        let container = this.containers[type];
433
+
434
+        if (!container) {
435
+            throw new Error(`container of type ${type} doesn't exist`);
436
+        }
437
+
438
+        return container;
439
+    }
440
+
441
+    /**
442
+     * Remove Large container of specified type.
443
+     * @param {string} type container type.
444
+     */
445
+    removeContainer (type) {
446
+        if (!this.containers[type]) {
447
+            throw new Error(`container of type ${type} doesn't exist`);
448
+        }
449
+
450
+        delete this.containers[type];
451
+    }
452
+
453
+    /**
454
+     * Show Large container of specified type.
455
+     * Does nothing if such container is already visible.
456
+     * @param {string} type container type.
457
+     * @returns {Promise}
458
+     */
459
+    showContainer (type) {
460
+        if (this.state === type) {
461
+            return Promise.resolve();
462
+        }
463
+
464
+        let oldContainer = this.containers[this.state];
465
+        // FIXME when video is being replaced with other content we need to hide
466
+        // companion icons/messages. It would be best if the container would
467
+        // be taking care of it by itself, but that is a bigger refactoring
468
+        if (this.state === VIDEO_CONTAINER_TYPE) {
469
+            this.showWatermark(false);
470
+            this.showLocalConnectionMessage(false);
471
+            this.showRemoteConnectionMessage(false);
472
+        }
473
+        oldContainer.hide();
474
+
475
+        this.state = type;
476
+        let container = this.getContainer(type);
477
+
478
+        return container.show().then(() => {
479
+            if (type === VIDEO_CONTAINER_TYPE) {
480
+                // FIXME when video appears on top of other content we need to
481
+                // show companion icons/messages. It would be best if
482
+                // the container would be taking care of it by itself, but that
483
+                // is a bigger refactoring
484
+                this.showWatermark(true);
485
+                // "avatar" and "video connection" can not be displayed both
486
+                // at the same time, but the latter is of higher priority and it
487
+                // will hide the avatar one if will be displayed.
488
+                this.showRemoteConnectionMessage(/* fet the current state */);
489
+                this.showLocalConnectionMessage(/* fetch the current state */);
490
+            }
491
+        });
492
+    }
493
+
494
+    /**
495
+     * Changes the flipX state of the local video.
496
+     * @param val {boolean} true if flipped.
497
+     */
498
+    onLocalFlipXChange(val) {
499
+        this.videoContainer.setLocalFlipX(val);
500
+    }
501
+}

+ 25
- 19
modules/UI/videolayout/LocalVideo.js Näytä tiedosto

@@ -11,7 +11,6 @@ function LocalVideo(VideoLayout, emitter) {
11 11
     this.videoSpanId = "localVideoContainer";
12 12
     this.container = $("#localVideoContainer").get(0);
13 13
     this.localVideoId = null;
14
-    this.bindHoverHandler();
15 14
     if(config.enableLocalVideoFlip)
16 15
         this._buildContextMenu();
17 16
     this.isLocal = true;
@@ -29,6 +28,7 @@ function LocalVideo(VideoLayout, emitter) {
29 28
     this.setDisplayName();
30 29
 
31 30
     this.createConnectionIndicator();
31
+    this.addAudioLevelIndicator();
32 32
 }
33 33
 
34 34
 LocalVideo.prototype = Object.create(SmallVideo.prototype);
@@ -44,7 +44,7 @@ function createEditDisplayNameButton() {
44 44
     editButton.className = 'displayname';
45 45
     UIUtil.setTooltip(editButton,
46 46
         "videothumbnail.editnickname",
47
-        "top");
47
+        "left");
48 48
     editButton.innerHTML = '<i class="icon-edit"></i>';
49 49
 
50 50
     return editButton;
@@ -61,7 +61,7 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
61 61
         return;
62 62
     }
63 63
 
64
-    var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
64
+    var nameSpan = $('#' + this.videoSpanId + ' .displayname');
65 65
     var defaultLocalDisplayName = APP.translation.generateTranslationHTML(
66 66
         interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
67 67
 
@@ -72,7 +72,10 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
72 72
             if (displayName && displayName.length > 0) {
73 73
                 meHTML = APP.translation.generateTranslationHTML("me");
74 74
                 $('#localDisplayName').html(
75
-                    UIUtil.escapeHtml(displayName) + ' (' + meHTML + ')'
75
+                    `${UIUtil.escapeHtml(displayName)} (${meHTML})`
76
+                );
77
+                $('#editDisplayName').val(
78
+                    `${UIUtil.escapeHtml(displayName)}`
76 79
                 );
77 80
             } else {
78 81
                 $('#localDisplayName').html(defaultLocalDisplayName);
@@ -80,11 +83,11 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
80 83
         }
81 84
         this.updateView();
82 85
     } else {
83
-        var editButton = createEditDisplayNameButton();
84
-
85 86
         nameSpan = document.createElement('span');
86 87
         nameSpan.className = 'displayname';
87
-        $('#' + this.videoSpanId)[0].appendChild(nameSpan);
88
+        document.getElementById(this.videoSpanId)
89
+            .querySelector('.videocontainer__toolbar')
90
+            .appendChild(nameSpan);
88 91
 
89 92
 
90 93
         if (displayName && displayName.length > 0) {
@@ -97,12 +100,11 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
97 100
 
98 101
 
99 102
         nameSpan.id = 'localDisplayName';
100
-        this.container.appendChild(editButton);
101 103
         //translates popover of edit button
102 104
         APP.translation.translateElement($("a.displayname"));
103 105
 
104 106
         var editableText = document.createElement('input');
105
-        editableText.className = 'displayname';
107
+        editableText.className = 'editdisplayname';
106 108
         editableText.type = 'text';
107 109
         editableText.id = 'editDisplayName';
108 110
 
@@ -119,26 +121,30 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
119 121
             JSON.stringify({name: "Jane Pink"}));
120 122
         editableText.setAttribute("placeholder", defaultNickname);
121 123
 
122
-        this.container.appendChild(editableText);
124
+        this.container
125
+            .querySelector('.videocontainer__toolbar')
126
+            .appendChild(editableText);
123 127
 
124 128
         var self = this;
125 129
         $('#localVideoContainer .displayname')
126 130
             .bind("click", function (e) {
131
+                let $editDisplayName = $('#editDisplayName');
132
+                let $localDisplayName = $('#localDisplayName');
127 133
 
128
-                var editDisplayName = $('#editDisplayName');
129 134
                 e.preventDefault();
130 135
                 e.stopPropagation();
131
-                $('#localDisplayName').hide();
132
-                editDisplayName.show();
133
-                editDisplayName.focus();
134
-                editDisplayName.select();
136
+                $localDisplayName.hide();
137
+                $editDisplayName.show();
138
+                $editDisplayName.focus();
139
+                $editDisplayName.select();
135 140
 
136
-                editDisplayName.one("focusout", function (e) {
141
+                $editDisplayName.one("focusout", function (e) {
137 142
                     self.emitter.emit(UIEvents.NICKNAME_CHANGED, this.value);
138
-                    $('#editDisplayName').hide();
143
+                    $editDisplayName.hide();
144
+                    $localDisplayName.show();
139 145
                 });
140 146
 
141
-                editDisplayName.on('keydown', function (e) {
147
+                $editDisplayName.on('keydown', function (e) {
142 148
                     if (e.keyCode === 13) {
143 149
                         e.preventDefault();
144 150
                         $('#editDisplayName').hide();
@@ -199,7 +205,7 @@ LocalVideo.prototype.changeVideo = function (stream) {
199 205
         localVideoContainer.removeChild(localVideo);
200 206
         // when removing only the video element and we are on stage
201 207
         // update the stage
202
-        if(this.VideoLayout.isCurrentlyOnLarge(this.id))
208
+        if(this.isCurrentlyOnLargeVideo())
203 209
             this.VideoLayout.updateLargeVideo(this.id);
204 210
         stream.off(TrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
205 211
     };

+ 171
- 30
modules/UI/videolayout/RemoteVideo.js Näytä tiedosto

@@ -8,19 +8,45 @@ import UIUtils from "../util/UIUtil";
8 8
 import UIEvents from '../../../service/UI/UIEvents';
9 9
 import JitsiPopover from "../util/JitsiPopover";
10 10
 
11
-function RemoteVideo(id, VideoLayout, emitter) {
12
-    this.id = id;
11
+/**
12
+ * Creates new instance of the <tt>RemoteVideo</tt>.
13
+ * @param user {JitsiParticipant} the user for whom remote video instance will
14
+ * be created.
15
+ * @param {VideoLayout} VideoLayout the video layout instance.
16
+ * @param {EventEmitter} emitter the event emitter which will be used by
17
+ * the new instance to emit events.
18
+ * @constructor
19
+ */
20
+function RemoteVideo(user, VideoLayout, emitter) {
21
+    this.user = user;
22
+    this.id = user.getId();
13 23
     this.emitter = emitter;
14
-    this.videoSpanId = `participant_${id}`;
24
+    this.videoSpanId = `participant_${this.id}`;
15 25
     SmallVideo.call(this, VideoLayout);
16 26
     this.hasRemoteVideoMenu = false;
17 27
     this.addRemoteVideoContainer();
18
-    this.connectionIndicator = new ConnectionIndicator(this, id);
28
+    this.connectionIndicator = new ConnectionIndicator(this, this.id);
19 29
     this.setDisplayName();
20
-    this.bindHoverHandler();
21 30
     this.flipX = false;
22 31
     this.isLocal = false;
23
-    this.isMuted = false;
32
+    /**
33
+     * The flag is set to <tt>true</tt> after the 'onplay' event has been
34
+     * triggered on the current video element. It goes back to <tt>false</tt>
35
+     * when the stream is removed. It is used to determine whether the video
36
+     * playback has ever started.
37
+     * @type {boolean}
38
+     */
39
+    this.wasVideoPlayed = false;
40
+    /**
41
+     * The flag is set to <tt>true</tt> if remote participant's video gets muted
42
+     * during his media connection disruption. This is to prevent black video
43
+     * being render on the thumbnail, because even though once the video has
44
+     * been played the image usually remains on the video element it seems that
45
+     * after longer period of the video element being hidden this image can be
46
+     * lost.
47
+     * @type {boolean}
48
+     */
49
+    this.mutedWhileDisconnected = false;
24 50
 }
25 51
 
26 52
 RemoteVideo.prototype = Object.create(SmallVideo.prototype);
@@ -34,13 +60,15 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
34 60
     if (APP.conference.isModerator) {
35 61
         this.addRemoteVideoMenu();
36 62
     }
37
-    let {thumbWidth, thumbHeight} = this.VideoLayout.resizeThumbnails();
38
-    AudioLevels.updateAudioLevelCanvas(this.id, thumbWidth, thumbHeight);
63
+
64
+    let { remoteVideo } = this.VideoLayout.resizeThumbnails(false, true);
65
+    let { thumbHeight, thumbWidth } = remoteVideo;
66
+
67
+    this.addAudioLevelIndicator();
39 68
 
40 69
     return this.container;
41 70
 };
42 71
 
43
-
44 72
 /**
45 73
  * Initializes the remote participant popup menu, by specifying previously
46 74
  * constructed popupMenuElement, containing all the menu items.
@@ -50,7 +78,7 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
50 78
  */
51 79
 RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) {
52 80
     this.popover = new JitsiPopover(
53
-        $("#" + this.videoSpanId + " > .remotevideomenu"),
81
+        $("#" + this.videoSpanId + " .remotevideomenu"),
54 82
         {   content: popupMenuElement.outerHTML,
55 83
             skin: "black"});
56 84
 
@@ -60,7 +88,7 @@ RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) {
60 88
     this.popover.show = function () {
61 89
         // update content by forcing it, to finish even if popover
62 90
         // is not visible
63
-        this.updateRemoteVideoMenu(this.isMuted, true);
91
+        this.updateRemoteVideoMenu(this.isAudioMuted, true);
64 92
         // call the original show, passing its actual this
65 93
         origShowFunc.call(this.popover);
66 94
     }.bind(this);
@@ -96,7 +124,7 @@ RemoteVideo.prototype._generatePopupContent = function () {
96 124
 
97 125
     muteLinkItem.id = "mutelink_" + this.id;
98 126
 
99
-    if (this.isMuted) {
127
+    if (this.isAudioMuted) {
100 128
         muteLinkItem.innerHTML = mutedHTML;
101 129
         muteLinkItem.className = 'mutelink disabled';
102 130
     }
@@ -108,7 +136,7 @@ RemoteVideo.prototype._generatePopupContent = function () {
108 136
     // Delegate event to the document.
109 137
     $(document).on("click", "#mutelink_" + this.id, function(){
110 138
 
111
-        if (this.isMuted)
139
+        if (this.isAudioMuted)
112 140
             return;
113 141
 
114 142
         this.emitter.emit(UIEvents.REMOTE_AUDIO_MUTED, this.id);
@@ -152,7 +180,7 @@ RemoteVideo.prototype._generatePopupContent = function () {
152 180
  */
153 181
 RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
154 182
 
155
-    this.isMuted = isMuted;
183
+    this.isAudioMuted = isMuted;
156 184
 
157 185
     // generate content, translate it and add it to document only if
158 186
     // popover is visible or we force to do so.
@@ -161,6 +189,33 @@ RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
161 189
     }
162 190
 };
163 191
 
192
+/**
193
+ * @inheritDoc
194
+ */
195
+RemoteVideo.prototype.setMutedView = function(isMuted) {
196
+    SmallVideo.prototype.setMutedView.call(this, isMuted);
197
+    // Update 'mutedWhileDisconnected' flag
198
+    this._figureOutMutedWhileDisconnected(this.isConnectionActive() === false);
199
+};
200
+
201
+/**
202
+ * Figures out the value of {@link #mutedWhileDisconnected} flag by taking into
203
+ * account remote participant's network connectivity and video muted status.
204
+ *
205
+ * @param {boolean} isDisconnected <tt>true</tt> if the remote participant is
206
+ * currently having connectivity issues or <tt>false</tt> otherwise.
207
+ *
208
+ * @private
209
+ */
210
+RemoteVideo.prototype._figureOutMutedWhileDisconnected
211
+= function(isDisconnected) {
212
+    if (isDisconnected && this.isVideoMuted) {
213
+        this.mutedWhileDisconnected = true;
214
+    } else if (!isDisconnected && !this.isVideoMuted) {
215
+        this.mutedWhileDisconnected = false;
216
+    }
217
+};
218
+
164 219
 /**
165 220
  * Adds the remote video menu element for the given <tt>id</tt> in the
166 221
  * given <tt>parentElement</tt>.
@@ -170,12 +225,16 @@ RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
170 225
  */
171 226
 if (!interfaceConfig.filmStripOnly) {
172 227
     RemoteVideo.prototype.addRemoteVideoMenu = function () {
173
-        var spanElement = document.createElement('div');
174
-        spanElement.className = 'remotevideomenu';
175
-        this.container.appendChild(spanElement);
228
+
229
+        var spanElement = document.createElement('span');
230
+        spanElement.className = 'remotevideomenu toolbar-icon right';
231
+
232
+        this.container
233
+            .querySelector('.videocontainer__toolbar')
234
+            .appendChild(spanElement);
176 235
 
177 236
         var menuElement = document.createElement('i');
178
-        menuElement.className = 'fa fa-angle-down';
237
+        menuElement.className = 'icon-menu-up';
179 238
         menuElement.title = 'Remote user controls';
180 239
         spanElement.appendChild(menuElement);
181 240
 
@@ -204,13 +263,88 @@ RemoteVideo.prototype.removeRemoteStreamElement = function (stream) {
204 263
     var select = $('#' + elementID);
205 264
     select.remove();
206 265
 
266
+    if (isVideo) {
267
+        this.wasVideoPlayed = false;
268
+    }
269
+
207 270
     console.info((isVideo ? "Video" : "Audio") +
208 271
                  " removed " + this.id, select);
209 272
 
210 273
     // when removing only the video element and we are on stage
211 274
     // update the stage
212
-    if (isVideo && this.VideoLayout.isCurrentlyOnLarge(this.id))
275
+    if (isVideo && this.isCurrentlyOnLargeVideo())
213 276
         this.VideoLayout.updateLargeVideo(this.id);
277
+    else
278
+        // Missing video stream will affect display mode
279
+        this.updateView();
280
+};
281
+
282
+/**
283
+ * Checks whether the remote user associated with this <tt>RemoteVideo</tt>
284
+ * has connectivity issues.
285
+ *
286
+ * @return {boolean} <tt>true</tt> if the user's connection is fine or
287
+ * <tt>false</tt> otherwise.
288
+ */
289
+RemoteVideo.prototype.isConnectionActive = function() {
290
+    return this.user.isConnectionActive();
291
+};
292
+
293
+/**
294
+ * The remote video is considered "playable" once the stream has started
295
+ * according to the {@link #hasVideoStarted} result.
296
+ *
297
+ * @inheritdoc
298
+ * @override
299
+ */
300
+RemoteVideo.prototype.isVideoPlayable = function () {
301
+    return SmallVideo.prototype.isVideoPlayable.call(this)
302
+        && this.hasVideoStarted() && !this.mutedWhileDisconnected;
303
+};
304
+
305
+/**
306
+ * @inheritDoc
307
+ */
308
+RemoteVideo.prototype.updateView = function () {
309
+
310
+    this.updateConnectionStatusIndicator(
311
+        null /* will obtain the status from 'conference' */);
312
+
313
+    // This must be called after 'updateConnectionStatusIndicator' because it
314
+    // affects the display mode by modifying 'mutedWhileDisconnected' flag
315
+    SmallVideo.prototype.updateView.call(this);
316
+};
317
+
318
+/**
319
+ * Updates the UI to reflect user's connectivity status.
320
+ * @param isActive {boolean|null} 'true' if user's connection is active or
321
+ * 'false' when the use is having some connectivity issues and a warning
322
+ * should be displayed. When 'null' is passed then the current value will be
323
+ * obtained from the conference instance.
324
+ */
325
+RemoteVideo.prototype.updateConnectionStatusIndicator = function (isActive) {
326
+    // Check for initial value if 'isActive' is not defined
327
+    if (typeof isActive !== "boolean") {
328
+        isActive = this.isConnectionActive();
329
+        if (isActive === null) {
330
+            // Cancel processing at this point - no update
331
+            return;
332
+        }
333
+    }
334
+
335
+    console.debug(this.id + " thumbnail is connection active ? " + isActive);
336
+
337
+    // Update 'mutedWhileDisconnected' flag
338
+    this._figureOutMutedWhileDisconnected(!isActive);
339
+
340
+    if(this.connectionIndicator)
341
+        this.connectionIndicator.updateConnectionStatusIndicator(isActive);
342
+
343
+    // Toggle thumbnail video problem filter
344
+    this.selectVideoElement().toggleClass(
345
+        "videoThumbnailProblemFilter", !isActive);
346
+    this.$avatar().toggleClass(
347
+        "videoThumbnailProblemFilter", !isActive);
214 348
 };
215 349
 
216 350
 /**
@@ -241,22 +375,23 @@ RemoteVideo.prototype.waitForPlayback = function (streamElement, stream) {
241 375
     // Register 'onplaying' listener to trigger 'videoactive' on VideoLayout
242 376
     // when video playback starts
243 377
     var onPlayingHandler = function () {
378
+        self.wasVideoPlayed = true;
244 379
         self.VideoLayout.videoactive(streamElement, self.id);
245 380
         streamElement.onplaying = null;
381
+        // Refresh to show the video
382
+        self.updateView();
246 383
     };
247 384
     streamElement.onplaying = onPlayingHandler;
248 385
 };
249 386
 
250 387
 /**
251
- * Checks whether or not video stream exists and has started for this
252
- * RemoteVideo instance. This is checked by trying to select video element in
253
- * this container and checking if 'currentTime' field's value is greater than 0.
388
+ * Checks whether the video stream has started for this RemoteVideo instance.
254 389
  *
255
- * @returns {*|boolean} true if this RemoteVideo has active video stream running
390
+ * @returns {boolean} true if this RemoteVideo has a video stream for which
391
+ * the playback has been started.
256 392
  */
257 393
 RemoteVideo.prototype.hasVideoStarted = function () {
258
-    var videoSelector = this.selectVideoElement();
259
-    return videoSelector.length && videoSelector[0].currentTime > 0;
394
+    return this.wasVideoPlayed;
260 395
 };
261 396
 
262 397
 RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
@@ -381,7 +516,7 @@ RemoteVideo.prototype.setDisplayName = function(displayName, key) {
381 516
         return;
382 517
     }
383 518
 
384
-    var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
519
+    var nameSpan = $('#' + this.videoSpanId + ' .displayname');
385 520
 
386 521
     // If we already have a display name for this video.
387 522
     if (nameSpan.length > 0) {
@@ -400,7 +535,9 @@ RemoteVideo.prototype.setDisplayName = function(displayName, key) {
400 535
     } else {
401 536
         nameSpan = document.createElement('span');
402 537
         nameSpan.className = 'displayname';
403
-        $('#' + this.videoSpanId)[0].appendChild(nameSpan);
538
+        $('#' + this.videoSpanId)[0]
539
+            .querySelector('.videocontainer__toolbar')
540
+            .appendChild(nameSpan);
404 541
 
405 542
         if (displayName && displayName.length > 0) {
406 543
             $(nameSpan).text(displayName);
@@ -418,7 +555,7 @@ RemoteVideo.prototype.setDisplayName = function(displayName, key) {
418 555
  * @param videoElementId the id of local or remote video element.
419 556
  */
420 557
 RemoteVideo.prototype.removeRemoteVideoMenu = function() {
421
-    var menuSpan = $('#' + this.videoSpanId + '>span.remotevideomenu');
558
+    var menuSpan = $('#' + this.videoSpanId + '> .remotevideomenu');
422 559
     if (menuSpan.length) {
423 560
         this.popover.forceHide();
424 561
         menuSpan.remove();
@@ -427,12 +564,16 @@ RemoteVideo.prototype.removeRemoteVideoMenu = function() {
427 564
 };
428 565
 
429 566
 RemoteVideo.createContainer = function (spanId) {
430
-    var container = document.createElement('span');
567
+    let container = document.createElement('span');
431 568
     container.id = spanId;
432 569
     container.className = 'videocontainer';
570
+
571
+    let toolbar = document.createElement('div');
572
+    toolbar.className = "videocontainer__toolbar";
573
+    container.appendChild(toolbar);
574
+
433 575
     var remotes = document.getElementById('remoteVideos');
434 576
     return remotes.appendChild(container);
435 577
 };
436 578
 
437
-
438 579
 export default RemoteVideo;

+ 210
- 136
modules/UI/videolayout/SmallVideo.js Näytä tiedosto

@@ -1,13 +1,34 @@
1
-/* global $, APP, JitsiMeetJS */
2
-/* jshint -W101 */
1
+/* global $, APP, JitsiMeetJS, interfaceConfig */
3 2
 import Avatar from "../avatar/Avatar";
4 3
 import UIUtil from "../util/UIUtil";
5 4
 import UIEvents from "../../../service/UI/UIEvents";
5
+import AudioLevels from "../audio_levels/AudioLevels";
6 6
 
7 7
 const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper;
8 8
 
9
+/**
10
+ * Display mode constant used when video is being displayed on the small video.
11
+ * @type {number}
12
+ * @constant
13
+ */
14
+const DISPLAY_VIDEO = 0;
15
+/**
16
+ * Display mode constant used when the user's avatar is being displayed on
17
+ * the small video.
18
+ * @type {number}
19
+ * @constant
20
+ */
21
+const DISPLAY_AVATAR = 1;
22
+/**
23
+ * Display mode constant used when neither video nor avatar is being displayed
24
+ * on the small video.
25
+ * @type {number}
26
+ * @constant
27
+ */
28
+const DISPLAY_BLACKNESS = 2;
29
+
9 30
 function SmallVideo(VideoLayout) {
10
-    this.isMuted = false;
31
+    this.isAudioMuted = false;
11 32
     this.hasAvatar = false;
12 33
     this.isVideoMuted = false;
13 34
     this.videoStream = null;
@@ -40,7 +61,7 @@ SmallVideo.prototype.isVisible = function () {
40 61
 };
41 62
 
42 63
 SmallVideo.prototype.showDisplayName = function(isShow) {
43
-    var nameSpan = $('#' + this.videoSpanId + '>span.displayname').get(0);
64
+    var nameSpan = $('#' + this.videoSpanId + ' .displayname').get(0);
44 65
     if (isShow) {
45 66
         if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length)
46 67
             nameSpan.setAttribute("style", "display:inline-block;");
@@ -171,26 +192,6 @@ SmallVideo.getStreamElementID = function (stream) {
171 192
     return (isVideo ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
172 193
 };
173 194
 
174
-/**
175
- * Configures hoverIn/hoverOut handlers.
176
- */
177
-SmallVideo.prototype.bindHoverHandler = function () {
178
-    // Add hover handler
179
-    var self = this;
180
-    $(this.container).hover(
181
-        function () {
182
-            self.showDisplayName(true);
183
-        },
184
-        function () {
185
-            // If the video has been "pinned" by the user we want to
186
-            // keep the display name on place.
187
-            if (!self.VideoLayout.isLargeVideoVisible() ||
188
-                !self.VideoLayout.isCurrentlyOnLarge(self.id))
189
-                self.showDisplayName(false);
190
-        }
191
-    );
192
-};
193
-
194 195
 /**
195 196
  * Updates the data for the indicator
196 197
  * @param id the id of the indicator
@@ -209,121 +210,164 @@ SmallVideo.prototype.hideIndicator = function () {
209 210
 
210 211
 
211 212
 /**
212
- * Shows audio muted indicator over small videos.
213
- * @param {string} isMuted
213
+ * Shows / hides the audio muted indicator over small videos.
214
+ *
215
+ * @param {boolean} isMuted indicates if the muted element should be shown
216
+ * or hidden
214 217
  */
215 218
 SmallVideo.prototype.showAudioIndicator = function(isMuted) {
216
-    var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted');
219
+
220
+    var audioMutedIndicator = this.getAudioMutedIndicator();
217 221
 
218 222
     if (!isMuted) {
219
-        if (audioMutedSpan.length > 0) {
220
-            audioMutedSpan.popover('hide');
221
-            audioMutedSpan.remove();
222
-        }
223
+        audioMutedIndicator.hide();
223 224
     }
224 225
     else {
225
-        if (!audioMutedSpan.length) {
226
-            audioMutedSpan = document.createElement('span');
227
-            audioMutedSpan.className = 'audioMuted';
228
-            UIUtil.setTooltip(audioMutedSpan,
229
-                "videothumbnail.mute",
230
-                "top");
231
-
232
-            this.container.appendChild(audioMutedSpan);
233
-            APP.translation.translateElement($('#' + this.videoSpanId + " > span"));
234
-            var mutedIndicator = document.createElement('i');
235
-            mutedIndicator.className = 'icon-mic-disabled';
236
-            audioMutedSpan.appendChild(mutedIndicator);
226
+        audioMutedIndicator.show();
227
+    }
228
+    this.isAudioMuted = isMuted;
229
+};
237 230
 
238
-        }
239
-        this.updateIconPositions();
231
+/**
232
+ * Returns the audio muted indicator jquery object. If it doesn't exists -
233
+ * creates it.
234
+ *
235
+ * @returns {jQuery|HTMLElement} the audio muted indicator
236
+ */
237
+SmallVideo.prototype.getAudioMutedIndicator = function () {
238
+    var audioMutedSpan = $('#' + this.videoSpanId + ' .audioMuted');
239
+
240
+    if (audioMutedSpan.length) {
241
+        return audioMutedSpan;
240 242
     }
241
-    this.isMuted = isMuted;
243
+
244
+    audioMutedSpan = document.createElement('span');
245
+    audioMutedSpan.className = 'audioMuted toolbar-icon';
246
+
247
+    UIUtil.setTooltip(audioMutedSpan,
248
+        "videothumbnail.mute",
249
+        "top");
250
+
251
+    this.container
252
+        .querySelector('.videocontainer__toolbar')
253
+        .appendChild(audioMutedSpan);
254
+
255
+
256
+    var mutedIndicator = document.createElement('i');
257
+    mutedIndicator.className = 'icon-mic-disabled';
258
+    audioMutedSpan.appendChild(mutedIndicator);
259
+
260
+    return $('#' + this.videoSpanId + ' .audioMuted');
242 261
 };
243 262
 
244 263
 /**
245 264
  * Shows video muted indicator over small videos and disables/enables avatar
246 265
  * if video muted.
266
+ *
267
+ * @param {boolean} isMuted indicates if we should set the view to muted view
268
+ * or not
247 269
  */
248
-SmallVideo.prototype.setMutedView = function(isMuted) {
270
+SmallVideo.prototype.setVideoMutedView = function(isMuted) {
249 271
     this.isVideoMuted = isMuted;
250 272
     this.updateView();
251 273
 
252
-    var videoMutedSpan = $('#' + this.videoSpanId + '>span.videoMuted');
274
+    var videoMutedSpan = this.getVideoMutedIndicator();
253 275
 
254
-    if (isMuted === false) {
255
-        if (videoMutedSpan.length > 0) {
256
-            videoMutedSpan.remove();
257
-        }
258
-    }
259
-    else {
260
-        if (!videoMutedSpan.length) {
261
-            videoMutedSpan = document.createElement('span');
262
-            videoMutedSpan.className = 'videoMuted';
263
-
264
-            this.container.appendChild(videoMutedSpan);
265
-
266
-            var mutedIndicator = document.createElement('i');
267
-            mutedIndicator.className = 'icon-camera-disabled';
268
-            UIUtil.setTooltip(mutedIndicator,
269
-                "videothumbnail.videomute",
270
-                "top");
271
-            videoMutedSpan.appendChild(mutedIndicator);
272
-            //translate texts for muted indicator
273
-            APP.translation.translateElement($('#' + this.videoSpanId  + " > span > i"));
274
-        }
275
-
276
-        this.updateIconPositions();
277
-    }
276
+    videoMutedSpan[isMuted ? 'show' : 'hide']();
278 277
 };
279 278
 
280
-SmallVideo.prototype.updateIconPositions = function () {
281
-    var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted');
282
-    var connectionIndicator = $('#' + this.videoSpanId + '>div.connectionindicator');
283
-    var videoMutedSpan = $('#' + this.videoSpanId + '>span.videoMuted');
284
-    if(connectionIndicator.length > 0 &&
285
-        connectionIndicator[0].style.display != "none") {
286
-        audioMutedSpan.css({right: "23px"});
287
-        videoMutedSpan.css({right: ((audioMutedSpan.length > 0? 23 : 0) + 30) + "px"});
288
-    } else {
289
-        audioMutedSpan.css({right: "0px"});
290
-        videoMutedSpan.css({right: (audioMutedSpan.length > 0? 30 : 0) + "px"});
279
+/**
280
+ * Returns the video muted indicator jquery object. If it doesn't exists -
281
+ * creates it.
282
+ *
283
+ * @returns {jQuery|HTMLElement} the video muted indicator
284
+ */
285
+SmallVideo.prototype.getVideoMutedIndicator = function () {
286
+    var videoMutedSpan = $('#' + this.videoSpanId + ' .videoMuted');
287
+
288
+    if (videoMutedSpan.length) {
289
+        return videoMutedSpan;
291 290
     }
291
+
292
+    videoMutedSpan = document.createElement('span');
293
+    videoMutedSpan.className = 'videoMuted toolbar-icon';
294
+
295
+    this.container
296
+        .querySelector('.videocontainer__toolbar')
297
+        .appendChild(videoMutedSpan);
298
+
299
+    var mutedIndicator = document.createElement('i');
300
+    mutedIndicator.className = 'icon-camera-disabled';
301
+
302
+    UIUtil.setTooltip(mutedIndicator,
303
+        "videothumbnail.videomute",
304
+        "top");
305
+
306
+    videoMutedSpan.appendChild(mutedIndicator);
307
+
308
+    return $('#' + this.videoSpanId + ' .videoMuted');
292 309
 };
293 310
 
294 311
 /**
295
- * Creates the element indicating the moderator(owner) of the conference.
312
+ * Adds the element indicating the moderator(owner) of the conference.
296 313
  */
297
-SmallVideo.prototype.createModeratorIndicatorElement = function () {
314
+SmallVideo.prototype.addModeratorIndicator = function () {
315
+
316
+    // Don't create moderator indicator if DISABLE_FOCUS_INDICATOR is true
317
+    if (interfaceConfig.DISABLE_FOCUS_INDICATOR)
318
+        return false;
319
+
298 320
     // Show moderator indicator
299 321
     var indicatorSpan = $('#' + this.videoSpanId + ' .focusindicator');
300 322
 
301
-    if (!indicatorSpan || indicatorSpan.length === 0) {
302
-        indicatorSpan = document.createElement('span');
303
-        indicatorSpan.className = 'focusindicator';
304
-
305
-        this.container.appendChild(indicatorSpan);
306
-        indicatorSpan = $('#' + this.videoSpanId + ' .focusindicator');
323
+    if (indicatorSpan.length) {
324
+        return;
307 325
     }
308 326
 
309
-    if (indicatorSpan.children().length !== 0)
310
-        return;
327
+    indicatorSpan = document.createElement('span');
328
+    indicatorSpan.className = 'focusindicator toolbar-icon right';
329
+
330
+    this.container
331
+        .querySelector('.videocontainer__toolbar')
332
+        .appendChild(indicatorSpan);
333
+
311 334
     var moderatorIndicator = document.createElement('i');
312 335
     moderatorIndicator.className = 'icon-star';
313
-    indicatorSpan[0].appendChild(moderatorIndicator);
314 336
 
315
-    UIUtil.setTooltip(indicatorSpan[0],
337
+    UIUtil.setTooltip(moderatorIndicator,
316 338
         "videothumbnail.moderator",
317
-        "top");
339
+        "top-left");
318 340
 
319
-    //translates text in focus indicators
320
-    APP.translation.translateElement($('#' + this.videoSpanId + ' .focusindicator'));
341
+    indicatorSpan.appendChild(moderatorIndicator);
342
+};
343
+
344
+/**
345
+ * Adds the element indicating the audio level of the participant.
346
+ */
347
+SmallVideo.prototype.addAudioLevelIndicator = function () {
348
+    var audioSpan = $('#' + this.videoSpanId + ' .audioindicator');
349
+
350
+    if (audioSpan.length) {
351
+        return;
352
+    }
353
+
354
+    this.container.appendChild(
355
+        AudioLevels.createThumbnailAudioLevelIndicator());
356
+};
357
+
358
+/**
359
+ * Updates the audio level for this small video.
360
+ *
361
+ * @param lvl the new audio level to set
362
+ */
363
+SmallVideo.prototype.updateAudioLevelIndicator = function (lvl) {
364
+    AudioLevels.updateThumbnailAudioLevel(this.videoSpanId, lvl);
321 365
 };
322 366
 
323 367
 /**
324 368
  * Removes the element indicating the moderator(owner) of the conference.
325 369
  */
326
-SmallVideo.prototype.removeModeratorIndicatorElement = function () {
370
+SmallVideo.prototype.removeModeratorIndicator = function () {
327 371
     $('#' + this.videoSpanId + ' .focusindicator').remove();
328 372
 };
329 373
 
@@ -341,6 +385,16 @@ SmallVideo.prototype.selectVideoElement = function () {
341 385
     return $(RTCUIHelper.findVideoElement($('#' + this.videoSpanId)[0]));
342 386
 };
343 387
 
388
+/**
389
+ * Selects the HTML image element which displays user's avatar.
390
+ *
391
+ * @return {jQuery|HTMLElement} a jQuery selector pointing to the HTML image
392
+ * element which displays the user's avatar.
393
+ */
394
+SmallVideo.prototype.$avatar = function () {
395
+    return $('#' + this.videoSpanId + ' .userAvatar');
396
+};
397
+
344 398
 /**
345 399
  * Enables / disables the css responsible for focusing/pinning a video
346 400
  * thumbnail.
@@ -363,6 +417,47 @@ SmallVideo.prototype.hasVideo = function () {
363 417
     return this.selectVideoElement().length !== 0;
364 418
 };
365 419
 
420
+/**
421
+ * Checks whether the user associated with this <tt>SmallVideo</tt> is currently
422
+ * being displayed on the "large video".
423
+ *
424
+ * @return {boolean} <tt>true</tt> if the user is displayed on the large video
425
+ * or <tt>false</tt> otherwise.
426
+ */
427
+SmallVideo.prototype.isCurrentlyOnLargeVideo = function () {
428
+    return this.VideoLayout.isCurrentlyOnLarge(this.id);
429
+};
430
+
431
+/**
432
+ * Checks whether there is a playable video stream available for the user
433
+ * associated with this <tt>SmallVideo</tt>.
434
+ *
435
+ * @return {boolean} <tt>true</tt> if there is a playable video stream available
436
+ * or <tt>false</tt> otherwise.
437
+ */
438
+SmallVideo.prototype.isVideoPlayable = function() {
439
+    return this.videoStream // Is there anything to display ?
440
+        && !this.isVideoMuted && !this.videoStream.isMuted() // Muted ?
441
+        && (this.isLocal || this.VideoLayout.isInLastN(this.id));
442
+};
443
+
444
+/**
445
+ * Determines what should be display on the thumbnail.
446
+ *
447
+ * @return {number} one of <tt>DISPLAY_VIDEO</tt>,<tt>DISPLAY_AVATAR</tt>
448
+ * or <tt>DISPLAY_BLACKNESS</tt>.
449
+ */
450
+SmallVideo.prototype.selectDisplayMode = function() {
451
+    // Display name is always and only displayed when user is on the stage
452
+    if (this.isCurrentlyOnLargeVideo()) {
453
+        return DISPLAY_BLACKNESS;
454
+    } else if (this.isVideoPlayable() && this.selectVideoElement().length) {
455
+        return DISPLAY_VIDEO;
456
+    } else {
457
+        return DISPLAY_AVATAR;
458
+    }
459
+};
460
+
366 461
 /**
367 462
  * Hides or shows the user's avatar.
368 463
  * This update assumes that large video had been updated and we will
@@ -382,48 +477,28 @@ SmallVideo.prototype.updateView = function () {
382 477
         }
383 478
     }
384 479
 
385
-    let video = this.selectVideoElement();
386
-
387
-    let avatar = $('#' + this.videoSpanId + ' .userAvatar');
388
-
389
-    var isCurrentlyOnLarge = this.VideoLayout.isCurrentlyOnLarge(this.id);
390
-
391
-    var showVideo = !this.isVideoMuted && !isCurrentlyOnLarge;
392
-    var showAvatar;
393
-    if ((!this.isLocal
394
-            && !this.VideoLayout.isInLastN(this.id))
395
-        || this.isVideoMuted) {
396
-        showAvatar = true;
397
-    } else {
398
-        // We want to show the avatar when the video is muted or not exists
399
-        // that is when 'true' or 'null' is returned
400
-        showAvatar = !this.videoStream || this.videoStream.isMuted();
401
-    }
402
-
403
-    showAvatar = showAvatar && !isCurrentlyOnLarge;
404
-
405
-    if (video && video.length > 0) {
406
-        setVisibility(video, showVideo);
407
-    }
408
-    setVisibility(avatar, showAvatar);
409
-
410
-    this.showDisplayName(!showVideo && !showAvatar);
480
+    // Determine whether video, avatar or blackness should be displayed
481
+    let displayMode = this.selectDisplayMode();
482
+    // Show/hide video
483
+    setVisibility(this.selectVideoElement(), displayMode === DISPLAY_VIDEO);
484
+    // Show/hide the avatar
485
+    setVisibility(this.$avatar(), displayMode === DISPLAY_AVATAR);
411 486
 };
412 487
 
413 488
 SmallVideo.prototype.avatarChanged = function (avatarUrl) {
414 489
     var thumbnail = $('#' + this.videoSpanId);
415
-    var avatar = $('#' + this.videoSpanId + ' .userAvatar');
490
+    var avatarSel = this.$avatar();
416 491
     this.hasAvatar = true;
417 492
 
418 493
     // set the avatar in the thumbnail
419
-    if (avatar && avatar.length > 0) {
420
-        avatar[0].src = avatarUrl;
494
+    if (avatarSel && avatarSel.length > 0) {
495
+        avatarSel[0].src = avatarUrl;
421 496
     } else {
422 497
         if (thumbnail && thumbnail.length > 0) {
423
-            avatar = document.createElement('img');
424
-            avatar.className = 'userAvatar';
425
-            avatar.src = avatarUrl;
426
-            thumbnail.append(avatar);
498
+            var avatarElement = document.createElement('img');
499
+            avatarElement.className = 'userAvatar';
500
+            avatarElement.src = avatarUrl;
501
+            thumbnail.append(avatarElement);
427 502
         }
428 503
     }
429 504
 };
@@ -445,7 +520,7 @@ SmallVideo.prototype.showDominantSpeakerIndicator = function (show) {
445 520
     indicatorSpan.innerHTML
446 521
         = "<i id='indicatoricon' class='fa fa-bullhorn'></i>";
447 522
     // adds a tooltip
448
-    UIUtil.setTooltip(indicatorSpan, "speaker", "left");
523
+    UIUtil.setTooltip(indicatorSpan, "speaker", "top");
449 524
     APP.translation.translateElement($(indicatorSpan));
450 525
 
451 526
     $(indicatorSpan).css("visibility", show ? "visible" : "hidden");
@@ -465,12 +540,11 @@ SmallVideo.prototype.showRaisedHandIndicator = function (show) {
465 540
     var indicatorSpanId = "raisehandindicator";
466 541
     var indicatorSpan = this.getIndicatorSpan(indicatorSpanId);
467 542
 
468
-    indicatorSpan.style.background = "#D6D61E";
469 543
     indicatorSpan.innerHTML
470
-        = "<i id='indicatoricon' class='fa fa-hand-paper-o'></i>";
544
+        = "<i id='indicatoricon' class='icon-raised-hand'></i>";
471 545
 
472 546
     // adds a tooltip
473
-    UIUtil.setTooltip(indicatorSpan, "raisedHand", "left");
547
+    UIUtil.setTooltip(indicatorSpan, "raisedHand", "top");
474 548
     APP.translation.translateElement($(indicatorSpan));
475 549
 
476 550
     $(indicatorSpan).css("visibility", show ? "visible" : "hidden");

modules/UI/videolayout/LargeVideo.js → modules/UI/videolayout/VideoContainer.js Näytä tiedosto

@@ -1,17 +1,16 @@
1 1
 /* global $, APP, interfaceConfig */
2 2
 /* jshint -W101 */
3 3
 
4
-import UIUtil from "../util/UIUtil";
5
-import UIEvents from "../../../service/UI/UIEvents";
6
-import LargeContainer from './LargeContainer';
7 4
 import FilmStrip from './FilmStrip';
8
-import Avatar from "../avatar/Avatar";
9
-import {createDeferred} from '../../util/helpers';
10
-
11
-const FADE_DURATION_MS = 300;
5
+import LargeContainer from './LargeContainer';
6
+import UIEvents from "../../../service/UI/UIEvents";
7
+import UIUtil from "../util/UIUtil";
12 8
 
9
+// FIXME should be 'video'
13 10
 export const VIDEO_CONTAINER_TYPE = "camera";
14 11
 
12
+const FADE_DURATION_MS = 300;
13
+
15 14
 /**
16 15
  * Get stream id.
17 16
  * @param {JitsiTrack?} stream
@@ -20,7 +19,8 @@ function getStreamOwnerId(stream) {
20 19
     if (!stream) {
21 20
         return;
22 21
     }
23
-    if (stream.isLocal()) { // local stream doesn't have method "getParticipantId"
22
+    // local stream doesn't have method "getParticipantId"
23
+    if (stream.isLocal()) {
24 24
         return APP.conference.getMyUserId();
25 25
     } else {
26 26
         return stream.getParticipantId();
@@ -154,7 +154,7 @@ function getDesktopVideoPosition(videoWidth,
154 154
 /**
155 155
  * Container for user video.
156 156
  */
157
-class VideoContainer extends LargeContainer {
157
+export class VideoContainer extends LargeContainer {
158 158
     // FIXME: With Temasys we have to re-select everytime
159 159
     get $video () {
160 160
         return $('#largeVideo');
@@ -164,23 +164,61 @@ class VideoContainer extends LargeContainer {
164 164
         return getStreamOwnerId(this.stream);
165 165
     }
166 166
 
167
-    constructor (onPlay) {
167
+    constructor (onPlay, emitter) {
168 168
         super();
169 169
         this.stream = null;
170 170
         this.videoType = null;
171 171
         this.localFlipX = true;
172
+        this.emitter = emitter;
172 173
 
173 174
         this.isVisible = false;
174 175
 
176
+        /**
177
+         * Flag indicates whether or not the avatar is currently displayed.
178
+         * @type {boolean}
179
+         */
180
+        this.avatarDisplayed = false;
175 181
         this.$avatar = $('#dominantSpeaker');
182
+
183
+        /**
184
+         * A jQuery selector of the remote connection message.
185
+         * @type {jQuery|HTMLElement}
186
+         */
187
+        this.$remoteConnectionMessage = $('#remoteConnectionMessage');
188
+
189
+        /**
190
+         * Indicates whether or not the video stream attached to the video
191
+         * element has started(which means that there is any image rendered
192
+         * even if the video is stalled).
193
+         * @type {boolean}
194
+         */
195
+        this.wasVideoRendered = false;
196
+
176 197
         this.$wrapper = $('#largeVideoWrapper');
177 198
 
178 199
         this.avatarHeight = $("#dominantSpeakerAvatar").height();
179 200
 
201
+        var onPlayCallback = function (event) {
202
+            if (typeof onPlay === 'function') {
203
+                onPlay(event);
204
+            }
205
+            this.wasVideoRendered = true;
206
+        }.bind(this);
180 207
         // This does not work with Temasys plugin - has to be a property to be
181 208
         // copied between new <object> elements
182 209
         //this.$video.on('play', onPlay);
183
-        this.$video[0].onplay = onPlay;
210
+        this.$video[0].onplay = onPlayCallback;
211
+    }
212
+
213
+    /**
214
+     * Enables a filter on the video which indicates that there are some
215
+     * problems with the local media connection.
216
+     *
217
+     * @param {boolean} enable <tt>true</tt> if the filter is to be enabled or
218
+     * <tt>false</tt> otherwise.
219
+     */
220
+    enableLocalConnectionProblemFilter (enable) {
221
+        this.$video.toggleClass("videoProblemFilter", enable);
184 222
     }
185 223
 
186 224
     /**
@@ -205,14 +243,14 @@ class VideoContainer extends LargeContainer {
205 243
         let { width, height } = this.getStreamSize();
206 244
         if (this.stream && this.isScreenSharing()) {
207 245
             return getDesktopVideoSize( width,
208
-                                        height,
209
-                                        containerWidth,
210
-                                        containerHeight);
246
+                height,
247
+                containerWidth,
248
+                containerHeight);
211 249
         } else {
212 250
             return getCameraVideoSize(  width,
213
-                                        height,
214
-                                        containerWidth,
215
-                                        containerHeight);
251
+                height,
252
+                containerWidth,
253
+                containerHeight);
216 254
         }
217 255
     }
218 256
 
@@ -228,29 +266,55 @@ class VideoContainer extends LargeContainer {
228 266
     getVideoPosition (width, height, containerWidth, containerHeight) {
229 267
         if (this.stream && this.isScreenSharing()) {
230 268
             return getDesktopVideoPosition( width,
231
-                                            height,
232
-                                            containerWidth,
233
-                                            containerHeight);
269
+                height,
270
+                containerWidth,
271
+                containerHeight);
234 272
         } else {
235 273
             return getCameraVideoPosition(  width,
236
-                                            height,
237
-                                            containerWidth,
238
-                                            containerHeight);
274
+                height,
275
+                containerWidth,
276
+                containerHeight);
239 277
         }
240 278
     }
241 279
 
280
+    /**
281
+     * Update position of the remote connection message which describes that
282
+     * the remote user is having connectivity issues.
283
+     */
284
+    positionRemoteConnectionMessage () {
285
+
286
+        if (this.avatarDisplayed) {
287
+            let $avatarImage = $("#dominantSpeakerAvatar");
288
+            this.$remoteConnectionMessage.css(
289
+                'top',
290
+                $avatarImage.offset().top + $avatarImage.height() + 10);
291
+        } else {
292
+            let height = this.$remoteConnectionMessage.height();
293
+            let parentHeight = this.$remoteConnectionMessage.parent().height();
294
+            this.$remoteConnectionMessage.css(
295
+                'top', (parentHeight/2) - (height/2));
296
+        }
297
+
298
+        let width = this.$remoteConnectionMessage.width();
299
+        let parentWidth = this.$remoteConnectionMessage.parent().width();
300
+        this.$remoteConnectionMessage.css(
301
+            'left', ((parentWidth/2) - (width/2)));
302
+    }
303
+
242 304
     resize (containerWidth, containerHeight, animate = false) {
243 305
         let [width, height]
244 306
             = this.getVideoSize(containerWidth, containerHeight);
245 307
         let { horizontalIndent, verticalIndent }
246 308
             = this.getVideoPosition(width, height,
247
-                                    containerWidth, containerHeight);
309
+            containerWidth, containerHeight);
248 310
 
249 311
         // update avatar position
250 312
         let top = containerHeight / 2 - this.avatarHeight / 4 * 3;
251 313
 
252 314
         this.$avatar.css('top', top);
253 315
 
316
+        this.positionRemoteConnectionMessage();
317
+
254 318
         this.$wrapper.animate({
255 319
             width: width,
256 320
             height: height,
@@ -272,6 +336,14 @@ class VideoContainer extends LargeContainer {
272 336
      * @param {string} videoType video type
273 337
      */
274 338
     setStream (stream, videoType) {
339
+
340
+        if (this.stream === stream) {
341
+            return;
342
+        } else {
343
+            // The stream has changed, so the image will be lost on detach
344
+            this.wasVideoRendered = false;
345
+        }
346
+
275 347
         // detach old stream
276 348
         if (this.stream) {
277 349
             this.stream.detach(this.$video[0]);
@@ -327,6 +399,21 @@ class VideoContainer extends LargeContainer {
327 399
             (show) ? interfaceConfig.DEFAULT_BACKGROUND : "#000");
328 400
 
329 401
         this.$avatar.css("visibility", show ? "visible" : "hidden");
402
+        this.avatarDisplayed = show;
403
+
404
+        this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED, show);
405
+    }
406
+
407
+    /**
408
+     * Indicates that the remote user who is currently displayed by this video
409
+     * container is having connectivity issues.
410
+     *
411
+     * @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide
412
+     * the indication.
413
+     */
414
+    showRemoteConnectionProblemIndicator (show) {
415
+        this.$video.toggleClass("remoteVideoProblemFilter", show);
416
+        this.$avatar.toggleClass("remoteVideoProblemFilter", show);
330 417
     }
331 418
 
332 419
     // We are doing fadeOut/fadeIn animations on parent div which wraps
@@ -380,304 +467,3 @@ class VideoContainer extends LargeContainer {
380 467
         return false;
381 468
     }
382 469
 }
383
-
384
-/**
385
- * Manager for all Large containers.
386
- */
387
-export default class LargeVideoManager {
388
-    constructor () {
389
-        this.containers = {};
390
-
391
-        this.state = VIDEO_CONTAINER_TYPE;
392
-        this.videoContainer = new VideoContainer(
393
-            () => this.resizeContainer(VIDEO_CONTAINER_TYPE));
394
-        this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
395
-
396
-        // use the same video container to handle and desktop tracks
397
-        this.addContainer("desktop", this.videoContainer);
398
-
399
-        this.width = 0;
400
-        this.height = 0;
401
-
402
-        this.$container = $('#largeVideoContainer');
403
-
404
-        this.$container.css({
405
-            display: 'inline-block'
406
-        });
407
-
408
-        if (interfaceConfig.SHOW_JITSI_WATERMARK) {
409
-            let leftWatermarkDiv
410
-                = this.$container.find("div.watermark.leftwatermark");
411
-
412
-            leftWatermarkDiv.css({display: 'block'});
413
-
414
-            leftWatermarkDiv.parent().attr(
415
-                'href', interfaceConfig.JITSI_WATERMARK_LINK);
416
-        }
417
-
418
-        if (interfaceConfig.SHOW_BRAND_WATERMARK) {
419
-            let rightWatermarkDiv
420
-                = this.$container.find("div.watermark.rightwatermark");
421
-
422
-            rightWatermarkDiv.css({
423
-                display: 'block',
424
-                backgroundImage: 'url(images/rightwatermark.png)'
425
-            });
426
-
427
-            rightWatermarkDiv.parent().attr(
428
-                'href', interfaceConfig.BRAND_WATERMARK_LINK);
429
-        }
430
-
431
-        if (interfaceConfig.SHOW_POWERED_BY) {
432
-            this.$container.children("a.poweredby").css({display: 'block'});
433
-        }
434
-
435
-        this.$container.hover(
436
-            e => this.onHoverIn(e),
437
-            e => this.onHoverOut(e)
438
-        );
439
-    }
440
-
441
-    onHoverIn (e) {
442
-        if (!this.state) {
443
-            return;
444
-        }
445
-        let container = this.getContainer(this.state);
446
-        container.onHoverIn(e);
447
-    }
448
-
449
-    onHoverOut (e) {
450
-        if (!this.state) {
451
-            return;
452
-        }
453
-        let container = this.getContainer(this.state);
454
-        container.onHoverOut(e);
455
-    }
456
-
457
-    get id () {
458
-        let container = this.getContainer(this.state);
459
-        return container.id;
460
-    }
461
-
462
-    scheduleLargeVideoUpdate () {
463
-        if (this.updateInProcess || !this.newStreamData) {
464
-            return;
465
-        }
466
-
467
-        this.updateInProcess = true;
468
-
469
-        let container = this.getContainer(this.state);
470
-
471
-        // Include hide()/fadeOut only if we're switching between users
472
-        let preUpdate;
473
-        if (this.newStreamData.id != this.id) {
474
-            preUpdate = container.hide();
475
-        } else {
476
-            preUpdate = Promise.resolve();
477
-        }
478
-
479
-        preUpdate.then(() => {
480
-            let {id, stream, videoType, resolve} = this.newStreamData;
481
-            this.newStreamData = null;
482
-
483
-            console.info("hover in %s", id);
484
-            this.state = videoType;
485
-            let container = this.getContainer(this.state);
486
-            container.setStream(stream, videoType);
487
-
488
-            // change the avatar url on large
489
-            this.updateAvatar(Avatar.getAvatarUrl(id));
490
-
491
-            // If we the continer is VIDEO_CONTAINER_TYPE, we need to check
492
-            // its stream whether exist and is muted to set isVideoMuted
493
-            // in rest of the cases it is false
494
-            let isVideoMuted = false;
495
-            if (videoType == VIDEO_CONTAINER_TYPE)
496
-                isVideoMuted = stream ? stream.isMuted() : true;
497
-
498
-            // show the avatar on large if needed
499
-            container.showAvatar(isVideoMuted);
500
-
501
-            let promise;
502
-
503
-            // do not show stream if video is muted
504
-            // but we still should show watermark
505
-            if (isVideoMuted) {
506
-                this.showWatermark(true);
507
-                promise = Promise.resolve();
508
-            } else {
509
-                promise = container.show();
510
-            }
511
-
512
-            // resolve updateLargeVideo promise after everything is done
513
-            promise.then(resolve);
514
-
515
-            return promise;
516
-        }).then(() => {
517
-            // after everything is done check again if there are any pending
518
-            // new streams.
519
-            this.updateInProcess = false;
520
-            this.scheduleLargeVideoUpdate();
521
-        });
522
-    }
523
-
524
-    /**
525
-     * Update large video.
526
-     * Switches to large video even if previously other container was visible.
527
-     * @param userID the userID of the participant associated with the stream
528
-     * @param {JitsiTrack?} stream new stream
529
-     * @param {string?} videoType new video type
530
-     * @returns {Promise}
531
-     */
532
-    updateLargeVideo (userID, stream, videoType) {
533
-        if (this.newStreamData) {
534
-            this.newStreamData.reject();
535
-        }
536
-
537
-        this.newStreamData = createDeferred();
538
-        this.newStreamData.id = userID;
539
-        this.newStreamData.stream = stream;
540
-        this.newStreamData.videoType = videoType;
541
-
542
-        this.scheduleLargeVideoUpdate();
543
-
544
-        return this.newStreamData.promise;
545
-    }
546
-
547
-    /**
548
-     * Update container size.
549
-     */
550
-    updateContainerSize () {
551
-        this.width = UIUtil.getAvailableVideoWidth();
552
-        this.height = window.innerHeight;
553
-    }
554
-
555
-    /**
556
-     * Resize Large container of specified type.
557
-     * @param {string} type type of container which should be resized.
558
-     * @param {boolean} [animate=false] if resize process should be animated.
559
-     */
560
-    resizeContainer (type, animate = false) {
561
-        let container = this.getContainer(type);
562
-        container.resize(this.width, this.height, animate);
563
-    }
564
-
565
-    /**
566
-     * Resize all Large containers.
567
-     * @param {boolean} animate if resize process should be animated.
568
-     */
569
-    resize (animate) {
570
-        // resize all containers
571
-        Object.keys(this.containers)
572
-            .forEach(type => this.resizeContainer(type, animate));
573
-
574
-        this.$container.animate({
575
-            width: this.width,
576
-            height: this.height
577
-        }, {
578
-            queue: false,
579
-            duration: animate ? 500 : 0
580
-        });
581
-    }
582
-
583
-    /**
584
-     * Enables/disables the filter indicating a video problem to the user.
585
-     *
586
-     * @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
587
-     */
588
-    enableVideoProblemFilter (enable) {
589
-        let container = this.getContainer(this.state);
590
-        container.$video.toggleClass("videoProblemFilter", enable);
591
-    }
592
-
593
-    /**
594
-     * Updates the src of the dominant speaker avatar
595
-     */
596
-    updateAvatar (avatarUrl) {
597
-        $("#dominantSpeakerAvatar").attr('src', avatarUrl);
598
-    }
599
-
600
-    /**
601
-     * Show or hide watermark.
602
-     * @param {boolean} show
603
-     */
604
-    showWatermark (show) {
605
-        $('.watermark').css('visibility', show ? 'visible' : 'hidden');
606
-    }
607
-
608
-    /**
609
-     * Add container of specified type.
610
-     * @param {string} type container type
611
-     * @param {LargeContainer} container container to add.
612
-     */
613
-    addContainer (type, container) {
614
-        if (this.containers[type]) {
615
-            throw new Error(`container of type ${type} already exist`);
616
-        }
617
-
618
-        this.containers[type] = container;
619
-        this.resizeContainer(type);
620
-    }
621
-
622
-    /**
623
-     * Get Large container of specified type.
624
-     * @param {string} type container type.
625
-     * @returns {LargeContainer}
626
-     */
627
-    getContainer (type) {
628
-        let container = this.containers[type];
629
-
630
-        if (!container) {
631
-            throw new Error(`container of type ${type} doesn't exist`);
632
-        }
633
-
634
-        return container;
635
-    }
636
-
637
-    /**
638
-     * Remove Large container of specified type.
639
-     * @param {string} type container type.
640
-     */
641
-    removeContainer (type) {
642
-        if (!this.containers[type]) {
643
-            throw new Error(`container of type ${type} doesn't exist`);
644
-        }
645
-
646
-        delete this.containers[type];
647
-    }
648
-
649
-    /**
650
-     * Show Large container of specified type.
651
-     * Does nothing if such container is already visible.
652
-     * @param {string} type container type.
653
-     * @returns {Promise}
654
-     */
655
-    showContainer (type) {
656
-        if (this.state === type) {
657
-            return Promise.resolve();
658
-        }
659
-
660
-        let oldContainer = this.containers[this.state];
661
-        if (this.state === VIDEO_CONTAINER_TYPE) {
662
-            this.showWatermark(false);
663
-        }
664
-        oldContainer.hide();
665
-
666
-        this.state = type;
667
-        let container = this.getContainer(type);
668
-
669
-        return container.show().then(() => {
670
-            if (type === VIDEO_CONTAINER_TYPE) {
671
-                this.showWatermark(true);
672
-            }
673
-        });
674
-    }
675
-
676
-    /**
677
-     * Changes the flipX state of the local video.
678
-     * @param val {boolean} true if flipped.
679
-     */
680
-    onLocalFlipXChange(val) {
681
-        this.videoContainer.setLocalFlipX(val);
682
-    }
683
-}

+ 102
- 49
modules/UI/videolayout/VideoLayout.js Näytä tiedosto

@@ -1,14 +1,14 @@
1 1
 /* global config, APP, $, interfaceConfig, JitsiMeetJS */
2 2
 /* jshint -W101 */
3 3
 
4
-import AudioLevels from "../audio_levels/AudioLevels";
5 4
 import Avatar from "../avatar/Avatar";
6 5
 import FilmStrip from "./FilmStrip";
7 6
 import UIEvents from "../../../service/UI/UIEvents";
8 7
 import UIUtil from "../util/UIUtil";
9 8
 
10 9
 import RemoteVideo from "./RemoteVideo";
11
-import LargeVideoManager, {VIDEO_CONTAINER_TYPE} from "./LargeVideo";
10
+import LargeVideoManager  from "./LargeVideoManager";
11
+import {VIDEO_CONTAINER_TYPE} from "./VideoContainer";
12 12
 import {SHARED_VIDEO_CONTAINER_TYPE} from '../shared_video/SharedVideo';
13 13
 import LocalVideo from "./LocalVideo";
14 14
 
@@ -102,32 +102,37 @@ var VideoLayout = {
102 102
             });
103 103
         localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
104 104
         // sets default video type of local video
105
+        // FIXME container type is totally different thing from the video type
105 106
         localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
106 107
         // if we do not resize the thumbs here, if there is no video device
107 108
         // the local video thumb maybe one pixel
108
-        let {thumbWidth, thumbHeight} = this.resizeThumbnails(false, true);
109
-        AudioLevels.updateAudioLevelCanvas(null, thumbWidth, thumbHeight);
109
+        let { localVideo } = this.resizeThumbnails(false, true);
110 110
 
111 111
         emitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
112 112
         this.lastNCount = config.channelLastN;
113 113
     },
114 114
 
115 115
     initLargeVideo () {
116
-        largeVideo = new LargeVideoManager();
116
+        largeVideo = new LargeVideoManager(eventEmitter);
117 117
         if(localFlipX) {
118 118
             largeVideo.onLocalFlipXChange(localFlipX);
119 119
         }
120 120
         largeVideo.updateContainerSize();
121
-        AudioLevels.init();
122 121
     },
123 122
 
123
+    /**
124
+     * Sets the audio level of the video elements associated to the given id.
125
+     *
126
+     * @param id the video identifier in the form it comes from the library
127
+     * @param lvl the new audio level to update to
128
+     */
124 129
     setAudioLevel(id, lvl) {
125
-        if (!largeVideo) {
126
-            return;
127
-        }
128
-        AudioLevels.updateAudioLevel(
129
-            id, lvl, largeVideo.id
130
-        );
130
+        let smallVideo = this.getSmallVideo(id);
131
+        if (smallVideo)
132
+            smallVideo.updateAudioLevelIndicator(lvl);
133
+
134
+        if (largeVideo && id === largeVideo.id)
135
+            largeVideo.updateLargeVideoAudioLevel(lvl);
131 136
     },
132 137
 
133 138
     isInLastN (resource) {
@@ -254,7 +259,8 @@ var VideoLayout = {
254 259
     electLastVisibleVideo () {
255 260
         // pick the last visible video in the row
256 261
         // if nobody else is left, this picks the local video
257
-        let thumbs = FilmStrip.getThumbs(true).filter('[id!="mixedstream"]');
262
+        let remoteThumbs = FilmStrip.getThumbs(true).remoteThumbs;
263
+        let thumbs = remoteThumbs.filter('[id!="mixedstream"]');
258 264
 
259 265
         let lastVisible = thumbs.filter(':visible:last');
260 266
         if (lastVisible.length) {
@@ -268,7 +274,7 @@ var VideoLayout = {
268 274
         }
269 275
 
270 276
         console.info("Last visible video no longer exists");
271
-        thumbs = FilmStrip.getThumbs();
277
+        thumbs = FilmStrip.getThumbs().remoteThumbs;
272 278
         if (thumbs.length) {
273 279
             let id = getPeerContainerResourceId(thumbs[0]);
274 280
             if (remoteVideos[id]) {
@@ -378,34 +384,49 @@ var VideoLayout = {
378 384
     },
379 385
 
380 386
     /**
381
-     * Creates a participant container for the given id and smallVideo.
387
+     * Creates or adds a participant container for the given id and smallVideo.
382 388
      *
383
-     * @param id the id of the participant to add
389
+     * @param {JitsiParticipant} user the participant to add
384 390
      * @param {SmallVideo} smallVideo optional small video instance to add as a
385
-     * remote video, if undefined RemoteVideo will be created
391
+     * remote video, if undefined <tt>RemoteVideo</tt> will be created
386 392
      */
387
-    addParticipantContainer (id, smallVideo) {
393
+    addParticipantContainer (user, smallVideo) {
394
+        let id = user.getId();
388 395
         let remoteVideo;
389 396
         if(smallVideo)
390 397
             remoteVideo = smallVideo;
391 398
         else
392
-            remoteVideo = new RemoteVideo(id, VideoLayout, eventEmitter);
399
+            remoteVideo = new RemoteVideo(user, VideoLayout, eventEmitter);
400
+        this.addRemoteVideoContainer(id, remoteVideo);
401
+    },
402
+
403
+    /**
404
+     * Adds remote video container for the given id and <tt>SmallVideo</tt>.
405
+     *
406
+     * @param {string} the id of the video to add
407
+     * @param {SmallVideo} smallVideo the small video instance to add as a
408
+     * remote video
409
+     */
410
+    addRemoteVideoContainer (id, remoteVideo) {
393 411
         remoteVideos[id] = remoteVideo;
394 412
 
395 413
         let videoType = VideoLayout.getRemoteVideoType(id);
396 414
         if (!videoType) {
397 415
             // make video type the default one (camera)
416
+            // FIXME container type is not a video type
398 417
             videoType = VIDEO_CONTAINER_TYPE;
399 418
         }
400 419
         remoteVideo.setVideoType(videoType);
401 420
 
402 421
         // In case this is not currently in the last n we don't show it.
403 422
         if (localLastNCount && localLastNCount > 0 &&
404
-            FilmStrip.getThumbs().length >= localLastNCount + 2) {
423
+            FilmStrip.getThumbs().remoteThumbs.length >= localLastNCount + 2) {
405 424
             remoteVideo.showPeerContainer('hide');
406 425
         } else {
407 426
             VideoLayout.resizeThumbnails(false, true);
408 427
         }
428
+        // Initialize the view
429
+        remoteVideo.updateView();
409 430
     },
410 431
 
411 432
     videoactive (videoelem, resourceJid) {
@@ -448,9 +469,9 @@ var VideoLayout = {
448 469
     showModeratorIndicator () {
449 470
         let isModerator = APP.conference.isModerator;
450 471
         if (isModerator) {
451
-            localVideoThumbnail.createModeratorIndicatorElement();
472
+            localVideoThumbnail.addModeratorIndicator();
452 473
         } else {
453
-            localVideoThumbnail.removeModeratorIndicatorElement();
474
+            localVideoThumbnail.removeModeratorIndicator();
454 475
         }
455 476
 
456 477
         APP.conference.listMembers().forEach(function (member) {
@@ -460,9 +481,10 @@ var VideoLayout = {
460 481
                 return;
461 482
 
462 483
             if (member.isModerator()) {
463
-                remoteVideo.removeRemoteVideoMenu();
464
-                remoteVideo.createModeratorIndicatorElement();
465
-            } else if (isModerator) {
484
+                remoteVideo.addModeratorIndicator();
485
+            }
486
+
487
+            if (isModerator) {
466 488
                 // We are moderator, but user is not - add menu
467 489
                 if(!remoteVideo.hasRemoteVideoMenu) {
468 490
                     remoteVideo.addRemoteVideoMenu();
@@ -479,6 +501,18 @@ var VideoLayout = {
479 501
         localVideoThumbnail.showAudioIndicator(isMuted);
480 502
     },
481 503
 
504
+    /**
505
+     * Shows/hides the indication about local connection being interrupted.
506
+     *
507
+     * @param {boolean} isInterrupted <tt>true</tt> if local connection is
508
+     * currently in the interrupted state or <tt>false</tt> if the connection
509
+     * is fine.
510
+     */
511
+    showLocalConnectionInterrupted (isInterrupted) {
512
+        localVideoThumbnail.connectionIndicator
513
+            .updateConnectionStatusIndicator(!isInterrupted);
514
+    },
515
+
482 516
     /**
483 517
      * Resizes thumbnails.
484 518
      */
@@ -486,19 +520,18 @@ var VideoLayout = {
486 520
                         forceUpdate = false,
487 521
                         onComplete = null) {
488 522
 
489
-        let {thumbWidth, thumbHeight}
523
+        let { localVideo, remoteVideo }
490 524
             = FilmStrip.calculateThumbnailSize();
491 525
 
492
-        $('.userAvatar').css('left', (thumbWidth - thumbHeight) / 2);
526
+        let {thumbWidth, thumbHeight} = remoteVideo;
493 527
 
494
-        FilmStrip.resizeThumbnails(thumbWidth, thumbHeight,
528
+        FilmStrip.resizeThumbnails(localVideo, remoteVideo,
495 529
             animate, forceUpdate)
496 530
             .then(function () {
497
-                AudioLevels.updateCanvasSize(thumbWidth, thumbHeight);
498 531
                 if (onComplete && typeof onComplete === "function")
499 532
                     onComplete();
500
-        });
501
-        return {thumbWidth, thumbHeight};
533
+            });
534
+        return { localVideo, remoteVideo };
502 535
     },
503 536
 
504 537
     /**
@@ -524,11 +557,11 @@ var VideoLayout = {
524 557
      */
525 558
     onVideoMute (id, value) {
526 559
         if (APP.conference.isLocalId(id)) {
527
-            localVideoThumbnail.setMutedView(value);
560
+            localVideoThumbnail.setVideoMutedView(value);
528 561
         } else {
529 562
             let remoteVideo = remoteVideos[id];
530 563
             if (remoteVideo)
531
-                remoteVideo.setMutedView(value);
564
+                remoteVideo.setVideoMutedView(value);
532 565
         }
533 566
 
534 567
         if (this.isCurrentlyOnLarge(id)) {
@@ -610,6 +643,35 @@ var VideoLayout = {
610 643
         }
611 644
     },
612 645
 
646
+    /**
647
+     * Shows/hides warning about remote user's connectivity issues.
648
+     *
649
+     * @param {string} id the ID of the remote participant(MUC nickname)
650
+     * @param {boolean} isActive true if the connection is ok or false when
651
+     * the user is having connectivity issues.
652
+     */
653
+    onParticipantConnectionStatusChanged (id, isActive) {
654
+        // Show/hide warning on the large video
655
+        if (this.isCurrentlyOnLarge(id)) {
656
+            if (largeVideo) {
657
+                // We have to trigger full large video update to transition from
658
+                // avatar to video on connectivity restored.
659
+                this.updateLargeVideo(id, true /* force update */);
660
+            }
661
+        }
662
+        // Show/hide warning on the thumbnail
663
+        let remoteVideo = remoteVideos[id];
664
+        if (remoteVideo) {
665
+            // Updating only connection status indicator is not enough, because
666
+            // when we the connection is restored while the avatar was displayed
667
+            // (due to 'muted while disconnected' condition) we may want to show
668
+            // the video stream again and in order to do that the display mode
669
+            // must be updated.
670
+            //remoteVideo.updateConnectionStatusIndicator(isActive);
671
+            remoteVideo.updateView();
672
+        }
673
+    },
674
+
613 675
     /**
614 676
      * On last N change event.
615 677
      *
@@ -656,7 +718,7 @@ var VideoLayout = {
656 718
         var updateLargeVideo = false;
657 719
 
658 720
         // Handle LastN/local LastN changes.
659
-        FilmStrip.getThumbs().each(( index, element ) => {
721
+        FilmStrip.getThumbs().remoteThumbs.each(( index, element ) => {
660 722
             var resourceJid = getPeerContainerResourceId(element);
661 723
             var smallVideo = remoteVideos[resourceJid];
662 724
 
@@ -945,28 +1007,18 @@ var VideoLayout = {
945 1007
      * Indicates that the video has been interrupted.
946 1008
      */
947 1009
     onVideoInterrupted () {
948
-        this.enableVideoProblemFilter(true);
949
-        let reconnectingKey = "connection.RECONNECTING";
950
-        $('#videoConnectionMessage')
951
-            .attr("data-i18n", reconnectingKey)
952
-            .text(APP.translation.translateString(reconnectingKey))
953
-            .css({display: "block"});
1010
+        if (largeVideo) {
1011
+            largeVideo.onVideoInterrupted();
1012
+        }
954 1013
     },
955 1014
 
956 1015
     /**
957 1016
      * Indicates that the video has been restored.
958 1017
      */
959 1018
     onVideoRestored () {
960
-        this.enableVideoProblemFilter(false);
961
-        $('#videoConnectionMessage').css({display: "none"});
962
-    },
963
-
964
-    enableVideoProblemFilter (enable) {
965
-        if (!largeVideo) {
966
-            return;
1019
+        if (largeVideo) {
1020
+            largeVideo.onVideoRestored();
967 1021
         }
968
-
969
-        largeVideo.enableVideoProblemFilter(enable);
970 1022
     },
971 1023
 
972 1024
     isLargeVideoVisible () {
@@ -994,6 +1046,7 @@ var VideoLayout = {
994 1046
 
995 1047
         if (!isOnLarge || forceUpdate) {
996 1048
             let videoType = this.getRemoteVideoType(id);
1049
+            // FIXME video type is not the same thing as container type
997 1050
             if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
998 1051
                 eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id);
999 1052
             }

+ 7
- 5
modules/UI/welcome_page/WelcomePage.js Näytä tiedosto

@@ -2,6 +2,7 @@
2 2
 var animateTimeout, updateTimeout;
3 3
 
4 4
 var RoomnameGenerator = require("../../util/RoomnameGenerator");
5
+import UIUtil from "../util/UIUtil";
5 6
 
6 7
 function enter_room() {
7 8
     var val = $("#enter_room_field").val();
@@ -39,10 +40,10 @@ function setupWelcomePage() {
39 40
             $("#welcome_page_header div[class='watermark leftwatermark']");
40 41
         if(leftWatermarkDiv && leftWatermarkDiv.length > 0) {
41 42
             leftWatermarkDiv.css({display: 'block'});
42
-            leftWatermarkDiv.parent().get(0).href =
43
-                interfaceConfig.JITSI_WATERMARK_LINK;
43
+            UIUtil.setLinkHref(
44
+                leftWatermarkDiv.parent(),
45
+                interfaceConfig.JITSI_WATERMARK_LINK);
44 46
         }
45
-
46 47
     }
47 48
 
48 49
     if (interfaceConfig.SHOW_BRAND_WATERMARK) {
@@ -50,8 +51,9 @@ function setupWelcomePage() {
50 51
             $("#welcome_page_header div[class='watermark rightwatermark']");
51 52
         if(rightWatermarkDiv && rightWatermarkDiv.length > 0) {
52 53
             rightWatermarkDiv.css({display: 'block'});
53
-            rightWatermarkDiv.parent().get(0).href =
54
-                interfaceConfig.BRAND_WATERMARK_LINK;
54
+            UIUtil.setLinkHref(
55
+                rightWatermarkDiv.parent(),
56
+                interfaceConfig.BRAND_WATERMARK_LINK);
55 57
             rightWatermarkDiv.get(0).style.backgroundImage =
56 58
                 "url(images/rightwatermark.png)";
57 59
         }

+ 1
- 8
modules/keyboardshortcut/keyboardshortcut.js Näytä tiedosto

@@ -74,13 +74,6 @@ var KeyboardShortcut = {
74 74
                 }
75 75
             }
76 76
         };
77
-        $('body').popover({ selector: '[data-toggle=popover]',
78
-            trigger: 'click hover',
79
-            content: function() {
80
-                return this.getAttribute("content")
81
-                    + self._getShortcutTooltip(this.getAttribute("shortcut"));
82
-            }
83
-        });
84 77
     },
85 78
 
86 79
     /**
@@ -128,7 +121,7 @@ var KeyboardShortcut = {
128 121
      * or an empty string if the shortcutAttr is null, an empty string or not
129 122
      * found in the shortcut mapping
130 123
      */
131
-    _getShortcutTooltip: function (shortcutAttr) {
124
+    getShortcutTooltip: function (shortcutAttr) {
132 125
         if (typeof shortcutAttr === "string" && shortcutAttr.length > 0) {
133 126
             for (var key in _shortcuts) {
134 127
                 if (_shortcuts.hasOwnProperty(key)

+ 8
- 4
modules/settings/Settings.js Näytä tiedosto

@@ -174,10 +174,12 @@ export default {
174 174
      * Set device id of the camera which is currently in use.
175 175
      * Empty string stands for default device.
176 176
      * @param {string} newId new camera device id
177
+     * @param {boolean} whether we need to store the value
177 178
      */
178
-    setCameraDeviceId: function (newId = '') {
179
+    setCameraDeviceId: function (newId, store) {
179 180
         cameraDeviceId = newId;
180
-        window.localStorage.cameraDeviceId = newId;
181
+        if (store)
182
+            window.localStorage.cameraDeviceId = newId;
181 183
     },
182 184
 
183 185
     /**
@@ -192,10 +194,12 @@ export default {
192 194
      * Set device id of the microphone which is currently in use.
193 195
      * Empty string stands for default device.
194 196
      * @param {string} newId new microphone device id
197
+     * @param {boolean} whether we need to store the value
195 198
      */
196
-    setMicDeviceId: function (newId = '') {
199
+    setMicDeviceId: function (newId, store) {
197 200
         micDeviceId = newId;
198
-        window.localStorage.micDeviceId = newId;
201
+        if (store)
202
+            window.localStorage.micDeviceId = newId;
199 203
     },
200 204
 
201 205
     /**

+ 14
- 6
package.json Näytä tiedosto

@@ -16,23 +16,24 @@
16 16
   "readmeFilename": "README.md",
17 17
   "//": "Callstats.io does not work with recent versions of jsSHA (2.0.1 in particular)",
18 18
   "dependencies": {
19
+    "@atlassian/aui": "^6.0.0",
19 20
     "async": "0.9.0",
20 21
     "autosize": "^1.18.13",
21 22
     "bootstrap": "3.1.1",
22 23
     "events": "*",
23 24
     "i18next-client": "1.7.7",
24
-    "jquery": "~2.1.1",
25 25
     "jQuery-Impromptu": "git+https://github.com/trentrichardson/jQuery-Impromptu.git#v6.0.0",
26
-    "lib-jitsi-meet": "git+https://github.com/jitsi/lib-jitsi-meet.git",
26
+    "jquery": "~2.1.1",
27 27
     "jquery-contextmenu": "*",
28 28
     "jquery-ui": "1.10.5",
29 29
     "jssha": "1.5.0",
30
+    "jws": "*",
31
+    "lib-jitsi-meet": "git+https://github.com/jitsi/lib-jitsi-meet.git",
32
+    "postis": "^2.2.0",
30 33
     "retry": "0.6.1",
31 34
     "strophe": "^1.2.2",
32 35
     "strophejs-plugins": "^0.0.6",
33
-    "toastr": "^2.0.3",
34
-    "postis": "^2.2.0",
35
-    "jws": "*"
36
+    "toastr": "^2.0.3"
36 37
   },
37 38
   "devDependencies": {
38 39
     "babel-polyfill": "*",
@@ -83,7 +84,11 @@
83 84
     "tooltip": "./node_modules/bootstrap/js/tooltip.js",
84 85
     "popover": "./node_modules/bootstrap/js/popover.js",
85 86
     "jQuery-Impromptu": "./node_modules/jQuery-Impromptu/dist/jquery-impromptu.js",
86
-    "autosize": "./node_modules/autosize/build/jquery.autosize.js"
87
+    "autosize": "./node_modules/autosize/build/jquery.autosize.js",
88
+    "aui": "./node_modules/@atlassian/aui/dist/aui/js/aui.js",
89
+    "aui-experimental": "./node_modules/@atlassian/aui/dist/aui/js/aui-experimental.js",
90
+    "aui-css": "./node_modules/@atlassian/aui/dist/aui/css/aui.min.css",
91
+    "aui-experimental-css": "./node_modules/@atlassian/aui/dist/aui/css/aui-experimental.min.css"
87 92
   },
88 93
   "browserify-shim": {
89 94
     "jquery": [
@@ -109,6 +114,9 @@
109 114
     "jQuery-Impromptu": {
110 115
       "depends": "jquery:jQuery"
111 116
     },
117
+    "aui-experimental": {
118
+      "depends": "aui:AJS"
119
+    },
112 120
     "jquery-contextmenu": {
113 121
       "depends": "jquery:jQuery"
114 122
     },

+ 1
- 1
prosody-plugins/mod_token_verification.lua Näytä tiedosto

@@ -60,7 +60,7 @@ local function verify_user(session, stanza)
60 60
 
61 61
 	local token = session.auth_token;
62 62
 	local auth_room = session.jitsi_meet_room;
63
-	if room ~= auth_room and disableRoomNameConstraints ~= true then
63
+	if disableRoomNameConstraints ~= true and room ~= string.lower(auth_room) then
64 64
 		log("error", "Token %s not allowed to join: %s",
65 65
 			tostring(token), tostring(auth_room));
66 66
 		session.send(

+ 11
- 2
service/UI/UIEvents.js Näytä tiedosto

@@ -29,7 +29,6 @@ export default {
29 29
      */
30 30
     UPDATE_SHARED_VIDEO: "UI.update_shared_video",
31 31
     ROOM_LOCK_CLICKED: "UI.room_lock_clicked",
32
-    USER_INVITED: "UI.user_invited",
33 32
     USER_KICKED: "UI.user_kicked",
34 33
     REMOTE_AUDIO_MUTED: "UI.remote_audio_muted",
35 34
     FULLSCREEN_TOGGLE: "UI.fullscreen_toggle",
@@ -105,5 +104,15 @@ export default {
105 104
      * event must contain the identifier of the container that has been toggled
106 105
      * and information about toggle on or off.
107 106
      */
108
-    SIDE_TOOLBAR_CONTAINER_TOGGLED: "UI.side_container_toggled"
107
+    SIDE_TOOLBAR_CONTAINER_TOGGLED: "UI.side_container_toggled",
108
+
109
+    /**
110
+     * Notifies that the raise hand has been changed.
111
+     */
112
+    LOCAL_RAISE_HAND_CHANGED: "UI.local_raise_hand_changed",
113
+
114
+    /**
115
+     * Notifies that the avatar is displayed or not on the largeVideo.
116
+     */
117
+    LARGE_VIDEO_AVATAR_DISPLAYED: "UI.large_video_avatar_displayed"
109 118
 };

+ 6
- 3
service/translation/languages.js Näytä tiedosto

@@ -9,16 +9,19 @@ module.exports = {
9 9
         return languages;
10 10
     },
11 11
     EN: "en",
12
+
12 13
     BG: "bg",
13 14
     DE: "de",
14
-    TR: "tr",
15
-    FR: "fr",
16 15
     ES: "es",
16
+    FR: "fr",
17 17
     HY: "hy",
18 18
     IT: "it",
19 19
     OC: "oc",
20
+    PL: "pl",
20 21
     PTBR: "ptBR",
22
+    RU: "ru",
21 23
     SK: "sk",
22 24
     SL: "sl",
23
-    SV: "sv"
25
+    SV: "sv",
26
+    TR: "tr"
24 27
 };

Loading…
Peruuta
Tallenna