Compare commits

...

231 Commits
1237 ... 1330

Author SHA1 Message Date
hristoterezov
480f0c703a Merge pull request #976 from jitsi/eslint
ESLint
2016-10-03 12:53:57 -05:00
Lyubomir Marinov
a2b076985a ESLint
Enable ESLint on jitsi-meet with the same configuration and the same
goals as in lib-jitsi-meet.
2016-10-03 11:12:27 -05:00
yanas
98bc16801c Merge pull request #967 from jitsi/analytics_feedback
feat(analytics): Implement sendFeedback method
2016-10-01 12:25:19 -05:00
hristoterezov
71790b07b7 feat(analytics): Implement sendFeedback method 2016-09-30 17:28:32 -05:00
yanas
4ffe013165 Merge pull request #965 from jitsi/raise-hand-update
Updates raised hand to overwrite dominant speaker.
2016-09-30 12:14:58 -05:00
damencho
6320ef1caa Updates raised hand to overwrite dominant speaker. 2016-09-30 11:47:43 -05:00
hristoterezov
d10158c9fb Merge pull request #964 from jitsi/webpack
Fix babel-preset-es2015's version
2016-09-30 11:20:26 -05:00
Lyubomir Marinov
92f1061db8 Fix babel-preset-es2015's version
The latest version of babel-preset-es2015 6.16.0 fails with 'Unsupported
preset format: undefined.' Use the previous one then.
2016-09-30 11:16:51 -05:00
Lyubomir Marinov
c8f18040f6 Merge branch 'master' into webpack 2016-09-30 11:16:22 -05:00
hristoterezov
17b57ea852 Merge pull request #957 from jitsi/webpack
Switch from Browserify to Webpack
2016-09-30 10:32:36 -05:00
Lyubomir Marinov
b2a70b263a Merge branch 'talk-muted' 2016-09-30 09:39:26 -05:00
Дамян Минков
b3f0620f5b Merge pull request #963 from m-voloshin/destroy-tooltips
Completely remove attributes to destroy tooltip properly
2016-09-30 09:28:26 -05:00
Lyubomir Marinov
c95a8e058c Merge branch 'master' into talk-muted 2016-09-30 08:57:28 -05:00
Lyubomir Marinov
ecf44498b8 Merge branch 'master' into webpack 2016-09-30 08:52:36 -05:00
Maxim Voloshin
2f92aa9645 Completely remove attributes to destroy tooltip properly 2016-09-30 14:06:57 +03:00
yanas
e894b0db43 Merge pull request #962 from jitsi/remove-tooltip
Remove tooltip update
2016-09-29 17:14:09 -05:00
damencho
da65bbaa2d Updates remove tooltip util method.
Destroy is just hiding current tooltip, we also need to remove other attributes to stop showing the tooltip.
2016-09-29 16:47:32 -05:00
ibauersachs
032509be15 Commit from translate.jitsi.org by user ibauersachs.: 265 of 265 strings translated (0 fuzzy). 2016-09-29 17:00:31 +00:00
Hristo Terezov
c6f81668de Remove an unnecessary exclude from Webpack 2016-09-29 11:31:28 -05:00
Lyubomir Marinov
43d0582b2f Don't use path.resolve 2016-09-29 11:31:28 -05:00
Lyubomir Marinov
818ddad2c3 Do not distribute unminimized artifacts
The build process is capable of bundling both minimized and unminimized
artifacts for lib-jitsi-meet, jitsi-meet and external_api. However,
there does not seem to be a good reason to (1) always wait for the
building of the two versions and (2) distributing the unminimized
artifacts.
2016-09-29 11:31:28 -05:00
Lyubomir Marinov
c8f79dbd2d Switch from Browserify to Webpack 2016-09-29 11:31:28 -05:00
Lyubomir Marinov
1ceb3f0129 Switch lib-jitsi-meet from Browserify to Webpack
Since the library lib-jitsi-meet does not publish its binaries, it is
always been necessary to produce the binaries i.e. lib-jitsi-meet.js and
lib-jitsi-meet.js as part of the npm install step. Which means that any
modifications to the devDependencies of lib-jitsi-meet's package.json
always have to be reflected in jitsi-meet's package.json. Because
Webpack replaced Browserify in lib-jitsi-meet, Webpack has to become a
devDependency of jitsi-meet.
2016-09-29 11:31:28 -05:00
yanas
fe7911b944 Merge pull request #959 from m-voloshin/keyboard-shortcuts-help
Removed duplicates from keyboard shortcuts
2016-09-29 10:42:27 -05:00
yanas
0dcf8add63 Merge pull request #961 from m-voloshin/username-editing
Allow user to edit initial username
2016-09-29 10:34:17 -05:00
Paweł Domas
ae2ea4f421 Merge pull request #954 from jitsi/handle-conference-left
Waits for conference left event before navigating away from the page.
2016-09-29 10:06:57 -05:00
Maxim Voloshin
664d7a4f67 Allow user to edit initial username 2016-09-29 17:40:26 +03:00
Maxim Voloshin
268a2ea7ce Removed duplicates from keyboard shortcuts 2016-09-29 15:31:45 +03:00
yanas
854fef35cb Merge pull request #955 from jitsi/audio-levels-redesign
Audio levels redesign. PR Review done from @damencho and @emcho.
2016-09-29 00:48:29 -05:00
yanas
b58556b6c3 Fixes audio level algorithm. 2016-09-29 00:22:05 -05:00
hristoterezov
97aaa36f7d Merge pull request #942 from jitsi/enable-languages
Match enabled languages to available languages
2016-09-28 19:22:27 -05:00
damencho
07f111abbd Fixes hangup when callstats is disabled. 2016-09-28 17:27:55 -05:00
yanas
3bb877cc3a Audio levels redesign. 2016-09-28 16:41:13 -05:00
Любомир Маринов
17bcc9bfcc Merge pull request #956 from jitsi/revert-903-webpack
Revert "Switch from Browserify to Webpack"
2016-09-28 16:38:42 -05:00
hristoterezov
c7cd771de2 Revert "Switch from Browserify to Webpack" 2016-09-28 16:37:24 -05:00
hristoterezov
77e65f727f Merge pull request #903 from jitsi/webpack
Switch from Browserify to Webpack
2016-09-28 16:37:07 -05:00
damencho
d793cdc797 Waits for conference left event before navigating away from the page. 2016-09-28 16:05:51 -05:00
Дамян Минков
4ec266ef11 Merge pull request #953 from jitsi/log_server
feat(log): Logs server field from jwt token
2016-09-28 15:47:56 -05:00
hristoterezov
fcc9532bde feat(log): Logs server field from jwt token 2016-09-28 15:19:16 -05:00
Paweł Domas
b4a191e27a Merge pull request #946 from jitsi/fix-moderator-notifications
Fixes moderator notifications on moderator indicator disabled
2016-09-28 14:26:00 -05:00
Lyubomir Marinov
973d40a877 Don't use path.resolve 2016-09-28 14:15:02 -05:00
Lyubomir Marinov
ce3090b8fe Do not distribute unminimized artifacts
The build process is capable of bundling both minimized and unminimized
artifacts for lib-jitsi-meet, jitsi-meet and external_api. However,
there does not seem to be a good reason to (1) always wait for the
building of the two versions and (2) distributing the unminimized
artifacts.
2016-09-28 13:57:53 -05:00
Lyubomir Marinov
f6662745d1 Switch from Browserify to Webpack 2016-09-28 13:57:53 -05:00
Lyubomir Marinov
4289df1681 Switch lib-jitsi-meet from Browserify to Webpack
Since the library lib-jitsi-meet does not publish its binaries, it is
always been necessary to produce the binaries i.e. lib-jitsi-meet.js and
lib-jitsi-meet.js as part of the npm install step. Which means that any
modifications to the devDependencies of lib-jitsi-meet's package.json
always have to be reflected in jitsi-meet's package.json. Because
Webpack replaced Browserify in lib-jitsi-meet, Webpack has to become a
devDependency of jitsi-meet.
2016-09-28 13:57:53 -05:00
paweldomas
dad3c57fad fix(conference): react to local role change only when it changes
We initialise the UI for isModerator = false on startup, so we should
not react to the event unless it gets out of sync.
2016-09-28 13:41:02 -05:00
Lyubomir Marinov
8f4b94f732 Merge branch 'm-voloshin-tooltips-global-handler' 2016-09-28 13:10:51 -05:00
yanas
76f8ca2116 Merge pull request #947 from jitsi/not-allowed-error
Not allowed error
2016-09-28 12:40:53 -05:00
Paweł Domas
94e5cda02d Merge pull request #951 from jitsi/prosody-tokens-case-insensitive-room
Prosody tokens case insensitive room name
2016-09-28 12:36:28 -05:00
yanas
066b4f16a0 Merge pull request #949 from m-voloshin/videospace-layout-fix
Removed initial animation of the video thumbnail
2016-09-28 11:50:46 -05:00
damencho
f3381b31ed Updates tokens room name verification to be case insensitive.
Room names used inside prosody are all lower case, when verify with room name from token make sure we use the room name in lower case.
2016-09-28 11:49:50 -05:00
damencho
15f4288e4a Fixes some jshint errors. 2016-09-28 11:29:47 -05:00
Lyubomir Marinov
4569970bc4 Merge branch 'tooltips-global-handler' of https://github.com/m-voloshin/jitsi-meet into m-voloshin-tooltips-global-handler 2016-09-28 11:29:15 -05:00
Maxim Voloshin
b2e0b49556 Updated JSDocs for 'TOOLTIP_POSITIONS' constant 2016-09-28 18:09:09 +03:00
Lyubomir Marinov
03152d65ab Merge branch 'tooltips-global-handler' of https://github.com/m-voloshin/jitsi-meet into m-voloshin-tooltips-global-handler 2016-09-28 09:58:49 -05:00
Maxim Voloshin
96735d47c4 Attached JSDocs for 'TOOLTIP_POSITIONS' constant 2016-09-28 17:52:27 +03:00
Lyubomir Marinov
16266e3622 Merge branch 'tooltips-global-handler' of https://github.com/m-voloshin/jitsi-meet into m-voloshin-tooltips-global-handler 2016-09-28 09:27:58 -05:00
Maxim Voloshin
87ed7b7989 Redesigned "setTooltip" method 2016-09-28 17:22:03 +03:00
Lyubomir Marinov
fef95f7cf1 Merge branch 'tooltips-global-handler' of https://github.com/m-voloshin/jitsi-meet into m-voloshin-tooltips-global-handler 2016-09-28 07:38:32 -05:00
Maxim Voloshin
36d1f7d06f Removed initial animation of the video thumbnail 2016-09-28 15:08:15 +03:00
hristoterezov
2a8700bca3 Merge pull request #938 from jitsi/participant_conn_status
Adds participant connection status notifications
2016-09-27 17:54:22 -05:00
damencho
1e54111aad Adds authentication error page.
When we receive a conference failed event with reason not allowed we show that page.
2016-09-27 17:26:38 -05:00
damencho
57003be3a3 Renames the style from close to redirect page. 2016-09-27 16:49:23 -05:00
yanas
f10177a352 Fixes moderator notifications on moderator indicator disabled 2016-09-27 14:32:54 -05:00
Maxim Voloshin
939b87ffed Tooltiped elements are mardked with "data-tooltip" attribute 2016-09-27 19:49:49 +03:00
Emil Ivov
6ccc58a060 Merge pull request #945 from jitsi/feedback-improvements
Feedback window improvements.
2016-09-26 23:14:19 -05:00
yanas
08b2fbe30f Fixes feedback message and button active state 2016-09-26 22:43:20 -05:00
Emil Ivov
cbd15f45a4 Merge pull request #944 from jitsi/pin-hover-improvements
Pin hover improvements
2016-09-26 22:42:37 -05:00
yanas
37dcc6c994 Improves the pin and hover borders 2016-09-26 22:26:05 -05:00
yanas
6249ff89ff Feedback window improvements. 2016-09-26 21:15:24 -05:00
yanas
07441b092c Merge pull request #943 from m-voloshin/fix-cursor-for-icon
Default cursor for icons
2016-09-26 15:43:51 -05:00
yanas
1c8535a2d5 Improves the pin and hover borders 2016-09-26 15:40:56 -05:00
Maxim Voloshin
047a2369b3 Default cursor for icons 2016-09-26 22:46:39 +03:00
paweldomas
7585413e7d fix(VideoLayout): from avatar to video transition
When user reconnected and the avatar is displayed we need to perform
full large video update in order to transition correctly.
2016-09-26 14:40:00 -05:00
paweldomas
6e0ba1de33 ref(VideoLayout): rename 'videoConnectionMessage'
Renames 'videoConnectionMessage' to 'localConnectionMessage', because
it is displayed when we're having problems with our local connection
and a different one will be shown for the remote connectivity issues.
2016-09-26 14:40:00 -05:00
paweldomas
661ea2cf45 feat(VideoLayout): add remote connection problems UI
Grey filter will be applied to the remote video/avatar displayed on
"large" and a message indicating remote connectivity issues will be
shown on top of that.
2016-09-26 14:39:58 -05:00
ibauersachs
bcee2a207d Commit from translate.jitsi.org by user ibauersachs.: 265 of 265 strings translated (0 fuzzy). 2016-09-26 19:36:34 +00:00
Ingo Bauersachs
e94bf73cc3 Match enabled languages to available languages 2016-09-26 21:34:46 +02:00
Ingo Bauersachs
03e8331e4f Add polish and russian 2016-09-26 21:28:55 +02:00
ibauersachs
bbd5e55e5e Commit from translate.jitsi.org by user ibauersachs.: 263 of 263 strings translated (0 fuzzy). 2016-09-26 19:03:16 +00:00
Maxim Voloshin
212798ad19 Global handler for tooltips 2016-09-26 21:29:40 +03:00
yanas
bb0f6e0989 Remove unused variable. 2016-09-26 13:02:59 -05:00
paweldomas
42fd3097de feat(VideoContainer): add 'wasVideoRendered' flag
The 'wasVideoRendered' flag will tell whether or not we have any video
image rendered(even if stalled) on the large video element.
2016-09-26 13:01:35 -05:00
paweldomas
5952261e87 ref(LargeVideoManager): introduce 'setVideoConnectionMessage' 2016-09-26 13:01:35 -05:00
paweldomas
62d2e3e2a4 feat(conference.js): add 'getParticipantDisplayName' 2016-09-26 13:01:35 -05:00
paweldomas
5843c6c569 ref(LargeVideoManager): rename 'enableVideoProblemFilter' 2016-09-26 13:01:35 -05:00
paweldomas
352e784cad fix(VideoLayout): show video when the connection is back 2016-09-26 13:01:35 -05:00
paweldomas
b8937e0349 fix(LargeVideoManager): hide video when avatar is displayed 2016-09-26 13:01:35 -05:00
paweldomas
0aea799b50 doc(LargeVideoManager): fills missing JS doc 2016-09-26 13:01:35 -05:00
paweldomas
46766ec239 fix(RemoteVideo): avoid black thumbnail
When the user is having connectivity issues we use the image cached in
the video element to show the preview in greyscale. It looks like this
cached image gets invalided after prolonged periods of time the video
element being hidden(and it is hidden when the video is muted). So we
never show this image if the user gets muted during connectivity
disruption in order to avoid blackness.
2016-09-26 13:01:35 -05:00
paweldomas
66bbc4d9fd fix(RemoteVideo): change hasVideoStarted logic
We used to rely on 'currentTime' of the video element, but we execute
'updateView' from the 'onplay' callback and on fast machines it may
happen that the value is 0 even though the video has just started.
2016-09-26 13:01:35 -05:00
paweldomas
40f2c593a2 ref(SmallVideo): figure out what is to be displayed
At any point of time we display one of the three: video, avatar or
blackness. The purpose of this commit is to make that fact more clear
in the code.
2016-09-26 13:01:35 -05:00
paweldomas
30cb948dcf feat(SmallVideo): make thumbnail grey
The video or the avatar on a thumbnail will be displayed in greyscale
when the user is having connectivity issues.
2016-09-26 13:01:35 -05:00
paweldomas
fceb512a03 ref(SmallVideo): add 'isCurrentlyOnLargeVideo' 2016-09-26 13:01:35 -05:00
paweldomas
4722054c3e ref(SmallVideo): adds avatar selector 2016-09-26 13:01:34 -05:00
paweldomas
9d1364b6fb feat(RemoteVideo): show disconnected GSM bars for remotes 2016-09-26 13:01:34 -05:00
paweldomas
5daceaead7 feat(conference.js): add isParticipantConnectionActive 2016-09-26 13:01:34 -05:00
paweldomas
8a43699a89 feat(ConnectionIndicator): show disconnected GSM bars on local thumbnail 2016-09-26 13:01:34 -05:00
paweldomas
3ef5dd20ef ref(RemoteVideo): store JitsiParticipant instead of id 2016-09-26 13:01:34 -05:00
paweldomas
e9445866a5 ref(ConnectionIndicator.js): make CQ 'object' optional 2016-09-26 13:01:34 -05:00
paweldomas
cf931f8a9f ref(ConnectionIndicator.js) pass icon class as an argument 2016-09-26 13:01:34 -05:00
yanas
3140257b69 Remove unused function. 2016-09-26 12:55:18 -05:00
Дамян Минков
b3d6e5876e Merge pull request #922 from jitsi/fix_filter_not_applied
Fix notification about network issues not displayed when expected
2016-09-26 10:04:14 -05:00
Дамян Минков
c437f64f35 Merge pull request #933 from jitsi/fix-everyone-moderator
Fixes moderator related elements when everyone is moderator
2016-09-23 18:04:53 -05:00
yanas
af91fb50b2 Fixes moderator related elements when everyone is moderator 2016-09-23 17:42:29 -05:00
Emil Ivov
a35e194a2d Merge pull request #932 from jitsi/fix_jibri
fix(FilmStrip): Add check for thumbnails
2016-09-23 16:46:49 -05:00
hristoterezov
42d9d0393d fix(FilmStrip): Add check for thumbnails 2016-09-23 16:44:32 -05:00
jitsi-pootle
d319e837f5 New files added from translate.jitsi.org based on templates 2016-09-23 19:49:12 +00:00
yanas
a054a0d61d Merge pull request #919 from kkrisstoff/add/raise-hand-icon
rise-hand-ico: blured class added
2016-09-23 11:59:40 -05:00
yanas
8e75da8540 Merge pull request #926 from jitsi/lock-fixes
Changes the state of room locker if room was locked not by current user.
2016-09-23 11:51:42 -05:00
damencho
975b13f868 Fixes setting remote room lock state change. 2016-09-23 11:34:21 -05:00
yanas
75f80ae877 Update _close.scss 2016-09-23 11:26:48 -05:00
Kostiantyn Pashura
7c824d9da0 rise-hand-ico: blured class added 2016-09-23 15:56:16 +03:00
hristoterezov
b772c151fc Merge pull request #925 from jitsi/invite-to-share-dialog
Invite to share dialog
2016-09-22 21:53:40 -05:00
yanas
d2e42c2a7d Update MessageHandler.js 2016-09-22 21:11:24 -05:00
yanas
b54f92b2ae Fixes the copy action. 2016-09-22 18:17:37 -05:00
yanas
5ef241ae66 Merge pull request #927 from jitsi/fix-feedback-resolve
Makes sure we always resolve(call the callback) in feedback dialog.
2016-09-22 17:47:17 -05:00
damencho
eb2e709749 Makes sure we always resolve(call the callback) in feedback dialog.
Call the callback even when clicking outside the dialog, or escaping to close it.
2016-09-22 16:55:08 -05:00
yanas
06247266ad Merge pull request #924 from jitsi/add-close-page
Adds close page
2016-09-22 16:20:14 -05:00
damencho
f9a5b62326 Changes the state of room locker if room was locked not by current user. 2016-09-22 16:10:17 -05:00
yanas
644faca306 Re-designs invite dialog as share link. 2016-09-22 15:50:09 -05:00
yanas
9cb0723f8d Remove duplicate styles. 2016-09-22 15:32:54 -05:00
yanas
48c99e796f Includes content to close html. 2016-09-22 15:32:54 -05:00
damencho
fcf7069b25 Adds close page.
When enabled after hanging up redirect to close page, if needed feedback will be displayed.
2016-09-22 15:32:54 -05:00
damencho
415619021f Moves handling of thank you dialog.
Moves handling of thank you dialog out of the Promise that handles the feedback and just before we redirect to welcome page.
2016-09-22 15:32:54 -05:00
yanas
88a45cf991 Fixes colors in dialogs. 2016-09-22 14:50:31 -05:00
yanas
7eb85fe7aa Merge pull request #923 from jitsi/fix_ringoverlay_sound
fix(ringoverlay): Path of the ogg file
2016-09-22 14:08:25 -05:00
hristoterezov
0e5fe88b5e fix(ringoverlay): Path of the ogg file 2016-09-22 13:50:10 -05:00
yanas
b754361268 Merge pull request #920 from jitsi/remove_prezi_leftovers
fix: remove Prezi leftovers
2016-09-22 12:45:20 -05:00
paweldomas
419950ca49 fix(VideoLayout): "connection interrupted" shown only on video
The message about having connectivity issues should be displayed only
on top of the video like the "video problems filter" is.
2016-09-22 12:43:23 -05:00
paweldomas
11953cbb60 feat(conference.js): add isConnectionInterrupted getter 2016-09-22 12:43:12 -05:00
paweldomas
ecfc05bcc8 fix(LargeVideoManager): enable video problems filter on VideoContainer
Only the VideoContainer is interested in showing the video problems
filter which is meant to be displayed when ICE is disconnected.
2016-09-22 12:43:03 -05:00
yanas
b0012b4d63 Merge pull request #921 from jitsi/contaclist-title-update
Always show number of participants badge in toolbar.
2016-09-22 12:40:39 -05:00
damencho
febaf49d07 Always show number of participants badge in toolbar.
Includes showing 1 when user is alone in the room.
2016-09-22 12:02:38 -05:00
paweldomas
e3f3287f14 fix: remove Prezi leftovers 2016-09-22 11:35:35 -05:00
Дамян Минков
fbd2879aa3 Merge pull request #918 from kkrisstoff/make-the-star-configurable
disable_star_indicator added
2016-09-22 10:19:16 -05:00
yanas
4db8faa526 Merge pull request #914 from jitsi/fix_toolbar_ringoverlay
fix(toolbar): Issue with toolbar is positioned under the ring overlay
2016-09-22 10:12:38 -05:00
Дамян Минков
586ea2ae0d Merge pull request #911 from jitsi/small_videolayout_refactoring
Small videolayout refactoring
2016-09-22 09:45:15 -05:00
paweldomas
e39648ce21 ref(LargeVideo): rename to LargeVideoManager
It is confusing when the name of the main class exported from the file
is not the same as the filename.
2016-09-22 08:57:14 -05:00
paweldomas
e0a05c5908 ref(LargeVideo): move VideoContainer to separate file
VideoContainer is a separate being which implements the LargeContainer
and it's confusing to have it in the same file. This was encouraging to
access private parts of the VideoContainer directly(not through
the interface).
2016-09-22 08:57:14 -05:00
paweldomas
2c01fde713 ref(SmallVideo): rename 'isMuted' to avoid confusion 2016-09-22 08:57:01 -05:00
Kostiantyn Pashura
82ebfd9945 disable_star_indicator added 2016-09-22 12:52:53 +03:00
yanas
92641c20f3 Merge pull request #916 from jitsi/contaclist-title-update
Moves the number in the code, not as a translation parameter.
2016-09-21 22:15:48 -05:00
yanas
f145d98a12 Merge pull request #915 from jitsi/update-tooltip-from-profile
Update tooltip from profile
2016-09-21 18:15:54 -05:00
yanas
a3d2c95d80 Merge pull request #913 from jitsi/adjust-feedback-question
Adjusts feedback dialog question.
2016-09-21 18:01:31 -05:00
yanas
a5a9936e25 Adjusts feedback text and takes into account comment 2016-09-21 18:00:21 -05:00
damencho
cfeb03740c Moves the number in the code, not as a translation parameter.
There is sometimes problems with cache and late loading of the translations, this commit avoids showing translation key in the contactlist title, by moving the parameter into the code.
2016-09-21 17:16:00 -05:00
damencho
4ab0fca4d7 Removes tooltip from profile avatar when not clickable. 2016-09-21 16:35:42 -05:00
damencho
df3b7e2dbc Adds utility method to remove tooltips. 2016-09-21 16:35:03 -05:00
hristoterezov
302b0cf776 fix(toolbar): Issue with toolbar is positioned under the ring overlay 2016-09-21 16:34:06 -05:00
yanas
e7cbacf9a2 Merge pull request #912 from jitsi/remove-download-logs-link
Remove download logs link
2016-09-21 16:18:18 -05:00
Любомир Маринов
f9817b12bf Merge pull request #910 from jitsi/fix_jitsi_track_error_import
fix(UI): Show the correct message on NO_DATA_FROM_SOURCE GUM errors
2016-09-21 15:57:32 -05:00
damencho
bbc7aedb48 Adds download logs method for debugging.
Adds a download logs method that can be called from console to take usefull messages for debugging.
console> APP.conference.saveLogs();
2016-09-21 15:46:10 -05:00
damencho
4fdc11c6fb Removes download logs references. 2016-09-21 15:45:08 -05:00
hristoterezov
e1cb75fe04 fix(UI): Show the correct message on NO_DATA_FROM_SOURCE GUM errors 2016-09-21 15:31:49 -05:00
yanas
0bf6d52eef Merge pull request #902 from jitsi/fix-authentication-failed-msg
Fixes message for authentication failed.
2016-09-20 12:47:53 -05:00
yanas
7453198472 Fixes message for authentication failed. 2016-09-20 12:08:44 -05:00
Дамян Минков
0c1120c1a8 Merge pull request #897 from jitsi/settings-css-adjustments
Some css and lang adjustments to settings and contact list.
2016-09-20 10:48:57 -05:00
yanas
efbc84d18b Merge pull request #898 from m-voloshin/tooltip-fix-layer
Adjustment for layer order
2016-09-20 10:45:57 -05:00
Дамян Минков
cdca1a46ef Merge pull request #901 from jitsi/remove-missing-method
Removes missing and unused method.
2016-09-20 10:20:23 -05:00
yanas
689f7dc8f3 Merge pull request #900 from jitsi/delete-css-ds_store
Delete css/.DS_Store
2016-09-20 10:17:35 -05:00
yanas
4ca9349de7 Removes missing and unused method. 2016-09-20 10:16:15 -05:00
Lyubomir Marinov
9d6253455a Delete css/.DS_Store 2016-09-20 10:12:56 -05:00
Maxim Voloshin
388f868165 Adjustment for layer order 2016-09-20 13:15:10 +03:00
yanas
aacb39a439 Some css adjustments to settings and contact list. 2016-09-20 01:14:00 -05:00
yanas
298338f076 Merge pull request #896 from jitsi/fix-feedback-dialog
Feedback dialog changes
2016-09-19 23:52:14 -05:00
yanas
2d2915967c Fixes focus in Feedback and makes animation configurable. 2016-09-19 23:25:03 -05:00
Kostiantyn Pashura
4572e1d344 feedback dialog changes 2016-09-19 22:27:41 -05:00
yanas
f8b200f32c Merge pull request #892 from m-voloshin/blue-badges
Blue badges
2016-09-19 22:27:10 -05:00
yanas
3d4addd9ef Fixes badge font and corrects file name. 2016-09-19 22:07:10 -05:00
yanas
2919a60403 Adds blue badges to contact list and chat. 2016-09-19 21:22:41 -05:00
Paweł Domas
a07858cc72 Merge pull request #891 from jitsi/video-thumbnail-toolbar-fixes
Video thumbnail toolbar fixes
2016-09-19 19:31:08 -05:00
yanas
d1d4674136 Fixes some jsdocs. 2016-09-19 18:04:55 -05:00
Paweł Domas
9b5d4b8ceb Merge pull request #856 from jitsi/implement_muted_ended_track_events
feat(UI): Add UI support for camera issue detection
2016-09-19 16:03:06 -05:00
hristoterezov
abe216a0bb feat(UI): Add UI support for camera issue detection 2016-09-19 14:49:50 -05:00
hristoterezov
84983c341e Merge pull request #844 from jitsi/device-selection-rework
Device selection rework
2016-09-19 13:56:21 -05:00
Paweł Domas
075423ee96 Merge pull request #895 from jitsi/fix-incorrect-json-file
Fix incorrect JSON file
2016-09-19 13:29:06 -05:00
Lyubomir Marinov
2ff77676e2 Fix incorrect JSON file
The extension of the file modules/UI/side_pannels/chat/smileys.json
suggests that the format of the file is JSON. However, it contains
JavaScript RegExp instances which do not represent valid JSON. Such
discrepancies between file extension and format cause failures in
certain tools such as Webpack. Convert the file (both extension and
format) into a valid ES2015 module.
2016-09-19 12:48:38 -05:00
yanas
2da3373e10 Merge pull request #890 from jitsi/updates-watermark-link
Updates handling links on watermarks.
2016-09-18 17:02:12 -05:00
yanas
1486eac752 Applies fixes to thumbnail video toolbar from m-voloshin 2016-09-18 16:58:34 -05:00
yanas
c5adecb6e1 Adds menu icon to font. 2016-09-18 16:42:33 -05:00
damencho
e4c4236386 Updates handling links on watermarks.
When link is missing disable clicking.
2016-09-16 15:17:34 -05:00
yanas
e5e7e043ee Merge pull request #889 from BeatC/tooltips-bg
Change color for tooltip
2016-09-16 12:54:15 -05:00
Ilya Daynatovich
f8d01b4312 Change color for tooltip 2016-09-16 19:05:23 +03:00
Дамян Минков
f0a898c674 Merge pull request #885 from jitsi/add-raise-hand-toggle
Adds raise hand toggle state.
2016-09-16 10:28:12 -05:00
yanas
8b7bdb4957 Changes the raise hand event name to fit better the action 2016-09-16 10:22:50 -05:00
yanas
bd46430434 Small color change 2016-09-16 10:22:50 -05:00
yanas
a1635ccc68 Adds javadoc 2016-09-16 10:22:50 -05:00
yanas
1853fa6fae Adds raise hand toggle state. 2016-09-16 10:22:50 -05:00
Дамян Минков
692f0792e1 Merge pull request #886 from jitsi/fix-some-tooltips
Fixes dominant speaker and raised hand tooltips
2016-09-16 10:18:23 -05:00
yanas
bc222c60e0 Fixes dominant speaker and raised hand tooltips 2016-09-16 00:37:29 -05:00
yanas
96bbf0419c Adds tooltips 2016-09-16 00:04:19 -05:00
yanas
531e3d2765 Merge pull request #884 from jitsi/updates-lock-room
Updates lock room
2016-09-15 22:24:58 -05:00
damencho
02165786f1 Handles case where somebody removed lock of the room while we attempt to join.
Receiving password required, marks the room as locked, but if we try to enter without password, mark it as unlocked till we receive a password required error or we successfully join.
2016-09-15 16:34:02 -05:00
damencho
a449223b40 Clears the password if user cancels password prompt.
Clears the password if user cancel attempt to enter password, as using one instance of locker for multiple attempts keeps the password.
2016-09-15 16:32:10 -05:00
yanas
12344ab486 Merge pull request #883 from jitsi/contactlist-displayname
Syncs contactlist display names with thumbnails.
2016-09-15 15:22:47 -05:00
yanas
b6e18d8a68 Merge pull request #882 from jitsi/fix-hide-toolbar
Fixes hiding toolbar.
2016-09-15 15:10:54 -05:00
damencho
1a0677cb59 Syncs contactlist display names with thumbnails.
Uses the same display names in the contact list as in the thumbnails, for local and remote.
2016-09-15 15:01:48 -05:00
damencho
c483587853 Fixes hiding toolbar.
Receiving messages docks the toolbar to be able to see number of unread messages. We need to undock it when we read the messages. We skip undocking if we are not in video mode (on large), cause stuff like youtube video share is docking/undocking the toolbar.
2016-09-15 14:02:56 -05:00
hristoterezov
9b25467080 Merge pull request #878 from jitsi/video-thumbnail-redesign
Video thumbnails redesign
2016-09-15 13:48:07 -05:00
yanas
f37fd15fca Merge pull request #876 from jitsi/ui-ringoverlay-stop
feat(ringoverlay): Stop ringing after 30s and change the message
2016-09-14 21:44:59 -05:00
yanas
5092f52716 Merge pull request #874 from jitsi/ui-fix-ringoverlay
feat(ringoverlay): Change the background when the avatar is displayed
2016-09-14 21:40:54 -05:00
yanas
0013745783 Video thumbnails redesign 2016-09-14 21:20:54 -05:00
hristoterezov
ad5fa13339 feat(ringoverlay): Stop ringing after 30s and change the message 2016-09-14 17:55:43 -05:00
yanas
e1fa5ecb34 Merge pull request #873 from jitsi/chat-updates
Chat updates
2016-09-14 17:08:34 -05:00
damencho
e1512e3776 Fixes focusing on write area or nickname input when chat is open. 2016-09-14 16:47:10 -05:00
hristoterezov
dab5252746 feat(ringoverlay): Change the background when the avatar is displayed 2016-09-14 16:26:17 -05:00
damencho
d8dd564b06 Fixes clearing message counter on opening the chat. 2016-09-14 15:22:36 -05:00
damencho
407b082780 Removes changing the message icon and flashing on new message. 2016-09-14 15:18:14 -05:00
damencho
a671093489 Introduces chat_container_id variable. 2016-09-14 15:13:15 -05:00
damencho
3ee61df319 Updates talk while muted indication after latest ui changes. 2016-09-14 13:26:28 -05:00
damencho
c43b1f54c7 Shows toolbar before showing talk while muted notification. 2016-09-14 13:26:28 -05:00
damencho
1a554828e1 Adds option to add custom timeout for hiding toolbar. 2016-09-14 13:26:28 -05:00
damencho
974a0334df Updates text for talk while muted. 2016-09-14 13:26:28 -05:00
damencho
e7e7c7d5a0 Handles talk while muted event. 2016-09-14 13:26:28 -05:00
Дамян Минков
29f0c0b311 Merge pull request #870 from jitsi/add-raised-hand-icon
Adds raise hand icon
2016-09-14 12:17:06 -05:00
yanas
955680018f Merge pull request #872 from jitsi/fix-hiding-toolbars
Fixes hiding toolbars.
2016-09-14 12:16:57 -05:00
damencho
686e85cd4f Fixes hiding toolbars.
Schedule new hide check if toolbar is hovered, overlay is shown or the sideBar container is visible (chat, contactlist , etc.).
2016-09-14 11:51:47 -05:00
hristoterezov
297d4e65fc style(gitignore): Add .sync-config.cson 2016-09-14 11:38:43 -05:00
yanas
0e94bf7e0b Merge pull request #871 from jitsi/fix-remote-video-thumb
Fixes parameters for VideoLayout.resizeThumbnails.
2016-09-14 11:00:14 -05:00
damencho
a5bc9625ef Fixes parameters for VideoLayout.resizeThumbnails. 2016-09-14 10:48:38 -05:00
yanas
cbde4f89b2 Adds raise hand icon 2016-09-14 09:00:59 -05:00
yanas
891c108191 Merges fix from m-voloshin for Firefox toolbar 2016-09-14 08:25:11 -05:00
yanas
764d767789 Update _variables.scss 2016-09-14 01:22:33 -05:00
yanas
7ded10cd8d Merge pull request #865 from jitsi/add-raise-hand-button
Adds a possibility to add raise hand as a button
2016-09-13 23:09:18 -05:00
yanas
3b05a16b32 Fix for button appearing in both toolbars 2016-09-13 22:10:13 -05:00
yanas
cf49c8c6ff Adds raise hand button to the side toolbar 2016-09-13 21:21:31 -05:00
yanas
99bf4bc44d Adds a possibility to add raise hand as a button 2016-09-13 21:21:31 -05:00
yanas
929639b06b Merge pull request #863 from jitsi/contact-list-update
Contact list update
2016-09-13 21:21:03 -05:00
damencho
4c72833f5a Adds an option and hide avatars in contact list. 2016-09-13 17:14:05 -05:00
damencho
e3eaac5bef Disables click toggler. 2016-09-13 17:12:10 -05:00
damencho
0683f94edb Skips storing devices (mic and camera) if there is no user selection.
Skips storing device ids in localstorage if the user hasn't selected a device to use, and keeps using system defaults. Removes calls to private library method for setting initial realDeviceIds, as this had been added to the library.
2016-09-07 16:48:57 -05:00
98 changed files with 4049 additions and 2248 deletions

8
.babelrc Normal file
View File

@@ -0,0 +1,8 @@
{
"plugins": [
"transform-object-rest-spread"
],
"presets": [
"es2015"
]
}

11
.eslintignore Normal file
View File

@@ -0,0 +1,11 @@
# The build artifacts of the jitsi-meet project.
build/*
# Third-party source code which we (1) do not want to modify or (2) try to
# modify as little as possible.
libs/*
# ESLint will by default ignore its own configuration file. However, there does
# not seem to be a reason why we will want to risk being inconsistent with our
# remaining JavaScript source code.
!.eslintrc.js

37
.eslintrc.js Normal file
View File

@@ -0,0 +1,37 @@
module.exports = {
'env': {
'browser': true,
'commonjs': true,
'es6': true
},
'extends': 'eslint:recommended',
'globals': {
// The globals that (1) are accessed but not defined within many of our
// files, (2) are certainly defined, and (3) we would like to use
// without explicitly specifying them (using a comment) inside of our
// files.
'__filename': false
},
'parserOptions': {
'ecmaFeatures': {
'experimentalObjectRestSpread': true
},
'sourceType': 'module'
},
'rules': {
'new-cap': [
'error',
{
'capIsNew': false // Behave like JSHint's newcap.
}
],
// While it is considered a best practice to avoid using methods on
// console in JavaScript that is designed to be executed in the browser
// and ESLint includes the rule among its set of recommended rules, (1)
// the general practice is to strip such calls before pushing to
// production and (2) we prefer to utilize console in lib-jitsi-meet
// (and jitsi-meet).
'no-console': 'off',
'semi': 'error'
}
};

2
.gitignore vendored
View File

@@ -1,4 +1,5 @@
node_modules
.DS_Store
*.swp
.idea/
*.iml
@@ -9,3 +10,4 @@ all.css
*css.map
unsupported_browser.css
.remote-sync.json
.sync-config.cson

View File

@@ -1,4 +1,5 @@
node_modules
libs
debian
libs
node_modules
analytics.js
webpack.config.babel.js

View File

@@ -1,20 +1,17 @@
NPM = npm
BROWSERIFY = ./node_modules/.bin/browserify
NODE_SASS = ./node_modules/.bin/node-sass
UGLIFYJS = ./node_modules/.bin/uglifyjs
EXORCIST = ./node_modules/.bin/exorcist
BUILD_DIR = build
CLEANCSS = ./node_modules/.bin/cleancss
STYLES_MAIN = css/main.scss
STYLES_UNSUPPORTED_BROWSER = css/unsupported_browser.scss
DEPLOY_DIR = libs
LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet/
NODE_SASS = ./node_modules/.bin/node-sass
NPM = npm
OUTPUT_DIR = .
STYLES_BUNDLE = css/all.bundle.css
STYLES_DESTINATION = css/all.css
DEPLOY_DIR = libs
BROWSERIFY_FLAGS = -d
OUTPUT_DIR = .
LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet/
IFRAME_API_DIR = ./modules/API/external
STYLES_MAIN = css/main.scss
STYLES_UNSUPPORTED_BROWSER = css/unsupported_browser.scss
WEBPACK = ./node_modules/.bin/webpack
all: update-deps compile compile-iframe-api uglify uglify-iframe-api deploy clean
all: update-deps compile deploy clean
# FIXME: there is a problem with node-sass not correctly installed (compiled)
# a quick fix to make sure it is installed on every update
@@ -23,13 +20,10 @@ update-deps:
$(NPM) update && $(NPM) install node-sass
compile:
$(BROWSERIFY) $(BROWSERIFY_FLAGS) -e app.js -s APP | $(EXORCIST) $(OUTPUT_DIR)/app.bundle.js.map > $(OUTPUT_DIR)/app.bundle.js
compile-iframe-api:
$(BROWSERIFY) $(BROWSERIFY_FLAGS) -e $(IFRAME_API_DIR)/external_api.js -s JitsiMeetExternalAPI | $(EXORCIST) $(OUTPUT_DIR)/external_api.js.map > $(OUTPUT_DIR)/external_api.js
$(WEBPACK) -p
clean:
rm -f $(OUTPUT_DIR)/app.bundle.* $(OUTPUT_DIR)/external_api.*
rm -fr $(BUILD_DIR)
deploy: deploy-init deploy-appbundle deploy-lib-jitsi-meet deploy-css deploy-local
@@ -37,20 +31,20 @@ deploy-init:
mkdir -p $(DEPLOY_DIR)
deploy-appbundle:
cp $(OUTPUT_DIR)/app.bundle.min.js $(OUTPUT_DIR)/app.bundle.min.map \
$(OUTPUT_DIR)/app.bundle.js $(OUTPUT_DIR)/app.bundle.js.map \
$(OUTPUT_DIR)/external_api.js.map $(OUTPUT_DIR)/external_api.js \
$(OUTPUT_DIR)/external_api.min.map $(OUTPUT_DIR)/external_api.min.js \
$(OUTPUT_DIR)/analytics.js \
$(DEPLOY_DIR)
cp \
$(BUILD_DIR)/app.bundle.min.js \
$(BUILD_DIR)/app.bundle.min.map \
$(BUILD_DIR)/external_api.min.js \
$(BUILD_DIR)/external_api.min.map \
$(OUTPUT_DIR)/analytics.js \
$(DEPLOY_DIR)
deploy-lib-jitsi-meet:
cp $(LIBJITSIMEET_DIR)/lib-jitsi-meet.min.js \
$(LIBJITSIMEET_DIR)/lib-jitsi-meet.min.map \
$(LIBJITSIMEET_DIR)/lib-jitsi-meet.js \
$(LIBJITSIMEET_DIR)/lib-jitsi-meet.js.map \
$(LIBJITSIMEET_DIR)/connection_optimization/external_connect.js \
$(DEPLOY_DIR)
cp \
$(LIBJITSIMEET_DIR)/lib-jitsi-meet.min.js \
$(LIBJITSIMEET_DIR)/lib-jitsi-meet.min.map \
$(LIBJITSIMEET_DIR)/connection_optimization/external_connect.js \
$(DEPLOY_DIR)
deploy-css:
$(NODE_SASS) css/unsupported_browser.scss css/unsupported_browser.css ; \
@@ -61,13 +55,6 @@ deploy-css:
deploy-local:
([ ! -x deploy-local.sh ] || ./deploy-local.sh)
uglify:
$(UGLIFYJS) -p relative $(OUTPUT_DIR)/app.bundle.js -o $(OUTPUT_DIR)/app.bundle.min.js --source-map $(OUTPUT_DIR)/app.bundle.min.map --in-source-map $(OUTPUT_DIR)/app.bundle.js.map
uglify-iframe-api:
$(UGLIFYJS) -p relative $(OUTPUT_DIR)/external_api.js -o $(OUTPUT_DIR)/external_api.min.js --source-map $(OUTPUT_DIR)/external_api.min.map --in-source-map $(OUTPUT_DIR)/external_api.js.map
source-package:
mkdir -p source_package/jitsi-meet/css && \
cp -r *.js *.html connection_optimization favicon.ico fonts images libs sounds LICENSE lang source_package/jitsi-meet && \

View File

@@ -1,5 +1,9 @@
/* global ga */
(function (ctx) {
function Analytics() {
/* eslint-disable */
/**
* Google Analytics
*/
@@ -8,6 +12,8 @@
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-319188-14', 'jit.si');
ga('send', 'pageview');
/* eslint-enable */
}
Analytics.prototype.sendEvent = function (action, data, label, browserName) {
@@ -19,5 +25,9 @@
action + '.' + browserName, label ? label : "", value ? value : null);
};
Analytics.prototype.sendFeedback = function (data, label, browserName) {
this.sendEvent('feedback.rating', data.overall, label, browserName);
};
ctx.Analytics = Analytics;
}(window));
}(window));

15
app.js
View File

@@ -1,4 +1,4 @@
/* global $, JitsiMeetJS, config, getRoomName */
/* global $, config, getRoomName */
/* application specific logic */
import "babel-polyfill";
@@ -8,10 +8,14 @@ import "jquery-ui";
import "strophe";
import "strophe-disco";
import "strophe-caps";
import "tooltip";
import "popover";
import "jQuery-Impromptu";
import "autosize";
import 'aui';
import 'aui-experimental';
import 'aui-css';
import 'aui-experimental-css';
window.toastr = require("toastr");
import URLProcessor from "./modules/config/URLProcessor";
@@ -106,6 +110,11 @@ function init() {
var isUIReady = APP.UI.start();
if (isUIReady) {
APP.conference.init({roomName: buildRoomName()}).then(function () {
let server = APP.tokenData.server;
if(server) {
APP.conference.logEvent("server." + server, 1);
}
APP.UI.initConference();
APP.UI.addListener(UIEvents.LANG_CHANGED, function (language) {

8
authError.html Normal file
View File

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

8
close.html Normal file
View File

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

View File

@@ -17,8 +17,8 @@ import mediaDeviceHelper from './modules/devices/mediaDeviceHelper';
import {reportError} from './modules/util/helpers';
import UIErrors from './modules/UI/UIErrors';
import UIUtil from './modules/UI/util/UIUtil';
const ConnectionEvents = JitsiMeetJS.events.connection;
const ConnectionErrors = JitsiMeetJS.errors.connection;
const ConferenceEvents = JitsiMeetJS.events.conference;
@@ -39,7 +39,14 @@ let connectionIsInterrupted = false;
*/
let DSExternalInstallationInProgress = false;
import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo";
/**
* Listens whether conference had been left from local user when we are trying
* to navigate away from current page.
* @type {ConferenceLeftListener}
*/
let conferenceLeftListener = null;
import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/VideoContainer";
/**
* Known custom conference commands.
@@ -151,29 +158,24 @@ function getDisplayName (id) {
/**
* Mute or unmute local audio stream if it exists.
* @param {boolean} muted if audio stream should be muted or unmuted.
* @param {boolean} indicates if this local audio mute was a result of user
* interaction
*
* @param {boolean} muted - if audio stream should be muted or unmuted.
* @param {boolean} userInteraction - indicates if this local audio mute was a
* result of user interaction
*/
function muteLocalAudio (muted, userInteraction) {
if (!localAudio) {
function muteLocalAudio (muted) {
muteLocalMedia(localAudio, muted, 'Audio');
}
function muteLocalMedia(localMedia, muted, localMediaTypeString) {
if (!localMedia) {
return;
}
if (muted) {
localAudio.mute().then(function(value) {},
function(value) {
console.warn('Audio Mute was rejected:', value);
}
);
} else {
localAudio.unmute().then(function(value) {},
function(value) {
console.warn('Audio unmute was rejected:', value);
}
);
}
const method = muted ? 'mute' : 'unmute';
localMedia[method]().catch(reason => {
console.warn(`${localMediaTypeString} ${method} was rejected:`, reason);
});
}
/**
@@ -181,29 +183,33 @@ function muteLocalAudio (muted, userInteraction) {
* @param {boolean} muted if video stream should be muted or unmuted.
*/
function muteLocalVideo (muted) {
if (!localVideo) {
return;
}
if (muted) {
localVideo.mute().then(function(value) {},
function(value) {
console.warn('Video mute was rejected:', value);
}
);
} else {
localVideo.unmute().then(function(value) {},
function(value) {
console.warn('Video unmute was rejected:', value);
}
);
}
muteLocalMedia(localVideo, muted, 'Video');
}
/**
* Check if the welcome page is enabled and redirects to it.
* If requested show a thank you dialog before that.
* If we have a close page enabled, redirect to it without
* showing any other dialog.
* @param {boolean} showThankYou whether we should show a thank you dialog
*/
function maybeRedirectToWelcomePage() {
function maybeRedirectToWelcomePage(showThankYou) {
// if close page is enabled redirect to it, without further action
if (config.enableClosePage) {
window.location.pathname = "close.html";
return;
}
if (showThankYou) {
APP.UI.messageHandler.openMessageDialog(
null, null, null,
APP.translation.translateString(
"dialog.thankYou", {appName:interfaceConfig.APP_NAME}
)
);
}
if (!config.enableWelcomePage) {
return;
}
@@ -235,7 +241,7 @@ function disconnectAndShowFeedback(requestFeedback) {
* @param {boolean} [requestFeedback=false] if user feedback should be requested
*/
function hangup (requestFeedback = false) {
const errCallback = (f, err) => {
const errCallback = (err) => {
// If we want to break out the chain in our error handler, it needs
// to return a rejected promise. In the case of feedback request
@@ -250,14 +256,69 @@ function hangup (requestFeedback = false) {
}
};
const disconnect = disconnectAndShowFeedback.bind(null, requestFeedback);
APP.conference._room.leave()
.then(disconnect)
.catch(errCallback.bind(null, disconnect))
.then(maybeRedirectToWelcomePage)
.catch(function(err){
console.log(err);
});
if (!conferenceLeftListener)
conferenceLeftListener = new ConferenceLeftListener();
// Make sure that leave is resolved successfully and the set the handlers
// to be invoked once conference had been left
APP.conference._room.leave()
.then(conferenceLeftListener.setHandler(disconnect, errCallback))
.catch(errCallback);
}
/**
* Listens for CONFERENCE_LEFT event so we can check whether it has finished.
* The handler will be called once the conference had been left or if it
* was already left when we are adding the handler.
*/
class ConferenceLeftListener {
/**
* Creates ConferenceLeftListener and start listening for conference
* failed event.
*/
constructor() {
room.on(ConferenceEvents.CONFERENCE_LEFT,
this._handleConferenceLeft.bind(this));
}
/**
* Handles the conference left event, if we have a handler we invoke it.
* @private
*/
_handleConferenceLeft() {
this.conferenceLeft = true;
if (this.handler)
this._handleLeave();
}
/**
* Sets the handlers. If we already left the conference invoke them.
* @param handler
* @param errCallback
*/
setHandler (handler, errCallback) {
this.handler = handler;
this.errCallback = errCallback;
if (this.conferenceLeft)
this._handleLeave();
}
/**
* Invokes the handlers.
* @private
*/
_handleLeave()
{
this.handler()
.catch(this.errCallback)
.then(maybeRedirectToWelcomePage)
.catch(function(err){
console.log(err);
});
}
}
/**
@@ -293,8 +354,13 @@ function createLocalTracks (options, checkForPermissionPrompt) {
firefox_fake_device: config.firefox_fake_device,
desktopSharingExtensionExternalInstallation:
options.desktopSharingExtensionExternalInstallation
}, checkForPermissionPrompt)
.catch(function (err) {
}, checkForPermissionPrompt).then( (tracks) => {
tracks.forEach((track) => {
track.on(TrackEvents.NO_DATA_FROM_SOURCE,
APP.UI.showTrackNotWorkingDialog.bind(null, track));
});
return tracks;
}).catch(function (err) {
console.error(
'failed to create local tracks', options.devices, err);
return Promise.reject(err);
@@ -345,7 +411,7 @@ class ConferenceConnector {
room.on(ConferenceEvents.CONFERENCE_ERROR,
this._onConferenceError.bind(this));
}
_handleConferenceFailed(err, msg) {
_handleConferenceFailed(err) {
this._unsubscribe();
this._reject(err);
}
@@ -357,6 +423,14 @@ class ConferenceConnector {
case ConferenceErrors.PASSWORD_REQUIRED:
APP.UI.markRoomLocked(true);
roomLocker.requirePassword().then(function () {
let pass = roomLocker.password;
// we received that password is required, but user is trying
// anyway to login without a password, mark room as not locked
// in case he succeeds (maybe someone removed the password
// meanwhile), if it is still locked another password required
// will be received and the room again will be marked as locked
if (!pass)
APP.UI.markRoomLocked(false);
room.join(roomLocker.password);
});
break;
@@ -368,6 +442,13 @@ class ConferenceConnector {
}
break;
case ConferenceErrors.NOT_ALLOWED_ERROR:
{
// let's show some auth not allowed page
window.location.pathname = "authError.html";
}
break;
case ConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE:
APP.UI.notifyBridgeDown();
break;
@@ -648,6 +729,61 @@ export default {
return this._room
&& this._room.getConnectionState();
},
/**
* Checks whether or not our connection is currently in interrupted and
* reconnect attempts are in progress.
*
* @returns {boolean} true if the connection is in interrupted state or
* false otherwise.
*/
isConnectionInterrupted () {
return connectionIsInterrupted;
},
/**
* Finds JitsiParticipant for given id.
*
* @param {string} id participant's identifier(MUC nickname).
*
* @returns {JitsiParticipant|null} participant instance for given id or
* null if not found.
*/
getParticipantById (id) {
return room ? room.getParticipantById(id) : null;
},
/**
* Checks whether the user identified by given id is currently connected.
*
* @param {string} id participant's identifier(MUC nickname)
*
* @returns {boolean|null} true if participant's connection is ok or false
* if the user is having connectivity issues.
*/
isParticipantConnectionActive (id) {
let participant = this.getParticipantById(id);
return participant ? participant.isConnectionActive() : null;
},
/**
* Gets the display name foe the <tt>JitsiParticipant</tt> identified by
* the given <tt>id</tt>.
*
* @param id {string} the participant's id(MUC nickname/JVB endpoint id)
*
* @return {string} the participant's display name or the default string if
* absent.
*/
getParticipantDisplayName (id) {
let displayName = getDisplayName(id);
if (displayName) {
return displayName;
} else {
if (APP.conference.isLocalId(id)) {
return APP.translation.generateTranslationHTML(
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
} else {
return interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
}
}
},
getMyUserId () {
return this._room
&& this._room.myUserId();
@@ -698,6 +834,30 @@ export default {
return room.getLogs();
},
/**
* Download logs, a function that can be called from console while
* debugging.
* @param filename (optional) specify target filename
*/
saveLogs (filename = 'meetlog.json') {
// this can be called from console and will not have reference to this
// that's why we reference the global var
let logs = APP.conference.getLogs();
let data = encodeURIComponent(JSON.stringify(logs, null, ' '));
let elem = document.createElement('a');
elem.download = filename;
elem.href = 'data:application/json;charset=utf-8,\n' + data;
elem.dataset.downloadurl
= ['text/json', elem.download, elem.href].join(':');
elem.dispatchEvent(new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: false
}));
},
/**
* Exposes a Command(s) API on this instance. It is necessitated by (1) the
* desire to keep room private to this instance and (2) the need of other
@@ -857,8 +1017,6 @@ export default {
return promise.then(function () {
if (stream) {
stream.on(TrackEvents.TRACK_AUDIO_NOT_WORKING,
APP.UI.showAudioNotWorkingDialog);
return room.addTrack(stream);
}
}).then(() => {
@@ -1020,7 +1178,7 @@ export default {
console.log('USER %s connnected', id, user);
APP.API.notifyUserJoined(id);
APP.UI.addUser(id, user.getDisplayName());
APP.UI.addUser(user);
// check the roles for the new user and reflect them
APP.UI.updateUserRole(user);
@@ -1036,8 +1194,10 @@ export default {
room.on(ConferenceEvents.USER_ROLE_CHANGED, (id, role) => {
if (this.isLocalId(id)) {
console.info(`My role changed, new role: ${role}`);
this.isModerator = room.isModerator();
APP.UI.updateLocalRole(room.isModerator());
if (this.isModerator !== room.isModerator()) {
this.isModerator = room.isModerator();
APP.UI.updateLocalRole(room.isModerator());
}
} else {
let user = room.getParticipantById(id);
if (user) {
@@ -1097,6 +1257,12 @@ export default {
APP.UI.setAudioLevel(id, lvl);
});
room.on(ConferenceEvents.TALK_WHILE_MUTED, () => {
APP.UI.showToolbar(6000);
UIUtil.animateShowElement($("#talkWhileMutedPopup"), true, 5000);
});
/*
room.on(ConferenceEvents.IN_LAST_N_CHANGED, (inLastN) => {
//FIXME
if (config.muteLocalVideoIfNotInLastN) {
@@ -1105,10 +1271,16 @@ export default {
// APP.UI.markVideoMuted(true/false);
}
});
*/
room.on(
ConferenceEvents.LAST_N_ENDPOINTS_CHANGED, (ids, enteringIds) => {
APP.UI.handleLastNEndpoints(ids, enteringIds);
});
room.on(
ConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
(id, isActive) => {
APP.UI.participantConnectionStatusChanged(id, isActive);
});
room.on(ConferenceEvents.DOMINANT_SPEAKER_CHANGED, (id) => {
if (this.isLocalId(id)) {
this.isDominantSpeaker = true;
@@ -1140,10 +1312,12 @@ export default {
room.on(ConferenceEvents.CONNECTION_INTERRUPTED, () => {
connectionIsInterrupted = true;
ConnectionQuality.updateLocalConnectionQuality(0);
APP.UI.showLocalConnectionInterrupted(true);
});
room.on(ConferenceEvents.CONNECTION_RESTORED, () => {
connectionIsInterrupted = false;
APP.UI.showLocalConnectionInterrupted(false);
});
room.on(ConferenceEvents.DISPLAY_NAME_CHANGED, (id, displayName) => {
@@ -1167,6 +1341,7 @@ export default {
console.log("Received channel password lock change: ", state,
error);
APP.UI.markRoomLocked(state);
roomLocker.lockedElsewhere = state;
});
room.on(ConferenceEvents.USER_STATUS_CHANGED, function (id, status) {
@@ -1294,15 +1469,6 @@ export default {
&& APP.UI.notifyInitiallyMuted();
});
APP.UI.addListener(UIEvents.USER_INVITED, (roomUrl) => {
APP.UI.inviteParticipants(
roomUrl,
APP.conference.roomName,
roomLocker.password,
APP.settings.getDisplayName()
);
});
room.on(
ConferenceEvents.AVAILABLE_DEVICES_CHANGED, function (id, devices) {
APP.UI.updateDevicesAvailability(id, devices);
@@ -1389,6 +1555,8 @@ export default {
APP.UI.addListener(UIEvents.PINNED_ENDPOINT, (smallVideo, isPinned) => {
var smallVideoId = smallVideo.getId();
// FIXME why VIDEO_CONTAINER_TYPE instead of checking if
// the participant is on the large video ?
if (smallVideo.getVideoType() === VIDEO_CONTAINER_TYPE
&& !APP.conference.isLocalId(smallVideoId)) {
@@ -1416,7 +1584,7 @@ export default {
.then(([stream]) => {
this.useVideoStream(stream);
console.log('switched local video device');
APP.settings.setCameraDeviceId(cameraDeviceId);
APP.settings.setCameraDeviceId(cameraDeviceId, true);
})
.catch((err) => {
APP.UI.showDeviceErrorDialog(null, err);
@@ -1438,7 +1606,7 @@ export default {
.then(([stream]) => {
this.useAudioStream(stream);
console.log('switched local audio device');
APP.settings.setMicDeviceId(micDeviceId);
APP.settings.setMicDeviceId(micDeviceId, true);
})
.catch((err) => {
APP.UI.showDeviceErrorDialog(err, null);
@@ -1533,13 +1701,13 @@ export default {
// storage and settings menu. This is a workaround until
// getConstraints() method will be implemented in browsers.
if (localAudio) {
localAudio._setRealDeviceIdFromDeviceList(devices);
APP.settings.setMicDeviceId(localAudio.getDeviceId());
APP.settings.setMicDeviceId(
localAudio.getDeviceId(), false);
}
if (localVideo) {
localVideo._setRealDeviceIdFromDeviceList(devices);
APP.settings.setCameraDeviceId(localVideo.getDeviceId());
APP.settings.setCameraDeviceId(
localVideo.getDeviceId(), false);
}
mediaDeviceHelper.setCurrentMediaDevices(devices);
@@ -1624,14 +1792,10 @@ export default {
},
/**
* Toggles the local "raised hand" status, if the current state allows
* toggling.
* Toggles the local "raised hand" status.
*/
maybeToggleRaisedHand() {
// If we are the dominant speaker, we don't enable "raise hand".
if (this.isHandRaised || !this.isDominantSpeaker) {
this.setRaisedHand(!this.isHandRaised);
}
this.setRaisedHand(!this.isHandRaised);
},
/**
@@ -1640,11 +1804,28 @@ export default {
setRaisedHand(raisedHand) {
if (raisedHand !== this.isHandRaised)
{
APP.UI.onLocalRaiseHandChanged(raisedHand);
this.isHandRaised = raisedHand;
// Advertise the updated status
room.setLocalParticipantProperty("raisedHand", raisedHand);
// Update the view
APP.UI.setLocalRaisedHandStatus(raisedHand);
}
},
/**
* Log event to callstats and analytics.
* @param {string} name the event name
* @param {int} value the value (it's int because google analytics supports
* only int).
* NOTE: Should be used after conference.init
*/
logEvent(name, value) {
if(JitsiMeetJS.analytics) {
JitsiMeetJS.analytics.sendEvent(name, value);
}
if(room) {
room.sendApplicationLog(JSON.stringify({name, value}));
}
}
};

View File

@@ -1,5 +1,6 @@
/* jshint -W101 */
var config = {
/* jshint maxlen:false */
var config = { // eslint-disable-line no-unused-vars
// configLocation: './config.json', // see ./modules/HttpConfigFetch.js
hosts: {
domain: 'jitsi-meet.example.com',
@@ -56,6 +57,8 @@ var config = {
//disableAdaptiveSimulcast: false,
enableRecording: false,
enableWelcomePage: true,
//enableClosePage: false, // enabling the close page will ignore the welcome
// page redirection when call is hangup
disableSimulcast: false,
logStats: false, // Enable logging of PeerConnection stats via the focus
// requireDisplayName: true, // Forces the participants that doesn't have display name to enter it when they enter the room.

BIN
css/.DS_Store vendored

Binary file not shown.

View File

@@ -81,24 +81,10 @@ form {
display: block;
}
#downloadlog {
display: none;
position: absolute;
bottom: 5;
left: 5;
overflow: visible;
color: rgba(255,255,255,.50);
}
.active {
background-color: #00ccff;
}
.glow
{
text-shadow: 0px 0px 30px #06a5df, 0px 0px 10px #06a5df, 0px 0px 5px #06a5df,0px 0px 3px #06a5df;
}
.watermark {
display: block;
position: absolute;
@@ -175,4 +161,15 @@ form {
display: -ms-flexbox !important;
display: -webkit-flex !important;
display: flex !important;
}
}
.tipsy {
z-index: $tooltipsZ;
&-inner {
background-color: $tooltipBg;
}
&-arrow {
border-color: $tooltipBg;
}
}

View File

@@ -104,11 +104,6 @@
color: #a7a7a7;
}
#unreadMessages {
font-size: 8px;
position: absolute;
}
#chat_container .username {
float: left;
padding-left: 5px;

View File

@@ -2,12 +2,11 @@
cursor: default;
> ul#contacts {
position: absolute;
top: 31px;
font-size: 12px;
bottom: 0px;
width: 100%;
margin: 0px;
padding: 0px;
width: 100%;
overflow-y: scroll;
overflow-x: hidden;
}
@@ -20,13 +19,13 @@
#contacts {
>li {
color: $defaultSideBarFontColor;
list-style-type: none;
text-align: left;
white-space: nowrap;
color: #FFF;
font-size: 10pt;
padding: 7px 10px;
margin: 2px;
padding: 6px 10%;
&:hover,
&:active {

View File

@@ -255,9 +255,6 @@
.fa-road:before {
content: "\f018";
}
.fa-download:before {
content: "\f019";
}
.fa-arrow-circle-o-down:before {
content: "\f01a";
}
@@ -842,9 +839,6 @@
.fa-exchange:before {
content: "\f0ec";
}
.fa-cloud-download:before {
content: "\f0ed";
}
.fa-cloud-upload:before {
content: "\f0ee";
}

View File

@@ -16,8 +16,9 @@
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 0.75em;
line-height: 1.22em;
font-size: 1.22em;
cursor: default;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
@@ -42,9 +43,6 @@
.icon-chat:before {
content: "\e906";
}
.icon-download:before {
content: "\e902";
}
.icon-edit:before {
content: "\e907";
}
@@ -57,12 +55,21 @@
.icon-kick:before {
content: "\e904";
}
.icon-menu-up:before {
content: "\e91f";
}
.icon-menu-down:before {
content: "\e920";
}
.icon-full-screen:before {
content: "\e90b";
}
.icon-exit-full-screen:before {
content: "\e90c";
}
.icon-star:before {
content: "\e916";
}
.icon-star-full:before {
content: "\e90a";
}
@@ -84,6 +91,9 @@
.icon-mic-disabled:before {
content: "\e912";
}
.icon-raised-hand:before {
content: "\e91e";
}
.icon-contactList:before {
content: "\e91b";
}
@@ -96,9 +106,6 @@
.icon-settings:before {
content: "\e915";
}
.icon-star:before {
content: "\e916";
}
.icon-share-desktop:before {
content: "\e917";
}
@@ -123,6 +130,7 @@
.icon-recEnable:before {
content: "\e614";
}
// FIXME not used anymore - consider removing in the next font update
.icon-presentation:before {
content: "\e603";
}

View File

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

View File

@@ -36,6 +36,19 @@
}
}
@mixin circle($diameter) {
width: $diameter;
height: $diameter;
border-radius: 50%;
}
@mixin absoluteAligning($sizeX, $sizeY) {
top: 50%;
left: 50%;
position: absolute;
@include transform(translate(-#{$sizeX / 2}, -#{$sizeY / 2}))
}
@mixin transform($func) {
-moz-transform: $func;
-ms-transform: $func;

12
css/_redirect_page.scss Normal file
View File

@@ -0,0 +1,12 @@
html, body {
width: 100%;
height:100%;
color: $defaultColor;
background: $defaultBackground;
}
.redirectPageMessage {
text-align: center;
font-size: 36px;
margin-top: 20%;
}

View File

@@ -12,12 +12,13 @@
background-color: rgba(0,0,0,0.8);
z-index: 800;
overflow: hidden;
letter-spacing: 1px;
/**
* Labels inside the side panel.
*/
label {
color: $defaultSemiDarkColor;
color: $defaultColor;
}
/**
@@ -70,16 +71,16 @@
*/
> div.title,
div.subTitle {
color: $defaultColor !important;
text-align: left;
margin: 10px 0px 10px 0px;
padding: 5px 10px 5px 10px;
}
/**
* Main title size.
*/
> div.title {
color: $defaultColor !important;
text-align: center;
font-size: 16px;
}
@@ -87,10 +88,10 @@
* Subtitle specific properties.
*/
> div.subTitle {
font-size: 12px;
background: $inputSemiBackground !important;
margin-top: 20px !important;
margin-bottom: 8px !important;
font-size: 11px;
font-weight: 500;
color: $defaultSideBarFontColor !important;
margin-left: 10%;
}
/**

View File

@@ -56,11 +56,11 @@
}
#extendedToolbar {
display: flex;
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-box;
display: -webkit-flex;
display: flex;
width: $defaultToolbarSize;
height: 100%;
top: 0px;
@@ -83,14 +83,6 @@
display: none;
}
#numberOfParticipants {
position: absolute;
top: 5px;
line-height: 13px;
font-weight: bold;
font-size: 11px;
}
#mainToolbar a.button:last-child::after {
content: none;
}
@@ -118,6 +110,11 @@
cursor: default;
}
.button.glow
{
text-shadow: 0px 0px 5px $toolbarToggleBackground;
}
a.button.unclickable:hover,
a.button.unclickable:active,
a.button.unclickable.selected{
@@ -129,6 +126,7 @@ a.button:hover,
a.button:active,
a.button.selected {
cursor: pointer;
text-decoration: none;
// sum opacity with background layer should give us 0.8
background: $toolbarSelectBackground;
}
@@ -144,6 +142,36 @@ a.button>#avatar {
margin-top: auto;
}
/**
* Round badge.
*/
.badge-round {
background-color: $toolbarBadgeBackground;
color: $toolbarBadgeColor;
font-size: 9px;
line-height: 13px;
font-weight: 700;
text-align: center;
border-radius: 50%;
min-width: 13px;
overflow: hidden;
text-overflow: ellipsis;
box-sizing: border-box;
vertical-align: middle;
// Do not inherit the font-family from the toolbar button, because it's an
// icon style.
font-family: $baseFontFamily;
}
/**
* Toolbar specific round badge.
*/
.toolbar .badge-round {
position: absolute;
right: 9px;
bottom: 9px;
}
/**
* START of slide in animation for extended toolbar.
*/

View File

@@ -1,5 +1,4 @@
/**
<<<<<<< HEAD
* Style variables
*/
$baseFontFamily: 'open_sanslight', 'Helvetica Neue', Helvetica, Arial, sans-serif;
@@ -11,20 +10,49 @@ $hangupFontSize: 2em;
*/
$defaultToolbarSize: 50px;
// Video layout.
$thumbnailIndicatorSize: 23px;
$thumbnailIndicatorBorder: 0px;
$thumbnailVideoMargin: 2px;
$thumbnailToolbarHeight: 25px;
/**
* Color variables.
*/
$defaultColor: #F1F1F1;
$defaultSemiDarkColor: #ACACAC;
$defaultSideBarFontColor: #44A5FF;
$defaultDarkColor: #4F4F4F;
$defaultBackground: #474747;
$tooltipBg: rgba(0,0,0, 0.7);
// Toolbar
$toolbarSelectBackground: rgba(0, 0, 0, .6);
$toolbarBadgeBackground: #165ECC;
$toolbarBadgeColor: #FFFFFF;
$toolbarToggleBackground: #165ECC;
// Main controls
$inputBackground: rgba(132, 132, 132, .5);
$inputSemiBackground: rgba(132, 132, 132, .8);
$inputLightBackground: #EBEBEB;
$inputBorderColor: #EBEBEB;
$buttonBackground: #44A5FF;
// Video layout.
$videoThumbnailHovered: #BFEBFF;
$videoThumbnailSelected: #165ECC;
$participantNameColor: #fff;
$thumbnailPictogramColor: #fff;
$dominantSpeakerBg: #165ecc;
$raiseHandBg: #D6D61E;
$audioLevelBg: #44A5FF;
$audioLevelShadow: rgba(9, 36, 77, 0.9);
$rateStarDefault: #ccc;
$rateStarActivity: #165ecc;
$rateStarLabelColor: #333;
/**
* Misc.
*/
@@ -34,9 +62,6 @@ $defaultWatermarkLink: '../images/watermark.png';
/**
* Z-indexes. TODO: Replace this by a function.
*/
$tooltipsZ: 901;
$toolbarZ: 900;
$overlayZ: 800;
$rateStarDefault: #ccc;
$rateStarActivity: #f6c342;
$rateStarLabelColor: #333;
$overlayZ: 800;

View File

@@ -1,5 +1,10 @@
#videoconference_page {
min-height: 100%;
}
#videospace {
display: block;
min-height: 100%;
position: absolute;
top: 0px;
left: 0px;
@@ -13,19 +18,19 @@
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
flex-direction: row;
flex-direction: row-reverse;
flex-wrap: nowrap;
justify-content: flex-end;
justify-content: flex-start;
position:absolute;
text-align:right;
height:196px;
padding: 18px;
padding: 10px 10px 10px 5px;
bottom: 0;
left: 0;
right: 20px;
right: 0;
width:auto;
border:1px solid transparent;
border: 2px solid transparent;
z-index: 5;
transition: bottom 2s;
overflow: visible !important;
@@ -43,33 +48,58 @@
#remoteVideos .videocontainer {
display: none;
position: relative;
background-color: black;
background-size: contain;
border-radius:1px;
border: 1px solid #212425;
margin: 0 $thumbnailVideoMargin;
}
#remoteVideos .videocontainer.videoContainerFocused {
/**
* The toolbar of the video thumbnail.
*/
.videocontainer__toolbar {
position: absolute;
bottom: 0;
left: 0;
z-index: 1;
width: 100%;
box-sizing: border-box; // Includes the padding in the 100% width.
height: $thumbnailToolbarHeight;
max-height: 100%;
background-color: rgba(0, 0, 0, 0.5);
padding: 0 5px 0 5px;
}
#remoteVideos .videocontainer.videoContainerFocused,
#remoteVideos .videocontainer:hover {
cursor: hand;
margin-right: $thumbnailVideoMargin - 2;
margin-left: $thumbnailVideoMargin - 2;
margin-top: -2px;
}
/**
* Focused video thumbnail.
*/
#remoteVideos .videocontainer.videoContainerFocused {
transition-duration: 0.5s;
-webkit-transition-duration: 0.5s;
-webkit-animation-name: greyPulse;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: 1;
border: 2px solid $videoThumbnailSelected !important;
box-shadow: inset 0 0 3px $videoThumbnailSelected,
0 0 3px $videoThumbnailSelected !important;
}
/**
* Hovered video thumbnail.
*/
#remoteVideos .videocontainer:hover {
border: 1px solid #c1c1c1;
}
#remoteVideos .videocontainer.videoContainerFocused {
box-shadow: inset 0 0 28px #006d91;
border: 1px solid #006d91;
}
#remoteVideos .videocontainer.videoContainerFocused:hover {
box-shadow: inset 0 0 5px #c1c1c1, 0 0 10px #c1c1c1, inset 0 0 60px #006d91;
border: 1px solid #c1c1c1;
cursor: hand;
border: 2px solid $videoThumbnailHovered;
box-shadow: inset 0 0 3px $videoThumbnailHovered,
0 0 3px $videoThumbnailHovered;
}
#localVideoWrapper {
@@ -113,7 +143,6 @@
object-fit: cover;
}
#presentation,
#sharedVideo,
#etherpad,
#localVideoWrapper>video,
@@ -132,8 +161,7 @@
height: 100%;
}
#etherpad,
#presentation {
#etherpad {
text-align: center;
}
@@ -141,47 +169,36 @@
z-index: 0;
}
#remoteVideos .videocontainer>span.focusindicator,
#remoteVideos .videocontainer>div.remotevideomenu {
position: absolute;
color: #FFFFFF;
top: 0;
left: 0;
padding: 5px 0px;
width: 25px;
font-size: 11pt;
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
border: 0px;
z-index: 2;
text-align: center;
}
#remoteVideos .videocontainer>span.focusindicator {
/**
* Positions video thumbnail display name and editor.
*/
.videocontainer .displayname,
.videocontainer .editdisplayname {
display: inline-block;
}
#remoteVideos .videocontainer>div.remotevideomenu {
display: block;
}
.videocontainer>span.displayname,
.videocontainer>input.displayname {
display: none;
position: absolute;
color: #FFFFFF;
background: rgba(0,0,0,.7);
left: 30%;
width: 40%;
color: $participantNameColor;
text-align: center;
text-overflow: ellipsis;
width: 70%;
height: 20%;
left: 15%;
top: 40%;
padding: 5px;
font-size: 11pt;
font-size: 12px;
font-weight: 100;
letter-spacing: 1px;
overflow: hidden;
white-space: nowrap;
line-height: $thumbnailToolbarHeight;
z-index: 2;
border-radius:3px;
}
/**
* Positions video thumbnail display name editor.
*/
.videocontainer .editdisplayname {
outline: none;
border: none;
background: none;
box-shadow: none;
padding: 0;
}
.videocontainer>span.status {
@@ -221,6 +238,12 @@
overflow: hidden;
}
.connection.connection_lost
{
color: #8B8B8B;
overflow: visible;
}
.connection.connection_full
{
color: #FFFFFF;/*#15A1ED*/
@@ -257,16 +280,16 @@
}
#localVideoContainer>span.status:hover,
#localVideoContainer>span.displayname:hover {
#localVideoContainer .displayname:hover {
cursor: text;
}
.videocontainer>span.status,
.videocontainer>span.displayname {
.videocontainer .displayname {
pointer-events: none;
}
.videocontainer>input.displayname {
.videocontainer .editdisplayname {
height: auto;
}
@@ -287,53 +310,103 @@
z-index: 2;
}
.videocontainer>span.audioMuted {
display: inline-block;
position: absolute;
color: #FFFFFF;
top: 0;
padding: 8px 5px;
width: 25px;
/**
* Video thumbnail toolbar icon.
*/
.videocontainer .toolbar-icon {
font-size: 8pt;
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
border: 0px;
z-index: 3;
text-align: center;
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
color: #FFFFFF;
width: 12px;
line-height: $thumbnailToolbarHeight;
height: $thumbnailToolbarHeight;
padding: 0;
border: 0;
margin: 0px 5px 0px 0px;
float: left;
}
.videocontainer>span.videoMuted {
display: inline-block;
position: absolute;
color: #FFFFFF;
top: 0;
right: 0;
padding: 8px 5px;
width: 25px;
font-size: 8pt;
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
border: 0px;
z-index: 3;
/**
* Toolbar icon internal i elements (font icons).
*/
.toolbar-icon>i {
line-height: $thumbnailToolbarHeight;
}
/**
* Toolbar icons positioned on the right.
*/
.toolbar-icon.right {
float: right;
margin: 0px 0px 0px 5px;
}
.videocontainer>span.indicator {
bottom: 0px;
position: absolute;
top: 0px;
left: 0px;
width: 25px;
height: 25px;
@include circle($thumbnailIndicatorSize);
box-sizing: border-box;
line-height: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
z-index: 3;
text-align: center;
border-radius: 50%;
background: #21B9FC;
margin: 5px;
background: $dominantSpeakerBg;
margin: 7px;
display: inline-block;
color: $thumbnailPictogramColor;
font-size: 8pt;
border: $thumbnailIndicatorBorder solid $thumbnailPictogramColor;
}
.videocontainer>#raisehandindicator {
background: $raiseHandBg;
}
/**
* Audio indicator on video thumbnails.
*/
.videocontainer>span.audioindicator {
position: absolute;
color: #FFFFFF;
font-size: 11pt;
border: 0px;
display: inline-block;
left: 6px;
top: 50%;
margin-top: -17px;
width: 6px;
height: 35px;
z-index: 2;
border: none;
.audiodot-top,
.audiodot-bottom,
.audiodot-middle {
opacity: 0;
display: inline-block;
@include circle(5px);
background: $audioLevelShadow;
margin: 1px 0 1px 0;
transition: opacity .25s ease-in-out;
-moz-transition: opacity .25s ease-in-out;
}
span.audiodot-top::after,
span.audiodot-bottom::after,
span.audiodot-middle::after {
content: "";
display: inline-block;
width: 5px;
height: 5px;
border-radius: 50%;
-webkit-filter: blur(0.5px);
filter: blur(0.5px);
background: $audioLevelBg;
}
}
#indicatoricon {
padding-top: 5px;
width: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
height: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
line-height: $thumbnailIndicatorSize - 2*$thumbnailIndicatorBorder;
}
#reloadPresentation {
@@ -366,25 +439,20 @@
width: 300px;
height: 300px;
margin: auto;
overflow: hidden;
position: relative;
}
#dominantSpeakerAudioLevel {
position: absolute;
top: 0px;
left: 0px;
z-index: 2;
visibility: inherit;
}
#mixedstream {
display:none !important;
}
#dominantSpeakerAvatar {
#dominantSpeakerAvatar,
.dynamic-shadow {
width: 200px;
height: 200px;
}
#dominantSpeakerAvatar {
top: 50px;
margin: auto;
position: relative;
@@ -394,11 +462,18 @@
background-color: #000000;
}
.userAvatar {
height: 100%;
.dynamic-shadow {
border-radius: 50%;
position: absolute;
left: 0;
border-radius: 2px;
top: 50%;
left: 50%;
margin: -100px 0 0 -100px;
transition: box-shadow 0.3s ease;
}
.userAvatar {
@include circle(60px);
@include absoluteAligning(60px, 60px);
}
.sharedVideoAvatar {
@@ -436,12 +511,44 @@
filter: grayscale(.5) opacity(0.8);
}
.remoteVideoProblemFilter {
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
.videoProblemFilter {
-webkit-filter: blur(10px) grayscale(.5) opacity(0.8);
filter: blur(10px) grayscale(.5) opacity(0.8);
}
#videoConnectionMessage {
.videoThumbnailProblemFilter {
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
#remoteConnectionMessage {
display: none;
position: absolute;
width: auto;
z-index: 1011;
font-weight: 600;
font-size: 14px;
text-align: center;
color: #FFF;
opacity: .80;
text-shadow: 0px 0px 1px rgba(0,0,0,0.3),
0px 1px 1px rgba(0,0,0,0.3),
1px 0px 1px rgba(0,0,0,0.3),
0px 0px 1px rgba(0,0,0,0.3);
background: rgba(0,0,0,.5);
border-radius: 5px;
padding: 5px;
padding-left: 10px;
padding-right: 10px;
}
#localConnectionMessage {
display: none;
position: absolute;
width: 100%;

View File

@@ -22,6 +22,8 @@
@import 'toastr';
@import 'base';
@import 'overlay/overlay';
@import 'modals/dialog';
@import 'modals/feedback/feedback';
@import 'videolayout_default';
@import 'jquery-impromptu';
@import 'modaldialog';
@@ -38,8 +40,9 @@
@import 'toolbars';
@import 'side_toolbar_container';
@import 'device_settings_dialog';
@import 'feedback';
@import 'jquery.contextMenu';
@import 'keyboard-shortcuts';
@import 'redirect_page';
/* Modules END */

53
css/modals/_dialog.scss Normal file
View File

@@ -0,0 +1,53 @@
.dialog{
visibility: visible;
height: auto;
p {
color: $defaultDarkColor;
}
textarea {
background: none;
border: 1px solid $inputBorderColor;
}
.aui-dialog2-content:last-child {
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
}
.aui-dialog2-content:first-child {
border-top-right-radius: 5px;
border-top-left-radius: 5px;
}
.aui-dialog2-footer{
border-top: 0;
border-radius: 0;
padding-top: 0;
background: none;
border: none;
height: auto;
margin-top: 10px;
}
.aui-button {
height: 28px;
font-size: 12px;
padding: 3px 6px 3px 6px;
border: none;
box-shadow: none;
outline: none;
&_close {
font-weight: 400 !important;
color: $buttonBackground;
background: none !important;
:hover {
text-decoration: underline;
}
}
&_submit {
font-weight: 700 !important;
color: $defaultColor;
background: $buttonBackground;
border-radius: 3px;
}
}
}

View File

@@ -33,6 +33,8 @@
}
.shake-rotate {
display: inline-block;
-webkit-animation-duration: .4s;
animation-duration: .4s;
-webkit-animation-iteration-count: infinite;
@@ -43,65 +45,64 @@
animation-timing-function: ease-in-out
}
.text-center {
text-align: center;
}
.feedbackDetails textarea {
resize: vertical;
min-height: 100px;
}
.feedback-rating {
line-height: 1.2;
padding: 20px 0;
.feedback {
h2 {
font-weight: 400;
font-size: 24px;
line-height: 1.2;
padding: auto;
margin: auto;
border: none;
}
p {
margin-top: 10px;
margin-left: 0px;
margin-bottom: 0px;
margin-right: 0px;
font-weight: 400;
font-size: 14px;
}
.star-label {
font-size: 16px;
color: $rateStarLabelColor;
&__content {
text-align: center;
textarea {
text-align: left;
min-height: 80px;
width: 100%;
}
}
&__footer {
.star-btn {
color: $rateStarDefault;
font-size: 36px;
position: relative;
cursor: pointer;
outline: none;
text-decoration: none;
@include transition(all .2s ease);
&.starHover,
&.active,
&:hover {
color: $rateStarActivity;
color: #287ade;
outline: 0;
}
}
&__rating {
line-height: 1.2;
padding: 20px 0;
.fa {
top: -6px;
}
};
&.rated:hover .fa {
top: 0;
p {
margin: 10px 0 0;
}
.fa {
.star-label {
font-size: 16px;
color: $rateStarLabelColor;
}
.star-btn {
color: $rateStarDefault;
font-size: 36px;
position: relative;
cursor: pointer;
outline: none;
text-decoration: none;
@include transition(all .2s ease);
&.starHover,
&.active,
&:hover {
color: $rateStarActivity;
> i:before {
content: "\e90a";
}
};
}
}
}

View File

@@ -9,6 +9,11 @@
background: linear-gradient(transparent, #000);
opacity: 0.8;
&.solidBG {
background: $defaultBackground;
opacity: 1;
}
&__content {
position: absolute;
width: 400px;
@@ -33,4 +38,4 @@
color: #333;
}
}
}
}

Binary file not shown.

View File

@@ -41,4 +41,7 @@
<glyph unicode="&#xe91b;" glyph-name="contactList" d="M704 746c-46 0-86-38-86-84s40-86 86-86 86 40 86 86-40 84-86 84zM704 512c-82 0-150 68-150 150s68 148 150 148 150-66 150-148-68-150-150-150zM320 746c-46 0-86-38-86-84s40-86 86-86 86 40 86 86-40 84-86 84zM320 512c-82 0-150 68-150 150s68 148 150 148 150-66 150-148-68-150-150-150zM918 278v52c0 24-110 76-214 76-46 0-90-12-128-24 14-16 22-32 22-52v-52h320zM534 278v52c0 24-110 76-214 76s-214-52-214-76v-52h428zM704 470c92 0 278-48 278-140v-116h-940v116c0 92 186 140 278 140 52 0 130-16 192-44 62 28 140 44 192 44z" />
<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" />
<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" />
<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" />
<glyph unicode="&#xe91f;" glyph-name="menu-up" d="M512 682l256-256-60-60-196 196-196-196-60 60z" />
<glyph unicode="&#xe920;" glyph-name="menu-down" d="M708 658l60-60-256-256-256 256 60 60 196-196z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -69,8 +69,10 @@
"tags": [
"account_circle"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 11,
"order": 60,
@@ -93,8 +95,10 @@
"tags": [
"autorenew"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 68,
"order": 84,
@@ -117,8 +121,10 @@
"tags": [
"call_end"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 122,
"order": 63,
@@ -141,8 +147,10 @@
"tags": [
"chat_bubble_outline"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 148,
"order": 61,
@@ -165,8 +173,10 @@
"tags": [
"cloud_download"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 164,
"order": 99,
@@ -189,8 +199,10 @@
"tags": [
"mode_edit"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 185,
"order": 89,
@@ -213,8 +225,10 @@
"tags": [
"description"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 206,
"order": 85,
@@ -237,8 +251,10 @@
"tags": [
"dialer_sip"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 215,
"order": 95,
@@ -261,8 +277,10 @@
"tags": [
"eject"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 242,
"order": 98,
@@ -275,6 +293,58 @@
"setId": 2,
"iconIdx": 243
},
{
"icon": {
"paths": [
"M512 342l256 256-60 60-196-196-196 196-60-60z"
],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"expand_less"
],
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 256,
"order": 106,
"ligatures": "expand_less",
"prevSize": 32,
"code": 59679,
"name": "menu-up"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 257
},
{
"icon": {
"paths": [
"M708 366l60 60-256 256-256-256 60-60 196 196z"
],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"expand_more"
],
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 257,
"order": 107,
"ligatures": "expand_more",
"prevSize": 32,
"code": 59680,
"name": "menu-down"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 258
},
{
"icon": {
"paths": [
@@ -285,8 +355,10 @@
"tags": [
"fullscreen"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 350,
"order": 94,
@@ -309,8 +381,10 @@
"tags": [
"fullscreen_exit"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 351,
"order": 92,
@@ -333,8 +407,10 @@
"tags": [
"star"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 363,
"order": 101,
@@ -357,8 +433,10 @@
"tags": [
"lock_open"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 473,
"order": 66,
@@ -381,8 +459,10 @@
"tags": [
"lock_outline"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 474,
"order": 65,
@@ -405,8 +485,10 @@
"tags": [
"sync"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 482,
"order": 67,
@@ -429,8 +511,10 @@
"tags": [
"mic"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 492,
"order": 68,
@@ -453,8 +537,10 @@
"tags": [
"mic_none"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 493,
"order": 69,
@@ -477,8 +563,10 @@
"tags": [
"mic_off"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 494,
"order": 70,
@@ -491,6 +579,32 @@
"setId": 2,
"iconIdx": 495
},
{
"icon": {
"paths": [
"M982 234v620c0 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 104v-508c0-36 28-64 64-64s64 28 64 64v300h42v-406c0-36 28-64 64-64s64 28 64 64v406h42v-364c0-36 28-64 64-64s64 28 64 64v364h44v-236c0-36 28-64 64-64s64 28 64 64z"
],
"isMulticolor": false,
"isMulticolor2": false,
"tags": [
"pan_tool"
],
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 539,
"order": 105,
"ligatures": "pan_tool",
"prevSize": 32,
"code": 59678,
"name": "raised-hand"
},
"setIdx": 0,
"setId": 2,
"iconIdx": 540
},
{
"icon": {
"paths": [
@@ -501,8 +615,10 @@
"tags": [
"people_outline"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 549,
"order": 100,
@@ -525,8 +641,10 @@
"tags": [
"person_add"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 559,
"order": 87,
@@ -549,8 +667,10 @@
"tags": [
"play_circle_outline"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 590,
"order": 82,
@@ -573,8 +693,10 @@
"tags": [
"settings"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 665,
"order": 81,
@@ -597,8 +719,10 @@
"tags": [
"star_border"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 717,
"order": 76,
@@ -621,8 +745,10 @@
"tags": [
"tv"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 783,
"order": 93,
@@ -645,8 +771,10 @@
"tags": [
"videocam"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 798,
"order": 77,
@@ -669,8 +797,10 @@
"tags": [
"videocam_off"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 799,
"order": 78,
@@ -693,8 +823,10 @@
"tags": [
"volume_up"
],
"grid": 0
"grid": 0,
"attrs": []
},
"attrs": [],
"properties": {
"id": 821,
"order": 79,

View File

@@ -120,26 +120,32 @@
</li>
</ul>
</span>
<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">
<span id="numberOfParticipants"></span>
<a class="button icon-contactList" id="toolbar_contact_list" shortcut="contactlistpopover">
<span class="badge-round">
<span id="numberOfParticipants"></span>
</span>
</a>
<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">
<span id="unreadMessages"></span>
<!--a class="button icon-link" id="toolbar_button_link"></a-->
<a class="button icon-chat" id="toolbar_button_chat" shortcut="toggleChatPopover">
<span class="badge-round">
<span id="unreadMessages"></span>
</span>
</a>
<a class="button" id="toolbar_button_record" data-container="body" data-toggle="popover" data-placement="right" style="display: none"></a>
<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>
<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>
<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">
<a class="button" id="toolbar_button_record" style="display: none"></a>
<a class="button icon-security" id="toolbar_button_security"></a>
<a class="button icon-share-doc" id="toolbar_button_etherpad"></a>
<a class="button icon-shared-video" id="toolbar_button_sharedvideo" style="display: none">
<ul id="sharedVideoMutedPopup" class="loginmenu extendedToolbarPopup">
<li data-i18n="[html]toolbar.sharedVideoMutedPopup"></li>
</ul>
</a>
<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>
<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>
<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>
<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>
<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>
<a class="button icon-feedback" id="feedbackButton" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[content]feedback"></a>
<a class="button icon-telephone" id="toolbar_button_sip" style="display: none"></a>
<a class="button icon-dialpad" id="toolbar_button_dialpad" style="display: none"></a>
<a class="button icon-settings" id="toolbar_button_settings"></a>
<a class="button icon-raised-hand" id="toolbar_button_raisehand" shortcut="raiseHandPopover"></a>
<a class="button icon-full-screen" id="toolbar_button_fullScreen" shortcut="toggleFullscreenPopover"></a>
<a class="button icon-toggle-filmstrip" id="toolbar_film_strip" data-container="body" shortcut="filmstripPopover"></a>
<a class="button icon-feedback" id="feedbackButton"></a>
<div id="sideToolbarContainer">
<div id="profile_container" class="sideToolbarContainer__inner">
<div class="title" data-i18n="profile.title"></div>
@@ -206,13 +212,11 @@
<input type="checkbox" id="followMeCheckBox">
<label class="followMeLabel" for="followMeCheckBox" data-i18n="settings.followMe"></label>
</div>
<a id="downloadlog" data-container="body" data-toggle="popover" data-placement="right" data-i18n="[data-content]downloadlogs" ><i class="icon-download"></i></a>
</div>
</div>
</div>
<div id="videospace">
<div id="largeVideoContainer" class="videocontainer">
<div id="presentation"></div>
<div id="sharedVideo"><div id="sharedVideoIFrame"></div></div>
<div id="etherpad"></div>
<a target="_new"><div class="watermark leftwatermark"></div></a>
@@ -221,13 +225,14 @@
<span data-i18n="poweredby"></span> jitsi.org
</a>
<div id="dominantSpeaker">
<div class="dynamic-shadow"></div>
<img id="dominantSpeakerAvatar" src=""/>
<canvas id="dominantSpeakerAudioLevel"></canvas>
</div>
<span id="remoteConnectionMessage"></span>
<div id="largeVideoWrapper">
<video id="largeVideo" muted="true" autoplay></video>
</div>
<span id="videoConnectionMessage"></span>
<span id="localConnectionMessage"></span>
<span id="videoResolutionLabel">HD</span>
<span id="recordingLabel" class="centeredVideoLabel">
<span id="recordingLabelText"></span>
@@ -236,12 +241,12 @@
</div>
<div id="remoteVideos">
<span id="localVideoContainer" class="videocontainer">
<span id="localVideoContainer" class="videocontainer videocontainer_small">
<span id="localVideoWrapper">
<!--<video id="localVideo" autoplay muted></video> - is now per stream generated -->
</span>
<audio id="localAudio" autoplay muted></audio>
<span class="focusindicator"></span>
<div class="videocontainer__toolbar"></div>
</span>
<audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
<audio id="userLeft" src="sounds/left.wav" preload="auto"></audio>
@@ -255,5 +260,6 @@
</ul>
</div>
</div>
<div id="aui-feedback-dialog" class="dialog feedback aui-layer aui-dialog2 aui-dialog2-medium" style="display: none;"></div>
</body>
</html>

View File

@@ -1,4 +1,4 @@
var interfaceConfig = {
var interfaceConfig = { // eslint-disable-line no-unused-vars
CANVAS_EXTRA: 104,
CANVAS_RADIUS: 0,
SHADOW_COLOR: '#ffffff',
@@ -20,18 +20,28 @@ var interfaceConfig = {
// the toolbar buttons line is intentionally left in one line, to be able
// to easily override values or remove them using regex
MAIN_TOOLBAR_BUTTONS: ['microphone', 'camera', 'desktop', 'invite', 'hangup'], // jshint ignore:line
TOOLBAR_BUTTONS: ['profile', 'authentication', 'microphone', 'camera', 'desktop', 'recording', 'security', 'invite', 'chat', 'etherpad', 'sharedvideo', 'fullscreen', 'sip', 'dialpad', 'settings', 'hangup', 'filmstrip', 'contacts'], // jshint ignore:line
TOOLBAR_BUTTONS: ['profile', 'authentication', 'microphone', 'camera', 'desktop', 'recording', 'security', 'raisehand', 'chat', 'etherpad', 'sharedvideo', 'fullscreen', 'sip', 'dialpad', 'settings', 'hangup', 'filmstrip', 'contacts'], // jshint ignore:line
SETTINGS_SECTIONS: ['language', 'devices', 'moderator'],
// Determines how the video would fit the screen. 'both' would fit the whole
// screen, 'height' would fit the original video height to the height of the
// screen, 'width' would fit the original video width to the width of the
// screen respecting ratio.
VIDEO_LAYOUT_FIT: 'both',
SHOW_CONTACTLIST_AVATARS: false,
/**
* Whether to only show the filmstrip (and hide the toolbar).
*/
filmStripOnly: false,
RANDOM_AVATAR_URL_PREFIX: false,
RANDOM_AVATAR_URL_SUFFIX: false,
FILM_STRIP_MAX_HEIGHT: 120
};
FILM_STRIP_MAX_HEIGHT: 120,
LOCAL_THUMBNAIL_RATIO_WIDTH: 16,
LOCAL_THUMBNAIL_RATIO_HEIGHT: 9,
REMOTE_THUMBNAIL_RATIO_WIDTH: 1,
REMOTE_THUMBNAIL_RATIO_HEIGHT: 1,
// Enables feedback star animation.
ENABLE_FEEDBACK_ANIMATION: false,
DISABLE_FOCUS_INDICATOR: false,
AUDIO_LEVEL_PRIMARY_COLOR: "rgba(255,255,255,0.7)",
AUDIO_LEVEL_SECONDARY_COLOR: "rgba(255,255,255,0.4)"
};

View File

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

15
lang/languages-pl.json Normal file
View File

@@ -0,0 +1,15 @@
{
"en": "Angielski",
"bg": "Bułgarski",
"de": "Niemiecki",
"es": "Hiszpański",
"fr": "Francuski",
"hy": "Ormiański",
"it": "Włoski",
"oc": "Prowansalski",
"ptBR": "portugalski (brazylijski)",
"sk": "Słowacki",
"sl": "Słoweński",
"sv": "Szwedzki",
"tr": "Turecki"
}

View File

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

View File

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

View File

@@ -1,17 +1,15 @@
{
"contactlist": "Kontaktliste",
"contactlist": "Im Gespräch",
"connectionsettings": "Verbindungseinstellungen",
"poweredby": "Betrieben von",
"downloadlogs": "Log herunterladen",
"feedback": "Wir freuen uns auf Ihr Feedback!",
"roomUrlDefaultMsg": "Die Konferenz wird erstellt...",
"participant": "Teilnehmer",
"me": "ich",
"speaker": "Sprecher",
"raisedHand": "Möchte sprechen",
"defaultNickname": "Bsp: Heidi Blau",
"defaultLink": "Bsp.: __url__",
"calling": "Rufe __name__ an...",
"callingName": "__name__",
"userMedia": {
"react-nativeGrantPermissions": "Bitte Berechtigungen zur Verwendung der Kamera und des Mikrofons durch anwählen von <b><i>Erlauben</i></b>",
"chromeGrantPermissions": "Bitte Berechtigungen zur Verwendung der Kamera und des Mikrofons durch anwählen von <b><i>Erlauben</i></b>",
@@ -27,7 +25,7 @@
"raiseHand": "Heben Sie Ihre Hand.",
"pushToTalk": "Drücken um zu sprechen.",
"toggleScreensharing": "Zwischen Kamera und Bildschirmfreigabe wechseln.",
"toggleFilmstrip": "Videovorschau anzeigen oder verstecken.",
"toggleFilmstrip": "Videos anzeigen oder verbergen.",
"toggleShortcuts": "Hilfe-Menu anzeigen oder verdecken.",
"focusLocal": "Lokales Video fokussieren.",
"focusRemote": "Andere Videos fokussieren.",
@@ -37,7 +35,7 @@
},
"welcomepage": {
"go": "Los",
"roomname": "Raumnamen eingeben",
"roomname": "Konferenzname eingeben",
"disable": "Diesen Hinweis nicht mehr anzeigen",
"feature1": {
"title": "Einfach zu benutzen",
@@ -49,7 +47,7 @@
},
"feature3": {
"title": "Open Source",
"content": "__app__ steht unter der Apache Lizenz. Es steht ihnen frei __app__ gemäß dieser Lizenz herunterzuladen, zu verändern oder zu verbreiten."
"content": "__app__ steht unter der Apache Lizenz. Es steht ihnen frei __app__ gemäss dieser Lizenz herunterzuladen, zu verändern oder zu verbreiten."
},
"feature4": {
"title": "Unbegrenzte Anzahl Benutzer",
@@ -76,16 +74,16 @@
"mute": "Stummschaltung aktivieren / deaktivieren",
"videomute": "Kamera starten / stoppen",
"authenticate": "Anmelden",
"lock": "Raum schützen / Schutz aufheben",
"lock": "Konferenz schützen / Schutz aufheben",
"invite": "Andere einladen",
"chat": "Chat öffnen / schließen",
"etherpad": "Geteiltes Dokument",
"sharedvideo": "Ein YouTube-Video teilen",
"chat": "Chat öffnen / schliessen",
"etherpad": "Dokument teilen",
"sharedvideo": "YouTube-Video teilen",
"sharescreen": "Bildschirm freigeben",
"fullscreen": "Vollbildmodus aktivieren / deaktivieren",
"sip": "SIP Nummer anrufen",
"Settings": "Einstellungen",
"hangup": "Auflegen",
"hangup": "Konferenz verlassen",
"login": "Anmelden",
"logout": "Abmelden",
"dialpad": "Tastenblock anzeigen",
@@ -93,17 +91,19 @@
"micMutedPopup": "Ihr Mikrofon wurde stumm geschaltet damit das<br/>geteilte Video genossen werden kann.",
"unableToUnmutePopup": "Die Stummschaltung kann nicht aufgehoben werden während das geteilte Video abgespielt wird.",
"cameraDisabled": "Keine Kamera verfügbar",
"micDisabled": "Kein Mikrofon verfügbar"
"micDisabled": "Kein Mikrofon verfügbar",
"filmstrip": "Videos anzeigen / verbergen",
"raiseHand": "Hand erheben um zu sprechen"
},
"bottomtoolbar": {
"chat": "Chat öffnen / schließen",
"filmstrip": "Videovorschau anzeigen / verstecken",
"contactlist": "Kontaktliste öffnen / schließen"
"chat": "Chat öffnen / schliessen",
"filmstrip": "Videos anzeigen / verbergen",
"contactlist": "Kontaktliste öffnen / schliessen"
},
"chat": {
"nickname": {
"title": "Nickname im Eingabefeld eingeben",
"popover": "Einen Namen auswählen"
"title": "Name eingeben",
"popover": "Name"
},
"messagebox": "Text eingeben..."
},
@@ -111,20 +111,29 @@
"title": "Einstellungen",
"update": "Aktualisieren",
"name": "Name",
"startAudioMuted": "Stumm beitreten",
"startVideoMuted": "Ohne Video beitreten",
"selectCamera": "Kamera auswählen",
"selectMic": "Mikrofon auswählen",
"selectAudioOutput": "Audio-Ausgabe auswählen",
"followMe": "Follow-me aktivieren",
"startAudioMuted": "Alle Teilnehmer treten stumm geschaltet bei",
"startVideoMuted": "Alle Teilnehmer treten ohne Video bei",
"selectCamera": "Kamera",
"selectMic": "Mikrofon",
"selectAudioOutput": "Audioausgabe",
"followMe": "Follow-me für alle Teilnehmer",
"noDevice": "Kein",
"noPermission": "Keine Berechtigung um das Gerät zu verwenden",
"avatarUrl": "Avatar URL"
"cameraAndMic": "Kamera und Mikrofon",
"moderator": "MODERATOR",
"password": "PASSWORT SETZEN",
"audioVideo": "AUDIO UND VIDEO",
"setPasswordLabel": "Konferenz mit einem Passwort schützen."
},
"profile": {
"title": "PROFIL",
"setDisplayNameLabel": "Anzeigename festlegen",
"setEmailLabel": "E-Mail Adresse für Gravatar"
},
"videothumbnail": {
"editnickname": "Klicken, um den Anzeigenamen zu bearbeiten",
"moderator": "Besitzer dieser Konferenz",
"videomute": "Teilnehmer hat die Kamera pausiert.",
"videomute": "Teilnehmer hat die Kamera pausiert",
"mute": "Teilnehmer ist stumm geschaltet",
"kick": "Hinauswerfen",
"muted": "Stummgeschaltet",
@@ -172,6 +181,7 @@
"connectError": "Oh! Es hat etwas nicht geklappt und der Konferenz konnte nicht beigetreten werden.",
"connectErrorWithMsg": "Oh! Es hat etwas nicht geklappt und der Konferenz konnte nicht beigetreten werden: __msg__",
"connecting": "Verbindung wird hergestellt",
"copy": "Kopieren",
"error": "Fehler",
"detectext": "Fehler bei der Erkennung der Bildschirmfreigabeerweiterung.",
"failtoinstall": "Die Bildschirmfreigabeerweiterung konnte nicht installiert werden.",
@@ -183,8 +193,8 @@
"lockMessage": "Die Konferenz konnte nicht gesperrt werden.",
"warning": "Warnung",
"passwordNotSupported": "Passwörter für Räume werden nicht unterstützt.",
"sorry": "Entschuldigung",
"internalError": "Interner Anwendungsfehler [setRemoteDescription]",
"internalErrorTitle": "Interner Fehler",
"internalError": "Ups! Es ist etwas schiefgegangen. Der Fehler [setRemoteDescription] ist aufgetreten.",
"unableToSwitch": "Der Videodatenstrom kann nicht gewechselt werden.",
"SLDFailure": "Oh! Die Stummschaltung konnte nicht aktiviert werden. (SLD Fehler)",
"SRDFailure": "Oh! Das Video konnte nicht gestoppt werden. (SRD Fehler)",
@@ -206,7 +216,7 @@
"logoutTitle": "Abmelden",
"logoutQuestion": "Sind Sie sicher, dass Sie sich abmelden und die Konferenz verlassen möchten?",
"sessTerminated": "Sitzung beendet",
"hungUp": "Anruf beendet",
"hungUp": "Konferenz beendet",
"joinAgain": "Erneut beitreten",
"Share": "Teilen",
"Save": "Speichern",
@@ -215,27 +225,29 @@
"Dial": "Wählen",
"sipMsg": "Geben Sie eine SIP Nummer ein",
"passwordCheck": "Sind Sie sicher, dass Sie das Passwort entfernen möchten?",
"passwordMsg": "Passwort setzen, um den Raum zu schützen",
"Invite": "Einladen",
"shareLink": "Teilen Sie diesen Link mit jedem den Sie einladen möchten",
"passwordMsg": "Passwort setzen um die Konferenz zu schützen",
"shareLink": "Diesen Link kopieren und teilen",
"settings1": "Konferenz einrichten",
"settings2": "Teilnehmer treten stummgeschaltet bei",
"settings3": "Nickname erforderlich<br/><br/>Setzen Sie ein Passwort, um den Raum zu schützen:",
"yourPassword": "Ihr Passwort",
"settings3": "Name erforderlich<br/><br/>Setzen Sie ein Passwort, um die Konferenz zu schützen:",
"yourPassword": "Neues Passwort eingeben",
"Back": "Zurück",
"serviceUnavailable": "Dienst nicht verfügbar",
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
"Yes": "Ja",
"reservationError": "Fehler im Reservationssystem",
"reservationErrorMsg": "Fehler, Nummer: __code__, Nachricht: __msg__",
"password": "Passwort",
"password": "Passwort eingeben",
"userPassword": "Benutzerpasswort",
"token": "Token",
"tokenAuthFailed": "Anmeldung am XMPP-Server fehlgeschlagen: ungültiges Token",
"tokenAuthFailedTitle": "Authentifizierungsfehler",
"tokenAuthFailed": "Sie sind nicht berechtigt dieser Konferenz beizutreten.",
"displayNameRequired": "Geben Sie Ihren Anzeigenamen ein",
"extensionRequired": "Erweiterung erforderlich:",
"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.",
"feedbackQuestion": "Wie war der Anruf?",
"rateExperience": "Bitte bewerten Sie diese Konferenz.",
"feedbackHelp": "Ihr Feedback hilft uns die Qualität der Konferenzen zu verbessern.",
"feedbackQuestion": "Anmerkungen zur Konferenz.",
"thankYou": "Danke für die Verwendung von __appName__!",
"sorryFeedback": "Tut uns leid. Möchten Sie uns mehr mitteilen?",
"liveStreaming": "Live-Streaming",
@@ -253,12 +265,17 @@
"cameraUnsupportedResolutionError": "Die Kamera unterstützt die erforderliche Auflösung nicht.",
"cameraUnknownError": "Die Kamera kann aus einem unbekannten Grund nicht verwendet werden.",
"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.",
"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.",
"cameraNotFoundError": "Kamera nicht gefunden.",
"cameraConstraintFailedError": "Ihre Kamera erfüllt die notwendigen Anforderungen nicht.",
"micUnknownError": "Das Mikrofon kann aus einem unbekannten Grund nicht verwendet werden.",
"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.",
"micNotFoundError": "Das angeforderte Mikrofon konnte nicht gefunden werden.",
"micConstraintFailedError": "Ihr Mikrofon erfüllt die notwendigen Anforderungen nicht."
"micNotFoundError": "Mikrofon nicht gefunden.",
"micConstraintFailedError": "Ihr Mikrofon erfüllt die notwendigen Anforderungen nicht.",
"micNotSendingData": "Das Mikrofon kann nicht verwendet werden. Bitte wählen Sie ein anderes Mikrofon in den Einstellungen oder laden Sie die Konferenz neu.",
"cameraNotSendingData": "Die Kamera kann nicht verwendet werden. Bitte wählen Sie eine andere Kamera in den Einstellungen oder laden Sie die Konferenz neu.",
"goToStore": "Zum Store",
"externalInstallationTitle": "Erweiterung erforderlich",
"externalInstallationMsg": "Die Bildschirmfreigabeerweiterung muss installiert werden."
},
"\u0005dialog": {},
"email": {

344
lang/main-pl.json Normal file
View File

@@ -0,0 +1,344 @@
{
"contactlist": "w trakcie rozmowy",
"connectionsettings": "ustawienia połączenia",
"poweredby": "Uruchomiono",
"feedback": "jaka jest twoja opinia ?",
"roomUrlDefaultMsg": "otwarto twoją konferencję",
"me": "to ja",
"speaker": "głośnik",
"raisedHand": "Chcesz się odezwać ?",
"defaultNickname": "np. Ziutek Kowalski",
"defaultLink": "np. _url_",
"callingName": "_nazwa_",
"userMedia": {
"react-nativeGrantPermissions": "",
"chromeGrantPermissions": "",
"androidGrantPermissions": "",
"firefoxGrantPermissions": "wyraź zgodę na użycie kamery i mikrofonu naciskając <b><i>Share Selected Device</i></b> przycisk",
"operaGrantPermissions": "wyraź zgodę na użycie kamery i mikrofonu naciskając <b><i>Allow</i></b> przycisk",
"iexplorerGrantPermissions": "",
"safariGrantPermissions": "wyraź zgodę na użycie kamery i mikrofonu naciskając <b><i>OK</i></b> przycisk",
"nwjsGrantPermissions": "wyraź zgodę na użycie kamery i mikrofonu"
},
"keyboardShortcuts": {
"keyboardShortcuts": "Skróty klawiaturowe:",
"raiseHand": "Unieś rękę.",
"pushToTalk": "naciśnij i mów",
"toggleScreensharing": "Przełączanie pomiędzy kamerą i wspóldzieleniem ekranu",
"toggleFilmstrip": "Pokaż lub ukryj klipy wideo",
"toggleShortcuts": "Pokaż lub ukryj pasek pomocy.",
"focusLocal": "Przełącz na lokalne wideo.",
"focusRemote": "Przełącz na któreś ze zdalnych wideo.",
"toggleChat": "Otwórz lub zamknij panel czat.",
"mute": "Wyłącz lub włącz mikrofon.",
"videoMute": "Start lub stop lokalne wideo."
},
"welcomepage": {
"go": "IDŹ",
"roomname": "Podaj nazwę sali konferencyjnej",
"disable": "nie pokazuj ponownie",
"feature1": {
"title": "użyj",
"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"
},
"feature2": {
"title": "za mała przepustowość",
"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. "
},
"feature3": {
"title": "Open source",
"content": "_app_ oparta jest na Apache License. Możesz swobodnie pobierać ją, używać, modyfikować i dzielić się nią."
},
"feature4": {
"title": "Nieograniczona liczba użytkowników",
"content": "Liczba użytkowników czy uczestników konferencji nie jest ograniczona. Determinuje ją moc serwera i dostępna przepustowość lącza."
},
"feature5": {
"title": "Współdzielenie ekranu",
"content": "Z łatwością podzielisz się ekranem z innymi. _app_ jest idealnym narzędziem do prezentacji, nauczania i udzielania zdalnej pomocy technicznej."
},
"feature6": {
"title": "Sale bezpieczne.",
"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ę."
},
"feature7": {
"title": "Współdzielenie uwag.",
"content": "_app_ zawiera Etherpad, współdzielony edytor tekstu doskonały dla redakcji zespołowych artykułów czy komentarzy."
},
"feature8": {
"title": "Statystyki użycia.",
"content": "Analizuj uczestników konferencji z łatwościa integrując dane z Piwik i Google Analitics i innymi systemami monitorującymi."
}
},
"toolbar": {
"mute": "Wycisz / Pogłośnij",
"videomute": "Kamera start / stop ",
"authenticate": "Uwierzytelnianie",
"lock": "Zamknij / Otwórz salę",
"invite": "Zaproś innych",
"chat": "",
"etherpad": "Udostępniaj dokument",
"sharedvideo": "Udostępniaj wideo w Youtube",
"sharescreen": "Udostępnij ekran",
"fullscreen": "Otwórz / Zamknij pełny ekran",
"sip": "Wykręć numer SIP",
"Settings": "",
"hangup": "Rozłącz",
"login": "Zaloguj",
"logout": "",
"dialpad": "Wyświetl panel wybierania",
"sharedVideoMutedPopup": "Współdzielone wideo zostało wyciszone i <br/> możesz zacząć rozmawiać z innymi.",
"micMutedPopup": "Mikrofon został wyłączony i <br/> możesz spokojnie konsumować współdzielone wideo",
"unableToUnmutePopup": "Nie możesz pogłośnić audio podczas współużytkowania wideo",
"cameraDisabled": "Kamera nie jest dostępna",
"micDisabled": "Mikrofon nie jest dostępny",
"filmstrip": "",
"raiseHand": "Podnieś rękę chcąc zabrać głos"
},
"bottomtoolbar": {
"chat": "Otwórz / Zamknij Czat",
"filmstrip": "Pokaż / Ukryj klipy wideo",
"contactlist": "Otwórz / Zamknij spis kontaktów"
},
"chat": {
"nickname": {
"title": "Podaj swój nick poniżej",
"popover": "Wybierz swój nick"
},
"messagebox": "Umieść tekst...."
},
"settings": {
"title": "Ustawienia",
"update": "Aktualizacja",
"name": "Nazwa",
"startAudioMuted": "Wszyscy się wyciszyli",
"startVideoMuted": "Wszyscy się ukryli",
"selectCamera": "Kamera",
"selectMic": "Mikrofon",
"selectAudioOutput": "Wyjście audio",
"followMe": "Wszyscy za mną",
"noDevice": "Brak",
"noPermission": "Nie ma zgody na użycie urządzenia",
"cameraAndMic": "Kamera i Mikrofon",
"moderator": "MODERATOR",
"password": "USTAW HASŁO",
"audioVideo": "AUDIO I WIDEO",
"setPasswordLabel": "Zamknij salę konferencyjną z hasłem"
},
"profile": {
"title": "PROFIL",
"setDisplayNameLabel": "Podaj swoją wyświetlaną nazwę",
"setEmailLabel": "Ustaw email swojego gravatara"
},
"videothumbnail": {
"editnickname": "Kliknij <br/>celem edycji swojej nazwy",
"moderator": "Gospodarz <br/>tej konferencji",
"videomute": "Uczestnik <br/>wyłączyl kamerę",
"mute": "Uczestnik ma wyciszone audio",
"kick": "Kick out",
"muted": "Wyciszony",
"domute": "Wyciszenie",
"flip": "Odwrócenie"
},
"connectionindicator": {
"bitrate": "Szybkość transmisji:",
"packetloss": "Strata pakietów:",
"resolution": "Rozdzielczość:",
"less": "Pokaż mniej",
"more": "Pokaż więcej",
"address": "Adres:",
"remoteport": "Zdalny port:Zdalne porty:",
"remoteport_plural_2": "",
"remoteport_plural_5": "",
"localport": "Lokalny port:Lokalne porty:",
"localport_plural_2": "",
"localport_plural_5": "",
"localaddress": "Lokalny adres:Lokalne Adresy:",
"localaddress_plural_2": "",
"localaddress_plural_5": "",
"remoteaddress": "Zdalny adres:Zdalne adresy:",
"remoteaddress_plural_2": "",
"remoteaddress_plural_5": "",
"transport": "Przekazywanie:",
"bandwidth": "Zakładana przepustowość:",
"na": "Po informację o połączeniu wróć gdy wystartuje konferencja"
},
"notify": {
"disconnected": "rozłączone",
"moderator": "Prawa moderatora przydzielone!",
"connected": "połączono",
"somebody": "Ktoś",
"me": "To ja",
"focus": "Fokus konferencji",
"focusFail": "_składnik_nie dostępny - zastosuj w _ms_sek",
"grantedTo": "Prawa moderatora przyznane _to_!",
"grantedToUnknown": "Prawa Moderatora przyznane $t(somebody)!",
"muted": "Masz wyciszony mikrofon",
"mutedTitle": "Jesteś wyciszony!",
"raisedHand": "Możesz mówić."
},
"dialog": {
"kickMessage": "Ocho! Zostałeś wyproszony z konferencji!",
"popupError": "Twoja przeglądarka blokuje wyskakujące okienka z tej witryny. Proszę, zmień w ustawieniach przeglądarki.",
"passwordError": "Ta konwersacja aktualnie jest zabezpieczona hasłem. Tylko gospodarz konferencji może zakładać hasło.",
"passwordError2": "Ta rozmowa nie jest zabezpieczona hasłem. Tylko gospodarz konferencji może ustanowić hasło zabezpieczające.",
"connectError": "Ocho! Cos poszło nie tak, nie można podłaczyć się do tej konferencji.",
"connectErrorWithMsg": "Ocho! Coś poszło nie tak i nie można podłączyć się do tej konferencji:_msg_",
"connecting": "",
"copy": "Kopiuj",
"error": "",
"detectext": "Błąd podczas rozpoznania rozszerzenia wspóldzielenia ekranu.",
"failtoinstall": "Instalacja współdzielenia ekranu nie powiodła się.",
"failedpermissions": "Brak akceptacji dla użycia kamery i mikrofonu",
"bridgeUnavailable": "Jitsi Videobridge aktualnie jest niedostępne. Proszę, spróbuj później!",
"jicofoUnavailable": "Jicofo jest aktualnie niedostępne. Proszę, spróbuj później!",
"maxUsersLimitReached": "Osiągnięto max liczbę uczestników konferencji. Proszę spróbuj później! ",
"lockTitle": "Nie powiodło się zabezpieczenie konferencji",
"lockMessage": "Zabezpieczenie konferencji nie powiodło się.",
"warning": "Uwaga",
"passwordNotSupported": "Hasła sali konferencyjnych są aktualnie niedostępne.",
"internalErrorTitle": "Błąd wewnętrzny",
"internalError": "Ocho! coś poszło nie tak. Wystąpił błąd: [setRemoteDescription]",
"unableToSwitch": "Nie można przełaczyć na strumień wideo",
"SLDFailure": "Ocho! Coś poszło nie tak i nie można wyciszyć! (SLD Failure)",
"SRDFailure": "Ocho! Coś poszło nie tak i nie można zatrzymać wideo! (SRD Failure)",
"oops": "Ups",
"defaultError": "Wystąpił jakiś błąd",
"passwordRequired": "Wymagane hasło",
"Ok": "Ok",
"Remove": "Usuń",
"shareVideoTitle": "Współdziel wideo",
"shareVideoLinkError": "Podaj proszę prawidłowy link youtube.",
"removeSharedVideoTitle": "Usuń wideo współdzielone",
"removeSharedVideoMsg": "Na pewno chcesz usunąć współdzielone wideo?",
"alreadySharedVideoMsg": "Inny uczestnik aktualnie współdzieli wideo. W tej konferencji tylko jedno wideo może być współdzielone.",
"WaitingForHost": "Oczekiwanie na komputer",
"WaitForHostMsg": "Konferencja <b>_room_</b> jeszcze nie wystartowała. Jeśli jesteś gospodarzem podaj dane autentykacji. Jeśli nie czekaj na gospodarza.",
"IamHost": "Jestem gospodarzem",
"Cancel": "Anuluj",
"retry": "Ponów",
"logoutTitle": "Wyloguj",
"logoutQuestion": "Na pewno chcesz się wylogować i zakończyć konferencję?",
"sessTerminated": "Sesja zakończona",
"hungUp": "Przerwałeś połączenie",
"joinAgain": "Ponownie przystąp",
"Share": "Współdziel",
"Save": "Zapisz",
"recording": "",
"recordingToken": "Proszę podać token nagrywania",
"Dial": "Dzwoń",
"sipMsg": "Podaj numer SIP",
"passwordCheck": "Czy na pewno chcesz usunąć swoje hasło ?",
"passwordMsg": "Podaj hasło aby zabezpieczyć salę konferencyjną",
"shareLink": "Skopiuj i udostępnij ten link",
"settings1": "Skonfiguruj swoją konferencję",
"settings2": "Wyciszenie współuczestników",
"settings3": "Wymagane nicki <br/><br/>Wprowadź hasło dla zabezpieczenia sali konferencyjnej:",
"yourPassword": "Proszę wprowadzić nowe hasło",
"Back": "Wstecz",
"serviceUnavailable": "Usługa jest niedostępna",
"gracefulShutdown": "Aktualnie serwis jest konserwowany. Prosze spróbować później.",
"Yes": "Tak",
"reservationError": "Błąd systemu rezerwacji",
"reservationErrorMsg": "Kod błędu: _code_, treść: _msg_",
"password": "Podaj hasło",
"userPassword": "hasło użytkownika",
"token": "token",
"tokenAuthFailedTitle": "Problem uwierzytelnienia",
"tokenAuthFailed": "Przepraszam, ale nie jesteś upoważniony do uczestnictwa w tym połączeniu",
"displayNameRequired": "Wprowadź swoją nazwę użytkownika",
"extensionRequired": "Wymagane jest rozszerzenie:",
"firefoxExtensionPrompt": "Potrzebujesz zainstalować rozszerzenie firefox aby móc współdzielić ekran. Spróbuj ponownie później <a href='__url__'>weź z</a>!",
"rateExperience": "Oceń proszę swoje doświadczenia z konferencji.",
"feedbackHelp": "Twoja opinia będzie pomocna w usprawnieniu naszego serwisu.",
"feedbackQuestion": "Powiedz nam o twoim połączeniu!",
"thankYou": "Dziękujemy Ci za używanie _appName_!",
"sorryFeedback": "Przykro nam to słyszeć. Czy możesz powiedzieć więcej na ten temat?",
"liveStreaming": "",
"streamKey": "Nazwa strumienia/klucz",
"startLiveStreaming": "Uruchom strumień live",
"stopStreamingWarning": "Czy jesteś pewny, że chcesz zatrzymać ten strumień live?",
"stopRecordingWarning": "Naprawdę chcesz zatrzymać nagrywanie?",
"stopLiveStreaming": "Zatrzymaj transmisję live",
"stopRecording": "Zatrzymaj nagrywanie",
"doNotShowWarningAgain": "Nie pokazuj tego ostrzeżenia ponownie",
"permissionDenied": "Brak uprawnień",
"screenSharingPermissionDeniedError": "Nie posiadasz uprawnień do współdzielenia ekranu.",
"micErrorPresent": "Wystąpił błąd w dostępie do mikrofonu.",
"cameraErrorPresent": "Wystąpił błąd w dostępie do twojej kamery.",
"cameraUnsupportedResolutionError": "Twoja kamera nie obsługuje wymaganej rozdzielczości.",
"cameraUnknownError": "Z nieznanej przyczyny nie można użyć kamery ",
"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ę. ",
"cameraNotFoundError": "Kamera nie znaleziona.",
"cameraConstraintFailedError": "Twoja kamera nie spełnia wymagań.",
"micUnknownError": "Z przyczyn nieznanych nie można użyć mikrofonu. ",
"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ć.",
"micNotFoundError": "Mikrofon nie jest odnaleziony.",
"micConstraintFailedError": "Twój mikrofon nie obsługuje wymaganych parametrów.",
"micNotSendingData": "Nie możemy mieć dostępu do twojego mikrofonu. Proszę, wskaż inne urządzenie lub przeładuj aplikację.",
"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ę.",
"goToStore": "Idź do sklepu",
"externalInstallationTitle": "Wymagane rozszerzenie",
"externalInstallationMsg": "Zainstaluj rozszerzenie naszego współdzielenia ekranu."
},
"email": {
"sharedKey": [
"Ta konferencja jest zabezpieczona hasłem. Aby się podłączyć proszę zastosuj następujący pin:",
"",
"",
"_sharedKey_",
"",
" "
],
"subject": "Zaproszenie do a_appName_(_conferenceName_)",
"body": [
"Witaj, I%27 zaprasza cię do udziału w konferencji_appName_.",
"",
"",
"Kliknij na poniższy link aby uczestniczyć w konferencji.",
"",
"",
"_roomUrl_",
"",
"",
"_sharedKeyTex_",
"Zauważ, że -appName_ możesz używać tylko przy pomocy _supportedBrowsers_.",
"",
"",
"Polączymy się błyskawicznie! "
],
"and": "i"
},
"connection": {
"ERROR": "Błąd",
"CONNECTING": "Nawiązywanie połączenia",
"RECONNECTING": "Wystąpił problem w sieci. Ponowienie połaczenia....",
"CONNFAIL": "Połączenie się nie powiodło",
"AUTHENTICATING": "Uwierzytelnianie",
"AUTHFAIL": "Uwierzytelnianie nie powiodło się",
"CONNECTED": "Połączono",
"DISCONNECTED": "Rozłączony",
"DISCONNECTING": "Rozłączanie",
"ATTACHED": "Załącznik"
},
"recording": {
"pending": "Nagrywanie oczekiwanie na uczestników konferencji.....",
"on": "Nagrywanie",
"off": "Nagrywanie zatrzymane",
"failedToStart": "Nagrywanie nie jest możliwe",
"buttonTooltip": "Nagrywanie start / stop",
"error": "Nagranie się nie powiodło. Proszę, spróbuj ponownie.",
"unavailable": "Serwis nagrywania jest aktualnie niedostępny. Proszę, spróbować później."
},
"liveStreaming": {
"pending": "Start strumieniowania live...",
"on": "Strumień live",
"off": "Strumieniowanie live zastopowane",
"unavailable": "Strumieniowanie live aktualnie jest niedostepne. Proszę spróbować później.",
"failedToStart": "Strumieniowanie live nie powiodło się",
"buttonTooltip": "Strumieniowanie live start / stop",
"streamIdRequired": "Proszę podaj id strumieniowania aby uruchomić live.",
"error": "Strumieniowanie live nie powiodło się. Spróbuj później.",
"busy": "Wszystkie nagrywarki są zajęte. Proszę, sprawdź ponownie później."
}
}

View File

@@ -1,17 +1,15 @@
{
"contactlist": "LISTA DE CONTATO",
"contactlist": "Na chamada",
"connectionsettings": "Configurações de conexão",
"poweredby": "distribuído por",
"downloadlogs": "Baixar registros",
"feedback": "Dê seus comentários",
"roomUrlDefaultMsg": "Sua conferência está sendo criado...",
"participant": "Participante",
"me": "eu",
"speaker": "Orador",
"raisedHand": "Gostaria de falar",
"defaultNickname": "ex. João Pedro",
"defaultLink": "i.e. __url__",
"calling": "Chamando __name__ ...",
"callingName": "__name__",
"userMedia": {
"react-nativeGrantPermissions": "Dê as permissões para usar sua câmera e microfone pressionando o botão <b> <i>Permitir</i> </b>",
"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 @@
"raiseHand": "Erguer sua mão.",
"pushToTalk": "Pressione para falar.",
"toggleScreensharing": "Trocar entre câmera e compartilhamento de tela.",
"toggleFilmstrip": "Mostrar ou ocultar a tira de filme.",
"toggleFilmstrip": "Mostrar ou ocultar os vídeos.",
"toggleShortcuts": "Mostrar ou ocultar este menu de ajuda.",
"focusLocal": "Foco no vídeo local.",
"focusRemote": "Foco em um dos vídeos remotos.",
@@ -93,11 +91,13 @@
"micMutedPopup": "Seu microfone está mudo assim que você<br/>pode curtir plenamente seu vídeo compartilhado.",
"unableToUnmutePopup": "Você não pode sair do mudo enquanto seu vídeo compartilhado está ativo.",
"cameraDisabled": "A câmera não está disponível",
"micDisabled": "O microfone não está disponível"
"micDisabled": "O microfone não está disponível",
"filmstrip": "",
"raiseHand": "Levantar a mão para falar"
},
"bottomtoolbar": {
"chat": "Abrir / fechar bate-papo",
"filmstrip": "Mostrar / ocultar a tira de usuários",
"filmstrip": "Mostrar/ocultar vídeos",
"contactlist": "Abrir / fechar a lista de contatos"
},
"chat": {
@@ -108,18 +108,27 @@
"messagebox": "Digite um texto..."
},
"settings": {
"title": "CONFIGURAÇÕES",
"title": "Configurações",
"update": "Atualizar",
"name": "Nome",
"startAudioMuted": "Iniciar sem áudio",
"startVideoMuted": "Iniciar sem vídeo",
"selectCamera": "Selecione a câmera",
"selectMic": "Selecionar o microfone",
"selectAudioOutput": "Selecionar a saída de áudio",
"followMe": "Habilitar o siga-me",
"startAudioMuted": "Todos iniciam mudos",
"startVideoMuted": "Todos iniciam ocultos",
"selectCamera": "Câmera",
"selectMic": "Microfone",
"selectAudioOutput": "Saída de áudio",
"followMe": "Todos me seguem",
"noDevice": "Nenhum",
"noPermission": "Permissão para usar o dispositivo não concedida",
"avatarUrl": "URL do Avatar"
"cameraAndMic": "Câmera e microfone",
"moderator": "MODERADOR",
"password": "DEFINIR SENHA",
"audioVideo": "ÁUDIO E VÍDEO",
"setPasswordLabel": "Trancar sua sala com uma senha."
},
"profile": {
"title": "PERFIL",
"setDisplayNameLabel": "Definir seu nome de exibição",
"setEmailLabel": "Definir seu email de gravatar"
},
"videothumbnail": {
"editnickname": "Clique para editar o seu <br/>nome de exibição",
@@ -172,6 +181,7 @@
"connectError": "Oops! Alguma coisa está errada e nós não pudemos conectar à conferência.",
"connectErrorWithMsg": "Oops! Alguma coisa está errada e não podemos conectar à conferência: __msg__",
"connecting": "Conectando",
"copy": "Copiar",
"error": "Erro",
"detectext": "Erro enquanto tenta detectar a extensão de compartilhamento de tela.",
"failtoinstall": "Falhou a instalação da extensão de compartilhamento de tela",
@@ -183,8 +193,8 @@
"lockMessage": "Falha ao travar a conferência.",
"warning": "Atenção",
"passwordNotSupported": "Senhas de salas não são suportadas atualmente.",
"sorry": "Desculpe",
"internalError": "Erro interno de aplicação [setRemoteDescription]",
"internalErrorTitle": "Erro interno",
"internalError": "Ops! Alguma coisa está errada. Ocorreu o seguinte erro: [setRemoteDescriptio]",
"unableToSwitch": "Impossível trocar o fluxo de vídeo.",
"SLDFailure": "Oops! Alguma coisa está errada e nós falhamos em silenciar! (Falha do SLD)",
"SRDFailure": "Oops! Alguma coisa está errada e nós falhamos em parar o vídeo! (Falha do SRD)",
@@ -216,26 +226,28 @@
"sipMsg": "Digite o número SIP",
"passwordCheck": "Você tem certeza que deseja remover sua senha?",
"passwordMsg": "Definir uma senha para trancar sua sala",
"Invite": "Convidar",
"shareLink": "Compartilhar este link com quem você espera convidar",
"shareLink": "Copiar e compartilhar este link",
"settings1": "Configure sua conferência",
"settings2": "Participantes entram mudos",
"settings3": "Requer apelidos<br/><br/>Defina uma senha para trancar sua sala:",
"yourPassword": "sua Senha",
"yourPassword": "Digite a nova senha",
"Back": "Voltar",
"serviceUnavailable": "Serviço indisponível",
"gracefulShutdown": "Nosso serviço está desligado para manutenção. Por favor, tente mais tarde.",
"Yes": "Sim",
"reservationError": "Erro de sistema de reserva",
"reservationErrorMsg": "Código do erro: __code__, mensagem: __msg__",
"password": "senha",
"password": "Insira a senha",
"userPassword": "senha do usuário",
"token": "token",
"tokenAuthFailed": "Falha em autenticar com o servidor XMPP: token inválido",
"tokenAuthFailedTitle": "Problema na autenticação",
"tokenAuthFailed": "Desculpe, você não está autorizado a entrar nesta chamada.",
"displayNameRequired": "Digite seu nome de exibição",
"extensionRequired": "Extensão requerida:",
"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>!",
"feedbackQuestion": "Como foi a chamada?",
"rateExperience": "Por favor, avalie sua experiência na reunião.",
"feedbackHelp": "Seu retorno nos ajudará a melhorar nossa experiência de vídeo.",
"feedbackQuestion": "Nos conte sobre sua chamada!",
"thankYou": "Obrigado por usar o __appName__!",
"sorryFeedback": "Lamentamos escutar isso. Gostaria de nos contar mais?",
"liveStreaming": "Live Streaming",
@@ -253,12 +265,17 @@
"cameraUnsupportedResolutionError": "Sua câmera não suporta a resolução de vídeo requerida.",
"cameraUnknownError": "Não pode usar a câmera por uma razão desconhecida.",
"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.",
"cameraNotFoundError": "Câmera solicitada não foi encontrada.",
"cameraNotFoundError": "A câmera não foi encontrada.",
"cameraConstraintFailedError": "Sua câmera não satisfaz algumas condições requeridas.",
"micUnknownError": "Não pode usar o microfone por uma razão desconhecida.",
"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.",
"micNotFoundError": "O microfone solicitado não foi encontrado.",
"micConstraintFailedError": "Seu microfone não satisfaz algumas condições requeridas."
"micNotFoundError": "O microfone não foi encontrado.",
"micConstraintFailedError": "Seu microfone não satisfaz algumas condições requeridas.",
"micNotSendingData": "Seu microfone está inacessível. Selecione outro dispositivo do menu de configurações ou tente reiniciar a aplicação.",
"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.",
"goToStore": "Vá para a loja virtual",
"externalInstallationTitle": "Extensão requerida",
"externalInstallationMsg": "Você precisa instalar nossa extensão de compartilhamento de tela."
},
"email": {
"sharedKey": [

View File

@@ -1,11 +1,9 @@
{
"contactlist": "ON CALL (__participants__)",
"contactlist": "On Call",
"connectionsettings": "Connection Settings",
"poweredby": "powered by",
"downloadlogs": "Download logs",
"feedback": "Give us your feedback",
"roomUrlDefaultMsg": "Your conference is currently being created...",
"participant": "Participant",
"me": "me",
"speaker": "Speaker",
"raisedHand": "Would like to speak",
@@ -33,6 +31,7 @@
"focusRemote": "Focus on one of the remote videos.",
"toggleChat": "Open or close the chat panel.",
"mute": "Mute or unmute the microphone.",
"fullScreen": "Enter or exit full screen mode.",
"videoMute": "Stop or start the local video."
},
"welcomepage":{
@@ -91,10 +90,12 @@
"dialpad": "Show dialpad",
"sharedVideoMutedPopup": "Your shared video has been muted so<br/>that you can talk to the other participants.",
"micMutedPopup": "Your microphone has been muted so that you<br/>would fully enjoy your shared video.",
"talkWhileMutedPopup": "Trying to speak? You are muted.",
"unableToUnmutePopup": "You cannot un-mute while the shared video is on.",
"cameraDisabled": "Camera is not available",
"micDisabled": "Microphone is not available",
"filmstrip": "Show / hide videos"
"filmstrip": "Show / hide videos",
"raiseHand": "Raise hand to speak"
},
"bottomtoolbar": {
"chat": "Open / close chat",
@@ -110,21 +111,21 @@
},
"settings":
{
"title": "SETTINGS",
"title": "Settings",
"update": "Update",
"name": "Name",
"startAudioMuted": "Everyone starts muted",
"startVideoMuted": "Everyone starts hidden",
"selectCamera": "Select camera",
"selectMic": "Select microphone",
"selectAudioOutput": "Select audio output",
"selectCamera": "Camera",
"selectMic": "Microphone",
"selectAudioOutput": "Audio output",
"followMe": "Everyone follows me",
"noDevice": "None",
"noPermission": "Permission to use device is not granted",
"cameraAndMic": "Camera and microphone",
"moderator": "MODERATOR",
"password": "SET PASSWORD",
"audioVideo": "AUDIO / VIDEO",
"audioVideo": "AUDIO AND VIDEO",
"setPasswordLabel": "Lock your room with a password."
},
"profile": {
@@ -136,7 +137,7 @@
{
"editnickname": "Click to edit your<br/>display name",
"moderator": "The owner of<br/>this conference",
"videomute": "Participant has<br/>stopped the camera.",
"videomute": "Participant has<br/>stopped the camera",
"mute": "Participant is muted",
"kick": "Kick out",
"muted": "Muted",
@@ -186,6 +187,7 @@
"connectError": "Oops! Something went wrong and we couldn't connect to the conference.",
"connectErrorWithMsg": "Oops! Something went wrong and we couldn't connect to the conference: __msg__",
"connecting": "Connecting",
"copy": "Copy",
"error": "Error",
"detectext": "Error when trying to detect desktopsharing extension.",
"failtoinstall": "Failed to install desktop sharing extension",
@@ -197,8 +199,8 @@
"lockMessage": "Failed to lock the conference.",
"warning": "Warning",
"passwordNotSupported": "Room passwords are currently not supported.",
"sorry": "Sorry",
"internalError": "Internal application error [setRemoteDescription]",
"internalErrorTitle": "Internal error",
"internalError": "Oups! Something went wrong. The following error occurred: [setRemoteDescription]",
"unableToSwitch": "Unable to switch video stream.",
"SLDFailure": "Oops! Something went wrong and we failed to mute! (SLD Failure)",
"SRDFailure": "Oops! Something went wrong and we failed to stop video! (SRD Failure)",
@@ -231,7 +233,6 @@
"passwordCheck": "Are you sure you would like to remove your password?",
"Remove": "Remove",
"passwordMsg": "Set a password to lock your room",
"Invite": "Invite",
"shareLink": "Copy and share this link",
"settings1": "Configure your conference",
"settings2": "Participants join muted",
@@ -246,13 +247,14 @@
"password": "Enter password",
"userPassword": "user password",
"token": "token",
"tokenAuthFailed": "Failed to authenticate with XMPP server: invalid token",
"tokenAuthFailedTitle": "Authentication problem",
"tokenAuthFailed": "Sorry, you're not allowed to join this call.",
"displayNameRequired": "Please enter your display name",
"extensionRequired": "Extension required:",
"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>!",
"rateExperience": "Please rate your meeting experience.",
"feedbackHelp": "Your feedback will help us to improve our video experience.",
"feedbackQuestion": "How was your call?",
"feedbackQuestion": "Tell us about your call!",
"thankYou": "Thank you for using __appName__!",
"sorryFeedback": "We're sorry to hear that. Would you like to tell us more?",
"liveStreaming": "Live Streaming",
@@ -277,6 +279,7 @@
"micNotFoundError": "Microphone was not found.",
"micConstraintFailedError": "Yor microphone does not satisfy some of required constraints.",
"micNotSendingData": "We are unable to access your microphone. Please select another device from the settings menu or try to restart the application.",
"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.",
"goToStore": "Go to the webstore",
"externalInstallationTitle": "Extension required",
"externalInstallationMsg": "You need to install our desktop sharing extension."
@@ -323,7 +326,8 @@
"ATTACHED": "Attached",
"FETCH_SESSION_ID": "Obtaining session-id...",
"GOT_SESSION_ID": "Obtaining session-id... Done",
"GET_SESSION_ID_ERROR": "Get session-id error: "
"GET_SESSION_ID_ERROR": "Get session-id error: ",
"USER_CONNECTION_INTERRUPTED": "__displayName__ is having connectivity issues..."
},
"recording":
{

View File

@@ -336,7 +336,6 @@ JitsiMeetExternalAPI.prototype.removeEventListener = function(event) {
* @param events array with the names of the events.
*/
JitsiMeetExternalAPI.prototype.removeEventListeners = function(events) {
var eventsArray = [];
for(var i = 0; i < events.length; i++)
this.removeEventListener(events[i]);
};

View File

@@ -261,25 +261,26 @@ class FollowMe {
* @param newValue the new value
* @private
*/
// eslint-disable-next-line no-unused-vars
_localPropertyChange (property, oldValue, newValue) {
// Only a moderator is allowed to send commands.
var conference = this._conference;
const conference = this._conference;
if (!conference.isModerator)
return;
var commands = conference.commands;
const commands = conference.commands;
// XXX The "Follow Me" command represents a snapshot of all states
// which are to be followed so don't forget to removeCommand before
// sendCommand!
commands.removeCommand(_COMMAND);
var self = this;
const local = this._local;
commands.sendCommandOnce(
_COMMAND,
{
attributes: {
filmStripVisible: self._local.filmStripVisible,
nextOnStage: self._local.nextOnStage,
sharedDocumentVisible: self._local.sharedDocumentVisible
filmStripVisible: local.filmStripVisible,
nextOnStage: local.nextOnStage,
sharedDocumentVisible: local.sharedDocumentVisible
}
});
}

View File

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

View File

@@ -1,321 +0,0 @@
/* global $, APP, config, interfaceConfig, JitsiMeetJS */
import UIEvents from "../../service/UI/UIEvents";
/**
* Constructs the html for the overall feedback window.
*
* @returns {string} the constructed html string
*/
var constructOverallFeedbackHtml = function() {
var feedbackQuestion = (Feedback.feedbackScore < 0)
? '<br/><br/>' + APP.translation
.translateString("dialog.feedbackQuestion")
: '';
var message = '<div class="feedback"><div>' +
'<div class="feedbackTitle">' +
APP.translation.translateString("dialog.thankYou",
{appName:interfaceConfig.APP_NAME}) +
'</div>' +
feedbackQuestion +
'</div><br/><br/>' +
'<div id="stars">' +
'<a><i class="icon-star icon-star-full"></i></a>' +
'<a><i class="icon-star icon-star-full"></i></a>' +
'<a><i class="icon-star icon-star-full"></i></a>' +
'<a><i class="icon-star icon-star-full"></i></a>' +
'<a><i class="icon-star icon-star-full"></i></a>' +
'</div></div>';
return message;
};
/**
* Constructs the html for the detailed feedback window.
*
* @returns {string} the contructed html string
*/
var constructDetailedFeedbackHtml = function() {
// Construct the html, which will be served as a dialog message.
var message = '<div class="feedback">' +
'<div class="feedbackTitle">' +
APP.translation.translateString("dialog.sorryFeedback") +
'</div><br/><br/>' +
'<div class="feedbackDetails">' +
'<textarea id="feedbackTextArea" rows="10" cols="50" autofocus>' +
'</textarea>' +
'</div></div>';
return message;
};
var createRateFeedbackHTML = function () {
var rate = APP.translation.translateString('dialog.rateExperience'),
help = APP.translation.translateString('dialog.feedbackHelp');
return `
<div class="feedback-rating text-center">
<h2>${ rate }</h2>
<p class="star-label">&nbsp;</p>
<div id="stars" class="feedback-stars">
<a class="star-btn">
<i class="fa fa-star shake-rotate"></i>
</a>
<a class="star-btn">
<i class="fa fa-star shake-rotate"></i>
</a>
<a class="star-btn">
<i class="fa fa-star shake-rotate"></i>
</a>
<a class="star-btn">
<i class="fa fa-star shake-rotate"></i>
</a>
<a class="star-btn">
<i class="fa fa-star shake-rotate"></i>
</a>
</div>
<p>&nbsp;</p>
<p>${ help }</p>
</div>
`;
};
/**
* The callback function corresponding to the openFeedbackWindow parameter.
*
* @type {function}
*/
var feedbackWindowCallback = null;
/**
* Shows / hides the feedback button.
* @private
*/
function _toggleFeedbackIcon() {
$('#feedbackButtonDiv').toggleClass("hidden");
}
/**
* Shows / hides the feedback button.
* @param {show} set to {true} to show the feedback button or to {false}
* to hide it
* @private
*/
function _showFeedbackButton (show) {
var feedbackButton = $("#feedbackButtonDiv");
if (show)
feedbackButton.css("display", "block");
else
feedbackButton.css("display", "none");
}
/**
* Defines all methods in connection to the Feedback window.
*
* @type {{feedbackScore: number, openFeedbackWindow: Function,
* toggleStars: Function, hoverStars: Function, unhoverStars: Function}}
*/
var Feedback = {
/**
* The feedback score. -1 indicates no score has been given for now.
*/
feedbackScore: -1,
/**
* Initialise the Feedback functionality.
* @param emitter the EventEmitter to associate with the Feedback.
*/
init: function (emitter) {
// CallStats is the way we send feedback, so we don't have to initialise
// if callstats isn't enabled.
if (!APP.conference.isCallstatsEnabled())
return;
// If enabled property is still undefined, i.e. it hasn't been set from
// some other module already, we set it to true by default.
if (typeof this.enabled == "undefined")
this.enabled = true;
_showFeedbackButton(this.enabled);
$("#feedbackButton").click(function (event) {
Feedback.openFeedbackWindow();
});
// Show / hide the feedback button whenever the film strip is
// shown / hidden.
emitter.addListener(UIEvents.TOGGLE_FILM_STRIP, function () {
_toggleFeedbackIcon();
});
},
/**
* Enables/ disabled the feedback feature.
*/
enableFeedback: function (enable) {
if (this.enabled !== enable)
_showFeedbackButton(enable);
this.enabled = enable;
},
/**
* Indicates if the feedback functionality is enabled.
*
* @return true if the feedback functionality is enabled, false otherwise.
*/
isEnabled: function() {
return this.enabled && APP.conference.isCallstatsEnabled();
},
/**
* Returns true if the feedback window is currently visible and false
* otherwise.
* @return {boolean} true if the feedback window is visible, false
* otherwise
*/
isVisible: function() {
return $(".feedback").is(":visible");
},
/**
* Opens the feedback window.
*/
openFeedbackWindow: function (callback) {
feedbackWindowCallback = callback;
// Add all mouse and click listeners.
var onLoadFunction = function (event) {
$('#stars >a').each(function(index) {
// On star mouse over.
$(this).get(0).onmouseover = function(){
Feedback.hoverStars(index);
};
// On star mouse leave.
$(this).get(0).onmouseleave = function(){
Feedback.unhoverStars(index);
};
// On star click.
$(this).get(0).onclick = function(){
Feedback.toggleStars(index);
Feedback.feedbackScore = index+1;
// If the feedback is less than 3 stars we're going to
// ask the user for more information.
if (Feedback.feedbackScore > 3) {
APP.conference.sendFeedback(Feedback.feedbackScore, "");
if (feedbackWindowCallback)
feedbackWindowCallback();
else
APP.UI.messageHandler.closeDialog();
}
else {
feedbackDialog.goToState('detailed_feedback');
}
};
// Init stars to correspond to previously entered feedback.
if (Feedback.feedbackScore > 0
&& index < Feedback.feedbackScore) {
Feedback.hoverStars(index);
Feedback.toggleStars(index);
}
});
};
// Defines the different states of the feedback window.
var states = {
overall_feedback: {
html: createRateFeedbackHTML(),
persistent: false,
buttons: {},
closeText: '',
focus: "div[id='stars']",
position: {width: 500}
},
detailed_feedback: {
html: constructDetailedFeedbackHtml(),
buttons: {"Submit": true, "Cancel": false},
closeText: '',
focus: "textarea[id='feedbackTextArea']",
position: {width: 500},
submit: function(e,v,m,f) {
e.preventDefault();
if (v) {
var feedbackDetails
= document.getElementById("feedbackTextArea").value;
if (feedbackDetails && feedbackDetails.length > 0) {
APP.conference.sendFeedback( Feedback.feedbackScore,
feedbackDetails);
}
if (feedbackWindowCallback)
feedbackWindowCallback();
else
APP.UI.messageHandler.closeDialog();
} else {
// User cancelled
if (feedbackWindowCallback)
feedbackWindowCallback();
else
APP.UI.messageHandler.closeDialog();
}
}
}
};
// Create the feedback dialog.
var feedbackDialog
= APP.UI.messageHandler.openDialogWithStates(
states,
{ persistent: false,
buttons: {},
closeText: '',
loaded: onLoadFunction,
position: {width: 500}}, null);
JitsiMeetJS.analytics.sendEvent('feedback.open');
},
/**
* Toggles the appropriate css class for the given number of stars, to
* indicate that those stars have been clicked/selected.
*
* @param starCount the number of stars, for which to toggle the css class
*/
toggleStars: function (starCount)
{
$('#stars >a >i').each(function(index) {
if (index <= starCount) {
$(this).removeClass("icon-star");
}
else
$(this).addClass("icon-star");
});
},
/**
* Toggles the appropriate css class for the given number of stars, to
* indicate that those stars have been hovered.
*
* @param starCount the number of stars, for which to toggle the css class
*/
hoverStars: function (starCount)
{
$('#stars >a >i').each(function(index) {
if (index <= starCount)
$(this).addClass("starHover");
});
},
/**
* Toggles the appropriate css class for the given number of stars, to
* indicate that those stars have been un-hovered.
*
* @param starCount the number of stars, for which to toggle the css class
*/
unhoverStars: function (starCount)
{
$('#stars >a >i').each(function(index) {
if (index <= starCount && $(this).hasClass("icon-star"))
$(this).removeClass("starHover");
});
}
};
// Exports the Feedback class.
module.exports = Feedback;

View File

@@ -1,5 +1,4 @@
/* global APP, JitsiMeetJS, $, config, interfaceConfig, toastr */
/* jshint -W101 */
var UI = {};
import Chat from "./side_pannels/chat/Chat";
@@ -10,11 +9,11 @@ import Avatar from "./avatar/Avatar";
import SideContainerToggler from "./side_pannels/SideContainerToggler";
import UIUtil from "./util/UIUtil";
import UIEvents from "../../service/UI/UIEvents";
import CQEvents from '../../service/connectionquality/CQEvents';
import EtherpadManager from './etherpad/Etherpad';
import SharedVideoManager from './shared_video/SharedVideo';
import Recording from "./recording/Recording";
import GumPermissionsOverlay from './gum_overlay/UserMediaPermissionsGuidanceOverlay';
import GumPermissionsOverlay
from './gum_overlay/UserMediaPermissionsGuidanceOverlay';
import VideoLayout from "./videolayout/VideoLayout";
import FilmStrip from "./videolayout/FilmStrip";
@@ -29,7 +28,7 @@ var EventEmitter = require("events");
UI.messageHandler = require("./util/MessageHandler");
var messageHandler = UI.messageHandler;
var JitsiPopover = require("./util/JitsiPopover");
var Feedback = require("./Feedback");
var Feedback = require("./feedback/Feedback");
import FollowMe from "../FollowMe";
@@ -60,6 +59,8 @@ JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[TrackErrors.NOT_FOUND]
= "dialog.cameraNotFoundError";
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[TrackErrors.CONSTRAINT_FAILED]
= "dialog.cameraConstraintFailedError";
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[TrackErrors.NO_DATA_FROM_SOURCE]
= "dialog.cameraNotSendingData";
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.GENERAL]
= "dialog.micUnknownError";
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.PERMISSION_DENIED]
@@ -68,6 +69,8 @@ JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.NOT_FOUND]
= "dialog.micNotFoundError";
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.CONSTRAINT_FAILED]
= "dialog.micConstraintFailedError";
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[TrackErrors.NO_DATA_FROM_SOURCE]
= "dialog.micNotSendingData";
/**
* Prompt user for nickname.
@@ -190,21 +193,16 @@ UI.notifyReservationError = function (code, msg) {
"dialog.reservationError");
var message = APP.translation.generateTranslationHTML(
"dialog.reservationErrorMsg", {code: code, msg: msg});
messageHandler.openDialog(
title,
message,
true, {},
function (event, value, message, formVals) {
return false;
}
);
messageHandler.openDialog(title, message, true, {}, () => false);
};
/**
* Notify user that he has been kicked from the server.
*/
UI.notifyKicked = function () {
messageHandler.openMessageDialog("dialog.sessTerminated", "dialog.kickMessage");
messageHandler.openMessageDialog(
"dialog.sessTerminated",
"dialog.kickMessage");
};
/**
@@ -214,13 +212,9 @@ UI.notifyKicked = function () {
UI.notifyConferenceDestroyed = function (reason) {
//FIXME: use Session Terminated from translation, but
// 'reason' text comes from XMPP packet and is not translated
var title = APP.translation.generateTranslationHTML("dialog.sessTerminated");
messageHandler.openDialog(
title, reason, true, {},
function (event, value, message, formVals) {
return false;
}
);
const title
= APP.translation.generateTranslationHTML("dialog.sessTerminated");
messageHandler.openDialog(title, reason, true, {}, () => false);
};
/**
@@ -257,6 +251,17 @@ UI.changeDisplayName = function (id, displayName) {
}
};
/**
* Shows/hides the indication about local connection being interrupted.
*
* @param {boolean} isInterrupted <tt>true</tt> if local connection is
* currently in the interrupted state or <tt>false</tt> if the connection
* is fine.
*/
UI.showLocalConnectionInterrupted = function (isInterrupted) {
VideoLayout.showLocalConnectionInterrupted(isInterrupted);
};
/**
* Sets the "raised hand" status for a participant.
*/
@@ -272,7 +277,9 @@ UI.setRaisedHandStatus = (participant, raisedHandStatus) => {
* Sets the local "raised hand" status.
*/
UI.setLocalRaisedHandStatus = (raisedHandStatus) => {
VideoLayout.setRaisedHandStatus(APP.conference.getMyUserId(), raisedHandStatus);
VideoLayout.setRaisedHandStatus(
APP.conference.getMyUserId(),
raisedHandStatus);
};
/**
@@ -292,11 +299,11 @@ UI.initConference = function () {
}
// Add myself to the contact list.
ContactList.addContact(id);
ContactList.addContact(id, true);
//update default button states before showing the toolbar
//if local role changes buttons state will be again updated
UI.updateLocalRole(false);
// Update default button states before showing the toolbar
// if local role changes buttons state will be again updated.
UI.updateLocalRole(APP.conference.isModerator);
UI.showToolbar();
@@ -325,6 +332,8 @@ UI.initConference = function () {
// to the UI (depending on the moderator role of the local participant) and
// (2) APP.conference as means of communication between the participants.
followMeHandler = new FollowMe(APP.conference, UI);
UIUtil.activateTooltips();
};
UI.mucJoined = function () {
@@ -339,6 +348,22 @@ UI.handleToggleFilmStrip = () => {
VideoLayout.resizeVideoArea(true, false);
};
/**
* Sets tooltip defaults.
*
* @private
*/
function _setTooltipDefaults() {
$.fn.tooltip.defaults = {
opacity: 1, //defaults to 1
offset: 1,
delayIn: 0, //defaults to 500
hoverable: true,
hideOnClick: true,
aria: true
};
}
/**
* Setup some UI event listeners.
*/
@@ -431,6 +456,9 @@ UI.start = function () {
// Set the defaults for prompt dialogs.
$.prompt.setDefaults({persistent: false});
// Set the defaults for tooltips.
_setTooltipDefaults();
registerListeners();
ToolbarToggler.init();
@@ -463,20 +491,10 @@ UI.start = function () {
$('#noticeText').text(config.noticeMessage);
$('#notice').css({display: 'block'});
}
$("#downloadlog").click(function (event) {
let logs = APP.conference.getLogs();
let data = encodeURIComponent(JSON.stringify(logs, null, ' '));
let elem = event.target.parentNode;
elem.download = 'meetlog.json';
elem.href = 'data:application/json;charset=utf-8,\n' + data;
});
} else {
$("#mainToolbarContainer").css("display", "none");
$("#downloadlog").css("display", "none");
FilmStrip.setupFilmStripOnly();
messageHandler.enableNotifications(false);
$('body').popover("disable");
JitsiPopover.enabled = false;
}
@@ -553,10 +571,6 @@ UI.removeRemoteStream = function (track) {
VideoLayout.onRemoteStreamRemoved(track);
};
function chatAddError(errorMessage, originalText) {
return Chat.chatAddError(errorMessage, originalText);
}
/**
* Update chat subject.
* @param {string} subject new chat subject
@@ -589,10 +603,11 @@ UI.getSharedDocumentManager = function () {
/**
* Show user on UI.
* @param {string} id user id
* @param {string} displayName user nickname
* @param {JitsiParticipant} user
*/
UI.addUser = function (id, displayName) {
UI.addUser = function (user) {
var id = user.getId();
var displayName = user.getDisplayName();
UI.hideRingOverLay();
ContactList.addContact(id);
@@ -605,7 +620,7 @@ UI.addUser = function (id, displayName) {
UIUtil.playSoundNotification('userJoined');
// Add Peer's container
VideoLayout.addParticipantContainer(id);
VideoLayout.addParticipantContainer(user);
// Configure avatar
UI.setUserEmail(id);
@@ -662,7 +677,9 @@ UI.updateLocalRole = function (isModerator) {
SettingsMenu.showFollowMeOptions(isModerator);
if (isModerator) {
messageHandler.notify(null, "notify.me", 'connected', "notify.moderator");
if (!interfaceConfig.DISABLE_FOCUS_INDICATOR)
messageHandler
.notify(null, "notify.me", 'connected', "notify.moderator");
Recording.checkAutoRecord();
}
@@ -676,7 +693,9 @@ UI.updateLocalRole = function (isModerator) {
UI.updateUserRole = function (user) {
VideoLayout.showModeratorIndicator();
if (!user.isModerator()) {
// We don't need to show moderator notifications when the focus (moderator)
// indicator is disabled.
if (!user.isModerator() || interfaceConfig.DISABLE_FOCUS_INDICATOR) {
return;
}
@@ -853,8 +872,8 @@ UI.clickOnVideo = function (videoNumber) {
};
//Used by torture
UI.showToolbar = function () {
return ToolbarToggler.showToolbar();
UI.showToolbar = function (timeout) {
return ToolbarToggler.showToolbar(timeout);
};
//Used by torture
@@ -928,9 +947,7 @@ UI.notifyConnectionFailed = function (stropheErrorMsg) {
"dialog.connectError");
}
messageHandler.openDialog(
title, message, true, {}, function (e, v, m, f) { return false; }
);
messageHandler.openDialog(title, message, true, {}, () => false);
};
@@ -944,9 +961,7 @@ UI.notifyMaxUsersLimitReached = function () {
var message = APP.translation.generateTranslationHTML(
"dialog.maxUsersLimitReached");
messageHandler.openDialog(
title, message, true, {}, function (e, v, m, f) { return false; }
);
messageHandler.openDialog(title, message, true, {}, () => false);
};
/**
@@ -954,8 +969,12 @@ UI.notifyMaxUsersLimitReached = function () {
*/
UI.notifyInitiallyMuted = function () {
messageHandler.notify(
null, "notify.mutedTitle", "connected", "notify.muted", null, {timeOut: 120000}
);
null,
"notify.mutedTitle",
"connected",
"notify.muted",
null,
{ timeOut: 120000 });
};
/**
@@ -970,6 +989,17 @@ UI.handleLastNEndpoints = function (ids, enteringIds) {
VideoLayout.onLastNEndpointsChanged(ids, enteringIds);
};
/**
* Will handle notification about participant's connectivity status change.
*
* @param {string} id the id of remote participant(MUC jid)
* @param {boolean} isActive true if the connection is ok or false if the user
* is having connectivity issues.
*/
UI.participantConnectionStatusChanged = function (id, isActive) {
VideoLayout.onParticipantConnectionStatusChanged(id, isActive);
};
/**
* Update audio level visualization for specified user.
* @param {string} id user id
@@ -1047,55 +1077,12 @@ UI.addMessage = function (from, displayName, message, stamp) {
Chat.updateChatConversation(from, displayName, message, stamp);
};
// eslint-disable-next-line no-unused-vars
UI.updateDTMFSupport = function (isDTMFSupported) {
//TODO: enable when the UI is ready
//Toolbar.showDialPadButton(dtmfSupport);
};
/**
* Invite participants to conference.
* @param {string} roomUrl
* @param {string} conferenceName
* @param {string} key
* @param {string} nick
*/
UI.inviteParticipants = function (roomUrl, conferenceName, key, nick) {
let keyText = "";
if (key) {
keyText = APP.translation.translateString(
"email.sharedKey", {sharedKey: key}
);
}
let and = APP.translation.translateString("email.and");
let supportedBrowsers = `Chromium, Google Chrome, Firefox ${and} Opera`;
let subject = APP.translation.translateString(
"email.subject", {appName:interfaceConfig.APP_NAME, conferenceName}
);
let body = APP.translation.translateString(
"email.body", {
appName:interfaceConfig.APP_NAME,
sharedKeyText: keyText,
roomUrl,
supportedBrowsers
}
);
body = body.replace(/\n/g, "%0D%0A");
if (nick) {
body += "%0D%0A%0D%0A" + UIUtil.escapeHtml(nick);
}
if (interfaceConfig.INVITATION_POWERED_BY) {
body += "%0D%0A%0D%0A--%0D%0Apowered by jitsi.org";
}
window.open(`mailto:?subject=${subject}&body=${body}`, '_blank');
};
/**
* Show user feedback dialog if its required or just show "thank you" dialog.
* @returns {Promise} when dialog is closed.
@@ -1103,12 +1090,15 @@ UI.inviteParticipants = function (roomUrl, conferenceName, key, nick) {
UI.requestFeedback = function () {
if (Feedback.isVisible())
return Promise.reject(UIErrors.FEEDBACK_REQUEST_IN_PROGRESS);
// Feedback has been submitted already.
else if (Feedback.isEnabled() && Feedback.isSubmitted())
return Promise.resolve();
else
return new Promise(function (resolve, reject) {
return new Promise(function (resolve) {
if (Feedback.isEnabled()) {
// If the user has already entered feedback, we'll show the
// window and immidiately start the conference dispose timeout.
if (Feedback.feedbackScore > 0) {
if (Feedback.getFeedbackScore() > 0) {
Feedback.openFeedbackWindow();
resolve();
@@ -1117,14 +1107,9 @@ UI.requestFeedback = function () {
}
} else {
// If the feedback functionality isn't enabled we show a thank
// you dialog.
messageHandler.openMessageDialog(
null, null, null,
APP.translation.translateString(
"dialog.thankYou", {appName:interfaceConfig.APP_NAME}
)
);
resolve();
// you dialog. Signaling it (true), so the caller
// of requestFeedback can act on it
resolve(true);
}
});
};
@@ -1134,11 +1119,13 @@ UI.updateRecordingState = function (state) {
};
UI.notifyTokenAuthFailed = function () {
messageHandler.showError("dialog.error", "dialog.tokenAuthFailed");
messageHandler.showError( "dialog.tokenAuthFailedTitle",
"dialog.tokenAuthFailed");
};
UI.notifyInternalError = function () {
messageHandler.showError("dialog.sorry", "dialog.internalError");
messageHandler.showError( "dialog.internalErrorTitle",
"dialog.internalError");
};
UI.notifyFocusDisconnected = function (focus, retrySec) {
@@ -1193,6 +1180,16 @@ UI.onStartMutedChanged = function (startAudioMuted, startVideoMuted) {
SettingsMenu.updateStartMutedBox(startAudioMuted, startVideoMuted);
};
/**
* Notifies interested listeners that the raise hand property has changed.
*
* @param {boolean} isRaisedHand indicates the current state of the
* "raised hand"
*/
UI.onLocalRaiseHandChanged = function (isRaisedHand) {
eventEmitter.emit(UIEvents.LOCAL_RAISE_HAND_CHANGED, isRaisedHand);
};
/**
* Update list of available physical devices.
* @param {object[]} devices new list of available devices
@@ -1264,7 +1261,7 @@ UI.showExtensionExternalInstallationDialog = function (url) {
null,
true,
"dialog.goToStore",
function(e,v,m,f){
function(e,v) {
if (v) {
e.preventDefault();
eventEmitter.emit(UIEvents.OPEN_EXTENSION_STORE, url);
@@ -1399,13 +1396,12 @@ UI.showDeviceErrorDialog = function (micError, cameraError) {
let title = "dialog.error";
if (micError && micError.name === TrackErrors.PERMISSION_DENIED) {
if (cameraError && cameraError.name === TrackErrors.PERMISSION_DENIED) {
title = "dialog.permissionDenied";
} else if (!cameraError) {
if (!cameraError
|| cameraError.name === TrackErrors.PERMISSION_DENIED) {
title = "dialog.permissionDenied";
}
} else if (cameraError &&
cameraError.name === TrackErrors.PERMISSION_DENIED) {
} else if (cameraError
&& cameraError.name === TrackErrors.PERMISSION_DENIED) {
title = "dialog.permissionDenied";
}
@@ -1415,12 +1411,13 @@ UI.showDeviceErrorDialog = function (micError, cameraError) {
/**
* Shows error dialog that informs the user that no data is received from the
* microphone.
* device.
*/
UI.showAudioNotWorkingDialog = function () {
UI.showTrackNotWorkingDialog = function (stream) {
messageHandler.openMessageDialog(
"dialog.error",
"dialog.micNotSendingData",
stream.isAudioTrack()? "dialog.micNotSendingData" :
"dialog.cameraNotSendingData",
null,
null);
};

View File

@@ -1,260 +1,165 @@
/* global APP, interfaceConfig, $ */
/* jshint -W101 */
/* global interfaceConfig */
import CanvasUtil from './CanvasUtils';
import FilmStrip from '../videolayout/FilmStrip';
const LOCAL_LEVEL = 'local';
let ASDrawContext = null;
let audioLevelCanvasCache = {};
let dominantSpeakerAudioElement = null;
function initDominantSpeakerAudioLevels(dominantSpeakerAvatarSize) {
let ASRadius = dominantSpeakerAvatarSize / 2;
let ASCenter = (dominantSpeakerAvatarSize + ASRadius) / 2;
// Draw a circle.
ASDrawContext.beginPath();
ASDrawContext.arc(ASCenter, ASCenter, ASRadius, 0, 2 * Math.PI);
ASDrawContext.closePath();
// Add a shadow around the circle
ASDrawContext.shadowColor = interfaceConfig.SHADOW_COLOR;
ASDrawContext.shadowOffsetX = 0;
ASDrawContext.shadowOffsetY = 0;
}
import UIUtil from "../util/UIUtil";
/**
* Resizes the given audio level canvas to match the given thumbnail size.
*/
function resizeAudioLevelCanvas(audioLevelCanvas, thumbnailWidth, thumbnailHeight) {
audioLevelCanvas.width = thumbnailWidth + interfaceConfig.CANVAS_EXTRA;
audioLevelCanvas.height = thumbnailHeight + interfaceConfig.CANVAS_EXTRA;
}
/**
* Draws the audio level canvas into the cached canvas object.
*
* @param id of the user for whom we draw the audio level
* @param audioLevel the newAudio level to render
*/
function drawAudioLevelCanvas(id, audioLevel) {
if (!audioLevelCanvasCache[id]) {
let videoSpanId = getVideoSpanId(id);
let audioLevelCanvasOrig = $(`#${videoSpanId}>canvas`).get(0);
/*
* FIXME Testing has shown that audioLevelCanvasOrig may not exist.
* In such a case, the method CanvasUtil.cloneCanvas may throw an
* error. Since audio levels are frequently updated, the errors have
* been observed to pile into the console, strain the CPU.
*/
if (audioLevelCanvasOrig) {
audioLevelCanvasCache[id]
= CanvasUtil.cloneCanvas(audioLevelCanvasOrig);
}
}
let canvas = audioLevelCanvasCache[id];
if (!canvas) {
return;
}
let drawContext = canvas.getContext('2d');
drawContext.clearRect(0, 0, canvas.width, canvas.height);
let shadowLevel = getShadowLevel(audioLevel);
if (shadowLevel > 0) {
// drawContext, x, y, w, h, r, shadowColor, shadowLevel
CanvasUtil.drawRoundRectGlow(
drawContext,
interfaceConfig.CANVAS_EXTRA / 2, interfaceConfig.CANVAS_EXTRA / 2,
canvas.width - interfaceConfig.CANVAS_EXTRA,
canvas.height - interfaceConfig.CANVAS_EXTRA,
interfaceConfig.CANVAS_RADIUS,
interfaceConfig.SHADOW_COLOR,
shadowLevel);
}
}
/**
* Returns the shadow/glow level for the given audio level.
*
* @param audioLevel the audio level from which we determine the shadow
* level
*/
function getShadowLevel (audioLevel) {
let shadowLevel = 0;
if (audioLevel <= 0.3) {
shadowLevel = Math.round(
interfaceConfig.CANVAS_EXTRA/2*(audioLevel/0.3));
} else if (audioLevel <= 0.6) {
shadowLevel = Math.round(
interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
} else {
shadowLevel = Math.round(
interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
}
return shadowLevel;
}
/**
* Returns the video span id corresponding to the given user id
*/
function getVideoSpanId(id) {
let videoSpanId = null;
if (id === LOCAL_LEVEL || APP.conference.isLocalId(id)) {
videoSpanId = 'localVideoContainer';
} else {
videoSpanId = `participant_${id}`;
}
return videoSpanId;
}
/**
* The audio Levels plugin.
* Responsible for drawing audio levels.
*/
const AudioLevels = {
init () {
dominantSpeakerAudioElement = $('#dominantSpeakerAudioLevel')[0];
ASDrawContext = dominantSpeakerAudioElement.getContext('2d');
let parentContainer = $("#dominantSpeaker");
let dominantSpeakerWidth = parentContainer.width();
let dominantSpeakerHeight = parentContainer.height();
dominantSpeakerAudioElement.width = dominantSpeakerWidth;
dominantSpeakerAudioElement.height = dominantSpeakerHeight;
let dominantSpeakerAvatar = $("#dominantSpeakerAvatar");
initDominantSpeakerAudioLevels(dominantSpeakerAvatar.width());
},
/**
* The number of dots.
*
* IMPORTANT: functions below assume that this is an odd number.
*/
_AUDIO_LEVEL_DOTS: 5,
/**
* Updates the audio level canvas for the given id. If the canvas
* didn't exist we create it.
* Creates the audio level indicator span element.
*
* IMPORTANT: This function assumes that the number of dots is an
* odd number.
*
* @return {Element} the document element representing audio levels
*/
updateAudioLevelCanvas (id, thumbWidth, thumbHeight) {
let videoSpanId = 'localVideoContainer';
if (id) {
videoSpanId = `participant_${id}`;
}
let videoSpan = document.getElementById(videoSpanId);
if (!videoSpan) {
if (id) {
console.error("No video element for id", id);
} else {
console.error("No video element for local video.");
}
return;
}
let audioLevelCanvas = $(`#${videoSpanId}>canvas`);
if (!audioLevelCanvas || audioLevelCanvas.length === 0) {
audioLevelCanvas = document.createElement('canvas');
audioLevelCanvas.className = "audiolevel";
audioLevelCanvas.style.bottom
= `-${interfaceConfig.CANVAS_EXTRA/2}px`;
audioLevelCanvas.style.left
= `-${interfaceConfig.CANVAS_EXTRA/2}px`;
resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
videoSpan.appendChild(audioLevelCanvas);
} else {
audioLevelCanvas = audioLevelCanvas.get(0);
resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
createThumbnailAudioLevelIndicator() {
let audioSpan = document.createElement('span');
audioSpan.className = 'audioindicator';
this.sideDotsCount = Math.floor(this._AUDIO_LEVEL_DOTS/2);
for (let i = 0; i < this._AUDIO_LEVEL_DOTS; i++) {
let audioDot = document.createElement('span');
// The median index will be equal to the number of dots on each
// side.
if (i === this.sideDotsCount)
audioDot.className = "audiodot-middle";
else
audioDot.className = (i < this.sideDotsCount)
? "audiodot-top"
: "audiodot-bottom";
audioSpan.appendChild(audioDot);
}
return audioSpan;
},
/**
* Updates the audio level UI for the given id.
*
* @param id id of the user for whom we draw the audio level
* @param audioLevel the newAudio level to render
* @param {string} id id of the user for whom we draw the audio level
* @param {number} audioLevel the newAudio level to render
*/
updateAudioLevel (id, audioLevel, largeVideoId) {
drawAudioLevelCanvas(id, audioLevel);
updateThumbnailAudioLevel (id, audioLevel) {
let videoSpanId = getVideoSpanId(id);
// First make sure we are sensitive enough.
audioLevel *= 1.2;
audioLevel = Math.min(audioLevel, 1);
let audioLevelCanvas = $(`#${videoSpanId}>canvas`).get(0);
// Let's now stretch the audio level over the number of dots we have.
let stretchedAudioLevel = (this.sideDotsCount + 1) * audioLevel;
let dotLevel = 0.0;
if (!audioLevelCanvas) {
return;
}
for (let i = 0; i < (this.sideDotsCount + 1); i++) {
let drawContext = audioLevelCanvas.getContext('2d');
let canvasCache = audioLevelCanvasCache[id];
drawContext.clearRect(
0, 0, audioLevelCanvas.width, audioLevelCanvas.height
);
drawContext.drawImage(canvasCache, 0, 0);
if (id === LOCAL_LEVEL) {
id = APP.conference.getMyUserId();
if (!id) {
return;
}
}
if(id === largeVideoId) {
window.requestAnimationFrame(function () {
AudioLevels.updateDominantSpeakerAudioLevel(audioLevel);
});
dotLevel = Math.min(1, Math.max(0, (stretchedAudioLevel - i)));
this._setDotLevel(id, i, dotLevel);
}
},
updateDominantSpeakerAudioLevel (audioLevel) {
if($("#dominantSpeaker").css("visibility") == "hidden"
|| ASDrawContext === null) {
/**
* Fills the dot(s) with the specified "index", with as much opacity as
* indicated by "opacity".
*
* @param {string} elementID the parent audio indicator span element
* @param {number} index the index of the dots to fill, where 0 indicates
* the middle dot and the following increments point toward the
* corresponding pair of dots.
* @param {number} opacity the opacity to set for the specified dot.
*/
_setDotLevel(elementID, index, opacity) {
let audioSpan = document.getElementById(elementID)
.getElementsByClassName("audioindicator");
// Make sure the audio span is still around.
if (audioSpan && audioSpan.length > 0)
audioSpan = audioSpan[0];
else
return;
let audioTopDots
= audioSpan.getElementsByClassName("audiodot-top");
let audioDotMiddle
= audioSpan.getElementsByClassName("audiodot-middle");
let audioBottomDots
= audioSpan.getElementsByClassName("audiodot-bottom");
// First take care of the middle dot case.
if (index === 0){
audioDotMiddle[0].style.opacity = opacity;
return;
}
ASDrawContext.clearRect(0, 0,
dominantSpeakerAudioElement.width,
dominantSpeakerAudioElement.height);
if (!audioLevel) {
return;
}
ASDrawContext.shadowBlur = getShadowLevel(audioLevel);
// Fill the shape.
ASDrawContext.fill();
// Index > 0 : we are setting non-middle dots.
index--;
audioBottomDots[index].style.opacity = opacity;
audioTopDots[this.sideDotsCount - index - 1].style.opacity = opacity;
},
updateCanvasSize (thumbWidth, thumbHeight) {
let canvasWidth = thumbWidth + interfaceConfig.CANVAS_EXTRA;
let canvasHeight = thumbHeight + interfaceConfig.CANVAS_EXTRA;
/**
* Updates the audio level of the large video.
*
* @param audioLevel the new audio level to set.
*/
updateLargeVideoAudioLevel(elementId, audioLevel) {
let element = document.getElementById(elementId);
FilmStrip.getThumbs().children('canvas').each(function () {
$(this).attr('width', canvasWidth);
$(this).attr('height', canvasHeight);
});
if(!UIUtil.isVisible(element))
return;
Object.keys(audioLevelCanvasCache).forEach(function (id) {
audioLevelCanvasCache[id].width = canvasWidth;
audioLevelCanvasCache[id].height = canvasHeight;
});
let level = parseFloat(audioLevel);
level = isNaN(level) ? 0 : level;
let shadowElement = element.getElementsByClassName("dynamic-shadow");
if (shadowElement && shadowElement.length > 0)
shadowElement = shadowElement[0];
shadowElement.style.boxShadow = this._updateLargeVideoShadow(level);
},
/**
* Updates the large video shadow effect.
*/
_updateLargeVideoShadow (level) {
var scale = 2,
// Internal circle audio level.
int = {
level: level > 0.15 ? 20 : 0,
color: interfaceConfig.AUDIO_LEVEL_PRIMARY_COLOR
},
// External circle audio level.
ext = {
level: (int.level * scale * level + int.level).toFixed(0),
color: interfaceConfig.AUDIO_LEVEL_SECONDARY_COLOR
};
// Internal blur.
int.blur = int.level ? 2 : 0;
// External blur.
ext.blur = ext.level ? 6 : 0;
return [
`0 0 ${ int.blur }px ${ int.level }px ${ int.color }`,
`0 0 ${ ext.blur }px ${ ext.level }px ${ ext.color }`
].join(', ');
}
};

View File

@@ -1,108 +0,0 @@
/**
* Utility class for drawing canvas shapes.
*/
const CanvasUtil = {
/**
* Draws a round rectangle with a glow. The glowWidth indicates the depth
* of the glow.
*
* @param drawContext the context of the canvas to draw to
* @param x the x coordinate of the round rectangle
* @param y the y coordinate of the round rectangle
* @param w the width of the round rectangle
* @param h the height of the round rectangle
* @param glowColor the color of the glow
* @param glowWidth the width of the glow
*/
drawRoundRectGlow (drawContext, x, y, w, h, r, glowColor, glowWidth) {
// Save the previous state of the context.
drawContext.save();
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
// Draw a round rectangle.
drawContext.beginPath();
drawContext.moveTo(x+r, y);
drawContext.arcTo(x+w, y, x+w, y+h, r);
drawContext.arcTo(x+w, y+h, x, y+h, r);
drawContext.arcTo(x, y+h, x, y, r);
drawContext.arcTo(x, y, x+w, y, r);
drawContext.closePath();
// Add a shadow around the rectangle
drawContext.shadowColor = glowColor;
drawContext.shadowBlur = glowWidth;
drawContext.shadowOffsetX = 0;
drawContext.shadowOffsetY = 0;
// Fill the shape.
drawContext.fill();
drawContext.save();
drawContext.restore();
// 1) Uncomment this line to use Composite Operation, which is doing the
// same as the clip function below and is also antialiasing the round
// border, but is said to be less fast performance wise.
// drawContext.globalCompositeOperation='destination-out';
drawContext.beginPath();
drawContext.moveTo(x+r, y);
drawContext.arcTo(x+w, y, x+w, y+h, r);
drawContext.arcTo(x+w, y+h, x, y+h, r);
drawContext.arcTo(x, y+h, x, y, r);
drawContext.arcTo(x, y, x+w, y, r);
drawContext.closePath();
// 2) Uncomment this line to use Composite Operation, which is doing the
// same as the clip function below and is also antialiasing the round
// border, but is said to be less fast performance wise.
// drawContext.fill();
// Comment these two lines if choosing to do the same with composite
// operation above 1 and 2.
drawContext.clip();
drawContext.clearRect(0, 0, 277, 200);
// Restore the previous context state.
drawContext.restore();
},
/**
* Clones the given canvas.
*
* @return the new cloned canvas.
*/
cloneCanvas (oldCanvas) {
/*
* FIXME Testing has shown that oldCanvas may not exist. In such a case,
* the method CanvasUtil.cloneCanvas may throw an error. Since audio
* levels are frequently updated, the errors have been observed to pile
* into the console, strain the CPU.
*/
if (!oldCanvas)
return oldCanvas;
//create a new canvas
var newCanvas = document.createElement('canvas');
var context = newCanvas.getContext('2d');
//set dimensions
newCanvas.width = oldCanvas.width;
newCanvas.height = oldCanvas.height;
//apply the old canvas to the new one
context.drawImage(oldCanvas, 0, 0);
//return the new canvas
return newCanvas;
}
};
export default CanvasUtil;

View File

@@ -4,7 +4,6 @@ import LoginDialog from './LoginDialog';
import UIUtil from '../util/UIUtil';
import {openConnection} from '../../../connection';
const ConferenceEvents = JitsiMeetJS.events.conference;
const ConnectionErrors = JitsiMeetJS.errors.connection;
let externalAuthWindow;
@@ -73,7 +72,6 @@ function redirectToTokenAuthService(roomName) {
* @param room the name fo the conference room.
*/
function initJWTTokenListener(room) {
var self = this;
var listener = function (event) {
if (externalAuthWindow !== event.source) {
console.warn("Ignored message not coming " +
@@ -279,15 +277,12 @@ function showXmppPasswordPrompt(roomName, connect) {
function requestAuth(roomName, connect) {
if (isTokenAuthEnabled) {
// This Promise never resolves as user gets redirected to another URL
return new Promise(function (resolve, reject) {
redirectToTokenAuthService(roomName);
});
return new Promise(() => redirectToTokenAuthService(roomName));
} else {
return showXmppPasswordPrompt(roomName, connect);
}
}
export default {
authenticate,
requireAuth,

View File

@@ -1,4 +1,4 @@
/* global $, APP, config*/
/* global APP, config */
/**
* Build html for "password required" dialog.
@@ -109,7 +109,7 @@ function LoginDialog(successCallback, cancelCallback) {
html: '<div id="errorMessage"></div>',
buttons: finishedButtons,
defaultButton: 0,
submit: function (e, v, m, f) {
submit: function (e, v) {
e.preventDefault();
if (v === 'retry') {
connDialog.goToState('login');

View File

@@ -51,7 +51,7 @@ function askForPassword () {
APP.UI.messageHandler.openTwoButtonDialog(
null, null, null, msg,
true, "dialog.Ok",
function (e, v, m, f) {}, null,
function () {}, null,
function (e, v, m, f) {
if (v && f.lockKey) {
resolve(UIUtil.escapeHtml(f.lockKey));
@@ -116,6 +116,13 @@ export default function createRoomLocker (room) {
let password;
let dialog = null;
/**
* If the room was locked from someone other than us, we indicate it with
* this property in order to have correct roomLocker state of isLocked.
* @type {boolean} whether room is locked, but not from us.
*/
let lockedElsewhere = false;
function lock (newPass) {
return room.lock(newPass).then(function () {
password = newPass;
@@ -135,13 +142,30 @@ export default function createRoomLocker (room) {
*/
return {
get isLocked () {
return !!password;
return !!password || lockedElsewhere;
},
get password () {
return password;
},
/**
* Sets that the room is locked from another user, not us.
* @param {boolean} value locked/unlocked state
*/
set lockedElsewhere (value) {
lockedElsewhere = value;
},
/**
* Whether room is locked from someone else.
* @returns {boolean} whether room is not locked locally,
* but it is still locked.
*/
get lockedElsewhere () {
return lockedElsewhere;
},
/**
* Allows to remove password from the conference (asks user first).
* @returns {Promise}
@@ -185,6 +209,10 @@ export default function createRoomLocker (room) {
newPass => { password = newPass; }
).catch(
reason => {
// user canceled, no pass was entered.
// clear, as if we use the same instance several times
// pass stays between attempts
password = null;
if (reason !== APP.UI.messageHandler.CANCEL)
console.error(reason);
}
@@ -202,7 +230,7 @@ export default function createRoomLocker (room) {
dialog = null;
};
if (password) {
if (this.isLocked) {
dialog = APP.UI.messageHandler
.openMessageDialog(null, "dialog.passwordError",
null, null, closeCallback);

View File

@@ -2,7 +2,6 @@
import VideoLayout from "../videolayout/VideoLayout";
import LargeContainer from '../videolayout/LargeContainer';
import UIUtil from "../util/UIUtil";
import UIEvents from "../../../service/UI/UIEvents";
import FilmStrip from '../videolayout/FilmStrip';
@@ -101,6 +100,7 @@ class Etherpad extends LargeContainer {
return document.getElementById('etherpad');
}
// eslint-disable-next-line no-unused-vars
resize (containerWidth, containerHeight, animate) {
let height = containerHeight - FilmStrip.getFilmStripHeight();
let width = containerWidth;

View File

@@ -0,0 +1,128 @@
/* global $, APP, JitsiMeetJS */
import UIEvents from "../../../service/UI/UIEvents";
import FeedabckWindow from "./FeedbackWindow";
/**
* Shows / hides the feedback button.
* @private
*/
function _toggleFeedbackIcon() {
$('#feedbackButtonDiv').toggleClass("hidden");
}
/**
* Shows / hides the feedback button.
* @param {show} set to {true} to show the feedback button or to {false}
* to hide it
* @private
*/
function _showFeedbackButton (show) {
var feedbackButton = $("#feedbackButtonDiv");
if (show)
feedbackButton.css("display", "block");
else
feedbackButton.css("display", "none");
}
/**
* Defines all methods in connection to the Feedback window.
*
* @type {{openFeedbackWindow: Function}}
*/
var Feedback = {
/**
* Initialise the Feedback functionality.
* @param emitter the EventEmitter to associate with the Feedback.
*/
init: function (emitter) {
// CallStats is the way we send feedback, so we don't have to initialise
// if callstats isn't enabled.
if (!APP.conference.isCallstatsEnabled())
return;
// If enabled property is still undefined, i.e. it hasn't been set from
// some other module already, we set it to true by default.
if (typeof this.enabled == "undefined")
this.enabled = true;
_showFeedbackButton(this.enabled);
this.window = new FeedabckWindow({});
$("#feedbackButton").click(Feedback.openFeedbackWindow);
// Show / hide the feedback button whenever the film strip is
// shown / hidden.
emitter.addListener(UIEvents.TOGGLE_FILM_STRIP, function () {
_toggleFeedbackIcon();
});
},
/**
* Enables/ disabled the feedback feature.
*/
enableFeedback: function (enable) {
if (this.enabled !== enable)
_showFeedbackButton(enable);
this.enabled = enable;
},
/**
* Indicates if the feedback functionality is enabled.
*
* @return true if the feedback functionality is enabled, false otherwise.
*/
isEnabled: function() {
return this.enabled && APP.conference.isCallstatsEnabled();
},
/**
* Returns true if the feedback window is currently visible and false
* otherwise.
* @return {boolean} true if the feedback window is visible, false
* otherwise
*/
isVisible: function() {
return $(".feedback").is(":visible");
},
/**
* Indicates if the feedback is submitted.
*
* @return {boolean} {true} to indicate if the feedback is submitted,
* {false} - otherwise
*/
isSubmitted: function() {
return Feedback.window.submitted;
},
/**
* Opens the feedback window.
*/
openFeedbackWindow: function (callback) {
Feedback.window.show(callback);
JitsiMeetJS.analytics.sendEvent('feedback.open');
},
/**
* Returns the feedback score.
*
* @returns {*}
*/
getFeedbackScore: function() {
return Feedback.window.feedbackScore;
},
/**
* Returns the feedback free text.
*
* @returns {null|*|message}
*/
getFeedbackText: function() {
return Feedback.window.feedbackText;
}
};
module.exports = Feedback;

View File

@@ -0,0 +1,197 @@
/* global $, APP, interfaceConfig, AJS */
const selector = '#aui-feedback-dialog';
/**
* Toggles the appropriate css class for the given number of stars, to
* indicate that those stars have been clicked/selected.
*
* @param starCount the number of stars, for which to toggle the css class
*/
function toggleStars(starCount) {
$('#stars > a').each(function(index, el) {
if (index <= starCount) {
el.classList.add("starHover");
} else
el.classList.remove("starHover");
});
}
/**
* Constructs the html for the rated feedback window.
*
* @returns {string} the contructed html string
*/
function createRateFeedbackHTML() {
let rateExperience
= APP.translation.translateString('dialog.rateExperience'),
feedbackHelp = APP.translation.translateString('dialog.feedbackHelp');
let starClassName = (interfaceConfig.ENABLE_FEEDBACK_ANIMATION)
? "icon-star shake-rotate"
: "icon-star";
return `
<div class="aui-dialog2-content feedback__content">
<form action="javascript:false;" onsubmit="return false;">
<div class="feedback__rating">
<h2>${ rateExperience }</h2>
<p class="star-label">&nbsp;</p>
<div id="stars" class="feedback-stars">
<a class="star-btn">
<i class=${ starClassName }></i>
</a>
<a class="star-btn">
<i class=${ starClassName }></i>
</a>
<a class="star-btn">
<i class=${ starClassName }></i>
</a>
<a class="star-btn">
<i class=${ starClassName }></i>
</a>
<a class="star-btn">
<i class=${ starClassName }></i>
</a>
</div>
<p>&nbsp;</p>
<p>${ feedbackHelp }</p>
</div>
<textarea id="feedbackTextArea" rows="10" cols="40" autofocus>
</textarea>
</form>
<footer class="aui-dialog2-footer feedback__footer">
<div class="aui-dialog2-footer-actions">
<button
id="dialog-close-button"
class="aui-button aui-button_close">Close</button>
<button
id="dialog-submit-button"
class="aui-button aui-button_submit">Submit</button>
</div>
</footer>
</div>
`;
}
/**
* Callback for Rate Feedback
*
* @param Feedback
*/
let onLoadRateFunction = function (Feedback) {
$('#stars > a').each((index, el) => {
el.onmouseover = function(){
toggleStars(index);
};
el.onmouseleave = function(){
toggleStars(Feedback.feedbackScore - 1);
};
el.onclick = function(){
Feedback.feedbackScore = index + 1;
};
});
// Init stars to correspond to previously entered feedback.
if (Feedback.feedbackScore > 0) {
toggleStars(Feedback.feedbackScore - 1);
}
if (Feedback.feedbackText && Feedback.feedbackText.length > 0)
$('#feedbackTextArea').text(Feedback.feedbackText);
let submitBtn = Feedback.$el.find('#dialog-submit-button');
let closeBtn = Feedback.$el.find('#dialog-close-button');
if (submitBtn && submitBtn.length) {
submitBtn.on('click', (e) => {
e.preventDefault();
Feedback.onFeedbackSubmitted();
});
}
if (closeBtn && closeBtn.length) {
closeBtn.on('click', (e) => {
e.preventDefault();
Feedback.hide();
});
}
$('#feedbackTextArea').focus();
};
/**
* @class Dialog
*
*/
export default class Dialog {
constructor(options) {
this.feedbackScore = -1;
this.feedbackText = null;
this.submitted = false;
this.onCloseCallback = null;
this.states = {
rate_feedback: {
getHtml: createRateFeedbackHTML,
onLoad: onLoadRateFunction
}
};
this.state = options.state || 'rate_feedback';
this.window = AJS.dialog2(selector, {
closeOnOutsideClick: true
});
this.$el = this.window.$el;
AJS.dialog2(selector).on("hide", function() {
if (this.onCloseCallback) {
this.onCloseCallback();
this.onCloseCallback = null;
}
}.bind(this));
this.setState();
}
setState(state) {
let newState = state || this.state;
let htmlStr = this.states[newState].getHtml(this);
this.$el.html(htmlStr);
this.states[newState].onLoad(this);
}
show(cb) {
this.setState('rate_feedback');
if (typeof cb == 'function') {
this.onCloseCallback = cb;
}
this.window.show();
}
hide() {
this.window.hide();
}
onFeedbackSubmitted() {
let message = this.$el.find('textarea').val();
let self = this;
if (message && message.length > 0) {
self.feedbackText = message;
}
APP.conference.sendFeedback(self.feedbackScore,
self.feedbackText);
// TO DO: make sendFeedback return true or false.
self.submitted = true;
this.hide();
}
}

View File

@@ -1,4 +1,4 @@
/* global $, APP, JitsiMeetJS */
/* global $, APP */
let $overlay;
@@ -43,4 +43,4 @@ export default {
hide() {
$overlay && $overlay.detach();
}
};
};

View File

@@ -17,7 +17,7 @@
import UIEvents from "../../../service/UI/UIEvents";
import UIUtil from '../util/UIUtil';
import VideoLayout from '../videolayout/VideoLayout';
import Feedback from '../Feedback.js';
import Feedback from '../feedback/Feedback.js';
import Toolbar from '../toolbars/Toolbar';
/**
@@ -97,7 +97,7 @@ function _requestLiveStreamId() {
],
focus: ':input:first',
defaultButton: 1,
submit: function (e, v, m, f) {
submit: function (e, v) {
e.preventDefault();
if (v === 0) {
reject(APP.UI.messageHandler.CANCEL);
@@ -177,7 +177,7 @@ function _showStopRecordingPrompt (recordingType) {
null,
false,
buttonKey,
function(e,v,m,f) {
function(e,v) {
if (v) {
resolve();
} else {
@@ -270,6 +270,9 @@ var Recording = {
initRecordingButton(recordingType) {
let selector = $('#toolbar_button_record');
let button = selector.get(0);
UIUtil.setTooltip(button, 'liveStreaming.buttonTooltip', 'right');
if (recordingType === 'jibri') {
this.baseClass = "fa fa-play-circle";
this.recordingTitle = "dialog.liveStreaming";

View File

@@ -1,5 +1,21 @@
/* global $ */
/* global $, APP */
/* jshint -W101 */
import UIEvents from "../../../service/UI/UIEvents";
/**
* Store the current ring overlay instance.
* Note: We want to have only 1 instance at a time.
*/
let overlay = null;
/**
* Handler for UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED event.
* @param {boolean} shown indicates whether the avatar on the large video is
* currently displayed or not.
*/
function onAvatarDisplayed(shown) {
overlay._changeBackground(shown);
}
/**
* Shows ring overlay
@@ -11,28 +27,49 @@ class RingOverlay {
constructor(callee) {
this._containerId = 'ringOverlay';
this._audioContainerId = 'ringOverlayRinging';
this.isRinging = true;
this.callee = callee;
this.render();
this.audio = document.getElementById(this._audioContainerId);
this.audio.play();
this._setAudioTimeout();
this._timeout = setTimeout(() => {
this.destroy();
this.render();
}, 30000);
}
/**
* Chagnes the background of the ring overlay.
* @param {boolean} solid - if true the new background will be the solid
* one, otherwise the background will be default one.
* NOTE: The method just toggles solidBG css class.
*/
_changeBackground(solid) {
const container = $("#" + this._containerId);
if(solid) {
container.addClass("solidBG");
} else {
container.removeClass("solidBG");
}
}
/**
* Builds and appends the ring overlay to the html document
*/
_getHtmlStr(callee) {
let callingLabel = this.isRinging? "<p>Calling...</p>" : "";
let callerStateLabel = this.isRinging? "" : " isn't available";
return `
<div id="${this._containerId}" class='ringing' >
<div class='ringing__content'>
<p>Calling...</p>
${callingLabel}
<img class='ringing__avatar' src="${callee.getAvatarUrl()}" />
<div class="ringing__caller-info">
<p>${callee.getName()}</p>
<p>${callee.getName()}${callerStateLabel}</p>
</div>
</div>
<audio id="${this._audioContainerId}" src="/sounds/ring.ogg" />
<audio id="${this._audioContainerId}" src="./sounds/ring.ogg" />
</div>`;
}
@@ -49,10 +86,7 @@ class RingOverlay {
* related to the ring overlay.
*/
destroy() {
if (this.interval) {
clearInterval(this.interval);
}
this._stopAudio();
this._detach();
}
@@ -64,6 +98,16 @@ class RingOverlay {
$(`#${this._containerId}`).remove();
}
_stopAudio() {
this.isRinging = false;
if (this.interval) {
clearInterval(this.interval);
}
if(this._timeout) {
clearTimeout(this._timeout);
}
}
/**
* Sets the interval that is going to play the ringing sound.
*/
@@ -74,12 +118,6 @@ class RingOverlay {
}
}
/**
* Store the current ring overlay instance.
* Note: We want to have only 1 instance at a time.
*/
let overlay = null;
export default {
/**
* Shows the ring overlay for the passed callee.
@@ -92,6 +130,8 @@ export default {
}
overlay = new RingOverlay(callee);
APP.UI.addListener(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED,
onAvatarDisplayed);
},
/**
@@ -104,6 +144,8 @@ export default {
}
overlay.destroy();
overlay = null;
APP.UI.removeListener(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED,
onAvatarDisplayed);
return true;
},

View File

@@ -243,7 +243,7 @@ export default class SharedVideoManager {
let thumb = new SharedVideoThumb(self.url);
thumb.setDisplayName(player.getVideoData().title);
VideoLayout.addParticipantContainer(self.url, thumb);
VideoLayout.addRemoteVideoContainer(self.url, thumb);
let iframe = player.getIframe();
self.sharedVideo = new SharedVideoContainer(
@@ -567,10 +567,6 @@ class SharedVideoContainer extends LargeContainer {
this.player = player;
}
get $video () {
return this.$iframe;
}
show () {
let self = this;
return new Promise(resolve => {
@@ -733,7 +729,7 @@ function showStopVideoPropmpt() {
null,
false,
"dialog.Remove",
function(e,v,m,f) {
function(e,v) {
if (v) {
resolve();
} else {
@@ -815,7 +811,7 @@ function requestVideoLink() {
],
focus: ':input:first',
defaultButton: 1,
submit: function (e, v, m, f) {
submit: function (e, v) {
e.preventDefault();
if (v === 0) {
reject();

View File

@@ -3,24 +3,26 @@
import {processReplacements, linkify} from './Replacement';
import CommandsProcessor from './Commands';
import ToolbarToggler from '../../toolbars/ToolbarToggler';
import VideoLayout from "../../videolayout/VideoLayout";
import UIUtil from '../../util/UIUtil';
import UIEvents from '../../../../service/UI/UIEvents';
var smileys = require("./smileys.json").smileys;
import { smileys } from './smileys';
var notificationInterval = false;
var unreadMessages = 0;
/**
* The container id, which is and the element id.
*/
var CHAT_CONTAINER_ID = "chat_container";
/**
* Shows/hides a visual notification, indicating that a message has arrived.
* Updates visual notification, indicating that a message has arrived.
*/
function setVisualNotification(show) {
function updateVisualNotification() {
var unreadMsgElement = document.getElementById('unreadMessages');
var glower = $('#toolbar_button_chat');
if (unreadMessages) {
unreadMsgElement.innerHTML = unreadMessages.toString();
@@ -37,28 +39,12 @@ function setVisualNotification(show) {
'style',
'top:' + topIndent +
'; left:' + leftIndent + ';');
if (!glower.hasClass('icon-chat-simple')) {
glower.removeClass('icon-chat');
glower.addClass('icon-chat-simple');
}
}
else {
unreadMsgElement.innerHTML = '';
glower.removeClass('icon-chat-simple');
glower.addClass('icon-chat');
}
if (show && !notificationInterval) {
notificationInterval = window.setInterval(function () {
glower.toggleClass('active');
}, 800);
}
else if (!show && notificationInterval) {
window.clearInterval(notificationInterval);
notificationInterval = false;
glower.removeClass('active');
}
$(unreadMsgElement).parent()[unreadMessages > 0 ? 'show' : 'hide']();
}
@@ -131,7 +117,7 @@ function addSmileys() {
*/
function resizeChatConversation() {
var msgareaHeight = $('#usermsg').outerHeight();
var chatspace = $('#chat_container');
var chatspace = $('#' + CHAT_CONTAINER_ID);
var width = chatspace.width();
var chat = $('#chatconversation');
var smileys = $('#smileysarea');
@@ -187,13 +173,30 @@ var Chat = {
};
usermsg.autosize({callback: onTextAreaResize});
$("#chat_container").bind("shown",
function () {
eventEmitter.on(UIEvents.SIDE_TOOLBAR_CONTAINER_TOGGLED,
function(containerId, isVisible) {
if (containerId !== CHAT_CONTAINER_ID || !isVisible)
return;
unreadMessages = 0;
setVisualNotification(false);
updateVisualNotification();
// Undock the toolbar when the chat is shown and if we're in a
// video mode.
if (VideoLayout.isLargeVideoVisible()) {
ToolbarToggler.dockToolbar(false);
}
// if we are in conversation mode focus on the text input
// if we are not, focus on the display name input
if (APP.settings.getDisplayName())
$('#usermsg').focus();
else
$('#nickinput').focus();
});
addSmileys();
updateVisualNotification();
},
/**
@@ -210,7 +213,7 @@ var Chat = {
if (!Chat.isVisible()) {
unreadMessages++;
UIUtil.playSoundNotification('chatNotification');
setVisualNotification(true);
updateVisualNotification();
}
}
@@ -271,12 +274,18 @@ var Chat = {
/**
* Sets the chat conversation mode.
* Conversation mode is the normal chat mode, non conversation mode is
* where we ask user to input its display name.
* @param {boolean} isConversationMode if chat should be in
* conversation mode or not.
*/
setChatConversationMode (isConversationMode) {
$('#chat_container')
$('#' + CHAT_CONTAINER_ID)
.toggleClass('is-conversation-mode', isConversationMode);
// this is needed when we transition from no conversation mode to
// conversation mode. When user enters his nickname and hits enter,
// to focus on the write area.
if (isConversationMode) {
$('#usermsg').focus();
}
@@ -286,7 +295,7 @@ var Chat = {
* Resizes the chat area.
*/
resizeChat (width, height) {
$('#chat_container').width(width).height(height);
$('#' + CHAT_CONTAINER_ID).width(width).height(height);
resizeChatConversation();
},
@@ -296,7 +305,7 @@ var Chat = {
*/
isVisible () {
return UIUtil.isVisible(
document.getElementById("chat_container"));
document.getElementById(CHAT_CONTAINER_ID));
},
/**
* Shows and hides the window with the smileys

View File

@@ -1,4 +1,3 @@
/* global APP */
import UIUtil from '../../util/UIUtil';
import UIEvents from '../../../../service/UI/UIEvents';

View File

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

View File

@@ -0,0 +1,47 @@
export const smileys = {
smiley1: ":)",
smiley2: ":(",
smiley3: ":D",
smiley4: "(y)",
smiley5: " :P",
smiley6: "(wave)",
smiley7: "(blush)",
smiley8: "(chuckle)",
smiley9: "(shocked)",
smiley10: ":*",
smiley11: "(n)",
smiley12: "(search)",
smiley13: " <3",
smiley14: "(oops)",
smiley15: "(angry)",
smiley16: "(angel)",
smiley17: "(sick)",
smiley18: ";(",
smiley19: "(bomb)",
smiley20: "(clap)",
smiley21: " ;)"
};
export const regexes = {
smiley2: /(:-\(\(|:-\(|:\(\(|:\(|\(sad\))/gi,
smiley3: /(:-\)\)|:\)\)|\(lol\)|:-D|:D)/gi,
smiley1: /(:-\)|:\))/gi,
smiley4: /(\(y\)|\(Y\)|\(ok\))/gi,
smiley5: /(:-P|:P|:-p|:p)/gi,
smiley6: /(\(wave\))/gi,
smiley7: /(\(blush\))/gi,
smiley8: /(\(chuckle\))/gi,
smiley9: /(:-0|\(shocked\))/gi,
smiley10: /(:-\*|:\*|\(kiss\))/gi,
smiley11: /(\(n\))/gi,
smiley12: /(\(search\))/g,
smiley13: /(<3|&lt;3|&amp;lt;3|\(L\)|\(l\)|\(H\)|\(h\))/gi,
smiley14: /(\(oops\))/gi,
smiley15: /(\(angry\))/gi,
smiley16: /(\(angel\))/gi,
smiley17: /(\(sick\))/gi,
smiley18: /(;-\(\(|;\(\(|;-\(|;\(|:"\(|:"-\(|:~-\(|:~\(|\(upset\))/gi,
smiley19: /(\(bomb\))/gi,
smiley20: /(\(clap\))/gi,
smiley21: /(;-\)|;\)|;-\)\)|;\)\)|;-D|;D|\(wink\))/gi
};

View File

@@ -1,48 +0,0 @@
{
"smileys": {
"smiley1": ":)",
"smiley2": ":(",
"smiley3": ":D",
"smiley4": "(y)",
"smiley5": " :P",
"smiley6": "(wave)",
"smiley7": "(blush)",
"smiley8": "(chuckle)",
"smiley9": "(shocked)",
"smiley10": ":*",
"smiley11": "(n)",
"smiley12": "(search)",
"smiley13": " <3",
"smiley14": "(oops)",
"smiley15": "(angry)",
"smiley16": "(angel)",
"smiley17": "(sick)",
"smiley18": ";(",
"smiley19": "(bomb)",
"smiley20": "(clap)",
"smiley21": " ;)"
},
"regexs": {
"smiley2": /(:-\(\(|:-\(|:\(\(|:\(|\(sad\))/gi,
"smiley3": /(:-\)\)|:\)\)|\(lol\)|:-D|:D)/gi,
"smiley1": /(:-\)|:\))/gi,
"smiley4": /(\(y\)|\(Y\)|\(ok\))/gi,
"smiley5": /(:-P|:P|:-p|:p)/gi,
"smiley6": /(\(wave\))/gi,
"smiley7": /(\(blush\))/gi,
"smiley8": /(\(chuckle\))/gi,
"smiley9": /(:-0|\(shocked\))/gi,
"smiley10": /(:-\*|:\*|\(kiss\))/gi,
"smiley11": /(\(n\))/gi,
"smiley12": /(\(search\))/g,
"smiley13": /(<3|&lt;3|&amp;lt;3|\(L\)|\(l\)|\(H\)|\(h\))/gi,
"smiley14": /(\(oops\))/gi,
"smiley15": /(\(angry\))/gi,
"smiley16": /(\(angel\))/gi,
"smiley17": /(\(sick\))/gi,
"smiley18": /(;-\(\(|;\(\(|;-\(|;\(|:"\(|:"-\(|:~-\(|:~\(|\(upset\))/gi,
"smiley19": /(\(bomb\))/gi,
"smiley20": /(\(clap\))/gi,
"smiley21": /(;-\)|;\)|;-\)\)|;\)\)|;-D|;D|\(wink\))/gi
}
}

View File

@@ -1,10 +1,9 @@
/* global $, APP */
/* global $, APP, interfaceConfig */
import Avatar from '../../avatar/Avatar';
import UIEvents from '../../../../service/UI/UIEvents';
import UIUtil from '../../util/UIUtil';
let numberOfContacts = 0;
let notificationInterval;
/**
* Updates the number of participants in the contact list button and sets
@@ -20,13 +19,11 @@ function updateNumberOfParticipants(delta) {
return;
}
let buttonIndicatorText = (numberOfContacts === 1) ? '' : numberOfContacts;
$("#numberOfParticipants").text(buttonIndicatorText);
$("#numberOfParticipants").text(numberOfContacts);
$("#contacts_container>div.title").text(
APP.translation.translateString(
"contactlist", {participants: numberOfContacts}
));
APP.translation.translateString("contactlist")
+ ' (' + numberOfContacts + ')');
}
/**
@@ -59,24 +56,10 @@ function createDisplayNameParagraph(key, displayName) {
return p;
}
function stopGlowing(glower) {
window.clearInterval(notificationInterval);
notificationInterval = false;
glower.removeClass('glowing');
if (!ContactList.isVisible()) {
glower.removeClass('active');
}
}
function getContactEl (id) {
return $(`#contacts>li[id="${id}"]`);
}
function contactElExists (id) {
return getContactEl(id).length > 0;
}
/**
* Contact list.
*/
@@ -96,9 +79,9 @@ var ContactList = {
/**
* Adds a contact for the given id.
*
* @param isLocal is an id for the local user.
*/
addContact (id) {
addContact (id, isLocal) {
let contactlist = $('#contacts');
let newContact = document.createElement('li');
@@ -110,8 +93,13 @@ var ContactList = {
}
};
newContact.appendChild(createAvatar(id));
newContact.appendChild(createDisplayNameParagraph("participant"));
if (interfaceConfig.SHOW_CONTACTLIST_AVATARS)
newContact.appendChild(createAvatar(id));
newContact.appendChild(
createDisplayNameParagraph(
isLocal ? interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME : null,
isLocal ? null : interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME));
if (APP.conference.isLocalId(id)) {
contactlist.prepend(newContact);

View File

@@ -1,7 +1,6 @@
/* global APP, $, JitsiMeetJS */
/* global $ */
import UIUtil from "../../util/UIUtil";
import UIEvents from "../../../../service/UI/UIEvents";
import languages from "../../../../service/translation/languages";
import Settings from '../../../settings/Settings';
export default {
@@ -58,4 +57,4 @@ export default {
changeAvatar (avatarUrl) {
$('#avatar').attr('src', avatarUrl);
}
};
};

View File

@@ -1,4 +1,4 @@
/* global APP, $, JitsiMeetJS, interfaceConfig */
/* global APP, $, JitsiMeetJS */
import UIUtil from "../../util/UIUtil";
import UIEvents from "../../../../service/UI/UIEvents";
import languages from "../../../../service/translation/languages";
@@ -74,10 +74,7 @@ export default {
}
});
// Only show the subtitle if this isn't the only setting section.
if (interfaceConfig.SETTINGS_SECTIONS.length > 1)
UIUtil.showElement("deviceOptionsTitle");
UIUtil.showElement("deviceOptionsTitle");
UIUtil.showElement("devicesOptions");
}
@@ -150,8 +147,7 @@ export default {
showStartMutedOptions (show) {
if (show && UIUtil.isSettingEnabled('moderator')) {
// Only show the subtitle if this isn't the only setting section.
if (!$("#moderatorOptionsTitle").is(":visible")
&& interfaceConfig.SETTINGS_SECTIONS.length > 1)
if (!$("#moderatorOptionsTitle").is(":visible"))
UIUtil.showElement("moderatorOptionsTitle");
UIUtil.showElement("startMutedOptions");
@@ -272,4 +268,4 @@ export default {
APP.translation.translateElement($('#settings_container option'));
}
};
};

View File

@@ -1,5 +1,4 @@
/* global APP, $, config, interfaceConfig, JitsiMeetJS */
/* jshint -W101 */
import UIUtil from '../util/UIUtil';
import UIEvents from '../../../service/UI/UIEvents';
import SideContainerToggler from "../side_pannels/SideContainerToggler";
@@ -7,7 +6,6 @@ import SideContainerToggler from "../side_pannels/SideContainerToggler";
let roomUrl = null;
let emitter = null;
/**
* Opens the invite link dialog.
*/
@@ -21,36 +19,46 @@ function openLinkDialog () {
inviteAttributes = "value=\"" + encodeURI(roomUrl) + "\"";
}
let inviteLinkId = "inviteLinkRef";
let focusInviteLink = function() {
$('#' + inviteLinkId).focus();
$('#' + inviteLinkId).select();
};
let title = APP.translation.generateTranslationHTML("dialog.shareLink");
APP.UI.messageHandler.openTwoButtonDialog(
null, null, null,
'<h2>' + title + '</h2>'
+ '<input id="inviteLinkRef" type="text" '
+ inviteAttributes + ' onclick="this.select();" readonly>',
false, "dialog.Invite",
null, title, null,
'<input id="' + inviteLinkId + '" type="text" '
+ inviteAttributes + ' readonly/>',
false, "dialog.copy",
function (e, v) {
if (v && roomUrl) {
JitsiMeetJS.analytics.sendEvent('toolbar.invite.button');
emitter.emit(UIEvents.USER_INVITED, roomUrl);
focusInviteLink();
document.execCommand('copy');
}
else {
JitsiMeetJS.analytics.sendEvent('toolbar.invite.cancel');
}
},
function (event) {
if (roomUrl) {
document.getElementById('inviteLinkRef').select();
} else {
if (!roomUrl) {
if (event && event.target) {
$(event.target).find('button[value=true]')
.prop('disabled', true);
}
}
else {
focusInviteLink();
}
},
function (e, v, m, f) {
if(!v && !m && !f)
JitsiMeetJS.analytics.sendEvent('toolbar.invite.close');
}
},
'Copy' // Focus Copy button.
);
}
@@ -168,14 +176,20 @@ const buttonHandlers = {
},
"toolbar_film_strip": function () {
JitsiMeetJS.analytics.sendEvent(
'bottomtoolbar.filmstrip.toggled');
'toolbar.filmstrip.toggled');
emitter.emit(UIEvents.TOGGLE_FILM_STRIP);
},
"toolbar_button_raisehand": function () {
JitsiMeetJS.analytics.sendEvent(
'toolbar.raiseHand.clicked');
APP.conference.maybeToggleRaisedHand();
}
};
const defaultToolbarButtons = {
'microphone': {
id: 'toolbar_button_mute',
tooltipKey: 'toolbar.mute',
className: "button icon-microphone",
shortcut: 'M',
shortcutAttr: 'mutePopover',
@@ -194,6 +208,11 @@ const defaultToolbarButtons = {
id: "unableToUnmutePopup",
className: "loginmenu",
dataAttr: "[html]toolbar.unableToUnmutePopup"
},
{
id: "talkWhileMutedPopup",
className: "loginmenu",
dataAttr: "[html]toolbar.talkWhileMutedPopup"
}
],
content: "Mute / Unmute",
@@ -201,6 +220,7 @@ const defaultToolbarButtons = {
},
'camera': {
id: 'toolbar_button_camera',
tooltipKey: 'toolbar.videomute',
className: "button icon-camera",
shortcut: 'V',
shortcutAttr: 'toggleVideoPopover',
@@ -214,6 +234,7 @@ const defaultToolbarButtons = {
},
'desktop': {
id: 'toolbar_button_desktopsharing',
tooltipKey: 'toolbar.sharescreen',
className: 'button icon-share-desktop',
shortcut: 'D',
shortcutAttr: 'toggleDesktopSharingPopover',
@@ -226,16 +247,19 @@ const defaultToolbarButtons = {
i18n: '[content]toolbar.sharescreen'
},
'security': {
id: 'toolbar_button_security'
id: 'toolbar_button_security',
tooltipKey: 'toolbar.lock'
},
'invite': {
id: 'toolbar_button_link',
tooltipKey: 'toolbar.invite',
className: 'button icon-link',
content: 'Invite others',
i18n: '[content]toolbar.invite'
},
'chat': {
id: 'toolbar_button_chat',
tooltipKey: 'toolbar.chat',
shortcut: 'C',
shortcutAttr: 'toggleChatPopover',
shortcutFunc: function() {
@@ -247,40 +271,47 @@ const defaultToolbarButtons = {
},
'contacts': {
id: 'toolbar_contact_list',
tooltipKey: 'bottomtoolbar.contactlist',
sideContainerId: 'contacts_container'
},
'profile': {
id: 'toolbar_button_profile',
tooltipKey: 'profile.setDisplayNameLabel',
sideContainerId: 'profile_container'
},
'etherpad': {
id: 'toolbar_button_etherpad'
id: 'toolbar_button_etherpad',
tooltipKey: 'toolbar.etherpad',
},
'fullscreen': {
id: 'toolbar_button_fullScreen',
tooltipKey: 'toolbar.fullscreen',
className: "button icon-full-screen",
shortcut: 'F',
shortcut: 'S',
shortcutAttr: 'toggleFullscreenPopover',
shortcutFunc: function() {
JitsiMeetJS.analytics.sendEvent('shortcut.fullscreen.toggled');
APP.UI.toggleFullScreen();
},
shortcutDescription: "keyboardShortcuts.toggleChat",
shortcutDescription: "keyboardShortcuts.fullScreen",
content: "Enter / Exit Full Screen",
i18n: "[content]toolbar.fullscreen"
},
'settings': {
id: 'toolbar_button_settings',
tooltipKey: 'toolbar.Settings',
sideContainerId: "settings_container"
},
'hangup': {
id: 'toolbar_button_hangup',
tooltipKey: 'toolbar.hangup',
className: "button icon-hangup",
content: "Hang Up",
i18n: "[content]toolbar.hangup"
},
'filmstrip': {
id: 'toolbar_film_strip',
tooltipKey: 'toolbar.filmstrip',
shortcut: "F",
shortcutAttr: "filmstripPopover",
shortcutFunc: function() {
@@ -288,6 +319,20 @@ const defaultToolbarButtons = {
APP.UI.toggleFilmStrip();
},
shortcutDescription: "keyboardShortcuts.toggleFilmstrip"
},
'raisehand': {
id: "toolbar_button_raisehand",
tooltipKey: 'toolbar.raiseHand',
className: "button icon-raised-hand",
shortcut: "R",
shortcutAttr: "raiseHandPopover",
shortcutFunc: function() {
JitsiMeetJS.analytics.sendEvent("shortcut.raisehand.clicked");
APP.conference.maybeToggleRaisedHand();
},
shortcutDescription: "keyboardShortcuts.raiseHand",
content: "Raise Hand",
i18n: "[content]toolbar.raiseHand"
}
};
@@ -304,7 +349,11 @@ function showSipNumberInput () {
APP.UI.messageHandler.openTwoButtonDialog(
null, null, null,
`<h2>${sipMsg}</h2>
<input name="sipNumber" type="text" value="${defaultNumber}" autofocus>`,
<input
name="sipNumber"
type="text"
value="${defaultNumber}"
autofocus>`,
false, "dialog.Dial",
function (e, v, m, f) {
if (v && f.sipNumber) {
@@ -323,14 +372,28 @@ const Toolbar = {
this.toolbarSelector = $("#mainToolbarContainer");
this.extendedToolbarSelector = $("#extendedToolbar");
this._initMainToolbarButtons();
// First hide all disabled buttons in the extended toolbar.
// TODO: Make the extended toolbar dynamically created.
UIUtil.hideDisabledButtons(defaultToolbarButtons);
// Initialise the main toolbar. The main toolbar will only take into
// account it's own configuration from interface_config.
this._initMainToolbarButtons();
Object.keys(defaultToolbarButtons).forEach(
id => {
if (UIUtil.isButtonEnabled(id)) {
var button = defaultToolbarButtons[id];
let button = defaultToolbarButtons[id];
let buttonElement = document.getElementById(button.id);
let tooltipPosition
= (interfaceConfig.MAIN_TOOLBAR_BUTTONS
.indexOf(id) > -1)
? "bottom" : "right";
UIUtil.setTooltip( buttonElement,
button.tooltipKey,
tooltipPosition);
if (button.shortcut)
APP.keyboardshortcut.registerShortcut(
@@ -355,8 +418,15 @@ const Toolbar = {
isVisible);
});
APP.UI.addListener(UIEvents.LOCAL_RAISE_HAND_CHANGED,
function(isRaisedHand) {
Toolbar._toggleRaiseHand(isRaisedHand);
});
if(!APP.tokenData.isGuest) {
$("#toolbar_button_profile").addClass("unclickable");
UIUtil.removeTooltip(
document.getElementById('toolbar_button_profile'));
}
},
/**
@@ -431,9 +501,11 @@ const Toolbar = {
// Shows or hides the 'shared video' button.
showSharedVideoButton () {
let $element = $('#toolbar_button_sharedvideo');
if (UIUtil.isButtonEnabled('sharedvideo')
&& config.disableThirdPartyRequests !== true) {
$('#toolbar_button_sharedvideo').css({display: "inline-block"});
$element.css({display: "inline-block"});
UIUtil.setTooltip($element.get(0), 'toolbar.sharedvideo', 'right');
} else {
$('#toolbar_button_sharedvideo').css({display: "none"});
}
@@ -518,6 +590,13 @@ const Toolbar = {
}
},
/**
* Toggles / untoggles the view for raised hand.
*/
_toggleRaiseHand(isRaisedHand) {
$('#toolbar_button_raisehand').toggleClass("glow", isRaisedHand);
},
/**
* Marks video icon as muted or not.
* @param {boolean} muted if icon should look like muted or not
@@ -663,7 +742,7 @@ const Toolbar = {
/**
* Handles the side toolbar toggle.
*/
_handleSideToolbarContainerToggled(containerId, isVisible) {
_handleSideToolbarContainerToggled(containerId) {
Object.keys(defaultToolbarButtons).forEach(
id => {
if (!UIUtil.isButtonEnabled(id))
@@ -679,16 +758,9 @@ const Toolbar = {
}
);
},
/**
* TODO: Fix mic popups
* <a class="button icon-microphone" id="toolbar_button_mute" data-container="body" data-toggle="popover" data-placement="bottom" shortcut="mutePopover" data-i18n="[content]toolbar.mute" content="Mute / Unmute">
* <ul id="micMutedPopup" class="loginmenu">
* <li data-i18n="[html]toolbar.micMutedPopup"></li>
* </ul>
* <ul id="unableToUnmutePopup" class="loginmenu">
* <li data-i18n="[html]toolbar.unableToUnmutePopup"></li>
* </ul>
* </a>
* Initialise main toolbar buttons.
*/
_initMainToolbarButtons() {
interfaceConfig.MAIN_TOOLBAR_BUTTONS.forEach((value, index) => {
@@ -730,7 +802,6 @@ const Toolbar = {
buttonElement.setAttribute("data-i18n", button.i18n);
buttonElement.setAttribute("data-container", "body");
buttonElement.setAttribute("data-toggle", "popover");
buttonElement.setAttribute("data-placement", "bottom");
this._addPopups(buttonElement, button.popups);

View File

@@ -3,7 +3,6 @@
import UIUtil from '../util/UIUtil';
import Toolbar from './Toolbar';
import SideContainerToggler from "../side_pannels/SideContainerToggler";
import FilmStrip from '../videolayout/FilmStrip.js';
let toolbarTimeoutObject;
let toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
@@ -27,7 +26,7 @@ function showDesktopSharingButton() {
* @param force {true} to force the hiding of the toolbar without caring about
* the extended toolbar side panels.
*/
function hideToolbar(force) {
function hideToolbar(force) { // eslint-disable-line no-unused-vars
if (alwaysVisibleToolbar) {
return;
}
@@ -35,9 +34,11 @@ function hideToolbar(force) {
clearTimeout(toolbarTimeoutObject);
toolbarTimeoutObject = null;
if (Toolbar.isHovered() || APP.UI.isRingOverlayVisible()) {
if (Toolbar.isHovered()
|| APP.UI.isRingOverlayVisible()
|| SideContainerToggler.isVisible()) {
toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
} else if (!SideContainerToggler.isVisible() || force) {
} else {
Toolbar.hide();
$('#subject').animate({top: "-=40"}, 300);
}
@@ -50,7 +51,8 @@ const ToolbarToggler = {
init() {
alwaysVisibleToolbar = (config.alwaysVisibleToolbar === true);
this._registerWindowClickListeners();
// disabled
//this._registerWindowClickListeners();
},
/**
@@ -86,8 +88,9 @@ const ToolbarToggler = {
/**
* Shows the main toolbar.
* @param timeout (optional) to specify custom timeout value
*/
showToolbar () {
showToolbar (timeout) {
if (interfaceConfig.filmStripOnly) {
return;
}
@@ -104,7 +107,8 @@ const ToolbarToggler = {
clearTimeout(toolbarTimeoutObject);
toolbarTimeoutObject = null;
}
toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout);
toolbarTimeoutObject
= setTimeout(hideToolbar, timeout || toolbarTimeout);
toolbarTimeout = interfaceConfig.TOOLBAR_TIMEOUT;
}

View File

@@ -1,5 +1,4 @@
/* global $, APP, jQuery, toastr, Impromptu */
/* jshint -W101 */
/* global $, APP, toastr, Impromptu */
import UIUtil from './UIUtil';
@@ -274,7 +273,8 @@ var messageHandler = {
displayNameSpan + '<br>' +
'<span class=' + cls + ' data-i18n="' + messageKey + '"' +
(messageArguments?
" data-i18n-options='" + JSON.stringify(messageArguments) + "'"
" data-i18n-options='" + JSON.stringify(messageArguments)
+ "'"
: "") + ">" +
APP.translation.translateString(messageKey,
messageArguments) +

View File

@@ -1,4 +1,22 @@
/* global $, config, interfaceConfig */
/* global $, APP, AJS, interfaceConfig */
import KeyboardShortcut from '../../keyboardshortcut/keyboardshortcut';
/**
* Associates tooltip element position (in the terms of
* {@link UIUtil#setTooltip} which do not look like CSS <tt>position</tt>) with
* AUI tooltip <tt>gravity</tt>.
*/
const TOOLTIP_POSITIONS = {
'bottom': 'n',
'bottom-left': 'ne',
'bottom-right': 'nw',
'left': 'e',
'right': 'w',
'top': 's',
'top-left': 'se',
'top-right': 'sw'
};
/**
* Created by hristo on 12/22/14.
@@ -82,12 +100,69 @@
context.putImageData(imgData, 0, 0);
},
/**
* Sets a global handler for all tooltips. Once invoked, create a new
* tooltip by merely updating a DOM node with the appropriate class (e.g.
* <tt>tooltip-n</tt>) and the attribute <tt>content</tt>.
*/
activateTooltips() {
AJS.$('[data-tooltip]').tooltip({
gravity() {
return this.getAttribute('data-tooltip');
},
title() {
return this.getAttribute('content');
},
html: true, // Handle multiline tooltips.
// The following two prevent tooltips from being stuck:
hoverable: false, // Make custom tooltips behave like native ones.
live: true // Attach listener to document element.
});
},
/**
* Sets the tooltip to the given element.
*
* @param element the element to set the tooltip to
* @param key the tooltip data-i18n key
* @param position the position of the tooltip in relation to the element
*/
setTooltip: function (element, key, position) {
element.setAttribute("data-i18n", "[data-content]" + key);
element.setAttribute("data-toggle", "popover");
element.setAttribute("data-placement", position);
element.setAttribute("data-html", true);
element.setAttribute("data-container", "body");
element.setAttribute('data-tooltip', TOOLTIP_POSITIONS[position]);
element.setAttribute('data-i18n', '[content]' + key);
APP.translation.translateElement($(element));
},
/**
* Removes the tooltip to the given element.
*
* @param element the element to remove the tooltip from
*/
removeTooltip: function (element) {
element.removeAttribute('data-tooltip', '');
element.removeAttribute('data-i18n','');
element.removeAttribute('content','');
},
/**
* Internal util function for generating tooltip title.
*
* @param element
* @returns {string|*}
* @private
*/
_getTooltipText: function (element) {
let title = element.getAttribute('content');
let shortcut = element.getAttribute('shortcut');
if(shortcut) {
let shortcutString = KeyboardShortcut.getShortcutTooltip(shortcut);
title += ` ${shortcutString}`;
}
return title;
},
/**
@@ -112,7 +187,8 @@
* is enabled, {false} - otherwise
*/
isButtonEnabled: function (name) {
return interfaceConfig.TOOLBAR_BUTTONS.indexOf(name) !== -1;
return interfaceConfig.TOOLBAR_BUTTONS.indexOf(name) !== -1
|| interfaceConfig.MAIN_TOOLBAR_BUTTONS.indexOf(name) !== -1;
},
/**
* Indicates if the setting section is enabled.
@@ -232,6 +308,23 @@
*/
parseCssInt(cssValue) {
return parseInt(cssValue) || 0;
},
/**
* Adds href value to 'a' link jquery object. If link value is null,
* undefined or empty string, disables the link.
* @param {object} aLinkElement the jquery object
* @param {string} link the link value
*/
setLinkHref(aLinkElement, link) {
if (link) {
aLinkElement.attr('href', link);
} else {
aLinkElement.css({
"pointer-events": "none",
"cursor": "default"
});
}
}
};

View File

@@ -245,13 +245,13 @@ ConnectionIndicator.prototype.showMore = function () {
};
function createIcon(classes) {
function createIcon(classes, iconClass) {
var icon = document.createElement("span");
for(var i in classes) {
icon.classList.add(classes[i]);
}
icon.appendChild(
document.createElement("i")).classList.add("icon-connection");
document.createElement("i")).classList.add(iconClass);
return icon;
}
@@ -282,9 +282,12 @@ ConnectionIndicator.prototype.create = function () {
}.bind(this);
this.emptyIcon = this.connectionIndicatorContainer.appendChild(
createIcon(["connection", "connection_empty"]));
createIcon(["connection", "connection_empty"], "icon-connection"));
this.fullIcon = this.connectionIndicatorContainer.appendChild(
createIcon(["connection", "connection_full"]));
createIcon(["connection", "connection_full"], "icon-connection"));
this.interruptedIndicator = this.connectionIndicatorContainer.appendChild(
createIcon(["connection", "connection_lost"],"icon-connection-lost"));
$(this.interruptedIndicator).hide();
};
/**
@@ -298,6 +301,27 @@ ConnectionIndicator.prototype.remove = function() {
this.popover.forceHide();
};
/**
* Updates the UI which displays warning about user's connectivity problems.
*
* @param {boolean} isActive true if the connection is working fine or false if
* the user is having connectivity issues.
*/
ConnectionIndicator.prototype.updateConnectionStatusIndicator
= function (isActive) {
this.isConnectionActive = isActive;
if (this.isConnectionActive) {
$(this.interruptedIndicator).hide();
$(this.emptyIcon).show();
$(this.fullIcon).show();
} else {
$(this.interruptedIndicator).show();
$(this.emptyIcon).hide();
$(this.fullIcon).hide();
this.updateConnectionQuality(0 /* zero bars */);
}
};
/**
* Updates the data of the indicator
* @param percent the percent of connection quality
@@ -312,15 +336,16 @@ ConnectionIndicator.prototype.updateConnectionQuality =
} else {
if(this.connectionIndicatorContainer.style.display == "none") {
this.connectionIndicatorContainer.style.display = "block";
this.videoContainer.updateIconPositions();
}
}
this.bandwidth = object.bandwidth;
this.bitrate = object.bitrate;
this.packetLoss = object.packetLoss;
this.transport = object.transport;
if (object.resolution) {
this.resolution = object.resolution;
if (object) {
this.bandwidth = object.bandwidth;
this.bitrate = object.bitrate;
this.packetLoss = object.packetLoss;
this.transport = object.transport;
if (object.resolution) {
this.resolution = object.resolution;
}
}
for (var quality in ConnectionIndicator.connectionQualityValues) {
if (percent >= quality) {
@@ -328,7 +353,7 @@ ConnectionIndicator.prototype.updateConnectionQuality =
ConnectionIndicator.connectionQualityValues[quality];
}
}
if (object.isResolutionHD) {
if (object && typeof object.isResolutionHD === 'boolean') {
this.isResolutionHD = object.isResolutionHD;
}
this.updateResolutionIndicator();
@@ -391,7 +416,7 @@ ConnectionIndicator.prototype.updateResolutionIndicator = function () {
else if (this.resolution !== null) {
let resolutions = this.resolution || {};
Object.keys(resolutions).map(function (ssrc) {
let {width, height} = resolutions[ssrc];
const { height } = resolutions[ssrc];
if (height >= config.minHDHeight)
showResolutionLabel = true;
});

View File

@@ -1,10 +1,8 @@
/* global $, APP, interfaceConfig, config*/
/* global $, interfaceConfig */
import UIEvents from "../../../service/UI/UIEvents";
import UIUtil from "../util/UIUtil";
const thumbAspectRatio = 1 / 1;
const FilmStrip = {
/**
*
@@ -26,7 +24,7 @@ const FilmStrip = {
*/
toggleFilmStrip (visible) {
if (typeof visible === 'boolean'
&& this.isFilmStripVisible() == visible) {
&& this.isFilmStripVisible() == visible) {
return;
}
@@ -36,8 +34,8 @@ const FilmStrip = {
var eventEmitter = this.eventEmitter;
if (eventEmitter) {
eventEmitter.emit(
UIEvents.TOGGLED_FILM_STRIP,
this.isFilmStripVisible());
UIEvents.TOGGLED_FILM_STRIP,
this.isFilmStripVisible());
}
},
@@ -66,13 +64,52 @@ const FilmStrip = {
- parseInt(this.filmStrip.css('paddingRight'), 10);
},
/**
* Calculates the thumbnail size.
*/
calculateThumbnailSize () {
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
calculateThumbnailSize() {
let availableSizes = this.calculateAvailableSize();
let width = availableSizes.availableWidth;
let height = availableSizes.availableHeight;
let numvids = this.getThumbs(true).length;
return this.calculateThumbnailSizeFromAvailable(width, height);
},
/**
* Normalizes local and remote thumbnail ratios
*/
normalizeThumbnailRatio () {
let remoteHeightRatio = interfaceConfig.REMOTE_THUMBNAIL_RATIO_HEIGHT;
let remoteWidthRatio = interfaceConfig.REMOTE_THUMBNAIL_RATIO_WIDTH;
let localHeightRatio = interfaceConfig.LOCAL_THUMBNAIL_RATIO_HEIGHT;
let localWidthRatio = interfaceConfig.LOCAL_THUMBNAIL_RATIO_WIDTH;
let commonHeightRatio = remoteHeightRatio * localHeightRatio;
let localRatioCoefficient = localWidthRatio / localHeightRatio;
let remoteRatioCoefficient = remoteWidthRatio / remoteHeightRatio;
remoteWidthRatio = commonHeightRatio * remoteRatioCoefficient;
remoteHeightRatio = commonHeightRatio;
localWidthRatio = commonHeightRatio * localRatioCoefficient;
localHeightRatio = commonHeightRatio;
let localRatio = {
widthRatio: localWidthRatio,
heightRatio: localHeightRatio
};
let remoteRatio = {
widthRatio: remoteWidthRatio,
heightRatio: remoteHeightRatio
};
return { localRatio, remoteRatio };
},
calculateAvailableSize() {
let availableHeight = interfaceConfig.FILM_STRIP_MAX_HEIGHT;
let thumbs = this.getThumbs(true);
let numvids = thumbs.remoteThumbs.length;
let localVideoContainer = $("#localVideoContainer");
@@ -83,20 +120,19 @@ const FilmStrip = {
*/
let videoAreaAvailableWidth
= UIUtil.getAvailableVideoWidth()
- UIUtil.parseCssInt(this.filmStrip.css('right'), 10)
- UIUtil.parseCssInt(this.filmStrip.css('paddingLeft'), 10)
- UIUtil.parseCssInt(this.filmStrip.css('paddingRight'), 10)
- UIUtil.parseCssInt(this.filmStrip.css('borderLeftWidth'), 10)
- UIUtil.parseCssInt(this.filmStrip.css('borderRightWidth'), 10)
- UIUtil.parseCssInt(this.filmStrip.css('right'), 10)
- UIUtil.parseCssInt(this.filmStrip.css('paddingLeft'), 10)
- UIUtil.parseCssInt(this.filmStrip.css('paddingRight'), 10)
- UIUtil.parseCssInt(this.filmStrip.css('borderLeftWidth'), 10)
- UIUtil.parseCssInt(this.filmStrip.css('borderRightWidth'), 10)
- 5;
let availableWidth = videoAreaAvailableWidth;
// If the number of videos is 0 or undefined we don't need to calculate
// further.
if (numvids)
// If local thumb is not hidden
if(thumbs.localThumb) {
availableWidth = Math.floor(
(videoAreaAvailableWidth - numvids * (
(videoAreaAvailableWidth - (
UIUtil.parseCssInt(
localVideoContainer.css('borderLeftWidth'), 10)
+ UIUtil.parseCssInt(
@@ -109,45 +145,99 @@ const FilmStrip = {
localVideoContainer.css('marginLeft'), 10)
+ UIUtil.parseCssInt(
localVideoContainer.css('marginRight'), 10)))
/ numvids);
);
}
// If the number of videos is 0 or undefined we don't need to calculate
// further.
if (numvids) {
let remoteVideoContainer = thumbs.remoteThumbs.eq(0);
availableWidth = Math.floor(
(videoAreaAvailableWidth - numvids * (
UIUtil.parseCssInt(
remoteVideoContainer.css('borderLeftWidth'), 10)
+ UIUtil.parseCssInt(
remoteVideoContainer.css('borderRightWidth'), 10)
+ UIUtil.parseCssInt(
remoteVideoContainer.css('paddingLeft'), 10)
+ UIUtil.parseCssInt(
remoteVideoContainer.css('paddingRight'), 10)
+ UIUtil.parseCssInt(
remoteVideoContainer.css('marginLeft'), 10)
+ UIUtil.parseCssInt(
remoteVideoContainer.css('marginRight'), 10)))
);
}
let maxHeight
// If the MAX_HEIGHT property hasn't been specified
// we have the static value.
= Math.min( interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120,
availableHeight);
= Math.min(interfaceConfig.FILM_STRIP_MAX_HEIGHT || 120,
availableHeight);
availableHeight
= Math.min( maxHeight, window.innerHeight - 18);
= Math.min(maxHeight, window.innerHeight - 18);
if (availableHeight < availableWidth) {
availableWidth = availableHeight;
return { availableWidth, availableHeight };
},
calculateThumbnailSizeFromAvailable(availableWidth, availableHeight) {
let { localRatio, remoteRatio } = this.normalizeThumbnailRatio();
let { remoteThumbs } = this.getThumbs(true);
let remoteProportion = remoteRatio.widthRatio * remoteThumbs.length;
let widthProportion = remoteProportion + localRatio.widthRatio;
let heightUnit = availableHeight / localRatio.heightRatio;
let widthUnit = availableWidth / widthProportion;
if (heightUnit < widthUnit) {
widthUnit = heightUnit;
}
else
availableHeight = availableWidth;
heightUnit = widthUnit;
let localVideo = {
thumbWidth: widthUnit * localRatio.widthRatio,
thumbHeight: heightUnit * localRatio.heightRatio
};
let remoteVideo = {
thumbWidth: widthUnit * remoteRatio.widthRatio,
thumbHeight: widthUnit * remoteRatio.heightRatio
};
return {
thumbWidth: availableWidth,
thumbHeight: availableHeight
localVideo,
remoteVideo
};
},
resizeThumbnails (thumbWidth, thumbHeight,
resizeThumbnails (local, remote,
animate = false, forceUpdate = false) {
return new Promise(resolve => {
this.getThumbs(!forceUpdate).animate({
height: thumbHeight,
width: thumbWidth
}, {
queue: false,
duration: animate ? 500 : 0,
complete: resolve
});
let thumbs = this.getThumbs(!forceUpdate);
if(thumbs.localThumb)
thumbs.localThumb.animate({
height: local.thumbHeight,
width: local.thumbWidth
}, {
queue: false,
duration: animate ? 500 : 0,
complete: resolve
});
if(thumbs.remoteThumbs)
thumbs.remoteThumbs.animate({
height: remote.thumbHeight,
width: remote.thumbWidth
}, {
queue: false,
duration: animate ? 500 : 0,
complete: resolve
});
this.filmStrip.animate({
// adds 2 px because of small video 1px border
height: thumbHeight + 2
height: remote.thumbHeight + 2
}, {
queue: false,
duration: animate ? 500 : 0
@@ -165,13 +255,19 @@ const FilmStrip = {
selector += ':visible';
}
let localThumb = $("#localVideoContainer");
let remoteThumbs = this.filmStrip.children(selector)
.not("#localVideoContainer");
// Exclude the local video container if it has been hidden.
if ($("#localVideoContainer").hasClass("hidden"))
return this.filmStrip.children(selector)
.not("#localVideoContainer");
else
return this.filmStrip.children(selector);
if (localThumb.hasClass("hidden")) {
return { remoteThumbs };
} else {
return { remoteThumbs, localThumb };
}
}
};
export default FilmStrip;

View File

@@ -24,19 +24,20 @@ export default class LargeContainer {
* @param {number} containerHeight available height
* @param {boolean} animate if container should animate it's resize process
*/
// eslint-disable-next-line no-unused-vars
resize (containerWidth, containerHeight, animate) {
}
/**
* Handler for "hover in" events.
*/
onHoverIn (e) {
onHoverIn (e) { // eslint-disable-line no-unused-vars
}
/**
* Handler for "hover out" events.
*/
onHoverOut (e) {
onHoverOut (e) { // eslint-disable-line no-unused-vars
}
/**
@@ -44,14 +45,14 @@ export default class LargeContainer {
* @param {JitsiTrack?} stream new stream
* @param {string} videoType video type
*/
setStream (stream, videoType) {
setStream (stream, videoType) { // eslint-disable-line no-unused-vars
}
/**
* Show or hide user avatar.
* @param {boolean} show
*/
showAvatar (show) {
showAvatar (show) { // eslint-disable-line no-unused-vars
}
/**

View File

@@ -0,0 +1,498 @@
/* global $, APP, interfaceConfig */
import Avatar from "../avatar/Avatar";
import {createDeferred} from '../../util/helpers';
import UIUtil from "../util/UIUtil";
import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer";
import AudioLevels from "../audio_levels/AudioLevels";
/**
* Manager for all Large containers.
*/
export default class LargeVideoManager {
constructor (emitter) {
/**
* The map of <tt>LargeContainer</tt>s where the key is the video
* container type.
* @type {Object.<string, LargeContainer>}
*/
this.containers = {};
this.state = VIDEO_CONTAINER_TYPE;
this.videoContainer = new VideoContainer(
() => this.resizeContainer(VIDEO_CONTAINER_TYPE), emitter);
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
// use the same video container to handle and desktop tracks
this.addContainer("desktop", this.videoContainer);
this.width = 0;
this.height = 0;
this.$container = $('#largeVideoContainer');
this.$container.css({
display: 'inline-block'
});
if (interfaceConfig.SHOW_JITSI_WATERMARK) {
let leftWatermarkDiv
= this.$container.find("div.watermark.leftwatermark");
leftWatermarkDiv.css({display: 'block'});
UIUtil.setLinkHref(
leftWatermarkDiv.parent(),
interfaceConfig.JITSI_WATERMARK_LINK);
}
if (interfaceConfig.SHOW_BRAND_WATERMARK) {
let rightWatermarkDiv
= this.$container.find("div.watermark.rightwatermark");
rightWatermarkDiv.css({
display: 'block',
backgroundImage: 'url(images/rightwatermark.png)'
});
UIUtil.setLinkHref(
rightWatermarkDiv.parent(),
interfaceConfig.BRAND_WATERMARK_LINK);
}
if (interfaceConfig.SHOW_POWERED_BY) {
this.$container.children("a.poweredby").css({display: 'block'});
}
this.$container.hover(
e => this.onHoverIn(e),
e => this.onHoverOut(e)
);
}
onHoverIn (e) {
if (!this.state) {
return;
}
let container = this.getContainer(this.state);
container.onHoverIn(e);
}
onHoverOut (e) {
if (!this.state) {
return;
}
let container = this.getContainer(this.state);
container.onHoverOut(e);
}
/**
* Called when the media connection has been interrupted.
*/
onVideoInterrupted () {
this.enableLocalConnectionProblemFilter(true);
this._setLocalConnectionMessage("connection.RECONNECTING");
// Show the message only if the video is currently being displayed
this.showLocalConnectionMessage(this.state === VIDEO_CONTAINER_TYPE);
}
/**
* Called when the media connection has been restored.
*/
onVideoRestored () {
this.enableLocalConnectionProblemFilter(false);
this.showLocalConnectionMessage(false);
}
get id () {
let container = this.getContainer(this.state);
return container.id;
}
scheduleLargeVideoUpdate () {
if (this.updateInProcess || !this.newStreamData) {
return;
}
this.updateInProcess = true;
let container = this.getContainer(this.state);
// Include hide()/fadeOut only if we're switching between users
let preUpdate;
let isUserSwitch = this.newStreamData.id != this.id;
if (isUserSwitch) {
preUpdate = container.hide();
} else {
preUpdate = Promise.resolve();
}
preUpdate.then(() => {
let {id, stream, videoType, resolve} = this.newStreamData;
this.newStreamData = null;
console.info("hover in %s", id);
this.state = videoType;
let container = this.getContainer(this.state);
container.setStream(stream, videoType);
// change the avatar url on large
this.updateAvatar(Avatar.getAvatarUrl(id));
// FIXME that does not really make sense, because the videoType
// (camera or desktop) is a completely different thing than
// the video container type (Etherpad, SharedVideo, VideoContainer).
// ----------------------------------------------------------------
// If we the continer is VIDEO_CONTAINER_TYPE, we need to check
// its stream whether exist and is muted to set isVideoMuted
// in rest of the cases it is false
let showAvatar = false;
if (videoType == VIDEO_CONTAINER_TYPE)
showAvatar = stream ? stream.isMuted() : true;
// If the user's connection is disrupted then the avatar will be
// displayed in case we have no video image cached. That is if
// there was a user switch(image is lost on stream detach) or if
// the video was not rendered, before the connection has failed.
let isHavingConnectivityIssues
= APP.conference.isParticipantConnectionActive(id) === false;
if (isHavingConnectivityIssues
&& (isUserSwitch | !container.wasVideoRendered)) {
showAvatar = true;
}
let promise;
// do not show stream if video is muted
// but we still should show watermark
if (showAvatar) {
this.showWatermark(true);
// If the intention of this switch is to show the avatar
// we need to make sure that the video is hidden
promise = container.hide();
} else {
promise = container.show();
}
// show the avatar on large if needed
container.showAvatar(showAvatar);
// Make sure no notification about remote failure is shown as
// it's UI conflicts with the one for local connection interrupted.
if (APP.conference.isConnectionInterrupted()) {
this.updateParticipantConnStatusIndication(id, true);
} else {
this.updateParticipantConnStatusIndication(
id, !isHavingConnectivityIssues);
}
// resolve updateLargeVideo promise after everything is done
promise.then(resolve);
return promise;
}).then(() => {
// after everything is done check again if there are any pending
// new streams.
this.updateInProcess = false;
this.scheduleLargeVideoUpdate();
});
}
/**
* Shows/hides notification about participant's connectivity issues to be
* shown on the large video area.
*
* @param {string} id the id of remote participant(MUC nickname)
* @param {boolean} isConnected true if the connection is active or false
* when the user is having connectivity issues.
*
* @private
*/
updateParticipantConnStatusIndication (id, isConnected) {
// Apply grey filter on the large video
this.videoContainer.showRemoteConnectionProblemIndicator(!isConnected);
if (isConnected) {
// Hide the message
this.showRemoteConnectionMessage(false);
} else {
// Get user's display name
let displayName
= APP.conference.getParticipantDisplayName(id);
this._setRemoteConnectionMessage(
"connection.USER_CONNECTION_INTERRUPTED",
{ displayName: displayName });
// Show it now only if the VideoContainer is on top
this.showRemoteConnectionMessage(
this.state === VIDEO_CONTAINER_TYPE);
}
}
/**
* Update large video.
* Switches to large video even if previously other container was visible.
* @param userID the userID of the participant associated with the stream
* @param {JitsiTrack?} stream new stream
* @param {string?} videoType new video type
* @returns {Promise}
*/
updateLargeVideo (userID, stream, videoType) {
if (this.newStreamData) {
this.newStreamData.reject();
}
this.newStreamData = createDeferred();
this.newStreamData.id = userID;
this.newStreamData.stream = stream;
this.newStreamData.videoType = videoType;
this.scheduleLargeVideoUpdate();
return this.newStreamData.promise;
}
/**
* Update container size.
*/
updateContainerSize () {
this.width = UIUtil.getAvailableVideoWidth();
this.height = window.innerHeight;
}
/**
* Resize Large container of specified type.
* @param {string} type type of container which should be resized.
* @param {boolean} [animate=false] if resize process should be animated.
*/
resizeContainer (type, animate = false) {
let container = this.getContainer(type);
container.resize(this.width, this.height, animate);
}
/**
* Resize all Large containers.
* @param {boolean} animate if resize process should be animated.
*/
resize (animate) {
// resize all containers
Object.keys(this.containers)
.forEach(type => this.resizeContainer(type, animate));
this.$container.animate({
width: this.width,
height: this.height
}, {
queue: false,
duration: animate ? 500 : 0
});
}
/**
* Enables/disables the filter indicating a video problem to the user caused
* by the problems with local media connection.
*
* @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
*/
enableLocalConnectionProblemFilter (enable) {
this.videoContainer.enableLocalConnectionProblemFilter(enable);
}
/**
* Updates the src of the dominant speaker avatar
*/
updateAvatar (avatarUrl) {
$("#dominantSpeakerAvatar").attr('src', avatarUrl);
}
/**
* Updates the audio level indicator of the large video.
*
* @param lvl the new audio level to set
*/
updateLargeVideoAudioLevel (lvl) {
AudioLevels.updateLargeVideoAudioLevel("dominantSpeaker", lvl);
}
/**
* Show or hide watermark.
* @param {boolean} show
*/
showWatermark (show) {
$('.watermark').css('visibility', show ? 'visible' : 'hidden');
}
/**
* Shows/hides the message indicating problems with local media connection.
* @param {boolean|null} show(optional) tells whether the message is to be
* displayed or not. If missing the condition will be based on the value
* obtained from {@link APP.conference.isConnectionInterrupted}.
*/
showLocalConnectionMessage (show) {
if (typeof show !== 'boolean') {
show = APP.conference.isConnectionInterrupted();
}
if (show) {
$('#localConnectionMessage').css({display: "block"});
// Avatar message conflicts with 'videoConnectionMessage',
// so it must be hidden
this.showRemoteConnectionMessage(false);
} else {
$('#localConnectionMessage').css({display: "none"});
}
}
/**
* Shows hides the "avatar" message which is to be displayed either in
* the middle of the screen or below the avatar image.
*
* @param {null|boolean} show (optional) <tt>true</tt> to show the avatar
* message or <tt>false</tt> to hide it. If not provided then the connection
* status of the user currently on the large video will be obtained form
* "APP.conference" and the message will be displayed if the user's
* connection is interrupted.
*/
showRemoteConnectionMessage (show) {
if (typeof show !== 'boolean') {
show = APP.conference.isParticipantConnectionActive(this.id);
}
if (show) {
$('#remoteConnectionMessage').css({display: "block"});
// 'videoConnectionMessage' message conflicts with 'avatarMessage',
// so it must be hidden
this.showLocalConnectionMessage(false);
} else {
$('#remoteConnectionMessage').hide();
}
}
/**
* Updates the text which describes that the remote user is having
* connectivity issues.
*
* @param {string} msgKey the translation key which will be used to get
* the message text.
* @param {object} msgOptions translation options object.
*
* @private
*/
_setRemoteConnectionMessage (msgKey, msgOptions) {
if (msgKey) {
let text = APP.translation.translateString(msgKey, msgOptions);
$('#remoteConnectionMessage')
.attr("data-i18n", msgKey).text(text);
}
this.videoContainer.positionRemoteConnectionMessage();
}
/**
* Updated the text which is to be shown on the top of large video, when
* local media connection is interrupted.
*
* @param {string} msgKey the translation key which will be used to get
* the message text to be displayed on the large video.
* @param {object} msgOptions translation options object
*
* @private
*/
_setLocalConnectionMessage (msgKey, msgOptions) {
$('#localConnectionMessage')
.attr("data-i18n", msgKey)
.text(APP.translation.translateString(msgKey, msgOptions));
}
/**
* Add container of specified type.
* @param {string} type container type
* @param {LargeContainer} container container to add.
*/
addContainer (type, container) {
if (this.containers[type]) {
throw new Error(`container of type ${type} already exist`);
}
this.containers[type] = container;
this.resizeContainer(type);
}
/**
* Get Large container of specified type.
* @param {string} type container type.
* @returns {LargeContainer}
*/
getContainer (type) {
let container = this.containers[type];
if (!container) {
throw new Error(`container of type ${type} doesn't exist`);
}
return container;
}
/**
* Remove Large container of specified type.
* @param {string} type container type.
*/
removeContainer (type) {
if (!this.containers[type]) {
throw new Error(`container of type ${type} doesn't exist`);
}
delete this.containers[type];
}
/**
* Show Large container of specified type.
* Does nothing if such container is already visible.
* @param {string} type container type.
* @returns {Promise}
*/
showContainer (type) {
if (this.state === type) {
return Promise.resolve();
}
let oldContainer = this.containers[this.state];
// FIXME when video is being replaced with other content we need to hide
// companion icons/messages. It would be best if the container would
// be taking care of it by itself, but that is a bigger refactoring
if (this.state === VIDEO_CONTAINER_TYPE) {
this.showWatermark(false);
this.showLocalConnectionMessage(false);
this.showRemoteConnectionMessage(false);
}
oldContainer.hide();
this.state = type;
let container = this.getContainer(type);
return container.show().then(() => {
if (type === VIDEO_CONTAINER_TYPE) {
// FIXME when video appears on top of other content we need to
// show companion icons/messages. It would be best if
// the container would be taking care of it by itself, but that
// is a bigger refactoring
this.showWatermark(true);
// "avatar" and "video connection" can not be displayed both
// at the same time, but the latter is of higher priority and it
// will hide the avatar one if will be displayed.
this.showRemoteConnectionMessage(/* fet the current state */);
this.showLocalConnectionMessage(/* fetch the current state */);
}
});
}
/**
* Changes the flipX state of the local video.
* @param val {boolean} true if flipped.
*/
onLocalFlipXChange(val) {
this.videoContainer.setLocalFlipX(val);
}
}

View File

@@ -11,7 +11,6 @@ function LocalVideo(VideoLayout, emitter) {
this.videoSpanId = "localVideoContainer";
this.container = $("#localVideoContainer").get(0);
this.localVideoId = null;
this.bindHoverHandler();
if(config.enableLocalVideoFlip)
this._buildContextMenu();
this.isLocal = true;
@@ -29,31 +28,16 @@ function LocalVideo(VideoLayout, emitter) {
this.setDisplayName();
this.createConnectionIndicator();
this.addAudioLevelIndicator();
}
LocalVideo.prototype = Object.create(SmallVideo.prototype);
LocalVideo.prototype.constructor = LocalVideo;
/**
* Creates the edit display name button.
*
* @returns {object} the edit button
*/
function createEditDisplayNameButton() {
var editButton = document.createElement('a');
editButton.className = 'displayname';
UIUtil.setTooltip(editButton,
"videothumbnail.editnickname",
"top");
editButton.innerHTML = '<i class="icon-edit"></i>';
return editButton;
}
/**
* Sets the display name for the given video span id.
*/
LocalVideo.prototype.setDisplayName = function(displayName, key) {
LocalVideo.prototype.setDisplayName = function(displayName) {
if (!this.container) {
console.warn(
"Unable to set displayName - " + this.videoSpanId +
@@ -61,7 +45,7 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
return;
}
var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
var nameSpan = $('#' + this.videoSpanId + ' .displayname');
var defaultLocalDisplayName = APP.translation.generateTranslationHTML(
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
@@ -72,7 +56,10 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
if (displayName && displayName.length > 0) {
meHTML = APP.translation.generateTranslationHTML("me");
$('#localDisplayName').html(
UIUtil.escapeHtml(displayName) + ' (' + meHTML + ')'
`${UIUtil.escapeHtml(displayName)} (${meHTML})`
);
$('#editDisplayName').val(
`${UIUtil.escapeHtml(displayName)}`
);
} else {
$('#localDisplayName').html(defaultLocalDisplayName);
@@ -80,11 +67,11 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
}
this.updateView();
} else {
var editButton = createEditDisplayNameButton();
nameSpan = document.createElement('span');
nameSpan.className = 'displayname';
$('#' + this.videoSpanId)[0].appendChild(nameSpan);
document.getElementById(this.videoSpanId)
.querySelector('.videocontainer__toolbar')
.appendChild(nameSpan);
if (displayName && displayName.length > 0) {
@@ -97,12 +84,11 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
nameSpan.id = 'localDisplayName';
this.container.appendChild(editButton);
//translates popover of edit button
APP.translation.translateElement($("a.displayname"));
var editableText = document.createElement('input');
editableText.className = 'displayname';
editableText.className = 'editdisplayname';
editableText.type = 'text';
editableText.id = 'editDisplayName';
@@ -119,26 +105,30 @@ LocalVideo.prototype.setDisplayName = function(displayName, key) {
JSON.stringify({name: "Jane Pink"}));
editableText.setAttribute("placeholder", defaultNickname);
this.container.appendChild(editableText);
this.container
.querySelector('.videocontainer__toolbar')
.appendChild(editableText);
var self = this;
$('#localVideoContainer .displayname')
.bind("click", function (e) {
let $editDisplayName = $('#editDisplayName');
let $localDisplayName = $('#localDisplayName');
var editDisplayName = $('#editDisplayName');
e.preventDefault();
e.stopPropagation();
$('#localDisplayName').hide();
editDisplayName.show();
editDisplayName.focus();
editDisplayName.select();
$localDisplayName.hide();
$editDisplayName.show();
$editDisplayName.focus();
$editDisplayName.select();
editDisplayName.one("focusout", function (e) {
$editDisplayName.one("focusout", function () {
self.emitter.emit(UIEvents.NICKNAME_CHANGED, this.value);
$('#editDisplayName').hide();
$editDisplayName.hide();
$localDisplayName.show();
});
editDisplayName.on('keydown', function (e) {
$editDisplayName.on('keydown', function (e) {
if (e.keyCode === 13) {
e.preventDefault();
$('#editDisplayName').hide();
@@ -199,7 +189,7 @@ LocalVideo.prototype.changeVideo = function (stream) {
localVideoContainer.removeChild(localVideo);
// when removing only the video element and we are on stage
// update the stage
if(this.VideoLayout.isCurrentlyOnLarge(this.id))
if(this.isCurrentlyOnLargeVideo())
this.VideoLayout.updateLargeVideo(this.id);
stream.off(TrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
};

View File

@@ -3,24 +3,49 @@
import ConnectionIndicator from './ConnectionIndicator';
import SmallVideo from "./SmallVideo";
import AudioLevels from "../audio_levels/AudioLevels";
import UIUtils from "../util/UIUtil";
import UIEvents from '../../../service/UI/UIEvents';
import JitsiPopover from "../util/JitsiPopover";
function RemoteVideo(id, VideoLayout, emitter) {
this.id = id;
/**
* Creates new instance of the <tt>RemoteVideo</tt>.
* @param user {JitsiParticipant} the user for whom remote video instance will
* be created.
* @param {VideoLayout} VideoLayout the video layout instance.
* @param {EventEmitter} emitter the event emitter which will be used by
* the new instance to emit events.
* @constructor
*/
function RemoteVideo(user, VideoLayout, emitter) {
this.user = user;
this.id = user.getId();
this.emitter = emitter;
this.videoSpanId = `participant_${id}`;
this.videoSpanId = `participant_${this.id}`;
SmallVideo.call(this, VideoLayout);
this.hasRemoteVideoMenu = false;
this.addRemoteVideoContainer();
this.connectionIndicator = new ConnectionIndicator(this, id);
this.connectionIndicator = new ConnectionIndicator(this, this.id);
this.setDisplayName();
this.bindHoverHandler();
this.flipX = false;
this.isLocal = false;
this.isMuted = false;
/**
* The flag is set to <tt>true</tt> after the 'onplay' event has been
* triggered on the current video element. It goes back to <tt>false</tt>
* when the stream is removed. It is used to determine whether the video
* playback has ever started.
* @type {boolean}
*/
this.wasVideoPlayed = false;
/**
* The flag is set to <tt>true</tt> if remote participant's video gets muted
* during his media connection disruption. This is to prevent black video
* being render on the thumbnail, because even though once the video has
* been played the image usually remains on the video element it seems that
* after longer period of the video element being hidden this image can be
* lost.
* @type {boolean}
*/
this.mutedWhileDisconnected = false;
}
RemoteVideo.prototype = Object.create(SmallVideo.prototype);
@@ -34,13 +59,14 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
if (APP.conference.isModerator) {
this.addRemoteVideoMenu();
}
let {thumbWidth, thumbHeight} = this.VideoLayout.resizeThumbnails();
AudioLevels.updateAudioLevelCanvas(this.id, thumbWidth, thumbHeight);
this.VideoLayout.resizeThumbnails(false, true);
this.addAudioLevelIndicator();
return this.container;
};
/**
* Initializes the remote participant popup menu, by specifying previously
* constructed popupMenuElement, containing all the menu items.
@@ -50,7 +76,7 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
*/
RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) {
this.popover = new JitsiPopover(
$("#" + this.videoSpanId + " > .remotevideomenu"),
$("#" + this.videoSpanId + " .remotevideomenu"),
{ content: popupMenuElement.outerHTML,
skin: "black"});
@@ -60,7 +86,7 @@ RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) {
this.popover.show = function () {
// update content by forcing it, to finish even if popover
// is not visible
this.updateRemoteVideoMenu(this.isMuted, true);
this.updateRemoteVideoMenu(this.isAudioMuted, true);
// call the original show, passing its actual this
origShowFunc.call(this.popover);
}.bind(this);
@@ -96,7 +122,7 @@ RemoteVideo.prototype._generatePopupContent = function () {
muteLinkItem.id = "mutelink_" + this.id;
if (this.isMuted) {
if (this.isAudioMuted) {
muteLinkItem.innerHTML = mutedHTML;
muteLinkItem.className = 'mutelink disabled';
}
@@ -108,7 +134,7 @@ RemoteVideo.prototype._generatePopupContent = function () {
// Delegate event to the document.
$(document).on("click", "#mutelink_" + this.id, function(){
if (this.isMuted)
if (this.isAudioMuted)
return;
this.emitter.emit(UIEvents.REMOTE_AUDIO_MUTED, this.id);
@@ -152,7 +178,7 @@ RemoteVideo.prototype._generatePopupContent = function () {
*/
RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
this.isMuted = isMuted;
this.isAudioMuted = isMuted;
// generate content, translate it and add it to document only if
// popover is visible or we force to do so.
@@ -161,6 +187,33 @@ RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
}
};
/**
* @inheritDoc
*/
RemoteVideo.prototype.setMutedView = function(isMuted) {
SmallVideo.prototype.setMutedView.call(this, isMuted);
// Update 'mutedWhileDisconnected' flag
this._figureOutMutedWhileDisconnected(this.isConnectionActive() === false);
};
/**
* Figures out the value of {@link #mutedWhileDisconnected} flag by taking into
* account remote participant's network connectivity and video muted status.
*
* @param {boolean} isDisconnected <tt>true</tt> if the remote participant is
* currently having connectivity issues or <tt>false</tt> otherwise.
*
* @private
*/
RemoteVideo.prototype._figureOutMutedWhileDisconnected
= function(isDisconnected) {
if (isDisconnected && this.isVideoMuted) {
this.mutedWhileDisconnected = true;
} else if (!isDisconnected && !this.isVideoMuted) {
this.mutedWhileDisconnected = false;
}
};
/**
* Adds the remote video menu element for the given <tt>id</tt> in the
* given <tt>parentElement</tt>.
@@ -170,12 +223,16 @@ RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
*/
if (!interfaceConfig.filmStripOnly) {
RemoteVideo.prototype.addRemoteVideoMenu = function () {
var spanElement = document.createElement('div');
spanElement.className = 'remotevideomenu';
this.container.appendChild(spanElement);
var spanElement = document.createElement('span');
spanElement.className = 'remotevideomenu toolbar-icon right';
this.container
.querySelector('.videocontainer__toolbar')
.appendChild(spanElement);
var menuElement = document.createElement('i');
menuElement.className = 'fa fa-angle-down';
menuElement.className = 'icon-menu-up';
menuElement.title = 'Remote user controls';
spanElement.appendChild(menuElement);
@@ -204,13 +261,88 @@ RemoteVideo.prototype.removeRemoteStreamElement = function (stream) {
var select = $('#' + elementID);
select.remove();
if (isVideo) {
this.wasVideoPlayed = false;
}
console.info((isVideo ? "Video" : "Audio") +
" removed " + this.id, select);
// when removing only the video element and we are on stage
// update the stage
if (isVideo && this.VideoLayout.isCurrentlyOnLarge(this.id))
if (isVideo && this.isCurrentlyOnLargeVideo())
this.VideoLayout.updateLargeVideo(this.id);
else
// Missing video stream will affect display mode
this.updateView();
};
/**
* Checks whether the remote user associated with this <tt>RemoteVideo</tt>
* has connectivity issues.
*
* @return {boolean} <tt>true</tt> if the user's connection is fine or
* <tt>false</tt> otherwise.
*/
RemoteVideo.prototype.isConnectionActive = function() {
return this.user.isConnectionActive();
};
/**
* The remote video is considered "playable" once the stream has started
* according to the {@link #hasVideoStarted} result.
*
* @inheritdoc
* @override
*/
RemoteVideo.prototype.isVideoPlayable = function () {
return SmallVideo.prototype.isVideoPlayable.call(this)
&& this.hasVideoStarted() && !this.mutedWhileDisconnected;
};
/**
* @inheritDoc
*/
RemoteVideo.prototype.updateView = function () {
this.updateConnectionStatusIndicator(
null /* will obtain the status from 'conference' */);
// This must be called after 'updateConnectionStatusIndicator' because it
// affects the display mode by modifying 'mutedWhileDisconnected' flag
SmallVideo.prototype.updateView.call(this);
};
/**
* Updates the UI to reflect user's connectivity status.
* @param isActive {boolean|null} 'true' if user's connection is active or
* 'false' when the use is having some connectivity issues and a warning
* should be displayed. When 'null' is passed then the current value will be
* obtained from the conference instance.
*/
RemoteVideo.prototype.updateConnectionStatusIndicator = function (isActive) {
// Check for initial value if 'isActive' is not defined
if (typeof isActive !== "boolean") {
isActive = this.isConnectionActive();
if (isActive === null) {
// Cancel processing at this point - no update
return;
}
}
console.debug(this.id + " thumbnail is connection active ? " + isActive);
// Update 'mutedWhileDisconnected' flag
this._figureOutMutedWhileDisconnected(!isActive);
if(this.connectionIndicator)
this.connectionIndicator.updateConnectionStatusIndicator(isActive);
// Toggle thumbnail video problem filter
this.selectVideoElement().toggleClass(
"videoThumbnailProblemFilter", !isActive);
this.$avatar().toggleClass(
"videoThumbnailProblemFilter", !isActive);
};
/**
@@ -241,22 +373,23 @@ RemoteVideo.prototype.waitForPlayback = function (streamElement, stream) {
// Register 'onplaying' listener to trigger 'videoactive' on VideoLayout
// when video playback starts
var onPlayingHandler = function () {
self.wasVideoPlayed = true;
self.VideoLayout.videoactive(streamElement, self.id);
streamElement.onplaying = null;
// Refresh to show the video
self.updateView();
};
streamElement.onplaying = onPlayingHandler;
};
/**
* Checks whether or not video stream exists and has started for this
* RemoteVideo instance. This is checked by trying to select video element in
* this container and checking if 'currentTime' field's value is greater than 0.
* Checks whether the video stream has started for this RemoteVideo instance.
*
* @returns {*|boolean} true if this RemoteVideo has active video stream running
* @returns {boolean} true if this RemoteVideo has a video stream for which
* the playback has been started.
*/
RemoteVideo.prototype.hasVideoStarted = function () {
var videoSelector = this.selectVideoElement();
return videoSelector.length && videoSelector[0].currentTime > 0;
return this.wasVideoPlayed;
};
RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
@@ -294,7 +427,6 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
return;
let streamElement = SmallVideo.createStreamElement(stream);
let newElementId = streamElement.id;
// Put new stream element always in front
UIUtils.prependChild(this.container, streamElement);
@@ -381,7 +513,7 @@ RemoteVideo.prototype.setDisplayName = function(displayName, key) {
return;
}
var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
var nameSpan = $('#' + this.videoSpanId + ' .displayname');
// If we already have a display name for this video.
if (nameSpan.length > 0) {
@@ -400,7 +532,9 @@ RemoteVideo.prototype.setDisplayName = function(displayName, key) {
} else {
nameSpan = document.createElement('span');
nameSpan.className = 'displayname';
$('#' + this.videoSpanId)[0].appendChild(nameSpan);
$('#' + this.videoSpanId)[0]
.querySelector('.videocontainer__toolbar')
.appendChild(nameSpan);
if (displayName && displayName.length > 0) {
$(nameSpan).text(displayName);
@@ -418,7 +552,7 @@ RemoteVideo.prototype.setDisplayName = function(displayName, key) {
* @param videoElementId the id of local or remote video element.
*/
RemoteVideo.prototype.removeRemoteVideoMenu = function() {
var menuSpan = $('#' + this.videoSpanId + '>span.remotevideomenu');
var menuSpan = $('#' + this.videoSpanId + '> .remotevideomenu');
if (menuSpan.length) {
this.popover.forceHide();
menuSpan.remove();
@@ -427,12 +561,16 @@ RemoteVideo.prototype.removeRemoteVideoMenu = function() {
};
RemoteVideo.createContainer = function (spanId) {
var container = document.createElement('span');
let container = document.createElement('span');
container.id = spanId;
container.className = 'videocontainer';
let toolbar = document.createElement('div');
toolbar.className = "videocontainer__toolbar";
container.appendChild(toolbar);
var remotes = document.getElementById('remoteVideos');
return remotes.appendChild(container);
};
export default RemoteVideo;

View File

@@ -1,13 +1,34 @@
/* global $, APP, JitsiMeetJS */
/* jshint -W101 */
/* global $, APP, JitsiMeetJS, interfaceConfig */
import Avatar from "../avatar/Avatar";
import UIUtil from "../util/UIUtil";
import UIEvents from "../../../service/UI/UIEvents";
import AudioLevels from "../audio_levels/AudioLevels";
const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper;
/**
* Display mode constant used when video is being displayed on the small video.
* @type {number}
* @constant
*/
const DISPLAY_VIDEO = 0;
/**
* Display mode constant used when the user's avatar is being displayed on
* the small video.
* @type {number}
* @constant
*/
const DISPLAY_AVATAR = 1;
/**
* Display mode constant used when neither video nor avatar is being displayed
* on the small video.
* @type {number}
* @constant
*/
const DISPLAY_BLACKNESS = 2;
function SmallVideo(VideoLayout) {
this.isMuted = false;
this.isAudioMuted = false;
this.hasAvatar = false;
this.isVideoMuted = false;
this.videoStream = null;
@@ -40,7 +61,7 @@ SmallVideo.prototype.isVisible = function () {
};
SmallVideo.prototype.showDisplayName = function(isShow) {
var nameSpan = $('#' + this.videoSpanId + '>span.displayname').get(0);
var nameSpan = $('#' + this.videoSpanId + ' .displayname').get(0);
if (isShow) {
if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length)
nameSpan.setAttribute("style", "display:inline-block;");
@@ -171,26 +192,6 @@ SmallVideo.getStreamElementID = function (stream) {
return (isVideo ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
};
/**
* Configures hoverIn/hoverOut handlers.
*/
SmallVideo.prototype.bindHoverHandler = function () {
// Add hover handler
var self = this;
$(this.container).hover(
function () {
self.showDisplayName(true);
},
function () {
// If the video has been "pinned" by the user we want to
// keep the display name on place.
if (!self.VideoLayout.isLargeVideoVisible() ||
!self.VideoLayout.isCurrentlyOnLarge(self.id))
self.showDisplayName(false);
}
);
};
/**
* Updates the data for the indicator
* @param id the id of the indicator
@@ -209,121 +210,164 @@ SmallVideo.prototype.hideIndicator = function () {
/**
* Shows audio muted indicator over small videos.
* @param {string} isMuted
* Shows / hides the audio muted indicator over small videos.
*
* @param {boolean} isMuted indicates if the muted element should be shown
* or hidden
*/
SmallVideo.prototype.showAudioIndicator = function(isMuted) {
var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted');
var audioMutedIndicator = this.getAudioMutedIndicator();
if (!isMuted) {
if (audioMutedSpan.length > 0) {
audioMutedSpan.popover('hide');
audioMutedSpan.remove();
}
audioMutedIndicator.hide();
}
else {
if (!audioMutedSpan.length) {
audioMutedSpan = document.createElement('span');
audioMutedSpan.className = 'audioMuted';
UIUtil.setTooltip(audioMutedSpan,
"videothumbnail.mute",
"top");
this.container.appendChild(audioMutedSpan);
APP.translation.translateElement($('#' + this.videoSpanId + " > span"));
var mutedIndicator = document.createElement('i');
mutedIndicator.className = 'icon-mic-disabled';
audioMutedSpan.appendChild(mutedIndicator);
}
this.updateIconPositions();
audioMutedIndicator.show();
}
this.isMuted = isMuted;
this.isAudioMuted = isMuted;
};
/**
* Returns the audio muted indicator jquery object. If it doesn't exists -
* creates it.
*
* @returns {jQuery|HTMLElement} the audio muted indicator
*/
SmallVideo.prototype.getAudioMutedIndicator = function () {
var audioMutedSpan = $('#' + this.videoSpanId + ' .audioMuted');
if (audioMutedSpan.length) {
return audioMutedSpan;
}
audioMutedSpan = document.createElement('span');
audioMutedSpan.className = 'audioMuted toolbar-icon';
UIUtil.setTooltip(audioMutedSpan,
"videothumbnail.mute",
"top");
this.container
.querySelector('.videocontainer__toolbar')
.appendChild(audioMutedSpan);
var mutedIndicator = document.createElement('i');
mutedIndicator.className = 'icon-mic-disabled';
audioMutedSpan.appendChild(mutedIndicator);
return $('#' + this.videoSpanId + ' .audioMuted');
};
/**
* Shows video muted indicator over small videos and disables/enables avatar
* if video muted.
*
* @param {boolean} isMuted indicates if we should set the view to muted view
* or not
*/
SmallVideo.prototype.setMutedView = function(isMuted) {
SmallVideo.prototype.setVideoMutedView = function(isMuted) {
this.isVideoMuted = isMuted;
this.updateView();
var videoMutedSpan = $('#' + this.videoSpanId + '>span.videoMuted');
var videoMutedSpan = this.getVideoMutedIndicator();
if (isMuted === false) {
if (videoMutedSpan.length > 0) {
videoMutedSpan.remove();
}
}
else {
if (!videoMutedSpan.length) {
videoMutedSpan = document.createElement('span');
videoMutedSpan.className = 'videoMuted';
this.container.appendChild(videoMutedSpan);
var mutedIndicator = document.createElement('i');
mutedIndicator.className = 'icon-camera-disabled';
UIUtil.setTooltip(mutedIndicator,
"videothumbnail.videomute",
"top");
videoMutedSpan.appendChild(mutedIndicator);
//translate texts for muted indicator
APP.translation.translateElement($('#' + this.videoSpanId + " > span > i"));
}
this.updateIconPositions();
}
};
SmallVideo.prototype.updateIconPositions = function () {
var audioMutedSpan = $('#' + this.videoSpanId + '>span.audioMuted');
var connectionIndicator = $('#' + this.videoSpanId + '>div.connectionindicator');
var videoMutedSpan = $('#' + this.videoSpanId + '>span.videoMuted');
if(connectionIndicator.length > 0 &&
connectionIndicator[0].style.display != "none") {
audioMutedSpan.css({right: "23px"});
videoMutedSpan.css({right: ((audioMutedSpan.length > 0? 23 : 0) + 30) + "px"});
} else {
audioMutedSpan.css({right: "0px"});
videoMutedSpan.css({right: (audioMutedSpan.length > 0? 30 : 0) + "px"});
}
videoMutedSpan[isMuted ? 'show' : 'hide']();
};
/**
* Creates the element indicating the moderator(owner) of the conference.
* Returns the video muted indicator jquery object. If it doesn't exists -
* creates it.
*
* @returns {jQuery|HTMLElement} the video muted indicator
*/
SmallVideo.prototype.createModeratorIndicatorElement = function () {
SmallVideo.prototype.getVideoMutedIndicator = function () {
var videoMutedSpan = $('#' + this.videoSpanId + ' .videoMuted');
if (videoMutedSpan.length) {
return videoMutedSpan;
}
videoMutedSpan = document.createElement('span');
videoMutedSpan.className = 'videoMuted toolbar-icon';
this.container
.querySelector('.videocontainer__toolbar')
.appendChild(videoMutedSpan);
var mutedIndicator = document.createElement('i');
mutedIndicator.className = 'icon-camera-disabled';
UIUtil.setTooltip(mutedIndicator,
"videothumbnail.videomute",
"top");
videoMutedSpan.appendChild(mutedIndicator);
return $('#' + this.videoSpanId + ' .videoMuted');
};
/**
* Adds the element indicating the moderator(owner) of the conference.
*/
SmallVideo.prototype.addModeratorIndicator = function () {
// Don't create moderator indicator if DISABLE_FOCUS_INDICATOR is true
if (interfaceConfig.DISABLE_FOCUS_INDICATOR)
return false;
// Show moderator indicator
var indicatorSpan = $('#' + this.videoSpanId + ' .focusindicator');
if (!indicatorSpan || indicatorSpan.length === 0) {
indicatorSpan = document.createElement('span');
indicatorSpan.className = 'focusindicator';
this.container.appendChild(indicatorSpan);
indicatorSpan = $('#' + this.videoSpanId + ' .focusindicator');
if (indicatorSpan.length) {
return;
}
if (indicatorSpan.children().length !== 0)
return;
indicatorSpan = document.createElement('span');
indicatorSpan.className = 'focusindicator toolbar-icon right';
this.container
.querySelector('.videocontainer__toolbar')
.appendChild(indicatorSpan);
var moderatorIndicator = document.createElement('i');
moderatorIndicator.className = 'icon-star';
indicatorSpan[0].appendChild(moderatorIndicator);
UIUtil.setTooltip(indicatorSpan[0],
UIUtil.setTooltip(moderatorIndicator,
"videothumbnail.moderator",
"top");
"top-left");
//translates text in focus indicators
APP.translation.translateElement($('#' + this.videoSpanId + ' .focusindicator'));
indicatorSpan.appendChild(moderatorIndicator);
};
/**
* Adds the element indicating the audio level of the participant.
*/
SmallVideo.prototype.addAudioLevelIndicator = function () {
var audioSpan = $('#' + this.videoSpanId + ' .audioindicator');
if (audioSpan.length) {
return;
}
this.container.appendChild(
AudioLevels.createThumbnailAudioLevelIndicator());
};
/**
* Updates the audio level for this small video.
*
* @param lvl the new audio level to set
*/
SmallVideo.prototype.updateAudioLevelIndicator = function (lvl) {
AudioLevels.updateThumbnailAudioLevel(this.videoSpanId, lvl);
};
/**
* Removes the element indicating the moderator(owner) of the conference.
*/
SmallVideo.prototype.removeModeratorIndicatorElement = function () {
SmallVideo.prototype.removeModeratorIndicator = function () {
$('#' + this.videoSpanId + ' .focusindicator').remove();
};
@@ -341,6 +385,16 @@ SmallVideo.prototype.selectVideoElement = function () {
return $(RTCUIHelper.findVideoElement($('#' + this.videoSpanId)[0]));
};
/**
* Selects the HTML image element which displays user's avatar.
*
* @return {jQuery|HTMLElement} a jQuery selector pointing to the HTML image
* element which displays the user's avatar.
*/
SmallVideo.prototype.$avatar = function () {
return $('#' + this.videoSpanId + ' .userAvatar');
};
/**
* Enables / disables the css responsible for focusing/pinning a video
* thumbnail.
@@ -363,6 +417,47 @@ SmallVideo.prototype.hasVideo = function () {
return this.selectVideoElement().length !== 0;
};
/**
* Checks whether the user associated with this <tt>SmallVideo</tt> is currently
* being displayed on the "large video".
*
* @return {boolean} <tt>true</tt> if the user is displayed on the large video
* or <tt>false</tt> otherwise.
*/
SmallVideo.prototype.isCurrentlyOnLargeVideo = function () {
return this.VideoLayout.isCurrentlyOnLarge(this.id);
};
/**
* Checks whether there is a playable video stream available for the user
* associated with this <tt>SmallVideo</tt>.
*
* @return {boolean} <tt>true</tt> if there is a playable video stream available
* or <tt>false</tt> otherwise.
*/
SmallVideo.prototype.isVideoPlayable = function() {
return this.videoStream // Is there anything to display ?
&& !this.isVideoMuted && !this.videoStream.isMuted() // Muted ?
&& (this.isLocal || this.VideoLayout.isInLastN(this.id));
};
/**
* Determines what should be display on the thumbnail.
*
* @return {number} one of <tt>DISPLAY_VIDEO</tt>,<tt>DISPLAY_AVATAR</tt>
* or <tt>DISPLAY_BLACKNESS</tt>.
*/
SmallVideo.prototype.selectDisplayMode = function() {
// Display name is always and only displayed when user is on the stage
if (this.isCurrentlyOnLargeVideo()) {
return DISPLAY_BLACKNESS;
} else if (this.isVideoPlayable() && this.selectVideoElement().length) {
return DISPLAY_VIDEO;
} else {
return DISPLAY_AVATAR;
}
};
/**
* Hides or shows the user's avatar.
* This update assumes that large video had been updated and we will
@@ -382,48 +477,28 @@ SmallVideo.prototype.updateView = function () {
}
}
let video = this.selectVideoElement();
let avatar = $('#' + this.videoSpanId + ' .userAvatar');
var isCurrentlyOnLarge = this.VideoLayout.isCurrentlyOnLarge(this.id);
var showVideo = !this.isVideoMuted && !isCurrentlyOnLarge;
var showAvatar;
if ((!this.isLocal
&& !this.VideoLayout.isInLastN(this.id))
|| this.isVideoMuted) {
showAvatar = true;
} else {
// We want to show the avatar when the video is muted or not exists
// that is when 'true' or 'null' is returned
showAvatar = !this.videoStream || this.videoStream.isMuted();
}
showAvatar = showAvatar && !isCurrentlyOnLarge;
if (video && video.length > 0) {
setVisibility(video, showVideo);
}
setVisibility(avatar, showAvatar);
this.showDisplayName(!showVideo && !showAvatar);
// Determine whether video, avatar or blackness should be displayed
let displayMode = this.selectDisplayMode();
// Show/hide video
setVisibility(this.selectVideoElement(), displayMode === DISPLAY_VIDEO);
// Show/hide the avatar
setVisibility(this.$avatar(), displayMode === DISPLAY_AVATAR);
};
SmallVideo.prototype.avatarChanged = function (avatarUrl) {
var thumbnail = $('#' + this.videoSpanId);
var avatar = $('#' + this.videoSpanId + ' .userAvatar');
var avatarSel = this.$avatar();
this.hasAvatar = true;
// set the avatar in the thumbnail
if (avatar && avatar.length > 0) {
avatar[0].src = avatarUrl;
if (avatarSel && avatarSel.length > 0) {
avatarSel[0].src = avatarUrl;
} else {
if (thumbnail && thumbnail.length > 0) {
avatar = document.createElement('img');
avatar.className = 'userAvatar';
avatar.src = avatarUrl;
thumbnail.append(avatar);
var avatarElement = document.createElement('img');
avatarElement.className = 'userAvatar';
avatarElement.src = avatarUrl;
thumbnail.append(avatarElement);
}
}
};
@@ -445,7 +520,7 @@ SmallVideo.prototype.showDominantSpeakerIndicator = function (show) {
indicatorSpan.innerHTML
= "<i id='indicatoricon' class='fa fa-bullhorn'></i>";
// adds a tooltip
UIUtil.setTooltip(indicatorSpan, "speaker", "left");
UIUtil.setTooltip(indicatorSpan, "speaker", "top");
APP.translation.translateElement($(indicatorSpan));
$(indicatorSpan).css("visibility", show ? "visible" : "hidden");
@@ -465,12 +540,11 @@ SmallVideo.prototype.showRaisedHandIndicator = function (show) {
var indicatorSpanId = "raisehandindicator";
var indicatorSpan = this.getIndicatorSpan(indicatorSpanId);
indicatorSpan.style.background = "#D6D61E";
indicatorSpan.innerHTML
= "<i id='indicatoricon' class='fa fa-hand-paper-o'></i>";
= "<i id='indicatoricon' class='icon-raised-hand'></i>";
// adds a tooltip
UIUtil.setTooltip(indicatorSpan, "raisedHand", "left");
UIUtil.setTooltip(indicatorSpan, "raisedHand", "top");
APP.translation.translateElement($(indicatorSpan));
$(indicatorSpan).css("visibility", show ? "visible" : "hidden");
@@ -500,7 +574,6 @@ SmallVideo.prototype.getIndicatorSpan = function(id) {
* is added, and will fire a RESOLUTION_CHANGED event.
*/
SmallVideo.prototype.waitForResolutionChange = function() {
let self = this;
let beforeChange = window.performance.now();
let videos = this.selectVideoElement();
if (!videos || !videos.length || videos.length <= 0)
@@ -508,17 +581,17 @@ SmallVideo.prototype.waitForResolutionChange = function() {
let video = videos[0];
let oldWidth = video.videoWidth;
let oldHeight = video.videoHeight;
video.onresize = (event) => {
video.onresize = () => {
if (video.videoWidth != oldWidth || video.videoHeight != oldHeight) {
// Only run once.
video.onresize = null;
let delay = window.performance.now() - beforeChange;
let emitter = self.VideoLayout.getEventEmitter();
let emitter = this.VideoLayout.getEventEmitter();
if (emitter) {
emitter.emit(
UIEvents.RESOLUTION_CHANGED,
self.getId(),
this.getId(),
oldWidth + "x" + oldHeight,
video.videoWidth + "x" + video.videoHeight,
delay);

View File

@@ -1,17 +1,16 @@
/* global $, APP, interfaceConfig */
/* jshint -W101 */
import UIUtil from "../util/UIUtil";
import UIEvents from "../../../service/UI/UIEvents";
import LargeContainer from './LargeContainer';
import FilmStrip from './FilmStrip';
import Avatar from "../avatar/Avatar";
import {createDeferred} from '../../util/helpers';
import LargeContainer from './LargeContainer';
import UIEvents from "../../../service/UI/UIEvents";
import UIUtil from "../util/UIUtil";
// FIXME should be 'video'
export const VIDEO_CONTAINER_TYPE = "camera";
const FADE_DURATION_MS = 300;
export const VIDEO_CONTAINER_TYPE = "camera";
/**
* Get stream id.
* @param {JitsiTrack?} stream
@@ -20,7 +19,8 @@ function getStreamOwnerId(stream) {
if (!stream) {
return;
}
if (stream.isLocal()) { // local stream doesn't have method "getParticipantId"
// local stream doesn't have method "getParticipantId"
if (stream.isLocal()) {
return APP.conference.getMyUserId();
} else {
return stream.getParticipantId();
@@ -139,11 +139,7 @@ function getCameraVideoPosition(videoWidth,
* @return an array with 2 elements, the horizontal indent and the vertical
* indent
*/
function getDesktopVideoPosition(videoWidth,
videoHeight,
videoSpaceWidth,
videoSpaceHeight) {
function getDesktopVideoPosition(videoWidth, videoHeight, videoSpaceWidth) {
let horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
let verticalIndent = 0;// Top aligned
@@ -154,7 +150,7 @@ function getDesktopVideoPosition(videoWidth,
/**
* Container for user video.
*/
class VideoContainer extends LargeContainer {
export class VideoContainer extends LargeContainer {
// FIXME: With Temasys we have to re-select everytime
get $video () {
return $('#largeVideo');
@@ -164,23 +160,61 @@ class VideoContainer extends LargeContainer {
return getStreamOwnerId(this.stream);
}
constructor (onPlay) {
constructor (onPlay, emitter) {
super();
this.stream = null;
this.videoType = null;
this.localFlipX = true;
this.emitter = emitter;
this.isVisible = false;
/**
* Flag indicates whether or not the avatar is currently displayed.
* @type {boolean}
*/
this.avatarDisplayed = false;
this.$avatar = $('#dominantSpeaker');
/**
* A jQuery selector of the remote connection message.
* @type {jQuery|HTMLElement}
*/
this.$remoteConnectionMessage = $('#remoteConnectionMessage');
/**
* Indicates whether or not the video stream attached to the video
* element has started(which means that there is any image rendered
* even if the video is stalled).
* @type {boolean}
*/
this.wasVideoRendered = false;
this.$wrapper = $('#largeVideoWrapper');
this.avatarHeight = $("#dominantSpeakerAvatar").height();
var onPlayCallback = function (event) {
if (typeof onPlay === 'function') {
onPlay(event);
}
this.wasVideoRendered = true;
}.bind(this);
// This does not work with Temasys plugin - has to be a property to be
// copied between new <object> elements
//this.$video.on('play', onPlay);
this.$video[0].onplay = onPlay;
this.$video[0].onplay = onPlayCallback;
}
/**
* Enables a filter on the video which indicates that there are some
* problems with the local media connection.
*
* @param {boolean} enable <tt>true</tt> if the filter is to be enabled or
* <tt>false</tt> otherwise.
*/
enableLocalConnectionProblemFilter (enable) {
this.$video.toggleClass("videoProblemFilter", enable);
}
/**
@@ -205,14 +239,14 @@ class VideoContainer extends LargeContainer {
let { width, height } = this.getStreamSize();
if (this.stream && this.isScreenSharing()) {
return getDesktopVideoSize( width,
height,
containerWidth,
containerHeight);
height,
containerWidth,
containerHeight);
} else {
return getCameraVideoSize( width,
height,
containerWidth,
containerHeight);
height,
containerWidth,
containerHeight);
}
}
@@ -228,29 +262,55 @@ class VideoContainer extends LargeContainer {
getVideoPosition (width, height, containerWidth, containerHeight) {
if (this.stream && this.isScreenSharing()) {
return getDesktopVideoPosition( width,
height,
containerWidth,
containerHeight);
height,
containerWidth,
containerHeight);
} else {
return getCameraVideoPosition( width,
height,
containerWidth,
containerHeight);
height,
containerWidth,
containerHeight);
}
}
/**
* Update position of the remote connection message which describes that
* the remote user is having connectivity issues.
*/
positionRemoteConnectionMessage () {
if (this.avatarDisplayed) {
let $avatarImage = $("#dominantSpeakerAvatar");
this.$remoteConnectionMessage.css(
'top',
$avatarImage.offset().top + $avatarImage.height() + 10);
} else {
let height = this.$remoteConnectionMessage.height();
let parentHeight = this.$remoteConnectionMessage.parent().height();
this.$remoteConnectionMessage.css(
'top', (parentHeight/2) - (height/2));
}
let width = this.$remoteConnectionMessage.width();
let parentWidth = this.$remoteConnectionMessage.parent().width();
this.$remoteConnectionMessage.css(
'left', ((parentWidth/2) - (width/2)));
}
resize (containerWidth, containerHeight, animate = false) {
let [width, height]
= this.getVideoSize(containerWidth, containerHeight);
let { horizontalIndent, verticalIndent }
= this.getVideoPosition(width, height,
containerWidth, containerHeight);
containerWidth, containerHeight);
// update avatar position
let top = containerHeight / 2 - this.avatarHeight / 4 * 3;
this.$avatar.css('top', top);
this.positionRemoteConnectionMessage();
this.$wrapper.animate({
width: width,
height: height,
@@ -272,6 +332,14 @@ class VideoContainer extends LargeContainer {
* @param {string} videoType video type
*/
setStream (stream, videoType) {
if (this.stream === stream) {
return;
} else {
// The stream has changed, so the image will be lost on detach
this.wasVideoRendered = false;
}
// detach old stream
if (this.stream) {
this.stream.detach(this.$video[0]);
@@ -327,6 +395,21 @@ class VideoContainer extends LargeContainer {
(show) ? interfaceConfig.DEFAULT_BACKGROUND : "#000");
this.$avatar.css("visibility", show ? "visible" : "hidden");
this.avatarDisplayed = show;
this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED, show);
}
/**
* Indicates that the remote user who is currently displayed by this video
* container is having connectivity issues.
*
* @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide
* the indication.
*/
showRemoteConnectionProblemIndicator (show) {
this.$video.toggleClass("remoteVideoProblemFilter", show);
this.$avatar.toggleClass("remoteVideoProblemFilter", show);
}
// We are doing fadeOut/fadeIn animations on parent div which wraps
@@ -341,7 +424,6 @@ class VideoContainer extends LargeContainer {
return Promise.resolve();
}
let $wrapper = this.$wrapper;
return new Promise((resolve) => {
this.$wrapper.css('visibility', 'visible').fadeTo(
FADE_DURATION_MS,
@@ -380,304 +462,3 @@ class VideoContainer extends LargeContainer {
return false;
}
}
/**
* Manager for all Large containers.
*/
export default class LargeVideoManager {
constructor () {
this.containers = {};
this.state = VIDEO_CONTAINER_TYPE;
this.videoContainer = new VideoContainer(
() => this.resizeContainer(VIDEO_CONTAINER_TYPE));
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
// use the same video container to handle and desktop tracks
this.addContainer("desktop", this.videoContainer);
this.width = 0;
this.height = 0;
this.$container = $('#largeVideoContainer');
this.$container.css({
display: 'inline-block'
});
if (interfaceConfig.SHOW_JITSI_WATERMARK) {
let leftWatermarkDiv
= this.$container.find("div.watermark.leftwatermark");
leftWatermarkDiv.css({display: 'block'});
leftWatermarkDiv.parent().attr(
'href', interfaceConfig.JITSI_WATERMARK_LINK);
}
if (interfaceConfig.SHOW_BRAND_WATERMARK) {
let rightWatermarkDiv
= this.$container.find("div.watermark.rightwatermark");
rightWatermarkDiv.css({
display: 'block',
backgroundImage: 'url(images/rightwatermark.png)'
});
rightWatermarkDiv.parent().attr(
'href', interfaceConfig.BRAND_WATERMARK_LINK);
}
if (interfaceConfig.SHOW_POWERED_BY) {
this.$container.children("a.poweredby").css({display: 'block'});
}
this.$container.hover(
e => this.onHoverIn(e),
e => this.onHoverOut(e)
);
}
onHoverIn (e) {
if (!this.state) {
return;
}
let container = this.getContainer(this.state);
container.onHoverIn(e);
}
onHoverOut (e) {
if (!this.state) {
return;
}
let container = this.getContainer(this.state);
container.onHoverOut(e);
}
get id () {
let container = this.getContainer(this.state);
return container.id;
}
scheduleLargeVideoUpdate () {
if (this.updateInProcess || !this.newStreamData) {
return;
}
this.updateInProcess = true;
let container = this.getContainer(this.state);
// Include hide()/fadeOut only if we're switching between users
let preUpdate;
if (this.newStreamData.id != this.id) {
preUpdate = container.hide();
} else {
preUpdate = Promise.resolve();
}
preUpdate.then(() => {
let {id, stream, videoType, resolve} = this.newStreamData;
this.newStreamData = null;
console.info("hover in %s", id);
this.state = videoType;
let container = this.getContainer(this.state);
container.setStream(stream, videoType);
// change the avatar url on large
this.updateAvatar(Avatar.getAvatarUrl(id));
// If we the continer is VIDEO_CONTAINER_TYPE, we need to check
// its stream whether exist and is muted to set isVideoMuted
// in rest of the cases it is false
let isVideoMuted = false;
if (videoType == VIDEO_CONTAINER_TYPE)
isVideoMuted = stream ? stream.isMuted() : true;
// show the avatar on large if needed
container.showAvatar(isVideoMuted);
let promise;
// do not show stream if video is muted
// but we still should show watermark
if (isVideoMuted) {
this.showWatermark(true);
promise = Promise.resolve();
} else {
promise = container.show();
}
// resolve updateLargeVideo promise after everything is done
promise.then(resolve);
return promise;
}).then(() => {
// after everything is done check again if there are any pending
// new streams.
this.updateInProcess = false;
this.scheduleLargeVideoUpdate();
});
}
/**
* Update large video.
* Switches to large video even if previously other container was visible.
* @param userID the userID of the participant associated with the stream
* @param {JitsiTrack?} stream new stream
* @param {string?} videoType new video type
* @returns {Promise}
*/
updateLargeVideo (userID, stream, videoType) {
if (this.newStreamData) {
this.newStreamData.reject();
}
this.newStreamData = createDeferred();
this.newStreamData.id = userID;
this.newStreamData.stream = stream;
this.newStreamData.videoType = videoType;
this.scheduleLargeVideoUpdate();
return this.newStreamData.promise;
}
/**
* Update container size.
*/
updateContainerSize () {
this.width = UIUtil.getAvailableVideoWidth();
this.height = window.innerHeight;
}
/**
* Resize Large container of specified type.
* @param {string} type type of container which should be resized.
* @param {boolean} [animate=false] if resize process should be animated.
*/
resizeContainer (type, animate = false) {
let container = this.getContainer(type);
container.resize(this.width, this.height, animate);
}
/**
* Resize all Large containers.
* @param {boolean} animate if resize process should be animated.
*/
resize (animate) {
// resize all containers
Object.keys(this.containers)
.forEach(type => this.resizeContainer(type, animate));
this.$container.animate({
width: this.width,
height: this.height
}, {
queue: false,
duration: animate ? 500 : 0
});
}
/**
* Enables/disables the filter indicating a video problem to the user.
*
* @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
*/
enableVideoProblemFilter (enable) {
let container = this.getContainer(this.state);
container.$video.toggleClass("videoProblemFilter", enable);
}
/**
* Updates the src of the dominant speaker avatar
*/
updateAvatar (avatarUrl) {
$("#dominantSpeakerAvatar").attr('src', avatarUrl);
}
/**
* Show or hide watermark.
* @param {boolean} show
*/
showWatermark (show) {
$('.watermark').css('visibility', show ? 'visible' : 'hidden');
}
/**
* Add container of specified type.
* @param {string} type container type
* @param {LargeContainer} container container to add.
*/
addContainer (type, container) {
if (this.containers[type]) {
throw new Error(`container of type ${type} already exist`);
}
this.containers[type] = container;
this.resizeContainer(type);
}
/**
* Get Large container of specified type.
* @param {string} type container type.
* @returns {LargeContainer}
*/
getContainer (type) {
let container = this.containers[type];
if (!container) {
throw new Error(`container of type ${type} doesn't exist`);
}
return container;
}
/**
* Remove Large container of specified type.
* @param {string} type container type.
*/
removeContainer (type) {
if (!this.containers[type]) {
throw new Error(`container of type ${type} doesn't exist`);
}
delete this.containers[type];
}
/**
* Show Large container of specified type.
* Does nothing if such container is already visible.
* @param {string} type container type.
* @returns {Promise}
*/
showContainer (type) {
if (this.state === type) {
return Promise.resolve();
}
let oldContainer = this.containers[this.state];
if (this.state === VIDEO_CONTAINER_TYPE) {
this.showWatermark(false);
}
oldContainer.hide();
this.state = type;
let container = this.getContainer(type);
return container.show().then(() => {
if (type === VIDEO_CONTAINER_TYPE) {
this.showWatermark(true);
}
});
}
/**
* Changes the flipX state of the local video.
* @param val {boolean} true if flipped.
*/
onLocalFlipXChange(val) {
this.videoContainer.setLocalFlipX(val);
}
}

View File

@@ -1,19 +1,14 @@
/* global config, APP, $, interfaceConfig, JitsiMeetJS */
/* jshint -W101 */
/* global config, APP, $, interfaceConfig */
import AudioLevels from "../audio_levels/AudioLevels";
import Avatar from "../avatar/Avatar";
import FilmStrip from "./FilmStrip";
import UIEvents from "../../../service/UI/UIEvents";
import UIUtil from "../util/UIUtil";
import RemoteVideo from "./RemoteVideo";
import LargeVideoManager, {VIDEO_CONTAINER_TYPE} from "./LargeVideo";
import {SHARED_VIDEO_CONTAINER_TYPE} from '../shared_video/SharedVideo';
import LargeVideoManager from "./LargeVideoManager";
import {VIDEO_CONTAINER_TYPE} from "./VideoContainer";
import LocalVideo from "./LocalVideo";
const RTCUIUtil = JitsiMeetJS.util.RTCUIHelper;
var remoteVideos = {};
var localVideoThumbnail = null;
@@ -102,33 +97,37 @@ var VideoLayout = {
});
localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
// sets default video type of local video
// FIXME container type is totally different thing from the video type
localVideoThumbnail.setVideoType(VIDEO_CONTAINER_TYPE);
// if we do not resize the thumbs here, if there is no video device
// the local video thumb maybe one pixel
let {thumbWidth, thumbHeight}
= this.resizeThumbnails(false, true, false);
AudioLevels.updateAudioLevelCanvas(null, thumbWidth, thumbHeight);
this.resizeThumbnails(false, true);
emitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
this.lastNCount = config.channelLastN;
},
initLargeVideo () {
largeVideo = new LargeVideoManager();
largeVideo = new LargeVideoManager(eventEmitter);
if(localFlipX) {
largeVideo.onLocalFlipXChange(localFlipX);
}
largeVideo.updateContainerSize();
AudioLevels.init();
},
/**
* Sets the audio level of the video elements associated to the given id.
*
* @param id the video identifier in the form it comes from the library
* @param lvl the new audio level to update to
*/
setAudioLevel(id, lvl) {
if (!largeVideo) {
return;
}
AudioLevels.updateAudioLevel(
id, lvl, largeVideo.id
);
let smallVideo = this.getSmallVideo(id);
if (smallVideo)
smallVideo.updateAudioLevelIndicator(lvl);
if (largeVideo && id === largeVideo.id)
largeVideo.updateLargeVideoAudioLevel(lvl);
},
isInLastN (resource) {
@@ -255,7 +254,8 @@ var VideoLayout = {
electLastVisibleVideo () {
// pick the last visible video in the row
// if nobody else is left, this picks the local video
let thumbs = FilmStrip.getThumbs(true).filter('[id!="mixedstream"]');
let remoteThumbs = FilmStrip.getThumbs(true).remoteThumbs;
let thumbs = remoteThumbs.filter('[id!="mixedstream"]');
let lastVisible = thumbs.filter(':visible:last');
if (lastVisible.length) {
@@ -269,7 +269,7 @@ var VideoLayout = {
}
console.info("Last visible video no longer exists");
thumbs = FilmStrip.getThumbs();
thumbs = FilmStrip.getThumbs().remoteThumbs;
if (thumbs.length) {
let id = getPeerContainerResourceId(thumbs[0]);
if (remoteVideos[id]) {
@@ -311,7 +311,8 @@ var VideoLayout = {
onRemoteStreamRemoved (stream) {
let id = stream.getParticipantId();
let remoteVideo = remoteVideos[id];
if (remoteVideo) { // remote stream may be removed after participant left the conference
// Remote stream may be removed after participant left the conference.
if (remoteVideo) {
remoteVideo.removeRemoteStreamElement(stream);
}
},
@@ -379,34 +380,49 @@ var VideoLayout = {
},
/**
* Creates a participant container for the given id and smallVideo.
* Creates or adds a participant container for the given id and smallVideo.
*
* @param id the id of the participant to add
* @param {JitsiParticipant} user the participant to add
* @param {SmallVideo} smallVideo optional small video instance to add as a
* remote video, if undefined RemoteVideo will be created
* remote video, if undefined <tt>RemoteVideo</tt> will be created
*/
addParticipantContainer (id, smallVideo) {
addParticipantContainer (user, smallVideo) {
let id = user.getId();
let remoteVideo;
if(smallVideo)
remoteVideo = smallVideo;
else
remoteVideo = new RemoteVideo(id, VideoLayout, eventEmitter);
remoteVideo = new RemoteVideo(user, VideoLayout, eventEmitter);
this.addRemoteVideoContainer(id, remoteVideo);
},
/**
* Adds remote video container for the given id and <tt>SmallVideo</tt>.
*
* @param {string} the id of the video to add
* @param {SmallVideo} smallVideo the small video instance to add as a
* remote video
*/
addRemoteVideoContainer (id, remoteVideo) {
remoteVideos[id] = remoteVideo;
let videoType = VideoLayout.getRemoteVideoType(id);
if (!videoType) {
// make video type the default one (camera)
// FIXME container type is not a video type
videoType = VIDEO_CONTAINER_TYPE;
}
remoteVideo.setVideoType(videoType);
// In case this is not currently in the last n we don't show it.
if (localLastNCount && localLastNCount > 0 &&
FilmStrip.getThumbs().length >= localLastNCount + 2) {
FilmStrip.getThumbs().remoteThumbs.length >= localLastNCount + 2) {
remoteVideo.showPeerContainer('hide');
} else {
VideoLayout.resizeThumbnails(false, true);
}
// Initialize the view
remoteVideo.updateView();
},
videoactive (videoelem, resourceJid) {
@@ -414,7 +430,7 @@ var VideoLayout = {
console.info(resourceJid + " video is now active", videoelem);
VideoLayout.resizeThumbnails(
false, false, false, function() {$(videoelem).show();});
false, false, function() {$(videoelem).show();});
// Update the large video to the last added video only if there's no
// current dominant, focused speaker or update it to
@@ -449,9 +465,9 @@ var VideoLayout = {
showModeratorIndicator () {
let isModerator = APP.conference.isModerator;
if (isModerator) {
localVideoThumbnail.createModeratorIndicatorElement();
localVideoThumbnail.addModeratorIndicator();
} else {
localVideoThumbnail.removeModeratorIndicatorElement();
localVideoThumbnail.removeModeratorIndicator();
}
APP.conference.listMembers().forEach(function (member) {
@@ -461,9 +477,10 @@ var VideoLayout = {
return;
if (member.isModerator()) {
remoteVideo.removeRemoteVideoMenu();
remoteVideo.createModeratorIndicatorElement();
} else if (isModerator) {
remoteVideo.addModeratorIndicator();
}
if (isModerator) {
// We are moderator, but user is not - add menu
if(!remoteVideo.hasRemoteVideoMenu) {
remoteVideo.addRemoteVideoMenu();
@@ -480,26 +497,34 @@ var VideoLayout = {
localVideoThumbnail.showAudioIndicator(isMuted);
},
/**
* Shows/hides the indication about local connection being interrupted.
*
* @param {boolean} isInterrupted <tt>true</tt> if local connection is
* currently in the interrupted state or <tt>false</tt> if the connection
* is fine.
*/
showLocalConnectionInterrupted (isInterrupted) {
localVideoThumbnail.connectionIndicator
.updateConnectionStatusIndicator(!isInterrupted);
},
/**
* Resizes thumbnails.
*/
resizeThumbnails ( animate = false,
forceUpdate = false,
onComplete = null) {
let {thumbWidth, thumbHeight}
const { localVideo, remoteVideo }
= FilmStrip.calculateThumbnailSize();
$('.userAvatar').css('left', (thumbWidth - thumbHeight) / 2);
FilmStrip.resizeThumbnails(thumbWidth, thumbHeight,
FilmStrip.resizeThumbnails(localVideo, remoteVideo,
animate, forceUpdate)
.then(function () {
AudioLevels.updateCanvasSize(thumbWidth, thumbHeight);
if (onComplete && typeof onComplete === "function")
onComplete();
});
return {thumbWidth, thumbHeight};
});
return { localVideo, remoteVideo };
},
/**
@@ -525,11 +550,11 @@ var VideoLayout = {
*/
onVideoMute (id, value) {
if (APP.conference.isLocalId(id)) {
localVideoThumbnail.setMutedView(value);
localVideoThumbnail.setVideoMutedView(value);
} else {
let remoteVideo = remoteVideos[id];
if (remoteVideo)
remoteVideo.setMutedView(value);
remoteVideo.setVideoMutedView(value);
}
if (this.isCurrentlyOnLarge(id)) {
@@ -561,6 +586,9 @@ var VideoLayout = {
? localVideoThumbnail : remoteVideos[id];
if (video) {
video.showRaisedHandIndicator(raisedHandStatus);
if (raisedHandStatus) {
video.showDominantSpeakerIndicator(false);
}
}
},
@@ -611,6 +639,36 @@ var VideoLayout = {
}
},
/**
* Shows/hides warning about remote user's connectivity issues.
*
* @param {string} id the ID of the remote participant(MUC nickname)
* @param {boolean} isActive true if the connection is ok or false when
* the user is having connectivity issues.
*/
// eslint-disable-next-line no-unused-vars
onParticipantConnectionStatusChanged (id, isActive) {
// Show/hide warning on the large video
if (this.isCurrentlyOnLarge(id)) {
if (largeVideo) {
// We have to trigger full large video update to transition from
// avatar to video on connectivity restored.
this.updateLargeVideo(id, true /* force update */);
}
}
// Show/hide warning on the thumbnail
let remoteVideo = remoteVideos[id];
if (remoteVideo) {
// Updating only connection status indicator is not enough, because
// when we the connection is restored while the avatar was displayed
// (due to 'muted while disconnected' condition) we may want to show
// the video stream again and in order to do that the display mode
// must be updated.
//remoteVideo.updateConnectionStatusIndicator(isActive);
remoteVideo.updateView();
}
},
/**
* On last N change event.
*
@@ -657,7 +715,7 @@ var VideoLayout = {
var updateLargeVideo = false;
// Handle LastN/local LastN changes.
FilmStrip.getThumbs().each(( index, element ) => {
FilmStrip.getThumbs().remoteThumbs.each(( index, element ) => {
var resourceJid = getPeerContainerResourceId(element);
var smallVideo = remoteVideos[resourceJid];
@@ -946,28 +1004,18 @@ var VideoLayout = {
* Indicates that the video has been interrupted.
*/
onVideoInterrupted () {
this.enableVideoProblemFilter(true);
let reconnectingKey = "connection.RECONNECTING";
$('#videoConnectionMessage')
.attr("data-i18n", reconnectingKey)
.text(APP.translation.translateString(reconnectingKey))
.css({display: "block"});
if (largeVideo) {
largeVideo.onVideoInterrupted();
}
},
/**
* Indicates that the video has been restored.
*/
onVideoRestored () {
this.enableVideoProblemFilter(false);
$('#videoConnectionMessage').css({display: "none"});
},
enableVideoProblemFilter (enable) {
if (!largeVideo) {
return;
if (largeVideo) {
largeVideo.onVideoRestored();
}
largeVideo.enableVideoProblemFilter(enable);
},
isLargeVideoVisible () {
@@ -995,6 +1043,7 @@ var VideoLayout = {
if (!isOnLarge || forceUpdate) {
let videoType = this.getRemoteVideoType(id);
// FIXME video type is not the same thing as container type
if (id !== currentId && videoType === VIDEO_CONTAINER_TYPE) {
eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id);
}

View File

@@ -2,6 +2,7 @@
var animateTimeout, updateTimeout;
var RoomnameGenerator = require("../../util/RoomnameGenerator");
import UIUtil from "../util/UIUtil";
function enter_room() {
var val = $("#enter_room_field").val();
@@ -39,10 +40,10 @@ function setupWelcomePage() {
$("#welcome_page_header div[class='watermark leftwatermark']");
if(leftWatermarkDiv && leftWatermarkDiv.length > 0) {
leftWatermarkDiv.css({display: 'block'});
leftWatermarkDiv.parent().get(0).href =
interfaceConfig.JITSI_WATERMARK_LINK;
UIUtil.setLinkHref(
leftWatermarkDiv.parent(),
interfaceConfig.JITSI_WATERMARK_LINK);
}
}
if (interfaceConfig.SHOW_BRAND_WATERMARK) {
@@ -50,8 +51,9 @@ function setupWelcomePage() {
$("#welcome_page_header div[class='watermark rightwatermark']");
if(rightWatermarkDiv && rightWatermarkDiv.length > 0) {
rightWatermarkDiv.css({display: 'block'});
rightWatermarkDiv.parent().get(0).href =
interfaceConfig.BRAND_WATERMARK_LINK;
UIUtil.setLinkHref(
rightWatermarkDiv.parent(),
interfaceConfig.BRAND_WATERMARK_LINK);
rightWatermarkDiv.get(0).style.backgroundImage =
"url(images/rightwatermark.png)";
}
@@ -73,8 +75,6 @@ function setupWelcomePage() {
});
if (interfaceConfig.GENERATE_ROOMNAMES_ON_WELCOME_PAGE !== false) {
var updateTimeout;
var animateTimeout;
var selector = $("#reload_roomname");
selector.click(function () {
clearTimeout(updateTimeout);

View File

@@ -32,7 +32,7 @@ var HttpConfig = {
var error = "Get config response status: " + textStatus;
complete(false, error);
},
success: function(data, textStatus, jqXHR) {
success: function(data) {
try {
configUtil.overrideConfigJSON(
config, interfaceConfig, data);
@@ -48,4 +48,4 @@ var HttpConfig = {
}
};
module.exports = HttpConfig;
module.exports = HttpConfig;

View File

@@ -1,4 +1,4 @@
/* global $, $iq, config, interfaceConfig, getConfigParamsFromUrl */
/* global config, interfaceConfig, getConfigParamsFromUrl */
var configUtils = require('./Util');
var params = {};

View File

@@ -1,4 +1,3 @@
/* global $ */
var ConfigUtil = {
/**
* Method overrides JSON properties in <tt>config</tt> and
@@ -42,4 +41,4 @@ var ConfigUtil = {
}
};
module.exports = ConfigUtil;
module.exports = ConfigUtil;

View File

@@ -1,5 +1,3 @@
/* global APP, require */
/* jshint -W101 */
import EventEmitter from "events";
import CQEvents from "../../service/connectionquality/CQEvents";

View File

@@ -1,4 +1,4 @@
/* global $, APP, JitsiMeetJS, config, interfaceConfig */
/* global APP, JitsiMeetJS */
let currentAudioInputDevices,
currentVideoInputDevices,
@@ -243,4 +243,4 @@ export default {
});
}
}
};
};

View File

@@ -17,11 +17,6 @@ function initGlobalShortcuts() {
APP.UI.toggleKeyboardShortcutsPanel();
}, "keyboardShortcuts.toggleShortcuts");
KeyboardShortcut.registerShortcut("R", null, function() {
JitsiMeetJS.analytics.sendEvent("shortcut.raisedhand.toggled");
APP.conference.maybeToggleRaisedHand();
}, "keyboardShortcuts.raiseHand");
KeyboardShortcut.registerShortcut("T", null, function() {
JitsiMeetJS.analytics.sendEvent("shortcut.talk.clicked");
APP.conference.muteAudio(true);
@@ -79,13 +74,6 @@ var KeyboardShortcut = {
}
}
};
$('body').popover({ selector: '[data-toggle=popover]',
trigger: 'click hover',
content: function() {
return this.getAttribute("content")
+ self._getShortcutTooltip(this.getAttribute("shortcut"));
}
});
},
/**
@@ -133,7 +121,7 @@ var KeyboardShortcut = {
* or an empty string if the shortcutAttr is null, an empty string or not
* found in the shortcut mapping
*/
_getShortcutTooltip: function (shortcutAttr) {
getShortcutTooltip: function (shortcutAttr) {
if (typeof shortcutAttr === "string" && shortcutAttr.length > 0) {
for (var key in _shortcuts) {
if (_shortcuts.hasOwnProperty(key)

View File

@@ -1,4 +1,4 @@
/* global APP, $, config */
/* global APP, config */
/**
* The (name of the) command which transports the recorder info.
@@ -26,7 +26,6 @@ class Recorder {
// which are to be followed so don't forget to removeCommand before
// sendCommand!
commands.removeCommand(_USER_INFO_COMMAND);
var self = this;
commands.sendCommand(
_USER_INFO_COMMAND,
{
@@ -38,4 +37,4 @@ class Recorder {
}
}
export default Recorder;
export default Recorder;

View File

@@ -174,10 +174,12 @@ export default {
* Set device id of the camera which is currently in use.
* Empty string stands for default device.
* @param {string} newId new camera device id
* @param {boolean} whether we need to store the value
*/
setCameraDeviceId: function (newId = '') {
setCameraDeviceId: function (newId, store) {
cameraDeviceId = newId;
window.localStorage.cameraDeviceId = newId;
if (store)
window.localStorage.cameraDeviceId = newId;
},
/**
@@ -192,10 +194,12 @@ export default {
* Set device id of the microphone which is currently in use.
* Empty string stands for default device.
* @param {string} newId new microphone device id
* @param {boolean} whether we need to store the value
*/
setMicDeviceId: function (newId = '') {
setMicDeviceId: function (newId, store) {
micDeviceId = newId;
window.localStorage.micDeviceId = newId;
if (store)
window.localStorage.micDeviceId = newId;
},
/**

View File

@@ -3,11 +3,9 @@ var i18n = require("i18next-client");
var languages = require("../../service/translation/languages");
var DEFAULT_LANG = languages.EN;
i18n.addPostProcessor("resolveAppName", function(value, key, options) {
return value.replace("__app__", interfaceConfig.APP_NAME);
});
i18n.addPostProcessor(
"resolveAppName",
value => value.replace("__app__", interfaceConfig.APP_NAME));
var defaultOptions = {
detectLngQS: "lang",
@@ -34,7 +32,7 @@ var defaultOptions = {
{ lng: lng, ns: ns });
i18n.functions.ajax({
url: url,
success: function(data, status, xhr) {
success: function(data) {
i18n.functions.log('loaded: ' + url);
done(null, data);
},
@@ -63,7 +61,7 @@ var defaultOptions = {
// localStorageExpirationTime: 86400000 // in ms, default 1 week
};
function initCompleted(t) {
function initCompleted() {
$("[data-i18n]").i18n();
}

View File

@@ -16,107 +16,60 @@
"readmeFilename": "README.md",
"//": "Callstats.io does not work with recent versions of jsSHA (2.0.1 in particular)",
"dependencies": {
"@atlassian/aui": "^6.0.0",
"async": "0.9.0",
"autosize": "^1.18.13",
"bootstrap": "3.1.1",
"events": "*",
"i18next-client": "1.7.7",
"jQuery-Impromptu": "trentrichardson/jQuery-Impromptu#v6.0.0",
"jquery": "~2.1.1",
"jQuery-Impromptu": "git+https://github.com/trentrichardson/jQuery-Impromptu.git#v6.0.0",
"lib-jitsi-meet": "git+https://github.com/jitsi/lib-jitsi-meet.git",
"jquery-contextmenu": "*",
"jquery-ui": "1.10.5",
"jssha": "1.5.0",
"jws": "*",
"lib-jitsi-meet": "jitsi/lib-jitsi-meet",
"postis": "^2.2.0",
"retry": "0.6.1",
"strophe": "^1.2.2",
"strophejs-plugins": "^0.0.6",
"toastr": "^2.0.3",
"postis": "^2.2.0",
"jws": "*"
"toastr": "^2.0.3"
},
"devDependencies": {
"babel-core": "*",
"babel-loader": "*",
"babel-plugin-transform-object-rest-spread": "*",
"babel-polyfill": "*",
"babel-preset-es2015": "*",
"babelify": "*",
"browserify": "11.1.x",
"browserify-css": "^0.9.2",
"browserify-shim": "^3.8.10",
"babel-preset-es2015": "6.14.0",
"babel-register": "*",
"clean-css": "*",
"exorcist": "*",
"css-loader": "*",
"eslint": "*",
"expose-loader": "*",
"file-loader": "*",
"imports-loader": "*",
"jshint": "2.8.0",
"node-sass": "^3.8.0",
"precommit-hook": "3.0.0",
"uglify-js": "2.4.24"
"string-replace-loader": "*",
"style-loader": "*",
"webpack": "*"
},
"license": "Apache-2.0",
"scripts": {
"lint": "./node_modules/.bin/jshint .",
"lint": "jshint . && eslint .",
"validate": "npm ls"
},
"pre-commit": [
"lint"
],
"browserify": {
"transform": [
"browserify-shim",
"browserify-css",
[
"babelify",
{
"ignore": "node_modules"
}
]
]
},
"babel": {
"presets": [
"es2015"
]
},
"browser": {
"jquery": "./node_modules/jquery/dist/jquery.js",
"jquery-ui": "./node_modules/jquery-ui/jquery-ui.js",
"strophe": "./node_modules/strophe/strophe.js",
"aui-css": "./node_modules/@atlassian/aui/dist/aui/css/aui.min.css",
"aui-experimental-css": "./node_modules/@atlassian/aui/dist/aui/css/aui-experimental.min.css",
"autosize": "./node_modules/autosize/build/jquery.autosize.js",
"popover": "./node_modules/bootstrap/js/popover.js",
"strophe-disco": "./node_modules/strophejs-plugins/disco/strophe.disco.js",
"strophe-caps": "./node_modules/strophejs-plugins/caps/strophe.caps.jsonly.js",
"toastr": "./node_modules/toastr/toastr.js",
"tooltip": "./node_modules/bootstrap/js/tooltip.js",
"popover": "./node_modules/bootstrap/js/popover.js",
"jQuery-Impromptu": "./node_modules/jQuery-Impromptu/dist/jquery-impromptu.js",
"autosize": "./node_modules/autosize/build/jquery.autosize.js"
},
"browserify-shim": {
"jquery": [
"$"
],
"strophe": {
"exports": "Strophe",
"depends": [
"jquery:$"
]
},
"strophe-disco": {
"depends": [
"strophe:Strophe"
]
},
"tooltip": {
"depends": "jquery:jQuery"
},
"popover": {
"depends": "jquery:jQuery"
},
"jQuery-Impromptu": {
"depends": "jquery:jQuery"
},
"jquery-contextmenu": {
"depends": "jquery:jQuery"
},
"autosize": {
"depends": "jquery:jQuery"
},
"browserify-css": {
"autoInject": true
}
"tooltip": "./node_modules/bootstrap/js/tooltip.js"
}
}

View File

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

View File

@@ -29,7 +29,6 @@ export default {
*/
UPDATE_SHARED_VIDEO: "UI.update_shared_video",
ROOM_LOCK_CLICKED: "UI.room_lock_clicked",
USER_INVITED: "UI.user_invited",
USER_KICKED: "UI.user_kicked",
REMOTE_AUDIO_MUTED: "UI.remote_audio_muted",
FULLSCREEN_TOGGLE: "UI.fullscreen_toggle",
@@ -105,5 +104,15 @@ export default {
* event must contain the identifier of the container that has been toggled
* and information about toggle on or off.
*/
SIDE_TOOLBAR_CONTAINER_TOGGLED: "UI.side_container_toggled"
SIDE_TOOLBAR_CONTAINER_TOGGLED: "UI.side_container_toggled",
/**
* Notifies that the raise hand has been changed.
*/
LOCAL_RAISE_HAND_CHANGED: "UI.local_raise_hand_changed",
/**
* Notifies that the avatar is displayed or not on the largeVideo.
*/
LARGE_VIDEO_AVATAR_DISPLAYED: "UI.large_video_avatar_displayed"
};

View File

@@ -9,16 +9,19 @@ module.exports = {
return languages;
},
EN: "en",
BG: "bg",
DE: "de",
TR: "tr",
FR: "fr",
ES: "es",
FR: "fr",
HY: "hy",
IT: "it",
OC: "oc",
PL: "pl",
PTBR: "ptBR",
RU: "ru",
SK: "sk",
SL: "sl",
SV: "sv"
SV: "sv",
TR: "tr"
};

View File

@@ -9,7 +9,7 @@
/**
* Builds and returns the room name.
*/
function getRoomName () {
function getRoomName () { // eslint-disable-line no-unused-vars
var path = window.location.pathname;
var roomName;
@@ -42,6 +42,7 @@ function getRoomName () {
* @param dontParse if false or undefined some transformations
* (for parsing the value as JSON) are going to be executed
*/
// eslint-disable-next-line no-unused-vars
function getConfigParamsFromUrl(source, dontParse) {
var paramStr = (source === "search")? location.search : location.hash;
if (!paramStr)

117
webpack.config.babel.js Normal file
View File

@@ -0,0 +1,117 @@
/* global __dirname */
import process from 'process';
const aui_css = __dirname + '/node_modules/@atlassian/aui/dist/aui/css/';
const minimize
= process.argv.indexOf('-p') != -1
|| process.argv.indexOf('--optimize-minimize') != -1;
const strophe = /\/node_modules\/strophe(js-plugins)?\/.*\.js$/;
// The base Webpack configuration to bundle the JavaScript artifacts of
// jitsi-meet such as app.bundle.js and external_api.js.
const config = {
devtool: 'source-map',
module: {
loaders: [{
// Transpile ES2015 (aka ES6) to ES5.
exclude: __dirname + '/node_modules/',
loader: 'babel',
test: /\.js$/
},{
// Expose jquery as the globals $ and jQuery because it is expected
// to be available in such a form by multiple jitsi-meet
// dependencies including AUI, lib-jitsi-meet.
loader: 'expose?$!expose?jQuery',
test: /\/node_modules\/jquery\/.*\.js$/
},{
// Disable AMD for the Strophe.js library or its imports will fail
// at runtime.
loader: 'imports?define=>false&this=>window',
test: strophe
},{
// Allow CSS to be imported into JavaScript.
loaders: [
'style',
'css'
],
test: /\.css$/
},{
// Emit the static assets of AUI such as images that are referenced
// by CSS into the output path.
include: aui_css,
loader: 'file',
query: {
context: aui_css,
name: '[path][name].[ext]'
},
test: /\.(gif|png|svg)$/
}],
noParse: [
// Do not parse the files of the Strophe.js library or at least
// parts of the properties of the Strophe global variable will be
// missing and strophejs-plugins will fail at runtime.
strophe
]
},
node: {
// Allow the use of the real filename of the module being executed. By
// default Webpack does not leak path-related information and provides a
// value that is a mock (/index.js).
__filename: true
},
output: {
filename: '[name]' + (minimize ? '.min' : '') + '.js',
libraryTarget: 'umd',
path: __dirname + '/build',
sourceMapFilename: '[name].' + (minimize ? 'min' : 'js') + '.map'
},
resolve: {
alias: {
aui:
'@atlassian/aui/dist/aui/js/aui'
+ (minimize ? '.min' : '')
+ '.js',
'aui-experimental':
'@atlassian/aui/dist/aui/js/aui-experimental'
+ (minimize ? '.min' : '')
+ '.js',
jquery: 'jquery/dist/jquery' + (minimize ? '.min' : '') + '.js',
'jQuery-Impromptu':
'jQuery-Impromptu/dist/jquery-impromptu'
+ (minimize ? '.min' : '')
+ '.js',
},
packageAlias: 'browser'
}
};
export default [{
// The Webpack configuration to bundle app.bundle.js (aka APP).
...config,
entry: {
'app.bundle': './app.js'
},
output: {
...config.output,
library: 'APP'
}
}, {
// The Webpack configuration to bundle external_api.js (aka
// JitsiMeetExternalAPI).
...config,
entry: {
'external_api': './modules/API/external/external_api.js'
},
output: {
...config.output,
library: 'JitsiMeetExternalAPI'
}
}];