您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975
  1. /* global APP, $, config, interfaceConfig */
  2. const logger = require('jitsi-meet-logger').getLogger(__filename);
  3. const UI = {};
  4. import messageHandler from './util/MessageHandler';
  5. import UIUtil from './util/UIUtil';
  6. import UIEvents from '../../service/UI/UIEvents';
  7. import EtherpadManager from './etherpad/Etherpad';
  8. import SharedVideoManager from './shared_video/SharedVideo';
  9. import VideoLayout from './videolayout/VideoLayout';
  10. import Filmstrip from './videolayout/Filmstrip';
  11. import { JitsiTrackErrors } from '../../react/features/base/lib-jitsi-meet';
  12. import {
  13. getLocalParticipant,
  14. showParticipantJoinedNotification
  15. } from '../../react/features/base/participants';
  16. import { toggleChat } from '../../react/features/chat';
  17. import { openDisplayNamePrompt } from '../../react/features/display-name';
  18. import { setEtherpadHasInitialzied } from '../../react/features/etherpad';
  19. import { setFilmstripVisible } from '../../react/features/filmstrip';
  20. import {
  21. setNotificationsEnabled,
  22. showWarningNotification
  23. } from '../../react/features/notifications';
  24. import {
  25. dockToolbox,
  26. setToolboxEnabled,
  27. showToolbox
  28. } from '../../react/features/toolbox';
  29. const EventEmitter = require('events');
  30. UI.messageHandler = messageHandler;
  31. import FollowMe from '../FollowMe';
  32. const eventEmitter = new EventEmitter();
  33. UI.eventEmitter = eventEmitter;
  34. let etherpadManager;
  35. let sharedVideoManager;
  36. let followMeHandler;
  37. const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
  38. microphone: {},
  39. camera: {}
  40. };
  41. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
  42. .camera[JitsiTrackErrors.UNSUPPORTED_RESOLUTION]
  43. = 'dialog.cameraUnsupportedResolutionError';
  44. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.GENERAL]
  45. = 'dialog.cameraUnknownError';
  46. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.PERMISSION_DENIED]
  47. = 'dialog.cameraPermissionDeniedError';
  48. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.NOT_FOUND]
  49. = 'dialog.cameraNotFoundError';
  50. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.CONSTRAINT_FAILED]
  51. = 'dialog.cameraConstraintFailedError';
  52. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
  53. .camera[JitsiTrackErrors.NO_DATA_FROM_SOURCE]
  54. = 'dialog.cameraNotSendingData';
  55. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[JitsiTrackErrors.GENERAL]
  56. = 'dialog.micUnknownError';
  57. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
  58. .microphone[JitsiTrackErrors.PERMISSION_DENIED]
  59. = 'dialog.micPermissionDeniedError';
  60. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[JitsiTrackErrors.NOT_FOUND]
  61. = 'dialog.micNotFoundError';
  62. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
  63. .microphone[JitsiTrackErrors.CONSTRAINT_FAILED]
  64. = 'dialog.micConstraintFailedError';
  65. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
  66. .microphone[JitsiTrackErrors.NO_DATA_FROM_SOURCE]
  67. = 'dialog.micNotSendingData';
  68. const UIListeners = new Map([
  69. [
  70. UIEvents.ETHERPAD_CLICKED,
  71. () => etherpadManager && etherpadManager.toggleEtherpad()
  72. ], [
  73. UIEvents.SHARED_VIDEO_CLICKED,
  74. () => sharedVideoManager && sharedVideoManager.toggleSharedVideo()
  75. ], [
  76. UIEvents.TOGGLE_FILMSTRIP,
  77. () => UI.toggleFilmstrip()
  78. ], [
  79. UIEvents.FOLLOW_ME_ENABLED,
  80. enabled => followMeHandler && followMeHandler.enableFollowMe(enabled)
  81. ]
  82. ]);
  83. /**
  84. * Indicates if we're currently in full screen mode.
  85. *
  86. * @return {boolean} {true} to indicate that we're currently in full screen
  87. * mode, {false} otherwise
  88. */
  89. UI.isFullScreen = function() {
  90. return UIUtil.isFullScreen();
  91. };
  92. /**
  93. * Returns true if the etherpad window is currently visible.
  94. * @returns {Boolean} - true if the etherpad window is currently visible.
  95. */
  96. UI.isEtherpadVisible = function() {
  97. return Boolean(etherpadManager && etherpadManager.isVisible());
  98. };
  99. /**
  100. * Returns true if there is a shared video which is being shown (?).
  101. * @returns {boolean} - true if there is a shared video which is being shown.
  102. */
  103. UI.isSharedVideoShown = function() {
  104. return Boolean(sharedVideoManager && sharedVideoManager.isSharedVideoShown);
  105. };
  106. /**
  107. * Notify user that server has shut down.
  108. */
  109. UI.notifyGracefulShutdown = function() {
  110. messageHandler.showError({
  111. descriptionKey: 'dialog.gracefulShutdown',
  112. titleKey: 'dialog.serviceUnavailable'
  113. });
  114. };
  115. /**
  116. * Notify user that reservation error happened.
  117. */
  118. UI.notifyReservationError = function(code, msg) {
  119. messageHandler.showError({
  120. descriptionArguments: {
  121. code,
  122. msg
  123. },
  124. descriptionKey: 'dialog.reservationErrorMsg',
  125. titleKey: 'dialog.reservationError'
  126. });
  127. };
  128. /**
  129. * Notify user that he has been kicked from the server.
  130. */
  131. UI.notifyKicked = function() {
  132. messageHandler.showError({
  133. hideErrorSupportLink: true,
  134. descriptionKey: 'dialog.kickMessage',
  135. titleKey: 'dialog.kickTitle'
  136. });
  137. };
  138. /**
  139. * Notify user that conference was destroyed.
  140. * @param reason {string} the reason text
  141. */
  142. UI.notifyConferenceDestroyed = function(reason) {
  143. // FIXME: use Session Terminated from translation, but
  144. // 'reason' text comes from XMPP packet and is not translated
  145. messageHandler.showError({
  146. description: reason,
  147. titleKey: 'dialog.sessTerminated'
  148. });
  149. };
  150. /**
  151. * Change nickname for the user.
  152. * @param {string} id user id
  153. * @param {string} displayName new nickname
  154. */
  155. UI.changeDisplayName = function(id, displayName) {
  156. VideoLayout.onDisplayNameChanged(id, displayName);
  157. };
  158. /**
  159. * Sets the "raised hand" status for a participant.
  160. *
  161. * @param {string} id - The id of the participant whose raised hand UI should
  162. * be updated.
  163. * @param {string} name - The name of the participant with the raised hand
  164. * update.
  165. * @param {boolean} raisedHandStatus - Whether the participant's hand is raised
  166. * or not.
  167. * @returns {void}
  168. */
  169. UI.setRaisedHandStatus = (id, name, raisedHandStatus) => {
  170. VideoLayout.setRaisedHandStatus(id, raisedHandStatus);
  171. if (raisedHandStatus) {
  172. messageHandler.participantNotification(
  173. name,
  174. 'notify.somebody',
  175. 'connected',
  176. 'notify.raisedHand');
  177. }
  178. };
  179. /**
  180. * Sets the local "raised hand" status.
  181. */
  182. UI.setLocalRaisedHandStatus
  183. = raisedHandStatus =>
  184. VideoLayout.setRaisedHandStatus(
  185. APP.conference.getMyUserId(),
  186. raisedHandStatus);
  187. /**
  188. * Initialize conference UI.
  189. */
  190. UI.initConference = function() {
  191. const { getState } = APP.store;
  192. const { id, name } = getLocalParticipant(getState);
  193. // Update default button states before showing the toolbar
  194. // if local role changes buttons state will be again updated.
  195. UI.updateLocalRole(APP.conference.isModerator);
  196. UI.showToolbar();
  197. const displayName = config.displayJids ? id : name;
  198. if (displayName) {
  199. UI.changeDisplayName('localVideoContainer', displayName);
  200. }
  201. // FollowMe attempts to copy certain aspects of the moderator's UI into the
  202. // other participants' UI. Consequently, it needs (1) read and write access
  203. // to the UI (depending on the moderator role of the local participant) and
  204. // (2) APP.conference as means of communication between the participants.
  205. followMeHandler = new FollowMe(APP.conference, UI);
  206. };
  207. /**
  208. * Returns the shared document manager object.
  209. * @return {EtherpadManager} the shared document manager object
  210. */
  211. UI.getSharedVideoManager = function() {
  212. return sharedVideoManager;
  213. };
  214. /**
  215. * Starts the UI module and initializes all related components.
  216. *
  217. * @returns {boolean} true if the UI is ready and the conference should be
  218. * established, false - otherwise (for example in the case of welcome page)
  219. */
  220. UI.start = function() {
  221. document.title = interfaceConfig.APP_NAME;
  222. // Set the defaults for prompt dialogs.
  223. $.prompt.setDefaults({ persistent: false });
  224. Filmstrip.init(eventEmitter);
  225. VideoLayout.init(eventEmitter);
  226. if (!interfaceConfig.filmStripOnly) {
  227. VideoLayout.initLargeVideo();
  228. }
  229. // Do not animate the video area on UI start (second argument passed into
  230. // resizeVideoArea) because the animation is not visible anyway. Plus with
  231. // the current dom layout, the quality label is part of the video layout and
  232. // will be seen animating in.
  233. VideoLayout.resizeVideoArea(true, false);
  234. sharedVideoManager = new SharedVideoManager(eventEmitter);
  235. if (interfaceConfig.filmStripOnly) {
  236. $('body').addClass('filmstrip-only');
  237. APP.store.dispatch(setNotificationsEnabled(false));
  238. } else if (config.iAmRecorder) {
  239. // in case of iAmSipGateway keep local video visible
  240. if (!config.iAmSipGateway) {
  241. VideoLayout.setLocalVideoVisible(false);
  242. }
  243. APP.store.dispatch(setToolboxEnabled(false));
  244. APP.store.dispatch(setNotificationsEnabled(false));
  245. UI.messageHandler.enablePopups(false);
  246. }
  247. document.title = interfaceConfig.APP_NAME;
  248. };
  249. /**
  250. * Setup some UI event listeners.
  251. */
  252. UI.registerListeners
  253. = () => UIListeners.forEach((value, key) => UI.addListener(key, value));
  254. /**
  255. * Setup some DOM event listeners.
  256. */
  257. UI.bindEvents = () => {
  258. /**
  259. *
  260. */
  261. function onResize() {
  262. VideoLayout.resizeVideoArea();
  263. }
  264. // Resize and reposition videos in full screen mode.
  265. $(document).on(
  266. 'webkitfullscreenchange mozfullscreenchange fullscreenchange',
  267. onResize);
  268. $(window).resize(onResize);
  269. };
  270. /**
  271. * Unbind some DOM event listeners.
  272. */
  273. UI.unbindEvents = () => {
  274. $(document).off(
  275. 'webkitfullscreenchange mozfullscreenchange fullscreenchange');
  276. $(window).off('resize');
  277. };
  278. /**
  279. * Show local stream on UI.
  280. * @param {JitsiTrack} track stream to show
  281. */
  282. UI.addLocalStream = track => {
  283. switch (track.getType()) {
  284. case 'audio':
  285. // Local audio is not rendered so no further action is needed at this
  286. // point.
  287. break;
  288. case 'video':
  289. VideoLayout.changeLocalVideo(track);
  290. break;
  291. default:
  292. logger.error(`Unknown stream type: ${track.getType()}`);
  293. break;
  294. }
  295. };
  296. /**
  297. * Removed remote stream from UI.
  298. * @param {JitsiTrack} track stream to remove
  299. */
  300. UI.removeRemoteStream = track => VideoLayout.onRemoteStreamRemoved(track);
  301. /**
  302. * Setup and show Etherpad.
  303. * @param {string} name etherpad id
  304. */
  305. UI.initEtherpad = name => {
  306. if (etherpadManager || !config.etherpad_base || !name) {
  307. return;
  308. }
  309. logger.log('Etherpad is enabled');
  310. etherpadManager
  311. = new EtherpadManager(config.etherpad_base, name, eventEmitter);
  312. APP.store.dispatch(setEtherpadHasInitialzied());
  313. };
  314. /**
  315. * Returns the shared document manager object.
  316. * @return {EtherpadManager} the shared document manager object
  317. */
  318. UI.getSharedDocumentManager = () => etherpadManager;
  319. /**
  320. * Show user on UI.
  321. * @param {JitsiParticipant} user
  322. */
  323. UI.addUser = function(user) {
  324. const id = user.getId();
  325. const displayName = user.getDisplayName();
  326. const status = user.getStatus();
  327. if (status) {
  328. // FIXME: move updateUserStatus in participantPresenceChanged action
  329. UI.updateUserStatus(user, status);
  330. } else {
  331. APP.store.dispatch(showParticipantJoinedNotification(displayName));
  332. }
  333. // set initial display name
  334. if (displayName) {
  335. UI.changeDisplayName(id, displayName);
  336. }
  337. };
  338. /**
  339. * Update videotype for specified user.
  340. * @param {string} id user id
  341. * @param {string} newVideoType new videotype
  342. */
  343. UI.onPeerVideoTypeChanged
  344. = (id, newVideoType) => VideoLayout.onVideoTypeChanged(id, newVideoType);
  345. /**
  346. * Update local user role and show notification if user is moderator.
  347. * @param {boolean} isModerator if local user is moderator or not
  348. */
  349. UI.updateLocalRole = isModerator => {
  350. VideoLayout.showModeratorIndicator();
  351. if (isModerator && !interfaceConfig.DISABLE_FOCUS_INDICATOR) {
  352. messageHandler.participantNotification(
  353. null, 'notify.me', 'connected', 'notify.moderator');
  354. }
  355. };
  356. /**
  357. * Check the role for the user and reflect it in the UI, moderator ui indication
  358. * and notifies user who is the moderator
  359. * @param user to check for moderator
  360. */
  361. UI.updateUserRole = user => {
  362. VideoLayout.showModeratorIndicator();
  363. // We don't need to show moderator notifications when the focus (moderator)
  364. // indicator is disabled.
  365. if (!user.isModerator() || interfaceConfig.DISABLE_FOCUS_INDICATOR) {
  366. return;
  367. }
  368. const displayName = user.getDisplayName();
  369. messageHandler.participantNotification(
  370. displayName,
  371. 'notify.somebody',
  372. 'connected',
  373. 'notify.grantedTo',
  374. { to: displayName
  375. ? UIUtil.escapeHtml(displayName) : '$t(notify.somebody)' });
  376. };
  377. /**
  378. * Updates the user status.
  379. *
  380. * @param {JitsiParticipant} user - The user which status we need to update.
  381. * @param {string} status - The new status.
  382. */
  383. UI.updateUserStatus = (user, status) => {
  384. const reduxState = APP.store.getState() || {};
  385. const { calleeInfoVisible } = reduxState['features/invite'] || {};
  386. if (!status || calleeInfoVisible) {
  387. return;
  388. }
  389. const displayName = user.getDisplayName();
  390. messageHandler.participantNotification(
  391. displayName,
  392. '',
  393. 'connected',
  394. 'dialOut.statusMessage',
  395. { status: UIUtil.escapeHtml(status) });
  396. };
  397. /**
  398. * Toggles filmstrip.
  399. */
  400. UI.toggleFilmstrip = function() {
  401. const { visible } = APP.store.getState()['features/filmstrip'];
  402. APP.store.dispatch(setFilmstripVisible(!visible));
  403. };
  404. /**
  405. * Checks if the filmstrip is currently visible or not.
  406. * @returns {true} if the filmstrip is currently visible, and false otherwise.
  407. */
  408. UI.isFilmstripVisible = () => Filmstrip.isFilmstripVisible();
  409. /**
  410. * Toggles the visibility of the chat panel.
  411. */
  412. UI.toggleChat = () => APP.store.dispatch(toggleChat());
  413. /**
  414. * Handle new user display name.
  415. */
  416. UI.inputDisplayNameHandler = function(newDisplayName) {
  417. eventEmitter.emit(UIEvents.NICKNAME_CHANGED, newDisplayName);
  418. };
  419. /**
  420. * Return the type of the remote video.
  421. * @param jid the jid for the remote video
  422. * @returns the video type video or screen.
  423. */
  424. UI.getRemoteVideoType = function(jid) {
  425. return VideoLayout.getRemoteVideoType(jid);
  426. };
  427. // FIXME check if someone user this
  428. UI.showLoginPopup = function(callback) {
  429. logger.log('password is required');
  430. const message
  431. = `<input name="username" type="text"
  432. placeholder="user@domain.net"
  433. class="input-control" autofocus>
  434. <input name="password" type="password"
  435. data-i18n="[placeholder]dialog.userPassword"
  436. class="input-control"
  437. placeholder="user password">`
  438. ;
  439. // eslint-disable-next-line max-params
  440. const submitFunction = (e, v, m, f) => {
  441. if (v && f.username && f.password) {
  442. callback(f.username, f.password);
  443. }
  444. };
  445. messageHandler.openTwoButtonDialog({
  446. titleKey: 'dialog.passwordRequired',
  447. msgString: message,
  448. leftButtonKey: 'dialog.Ok',
  449. submitFunction,
  450. focus: ':input:first'
  451. });
  452. };
  453. UI.askForNickname = function() {
  454. // eslint-disable-next-line no-alert
  455. return window.prompt('Your nickname (optional)');
  456. };
  457. /**
  458. * Sets muted audio state for participant
  459. */
  460. UI.setAudioMuted = function(id, muted) {
  461. VideoLayout.onAudioMute(id, muted);
  462. if (APP.conference.isLocalId(id)) {
  463. APP.conference.updateAudioIconEnabled();
  464. }
  465. };
  466. /**
  467. * Sets muted video state for participant
  468. */
  469. UI.setVideoMuted = function(id, muted) {
  470. VideoLayout.onVideoMute(id, muted);
  471. if (APP.conference.isLocalId(id)) {
  472. APP.conference.updateVideoIconEnabled();
  473. }
  474. };
  475. /**
  476. * Triggers an update of remote video and large video displays so they may pick
  477. * up any state changes that have occurred elsewhere.
  478. *
  479. * @returns {void}
  480. */
  481. UI.updateAllVideos = () => VideoLayout.updateAllVideos();
  482. /**
  483. * Adds a listener that would be notified on the given type of event.
  484. *
  485. * @param type the type of the event we're listening for
  486. * @param listener a function that would be called when notified
  487. */
  488. UI.addListener = function(type, listener) {
  489. eventEmitter.on(type, listener);
  490. };
  491. /**
  492. * Removes the all listeners for all events.
  493. *
  494. * @returns {void}
  495. */
  496. UI.removeAllListeners = function() {
  497. eventEmitter.removeAllListeners();
  498. };
  499. /**
  500. * Removes the given listener for the given type of event.
  501. *
  502. * @param type the type of the event we're listening for
  503. * @param listener the listener we want to remove
  504. */
  505. UI.removeListener = function(type, listener) {
  506. eventEmitter.removeListener(type, listener);
  507. };
  508. /**
  509. * Emits the event of given type by specifying the parameters in options.
  510. *
  511. * @param type the type of the event we're emitting
  512. * @param options the parameters for the event
  513. */
  514. UI.emitEvent = (type, ...options) => eventEmitter.emit(type, ...options);
  515. UI.clickOnVideo = videoNumber => VideoLayout.togglePin(videoNumber);
  516. // Used by torture.
  517. UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
  518. // Used by torture.
  519. UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock));
  520. /**
  521. * Updates the displayed avatar for participant.
  522. *
  523. * @param {string} id - User id whose avatar should be updated.
  524. * @param {string} avatarURL - The URL to avatar image to display.
  525. * @returns {void}
  526. */
  527. UI.refreshAvatarDisplay = function(id, avatarURL) {
  528. VideoLayout.changeUserAvatar(id, avatarURL);
  529. };
  530. /**
  531. * Notify user that connection failed.
  532. * @param {string} stropheErrorMsg raw Strophe error message
  533. */
  534. UI.notifyConnectionFailed = function(stropheErrorMsg) {
  535. let descriptionKey;
  536. let descriptionArguments;
  537. if (stropheErrorMsg) {
  538. descriptionKey = 'dialog.connectErrorWithMsg';
  539. descriptionArguments = { msg: stropheErrorMsg };
  540. } else {
  541. descriptionKey = 'dialog.connectError';
  542. }
  543. messageHandler.showError({
  544. descriptionArguments,
  545. descriptionKey,
  546. titleKey: 'connection.CONNFAIL'
  547. });
  548. };
  549. /**
  550. * Notify user that maximum users limit has been reached.
  551. */
  552. UI.notifyMaxUsersLimitReached = function() {
  553. messageHandler.showError({
  554. hideErrorSupportLink: true,
  555. descriptionKey: 'dialog.maxUsersLimitReached',
  556. titleKey: 'dialog.maxUsersLimitReachedTitle'
  557. });
  558. };
  559. /**
  560. * Notify user that he was automatically muted when joned the conference.
  561. */
  562. UI.notifyInitiallyMuted = function() {
  563. messageHandler.participantNotification(
  564. null,
  565. 'notify.mutedTitle',
  566. 'connected',
  567. 'notify.muted',
  568. null);
  569. };
  570. UI.handleLastNEndpoints = function(leavingIds, enteringIds) {
  571. VideoLayout.onLastNEndpointsChanged(leavingIds, enteringIds);
  572. };
  573. /**
  574. * Prompt user for nickname.
  575. */
  576. UI.promptDisplayName = () => {
  577. APP.store.dispatch(openDisplayNamePrompt(undefined));
  578. };
  579. /**
  580. * Update audio level visualization for specified user.
  581. * @param {string} id user id
  582. * @param {number} lvl audio level
  583. */
  584. UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
  585. /**
  586. * Hide connection quality statistics from UI.
  587. */
  588. UI.hideStats = function() {
  589. VideoLayout.hideStats();
  590. };
  591. UI.notifyTokenAuthFailed = function() {
  592. messageHandler.showError({
  593. descriptionKey: 'dialog.tokenAuthFailed',
  594. titleKey: 'dialog.tokenAuthFailedTitle'
  595. });
  596. };
  597. UI.notifyInternalError = function(error) {
  598. messageHandler.showError({
  599. descriptionArguments: { error },
  600. descriptionKey: 'dialog.internalError',
  601. titleKey: 'dialog.internalErrorTitle'
  602. });
  603. };
  604. UI.notifyFocusDisconnected = function(focus, retrySec) {
  605. messageHandler.participantNotification(
  606. null, 'notify.focus',
  607. 'disconnected', 'notify.focusFail',
  608. { component: focus,
  609. ms: retrySec }
  610. );
  611. };
  612. /**
  613. * Notifies interested listeners that the raise hand property has changed.
  614. *
  615. * @param {boolean} isRaisedHand indicates the current state of the
  616. * "raised hand"
  617. */
  618. UI.onLocalRaiseHandChanged = function(isRaisedHand) {
  619. eventEmitter.emit(UIEvents.LOCAL_RAISE_HAND_CHANGED, isRaisedHand);
  620. };
  621. /**
  622. * Update list of available physical devices.
  623. */
  624. UI.onAvailableDevicesChanged = function() {
  625. APP.conference.updateAudioIconEnabled();
  626. APP.conference.updateVideoIconEnabled();
  627. };
  628. /**
  629. * Returns the id of the current video shown on large.
  630. * Currently used by tests (torture).
  631. */
  632. UI.getLargeVideoID = function() {
  633. return VideoLayout.getLargeVideoID();
  634. };
  635. /**
  636. * Returns the current video shown on large.
  637. * Currently used by tests (torture).
  638. */
  639. UI.getLargeVideo = function() {
  640. return VideoLayout.getLargeVideo();
  641. };
  642. /**
  643. * Shows "Please go to chrome webstore to install the desktop sharing extension"
  644. * 2 button dialog with buttons - cancel and go to web store.
  645. * @param url {string} the url of the extension.
  646. */
  647. UI.showExtensionExternalInstallationDialog = function(url) {
  648. let openedWindow = null;
  649. const submitFunction = function(e, v) {
  650. if (v) {
  651. e.preventDefault();
  652. if (openedWindow === null || openedWindow.closed) {
  653. openedWindow
  654. = window.open(
  655. url,
  656. 'extension_store_window',
  657. 'resizable,scrollbars=yes,status=1');
  658. } else {
  659. openedWindow.focus();
  660. }
  661. }
  662. };
  663. const closeFunction = function(e, v) {
  664. if (openedWindow) {
  665. // Ideally we would close the popup, but this does not seem to work
  666. // on Chrome. Leaving it uncommented in case it could work
  667. // in some version.
  668. openedWindow.close();
  669. openedWindow = null;
  670. }
  671. if (!v) {
  672. eventEmitter.emit(UIEvents.EXTERNAL_INSTALLATION_CANCELED);
  673. }
  674. };
  675. messageHandler.openTwoButtonDialog({
  676. titleKey: 'dialog.externalInstallationTitle',
  677. msgKey: 'dialog.externalInstallationMsg',
  678. leftButtonKey: 'dialog.goToStore',
  679. submitFunction,
  680. loadedFunction: $.noop,
  681. closeFunction
  682. });
  683. };
  684. /**
  685. * Shows a dialog which asks user to install the extension. This one is
  686. * displayed after installation is triggered from the script, but fails because
  687. * it must be initiated by user gesture.
  688. * @param callback {function} function to be executed after user clicks
  689. * the install button - it should make another attempt to install the extension.
  690. */
  691. UI.showExtensionInlineInstallationDialog = function(callback) {
  692. const submitFunction = function(e, v) {
  693. if (v) {
  694. callback();
  695. }
  696. };
  697. const closeFunction = function(e, v) {
  698. if (!v) {
  699. eventEmitter.emit(UIEvents.EXTERNAL_INSTALLATION_CANCELED);
  700. }
  701. };
  702. messageHandler.openTwoButtonDialog({
  703. titleKey: 'dialog.externalInstallationTitle',
  704. msgKey: 'dialog.inlineInstallationMsg',
  705. leftButtonKey: 'dialog.inlineInstallExtension',
  706. submitFunction,
  707. loadedFunction: $.noop,
  708. closeFunction
  709. });
  710. };
  711. /**
  712. * Shows a notifications about the passed in microphone error.
  713. *
  714. * @param {JitsiTrackError} micError - An error object related to using or
  715. * acquiring an audio stream.
  716. * @returns {void}
  717. */
  718. UI.showMicErrorNotification = function(micError) {
  719. if (!micError) {
  720. return;
  721. }
  722. const { message, name } = micError;
  723. const micJitsiTrackErrorMsg
  724. = JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[name];
  725. const micErrorMsg = micJitsiTrackErrorMsg
  726. || JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
  727. .microphone[JitsiTrackErrors.GENERAL];
  728. const additionalMicErrorMsg = micJitsiTrackErrorMsg ? null : message;
  729. APP.store.dispatch(showWarningNotification({
  730. description: additionalMicErrorMsg,
  731. descriptionKey: micErrorMsg,
  732. titleKey: name === JitsiTrackErrors.PERMISSION_DENIED
  733. ? 'deviceError.microphonePermission'
  734. : 'deviceError.microphoneError'
  735. }));
  736. };
  737. /**
  738. * Shows a notifications about the passed in camera error.
  739. *
  740. * @param {JitsiTrackError} cameraError - An error object related to using or
  741. * acquiring a video stream.
  742. * @returns {void}
  743. */
  744. UI.showCameraErrorNotification = function(cameraError) {
  745. if (!cameraError) {
  746. return;
  747. }
  748. const { message, name } = cameraError;
  749. const cameraJitsiTrackErrorMsg
  750. = JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[name];
  751. const cameraErrorMsg = cameraJitsiTrackErrorMsg
  752. || JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
  753. .camera[JitsiTrackErrors.GENERAL];
  754. const additionalCameraErrorMsg = cameraJitsiTrackErrorMsg ? null : message;
  755. APP.store.dispatch(showWarningNotification({
  756. description: additionalCameraErrorMsg,
  757. descriptionKey: cameraErrorMsg,
  758. titleKey: name === JitsiTrackErrors.PERMISSION_DENIED
  759. ? 'deviceError.cameraPermission' : 'deviceError.cameraError'
  760. }));
  761. };
  762. /**
  763. * Shows error dialog that informs the user that no data is received from the
  764. * device.
  765. *
  766. * @param {boolean} isAudioTrack - Whether or not the dialog is for an audio
  767. * track error.
  768. * @returns {void}
  769. */
  770. UI.showTrackNotWorkingDialog = function(isAudioTrack) {
  771. messageHandler.showError({
  772. descriptionKey: isAudioTrack
  773. ? 'dialog.micNotSendingData' : 'dialog.cameraNotSendingData',
  774. titleKey: isAudioTrack
  775. ? 'dialog.micNotSendingDataTitle'
  776. : 'dialog.cameraNotSendingDataTitle'
  777. });
  778. };
  779. UI.updateDevicesAvailability = function(id, devices) {
  780. VideoLayout.setDeviceAvailabilityIcons(id, devices);
  781. };
  782. /**
  783. * Show shared video.
  784. * @param {string} id the id of the sender of the command
  785. * @param {string} url video url
  786. * @param {string} attributes
  787. */
  788. UI.onSharedVideoStart = function(id, url, attributes) {
  789. if (sharedVideoManager) {
  790. sharedVideoManager.onSharedVideoStart(id, url, attributes);
  791. }
  792. };
  793. /**
  794. * Update shared video.
  795. * @param {string} id the id of the sender of the command
  796. * @param {string} url video url
  797. * @param {string} attributes
  798. */
  799. UI.onSharedVideoUpdate = function(id, url, attributes) {
  800. if (sharedVideoManager) {
  801. sharedVideoManager.onSharedVideoUpdate(id, url, attributes);
  802. }
  803. };
  804. /**
  805. * Stop showing shared video.
  806. * @param {string} id the id of the sender of the command
  807. * @param {string} attributes
  808. */
  809. UI.onSharedVideoStop = function(id, attributes) {
  810. if (sharedVideoManager) {
  811. sharedVideoManager.onSharedVideoStop(id, attributes);
  812. }
  813. };
  814. /**
  815. * Handles user's features changes.
  816. */
  817. UI.onUserFeaturesChanged = user => VideoLayout.onUserFeaturesChanged(user);
  818. /**
  819. * Returns the number of known remote videos.
  820. *
  821. * @returns {number} The number of remote videos.
  822. */
  823. UI.getRemoteVideosCount = () => VideoLayout.getRemoteVideosCount();
  824. /**
  825. * Sets the remote control active status for a remote participant.
  826. *
  827. * @param {string} participantID - The id of the remote participant.
  828. * @param {boolean} isActive - The new remote control active status.
  829. * @returns {void}
  830. */
  831. UI.setRemoteControlActiveStatus = function(participantID, isActive) {
  832. VideoLayout.setRemoteControlActiveStatus(participantID, isActive);
  833. };
  834. /**
  835. * Sets the remote control active status for the local participant.
  836. *
  837. * @returns {void}
  838. */
  839. UI.setLocalRemoteControlActiveChanged = function() {
  840. VideoLayout.setLocalRemoteControlActiveChanged();
  841. };
  842. // TODO: Export every function separately. For now there is no point of doing
  843. // this because we are importing everything.
  844. export default UI;