Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

UI.js 36KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296
  1. /* global APP, $, config, interfaceConfig */
  2. const logger = require('jitsi-meet-logger').getLogger(__filename);
  3. const UI = {};
  4. import Chat from './side_pannels/chat/Chat';
  5. import SidePanels from './side_pannels/SidePanels';
  6. import SideContainerToggler from './side_pannels/SideContainerToggler';
  7. import messageHandler from './util/MessageHandler';
  8. import UIUtil from './util/UIUtil';
  9. import UIEvents from '../../service/UI/UIEvents';
  10. import EtherpadManager from './etherpad/Etherpad';
  11. import SharedVideoManager from './shared_video/SharedVideo';
  12. import Recording from './recording/Recording';
  13. import VideoLayout from './videolayout/VideoLayout';
  14. import Filmstrip from './videolayout/Filmstrip';
  15. import Profile from './side_pannels/profile/Profile';
  16. import {
  17. openDeviceSelectionDialog
  18. } from '../../react/features/device-selection';
  19. import { updateDeviceList } from '../../react/features/base/devices';
  20. import { JitsiTrackErrors } from '../../react/features/base/lib-jitsi-meet';
  21. import {
  22. getLocalParticipant,
  23. participantPresenceChanged,
  24. showParticipantJoinedNotification
  25. } from '../../react/features/base/participants';
  26. import { openDisplayNamePrompt } from '../../react/features/display-name';
  27. import {
  28. setNotificationsEnabled,
  29. showWarningNotification
  30. } from '../../react/features/notifications';
  31. import {
  32. checkAutoEnableDesktopSharing,
  33. clearButtonPopup,
  34. dockToolbox,
  35. setButtonPopupTimeout,
  36. setToolbarButton,
  37. showDialPadButton,
  38. showEtherpadButton,
  39. showSharedVideoButton,
  40. showToolbox
  41. } from '../../react/features/toolbox';
  42. const EventEmitter = require('events');
  43. UI.messageHandler = messageHandler;
  44. import FollowMe from '../FollowMe';
  45. const eventEmitter = new EventEmitter();
  46. UI.eventEmitter = eventEmitter;
  47. let etherpadManager;
  48. let sharedVideoManager;
  49. let followMeHandler;
  50. const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
  51. microphone: {},
  52. camera: {}
  53. };
  54. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
  55. .camera[JitsiTrackErrors.UNSUPPORTED_RESOLUTION]
  56. = 'dialog.cameraUnsupportedResolutionError';
  57. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.GENERAL]
  58. = 'dialog.cameraUnknownError';
  59. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.PERMISSION_DENIED]
  60. = 'dialog.cameraPermissionDeniedError';
  61. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.NOT_FOUND]
  62. = 'dialog.cameraNotFoundError';
  63. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.CONSTRAINT_FAILED]
  64. = 'dialog.cameraConstraintFailedError';
  65. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
  66. .camera[JitsiTrackErrors.NO_DATA_FROM_SOURCE]
  67. = 'dialog.cameraNotSendingData';
  68. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[JitsiTrackErrors.GENERAL]
  69. = 'dialog.micUnknownError';
  70. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
  71. .microphone[JitsiTrackErrors.PERMISSION_DENIED]
  72. = 'dialog.micPermissionDeniedError';
  73. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[JitsiTrackErrors.NOT_FOUND]
  74. = 'dialog.micNotFoundError';
  75. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
  76. .microphone[JitsiTrackErrors.CONSTRAINT_FAILED]
  77. = 'dialog.micConstraintFailedError';
  78. JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
  79. .microphone[JitsiTrackErrors.NO_DATA_FROM_SOURCE]
  80. = 'dialog.micNotSendingData';
  81. const UIListeners = new Map([
  82. [
  83. UIEvents.ETHERPAD_CLICKED,
  84. () => etherpadManager && etherpadManager.toggleEtherpad()
  85. ], [
  86. UIEvents.SHARED_VIDEO_CLICKED,
  87. () => sharedVideoManager && sharedVideoManager.toggleSharedVideo()
  88. ], [
  89. UIEvents.TOGGLE_FULLSCREEN,
  90. () => UI.toggleFullScreen()
  91. ], [
  92. UIEvents.TOGGLE_CHAT,
  93. () => UI.toggleChat()
  94. ], [
  95. UIEvents.TOGGLE_SETTINGS,
  96. () => {
  97. // Opening of device selection is special-cased as it is a dialog
  98. // opened through a button in settings and not directly displayed in
  99. // settings itself. As it is not useful to only have a settings menu
  100. // with a button to open a dialog, open the dialog directly instead.
  101. if (interfaceConfig.SETTINGS_SECTIONS.length === 1
  102. && UIUtil.isSettingEnabled('devices')) {
  103. APP.store.dispatch(openDeviceSelectionDialog());
  104. } else {
  105. UI.toggleSidePanel('settings_container');
  106. }
  107. }
  108. ], [
  109. UIEvents.TOGGLE_CONTACT_LIST,
  110. () => UI.toggleContactList()
  111. ], [
  112. UIEvents.TOGGLE_PROFILE,
  113. () => UI.toggleSidePanel('profile_container')
  114. ], [
  115. UIEvents.TOGGLE_FILMSTRIP,
  116. () => UI.handleToggleFilmstrip()
  117. ], [
  118. UIEvents.FOLLOW_ME_ENABLED,
  119. enabled => followMeHandler && followMeHandler.enableFollowMe(enabled)
  120. ]
  121. ]);
  122. /**
  123. * Toggles the application in and out of full screen mode
  124. * (a.k.a. presentation mode in Chrome).
  125. */
  126. UI.toggleFullScreen = function() {
  127. UIUtil.isFullScreen() ? UIUtil.exitFullScreen() : UIUtil.enterFullScreen();
  128. };
  129. /**
  130. * Indicates if we're currently in full screen mode.
  131. *
  132. * @return {boolean} {true} to indicate that we're currently in full screen
  133. * mode, {false} otherwise
  134. */
  135. UI.isFullScreen = function() {
  136. return UIUtil.isFullScreen();
  137. };
  138. /**
  139. * Returns true if the etherpad window is currently visible.
  140. * @returns {Boolean} - true if the etherpad window is currently visible.
  141. */
  142. UI.isEtherpadVisible = function() {
  143. return Boolean(etherpadManager && etherpadManager.isVisible());
  144. };
  145. /**
  146. * Returns true if there is a shared video which is being shown (?).
  147. * @returns {boolean} - true if there is a shared video which is being shown.
  148. */
  149. UI.isSharedVideoShown = function() {
  150. return Boolean(sharedVideoManager && sharedVideoManager.isSharedVideoShown);
  151. };
  152. /**
  153. * Notify user that server has shut down.
  154. */
  155. UI.notifyGracefulShutdown = function() {
  156. messageHandler.showError({
  157. descriptionKey: 'dialog.gracefulShutdown',
  158. titleKey: 'dialog.serviceUnavailable'
  159. });
  160. };
  161. /**
  162. * Notify user that reservation error happened.
  163. */
  164. UI.notifyReservationError = function(code, msg) {
  165. messageHandler.showError({
  166. descriptionArguments: {
  167. code,
  168. msg
  169. },
  170. descriptionKey: 'dialog.reservationErrorMsg',
  171. titleKey: 'dialog.reservationError'
  172. });
  173. };
  174. /**
  175. * Notify user that he has been kicked from the server.
  176. */
  177. UI.notifyKicked = function() {
  178. messageHandler.showError({
  179. hideErrorSupportLink: true,
  180. descriptionKey: 'dialog.kickMessage',
  181. titleKey: 'dialog.sessTerminated'
  182. });
  183. };
  184. /**
  185. * Notify user that conference was destroyed.
  186. * @param reason {string} the reason text
  187. */
  188. UI.notifyConferenceDestroyed = function(reason) {
  189. // FIXME: use Session Terminated from translation, but
  190. // 'reason' text comes from XMPP packet and is not translated
  191. messageHandler.showError({
  192. description: reason,
  193. titleKey: 'dialog.sessTerminated'
  194. });
  195. };
  196. /**
  197. * Show chat error.
  198. * @param err the Error
  199. * @param msg
  200. */
  201. UI.showChatError = function(err, msg) {
  202. if (!interfaceConfig.filmStripOnly) {
  203. Chat.chatAddError(err, msg);
  204. }
  205. };
  206. /**
  207. * Change nickname for the user.
  208. * @param {string} id user id
  209. * @param {string} displayName new nickname
  210. */
  211. UI.changeDisplayName = function(id, displayName) {
  212. VideoLayout.onDisplayNameChanged(id, displayName);
  213. if (APP.conference.isLocalId(id) || id === 'localVideoContainer') {
  214. Profile.changeDisplayName(displayName);
  215. Chat.setChatConversationMode(Boolean(displayName));
  216. }
  217. };
  218. /**
  219. * Shows/hides the indication about local connection being interrupted.
  220. *
  221. * @param {boolean} isInterrupted <tt>true</tt> if local connection is
  222. * currently in the interrupted state or <tt>false</tt> if the connection
  223. * is fine.
  224. */
  225. UI.showLocalConnectionInterrupted = function(isInterrupted) {
  226. VideoLayout.showLocalConnectionInterrupted(isInterrupted);
  227. };
  228. /**
  229. * Sets the "raised hand" status for a participant.
  230. */
  231. UI.setRaisedHandStatus = (participant, raisedHandStatus) => {
  232. VideoLayout.setRaisedHandStatus(participant.getId(), raisedHandStatus);
  233. if (raisedHandStatus) {
  234. messageHandler.participantNotification(
  235. participant.getDisplayName(),
  236. 'notify.somebody',
  237. 'connected',
  238. 'notify.raisedHand');
  239. }
  240. };
  241. /**
  242. * Sets the local "raised hand" status.
  243. */
  244. UI.setLocalRaisedHandStatus
  245. = raisedHandStatus =>
  246. VideoLayout.setRaisedHandStatus(
  247. APP.conference.getMyUserId(),
  248. raisedHandStatus);
  249. /**
  250. * Initialize conference UI.
  251. */
  252. UI.initConference = function() {
  253. const { dispatch, getState } = APP.store;
  254. const { email, id, name } = getLocalParticipant(getState);
  255. // Update default button states before showing the toolbar
  256. // if local role changes buttons state will be again updated.
  257. UI.updateLocalRole(APP.conference.isModerator);
  258. UI.showToolbar();
  259. const displayName = config.displayJids ? id : name;
  260. if (displayName) {
  261. UI.changeDisplayName('localVideoContainer', displayName);
  262. }
  263. // Make sure we configure our avatar id, before creating avatar for us
  264. if (email) {
  265. UI.setUserEmail(id, email);
  266. }
  267. dispatch(checkAutoEnableDesktopSharing());
  268. // FollowMe attempts to copy certain aspects of the moderator's UI into the
  269. // other participants' UI. Consequently, it needs (1) read and write access
  270. // to the UI (depending on the moderator role of the local participant) and
  271. // (2) APP.conference as means of communication between the participants.
  272. followMeHandler = new FollowMe(APP.conference, UI);
  273. };
  274. UI.mucJoined = function() {
  275. VideoLayout.mucJoined();
  276. // Update local video now that a conference is joined a user ID should be
  277. // set.
  278. UI.changeDisplayName(
  279. 'localVideoContainer',
  280. APP.conference.getLocalDisplayName());
  281. };
  282. /** *
  283. * Handler for toggling filmstrip
  284. */
  285. UI.handleToggleFilmstrip = () => UI.toggleFilmstrip();
  286. /**
  287. * Returns the shared document manager object.
  288. * @return {EtherpadManager} the shared document manager object
  289. */
  290. UI.getSharedVideoManager = function() {
  291. return sharedVideoManager;
  292. };
  293. /**
  294. * Starts the UI module and initializes all related components.
  295. *
  296. * @returns {boolean} true if the UI is ready and the conference should be
  297. * established, false - otherwise (for example in the case of welcome page)
  298. */
  299. UI.start = function() {
  300. document.title = interfaceConfig.APP_NAME;
  301. // Set the defaults for prompt dialogs.
  302. $.prompt.setDefaults({ persistent: false });
  303. SideContainerToggler.init(eventEmitter);
  304. Filmstrip.init(eventEmitter);
  305. VideoLayout.init(eventEmitter);
  306. if (!interfaceConfig.filmStripOnly) {
  307. VideoLayout.initLargeVideo();
  308. }
  309. VideoLayout.resizeVideoArea(true, true);
  310. sharedVideoManager = new SharedVideoManager(eventEmitter);
  311. // eslint-disable-next-line no-negated-condition
  312. if (!interfaceConfig.filmStripOnly) {
  313. // Initialise the recording module.
  314. if (config.enableRecording) {
  315. Recording.init(eventEmitter, config.recordingType);
  316. }
  317. // Initialize side panels
  318. SidePanels.init(eventEmitter);
  319. } else {
  320. $('body').addClass('filmstrip-only');
  321. UI.showToolbar();
  322. Filmstrip.setFilmstripOnly();
  323. APP.store.dispatch(setNotificationsEnabled(false));
  324. }
  325. if (interfaceConfig.VERTICAL_FILMSTRIP) {
  326. $('body').addClass('vertical-filmstrip');
  327. }
  328. document.title = interfaceConfig.APP_NAME;
  329. };
  330. /**
  331. * Invokes cleanup of any deferred execution within relevant UI modules.
  332. *
  333. * @returns {void}
  334. */
  335. UI.stopDaemons = () => {
  336. VideoLayout.resetLargeVideo();
  337. };
  338. /**
  339. * Setup some UI event listeners.
  340. */
  341. UI.registerListeners
  342. = () => UIListeners.forEach((value, key) => UI.addListener(key, value));
  343. /**
  344. * Unregister some UI event listeners.
  345. */
  346. UI.unregisterListeners
  347. = () => UIListeners.forEach((value, key) => UI.removeListener(key, value));
  348. /**
  349. * Setup some DOM event listeners.
  350. */
  351. UI.bindEvents = () => {
  352. /**
  353. *
  354. */
  355. function onResize() {
  356. SideContainerToggler.resize();
  357. VideoLayout.resizeVideoArea();
  358. }
  359. // Resize and reposition videos in full screen mode.
  360. $(document).on(
  361. 'webkitfullscreenchange mozfullscreenchange fullscreenchange',
  362. () => {
  363. eventEmitter.emit(
  364. UIEvents.FULLSCREEN_TOGGLED,
  365. UIUtil.isFullScreen());
  366. onResize();
  367. });
  368. $(window).resize(onResize);
  369. };
  370. /**
  371. * Unbind some DOM event listeners.
  372. */
  373. UI.unbindEvents = () => {
  374. $(document).off(
  375. 'webkitfullscreenchange mozfullscreenchange fullscreenchange');
  376. $(window).off('resize');
  377. };
  378. /**
  379. * Show local stream on UI.
  380. * @param {JitsiTrack} track stream to show
  381. */
  382. UI.addLocalStream = track => {
  383. switch (track.getType()) {
  384. case 'audio':
  385. // Local audio is not rendered so no further action is needed at this
  386. // point.
  387. break;
  388. case 'video':
  389. VideoLayout.changeLocalVideo(track);
  390. break;
  391. default:
  392. logger.error(`Unknown stream type: ${track.getType()}`);
  393. break;
  394. }
  395. };
  396. /**
  397. * Show remote stream on UI.
  398. * @param {JitsiTrack} track stream to show
  399. */
  400. UI.addRemoteStream = track => VideoLayout.onRemoteStreamAdded(track);
  401. /**
  402. * Removed remote stream from UI.
  403. * @param {JitsiTrack} track stream to remove
  404. */
  405. UI.removeRemoteStream = track => VideoLayout.onRemoteStreamRemoved(track);
  406. /**
  407. * Update chat subject.
  408. * @param {string} subject new chat subject
  409. */
  410. UI.setSubject = subject => Chat.setSubject(subject);
  411. /**
  412. * Setup and show Etherpad.
  413. * @param {string} name etherpad id
  414. */
  415. UI.initEtherpad = name => {
  416. if (etherpadManager || !config.etherpad_base || !name) {
  417. return;
  418. }
  419. logger.log('Etherpad is enabled');
  420. etherpadManager
  421. = new EtherpadManager(config.etherpad_base, name, eventEmitter);
  422. APP.store.dispatch(showEtherpadButton());
  423. };
  424. /**
  425. * Returns the shared document manager object.
  426. * @return {EtherpadManager} the shared document manager object
  427. */
  428. UI.getSharedDocumentManager = () => etherpadManager;
  429. /**
  430. * Show user on UI.
  431. * @param {JitsiParticipant} user
  432. */
  433. UI.addUser = function(user) {
  434. const id = user.getId();
  435. const displayName = user.getDisplayName();
  436. const status = user.getStatus();
  437. if (status) {
  438. // if user has initial status dispatch it
  439. // and skip 'connected' notifications
  440. APP.store.dispatch(participantPresenceChanged(id, status));
  441. // FIXME: move updateUserStatus in participantPresenceChanged action
  442. UI.updateUserStatus(user, status);
  443. } else {
  444. APP.store.dispatch(showParticipantJoinedNotification(displayName));
  445. }
  446. if (!config.startAudioMuted
  447. || config.startAudioMuted > APP.conference.membersCount) {
  448. UIUtil.playSoundNotification('userJoined');
  449. }
  450. // Add Peer's container
  451. VideoLayout.addParticipantContainer(user);
  452. // Configure avatar
  453. UI.setUserEmail(id);
  454. // set initial display name
  455. if (displayName) {
  456. UI.changeDisplayName(id, displayName);
  457. }
  458. };
  459. /**
  460. * Remove user from UI.
  461. * @param {string} id user id
  462. * @param {string} displayName user nickname
  463. */
  464. UI.removeUser = function(id, displayName) {
  465. messageHandler.participantNotification(
  466. displayName, 'notify.somebody', 'disconnected', 'notify.disconnected');
  467. if (!config.startAudioMuted
  468. || config.startAudioMuted > APP.conference.membersCount) {
  469. UIUtil.playSoundNotification('userLeft');
  470. }
  471. VideoLayout.removeParticipantContainer(id);
  472. };
  473. /**
  474. * Update videotype for specified user.
  475. * @param {string} id user id
  476. * @param {string} newVideoType new videotype
  477. */
  478. UI.onPeerVideoTypeChanged
  479. = (id, newVideoType) => VideoLayout.onVideoTypeChanged(id, newVideoType);
  480. /**
  481. * Update local user role and show notification if user is moderator.
  482. * @param {boolean} isModerator if local user is moderator or not
  483. */
  484. UI.updateLocalRole = isModerator => {
  485. VideoLayout.showModeratorIndicator();
  486. APP.store.dispatch(showSharedVideoButton());
  487. Recording.showRecordingButton(isModerator);
  488. if (isModerator) {
  489. if (!interfaceConfig.DISABLE_FOCUS_INDICATOR) {
  490. messageHandler.participantNotification(
  491. null, 'notify.me', 'connected', 'notify.moderator');
  492. }
  493. Recording.checkAutoRecord();
  494. }
  495. };
  496. /**
  497. * Check the role for the user and reflect it in the UI, moderator ui indication
  498. * and notifies user who is the moderator
  499. * @param user to check for moderator
  500. */
  501. UI.updateUserRole = user => {
  502. VideoLayout.showModeratorIndicator();
  503. // We don't need to show moderator notifications when the focus (moderator)
  504. // indicator is disabled.
  505. if (!user.isModerator() || interfaceConfig.DISABLE_FOCUS_INDICATOR) {
  506. return;
  507. }
  508. const displayName = user.getDisplayName();
  509. if (displayName) {
  510. messageHandler.participantNotification(
  511. displayName,
  512. 'notify.somebody',
  513. 'connected',
  514. 'notify.grantedTo',
  515. { to: UIUtil.escapeHtml(displayName) });
  516. } else {
  517. messageHandler.participantNotification(
  518. '',
  519. 'notify.somebody',
  520. 'connected',
  521. 'notify.grantedToUnknown');
  522. }
  523. };
  524. /**
  525. * Updates the user status.
  526. *
  527. * @param {JitsiParticipant} user - The user which status we need to update.
  528. * @param {string} status - The new status.
  529. */
  530. UI.updateUserStatus = (user, status) => {
  531. if (!status) {
  532. return;
  533. }
  534. const displayName = user.getDisplayName();
  535. messageHandler.participantNotification(
  536. displayName,
  537. '',
  538. 'connected',
  539. 'dialOut.statusMessage',
  540. { status: UIUtil.escapeHtml(status) });
  541. };
  542. /**
  543. * Toggles smileys in the chat.
  544. */
  545. UI.toggleSmileys = () => Chat.toggleSmileys();
  546. /**
  547. * Toggles filmstrip.
  548. */
  549. UI.toggleFilmstrip = function() {
  550. // eslint-disable-next-line prefer-rest-params
  551. Filmstrip.toggleFilmstrip(...arguments);
  552. VideoLayout.resizeVideoArea(true, false);
  553. };
  554. /**
  555. * Checks if the filmstrip is currently visible or not.
  556. * @returns {true} if the filmstrip is currently visible, and false otherwise.
  557. */
  558. UI.isFilmstripVisible = () => Filmstrip.isFilmstripVisible();
  559. /**
  560. * @returns {true} if the chat panel is currently visible, and false otherwise.
  561. */
  562. UI.isChatVisible = () => Chat.isVisible();
  563. /**
  564. * Toggles chat panel.
  565. */
  566. UI.toggleChat = () => UI.toggleSidePanel('chat_container');
  567. /**
  568. * Toggles contact list panel.
  569. */
  570. UI.toggleContactList = () => UI.toggleSidePanel('contacts_container');
  571. /**
  572. * Toggles the given side panel.
  573. *
  574. * @param {String} sidePanelId the identifier of the side panel to toggle
  575. */
  576. UI.toggleSidePanel = sidePanelId => SideContainerToggler.toggle(sidePanelId);
  577. /**
  578. * Handle new user display name.
  579. */
  580. UI.inputDisplayNameHandler = function(newDisplayName) {
  581. eventEmitter.emit(UIEvents.NICKNAME_CHANGED, newDisplayName);
  582. };
  583. /**
  584. * Show custom popup/tooltip for a specified button.
  585. *
  586. * @param {string} buttonName - The name of the button as specified in the
  587. * button configurations for the toolbar.
  588. * @param {string} popupSelectorID - The id of the popup to show as specified in
  589. * the button configurations for the toolbar.
  590. * @param {boolean} show - True or false/show or hide the popup
  591. * @param {number} timeout - The time to show the popup
  592. * @returns {void}
  593. */
  594. // eslint-disable-next-line max-params
  595. UI.showCustomToolbarPopup = function(buttonName, popupID, show, timeout) {
  596. const action
  597. = show
  598. ? setButtonPopupTimeout(buttonName, popupID, timeout)
  599. : clearButtonPopup(buttonName);
  600. APP.store.dispatch(action);
  601. };
  602. /**
  603. * Return the type of the remote video.
  604. * @param jid the jid for the remote video
  605. * @returns the video type video or screen.
  606. */
  607. UI.getRemoteVideoType = function(jid) {
  608. return VideoLayout.getRemoteVideoType(jid);
  609. };
  610. // FIXME check if someone user this
  611. UI.showLoginPopup = function(callback) {
  612. logger.log('password is required');
  613. const message
  614. = `<input name="username" type="text"
  615. placeholder="user@domain.net"
  616. class="input-control" autofocus>
  617. <input name="password" type="password"
  618. data-i18n="[placeholder]dialog.userPassword"
  619. class="input-control"
  620. placeholder="user password">`
  621. ;
  622. // eslint-disable-next-line max-params
  623. const submitFunction = (e, v, m, f) => {
  624. if (v && f.username && f.password) {
  625. callback(f.username, f.password);
  626. }
  627. };
  628. messageHandler.openTwoButtonDialog({
  629. titleKey: 'dialog.passwordRequired',
  630. msgString: message,
  631. leftButtonKey: 'dialog.Ok',
  632. submitFunction,
  633. focus: ':input:first'
  634. });
  635. };
  636. UI.askForNickname = function() {
  637. // eslint-disable-next-line no-alert
  638. return window.prompt('Your nickname (optional)');
  639. };
  640. /**
  641. * Sets muted audio state for participant
  642. */
  643. UI.setAudioMuted = function(id, muted) {
  644. VideoLayout.onAudioMute(id, muted);
  645. if (APP.conference.isLocalId(id)) {
  646. APP.conference.updateAudioIconEnabled();
  647. }
  648. };
  649. /**
  650. * Sets muted video state for participant
  651. */
  652. UI.setVideoMuted = function(id, muted) {
  653. VideoLayout.onVideoMute(id, muted);
  654. if (APP.conference.isLocalId(id)) {
  655. APP.conference.updateVideoIconEnabled();
  656. }
  657. };
  658. /**
  659. * Triggers an update of remote video and large video displays so they may pick
  660. * up any state changes that have occurred elsewhere.
  661. *
  662. * @returns {void}
  663. */
  664. UI.updateAllVideos = () => VideoLayout.updateAllVideos();
  665. /**
  666. * Adds a listener that would be notified on the given type of event.
  667. *
  668. * @param type the type of the event we're listening for
  669. * @param listener a function that would be called when notified
  670. */
  671. UI.addListener = function(type, listener) {
  672. eventEmitter.on(type, listener);
  673. };
  674. /**
  675. * Removes the given listener for the given type of event.
  676. *
  677. * @param type the type of the event we're listening for
  678. * @param listener the listener we want to remove
  679. */
  680. UI.removeListener = function(type, listener) {
  681. eventEmitter.removeListener(type, listener);
  682. };
  683. /**
  684. * Emits the event of given type by specifying the parameters in options.
  685. *
  686. * @param type the type of the event we're emitting
  687. * @param options the parameters for the event
  688. */
  689. UI.emitEvent = (type, ...options) => eventEmitter.emit(type, ...options);
  690. UI.clickOnVideo = function(videoNumber) {
  691. const videos = $('#remoteVideos .videocontainer:not(#mixedstream)');
  692. const videosLength = videos.length;
  693. if (videosLength <= videoNumber) {
  694. return;
  695. }
  696. const videoIndex = videoNumber === 0 ? 0 : videosLength - videoNumber;
  697. videos[videoIndex].click();
  698. };
  699. // Used by torture.
  700. UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
  701. // Used by torture.
  702. UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock));
  703. /**
  704. * Update user email.
  705. * @param {string} id user id
  706. * @param {string} email user email
  707. */
  708. UI.setUserEmail = function(id, email) {
  709. if (APP.conference.isLocalId(id)) {
  710. Profile.changeEmail(email);
  711. }
  712. };
  713. /**
  714. * Updates the displayed avatar for participant.
  715. *
  716. * @param {string} id - User id whose avatar should be updated.
  717. * @param {string} avatarURL - The URL to avatar image to display.
  718. * @returns {void}
  719. */
  720. UI.refreshAvatarDisplay = function(id, avatarURL) {
  721. VideoLayout.changeUserAvatar(id, avatarURL);
  722. };
  723. /**
  724. * Notify user that connection failed.
  725. * @param {string} stropheErrorMsg raw Strophe error message
  726. */
  727. UI.notifyConnectionFailed = function(stropheErrorMsg) {
  728. let descriptionKey;
  729. let descriptionArguments;
  730. if (stropheErrorMsg) {
  731. descriptionKey = 'dialog.connectErrorWithMsg';
  732. descriptionArguments = { msg: stropheErrorMsg };
  733. } else {
  734. descriptionKey = 'dialog.connectError';
  735. }
  736. messageHandler.showError({
  737. descriptionArguments,
  738. descriptionKey,
  739. titleKey: 'connection.CONNFAIL'
  740. });
  741. };
  742. /**
  743. * Notify user that maximum users limit has been reached.
  744. */
  745. UI.notifyMaxUsersLimitReached = function() {
  746. messageHandler.showError({
  747. hideErrorSupportLink: true,
  748. descriptionKey: 'dialog.maxUsersLimitReached',
  749. titleKey: 'dialog.maxUsersLimitReachedTitle'
  750. });
  751. };
  752. /**
  753. * Notify user that he was automatically muted when joned the conference.
  754. */
  755. UI.notifyInitiallyMuted = function() {
  756. messageHandler.participantNotification(
  757. null,
  758. 'notify.mutedTitle',
  759. 'connected',
  760. 'notify.muted',
  761. null);
  762. };
  763. /**
  764. * Mark user as dominant speaker.
  765. * @param {string} id user id
  766. */
  767. UI.markDominantSpeaker = function(id) {
  768. VideoLayout.onDominantSpeakerChanged(id);
  769. };
  770. UI.handleLastNEndpoints = function(leavingIds, enteringIds) {
  771. VideoLayout.onLastNEndpointsChanged(leavingIds, enteringIds);
  772. };
  773. /**
  774. * Will handle notification about participant's connectivity status change.
  775. *
  776. * @param {string} id the id of remote participant(MUC jid)
  777. */
  778. UI.participantConnectionStatusChanged = function(id) {
  779. VideoLayout.onParticipantConnectionStatusChanged(id);
  780. };
  781. /**
  782. * Prompt user for nickname.
  783. */
  784. UI.promptDisplayName = () => {
  785. APP.store.dispatch(openDisplayNamePrompt());
  786. };
  787. /**
  788. * Update audio level visualization for specified user.
  789. * @param {string} id user id
  790. * @param {number} lvl audio level
  791. */
  792. UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
  793. /**
  794. * Update state of desktop sharing buttons.
  795. *
  796. * @returns {void}
  797. */
  798. UI.updateDesktopSharingButtons
  799. = () =>
  800. APP.store.dispatch(setToolbarButton('desktop', {
  801. toggled: APP.conference.isSharingScreen
  802. }));
  803. /**
  804. * Hide connection quality statistics from UI.
  805. */
  806. UI.hideStats = function() {
  807. VideoLayout.hideStats();
  808. };
  809. /**
  810. * Mark video as interrupted or not.
  811. * @param {boolean} interrupted if video is interrupted
  812. */
  813. UI.markVideoInterrupted = function(interrupted) {
  814. if (interrupted) {
  815. VideoLayout.onVideoInterrupted();
  816. } else {
  817. VideoLayout.onVideoRestored();
  818. }
  819. };
  820. /**
  821. * Add chat message.
  822. * @param {string} from user id
  823. * @param {string} displayName user nickname
  824. * @param {string} message message text
  825. * @param {number} stamp timestamp when message was created
  826. */
  827. // eslint-disable-next-line max-params
  828. UI.addMessage = function(from, displayName, message, stamp) {
  829. Chat.updateChatConversation(from, displayName, message, stamp);
  830. };
  831. UI.updateDTMFSupport
  832. = isDTMFSupported => APP.store.dispatch(showDialPadButton(isDTMFSupported));
  833. UI.updateRecordingState = function(state) {
  834. Recording.updateRecordingState(state);
  835. };
  836. UI.notifyTokenAuthFailed = function() {
  837. messageHandler.showError({
  838. descriptionKey: 'dialog.tokenAuthFailed',
  839. titleKey: 'dialog.tokenAuthFailedTitle'
  840. });
  841. };
  842. UI.notifyInternalError = function(error) {
  843. messageHandler.showError({
  844. descriptionArguments: { error },
  845. descriptionKey: 'dialog.internalError',
  846. titleKey: 'dialog.internalErrorTitle'
  847. });
  848. };
  849. UI.notifyFocusDisconnected = function(focus, retrySec) {
  850. messageHandler.participantNotification(
  851. null, 'notify.focus',
  852. 'disconnected', 'notify.focusFail',
  853. { component: focus,
  854. ms: retrySec }
  855. );
  856. };
  857. /**
  858. * Updates auth info on the UI.
  859. * @param {boolean} isAuthEnabled if authentication is enabled
  860. * @param {string} [login] current login
  861. */
  862. UI.updateAuthInfo = function(isAuthEnabled, login) {
  863. const showAuth = isAuthEnabled && UIUtil.isAuthenticationEnabled();
  864. const loggedIn = Boolean(login);
  865. Profile.showAuthenticationButtons(showAuth);
  866. if (showAuth) {
  867. Profile.setAuthenticatedIdentity(login);
  868. Profile.showLoginButton(!loggedIn);
  869. Profile.showLogoutButton(loggedIn);
  870. }
  871. };
  872. /**
  873. * Notifies interested listeners that the raise hand property has changed.
  874. *
  875. * @param {boolean} isRaisedHand indicates the current state of the
  876. * "raised hand"
  877. */
  878. UI.onLocalRaiseHandChanged = function(isRaisedHand) {
  879. eventEmitter.emit(UIEvents.LOCAL_RAISE_HAND_CHANGED, isRaisedHand);
  880. };
  881. /**
  882. * Update list of available physical devices.
  883. * @param {object[]} devices new list of available devices
  884. */
  885. UI.onAvailableDevicesChanged = function(devices) {
  886. APP.store.dispatch(updateDeviceList(devices));
  887. APP.conference.updateAudioIconEnabled();
  888. APP.conference.updateVideoIconEnabled();
  889. };
  890. /**
  891. * Returns the id of the current video shown on large.
  892. * Currently used by tests (torture).
  893. */
  894. UI.getLargeVideoID = function() {
  895. return VideoLayout.getLargeVideoID();
  896. };
  897. /**
  898. * Returns the current video shown on large.
  899. * Currently used by tests (torture).
  900. */
  901. UI.getLargeVideo = function() {
  902. return VideoLayout.getLargeVideo();
  903. };
  904. /**
  905. * Returns whether or not the passed in user id is currently pinned to the large
  906. * video.
  907. *
  908. * @param {string} userId - The id of the user to check is pinned or not.
  909. * @returns {boolean} True if the user is currently pinned to the large video.
  910. */
  911. UI.isPinned = userId => VideoLayout.getPinnedId() === userId;
  912. /**
  913. * Shows dialog with a link to FF extension.
  914. */
  915. UI.showExtensionRequiredDialog = function(url) {
  916. messageHandler.openMessageDialog(
  917. 'dialog.extensionRequired',
  918. '[html]dialog.firefoxExtensionPrompt',
  919. { url });
  920. };
  921. /**
  922. * Shows "Please go to chrome webstore to install the desktop sharing extension"
  923. * 2 button dialog with buttons - cancel and go to web store.
  924. * @param url {string} the url of the extension.
  925. */
  926. UI.showExtensionExternalInstallationDialog = function(url) {
  927. let openedWindow = null;
  928. const submitFunction = function(e, v) {
  929. if (v) {
  930. e.preventDefault();
  931. if (openedWindow === null || openedWindow.closed) {
  932. openedWindow
  933. = window.open(
  934. url,
  935. 'extension_store_window',
  936. 'resizable,scrollbars=yes,status=1');
  937. } else {
  938. openedWindow.focus();
  939. }
  940. }
  941. };
  942. const closeFunction = function(e, v) {
  943. if (openedWindow) {
  944. // Ideally we would close the popup, but this does not seem to work
  945. // on Chrome. Leaving it uncommented in case it could work
  946. // in some version.
  947. openedWindow.close();
  948. openedWindow = null;
  949. }
  950. if (!v) {
  951. eventEmitter.emit(UIEvents.EXTERNAL_INSTALLATION_CANCELED);
  952. }
  953. };
  954. messageHandler.openTwoButtonDialog({
  955. titleKey: 'dialog.externalInstallationTitle',
  956. msgKey: 'dialog.externalInstallationMsg',
  957. leftButtonKey: 'dialog.goToStore',
  958. submitFunction,
  959. loadedFunction: $.noop,
  960. closeFunction
  961. });
  962. };
  963. /**
  964. * Shows a dialog which asks user to install the extension. This one is
  965. * displayed after installation is triggered from the script, but fails because
  966. * it must be initiated by user gesture.
  967. * @param callback {function} function to be executed after user clicks
  968. * the install button - it should make another attempt to install the extension.
  969. */
  970. UI.showExtensionInlineInstallationDialog = function(callback) {
  971. const submitFunction = function(e, v) {
  972. if (v) {
  973. callback();
  974. }
  975. };
  976. const closeFunction = function(e, v) {
  977. if (!v) {
  978. eventEmitter.emit(UIEvents.EXTERNAL_INSTALLATION_CANCELED);
  979. }
  980. };
  981. messageHandler.openTwoButtonDialog({
  982. titleKey: 'dialog.externalInstallationTitle',
  983. msgKey: 'dialog.inlineInstallationMsg',
  984. leftButtonKey: 'dialog.inlineInstallExtension',
  985. submitFunction,
  986. loadedFunction: $.noop,
  987. closeFunction
  988. });
  989. };
  990. /**
  991. * Shows a notifications about the passed in microphone error.
  992. *
  993. * @param {JitsiTrackError} micError - An error object related to using or
  994. * acquiring an audio stream.
  995. * @returns {void}
  996. */
  997. UI.showMicErrorNotification = function(micError) {
  998. if (!micError) {
  999. return;
  1000. }
  1001. const { message, name } = micError;
  1002. const micJitsiTrackErrorMsg
  1003. = JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[name];
  1004. const micErrorMsg = micJitsiTrackErrorMsg
  1005. || JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
  1006. .microphone[JitsiTrackErrors.GENERAL];
  1007. const additionalMicErrorMsg = micJitsiTrackErrorMsg ? null : message;
  1008. APP.store.dispatch(showWarningNotification({
  1009. description: additionalMicErrorMsg,
  1010. descriptionKey: micErrorMsg,
  1011. titleKey: name === JitsiTrackErrors.PERMISSION_DENIED
  1012. ? 'deviceError.microphonePermission'
  1013. : 'deviceError.microphoneError'
  1014. }));
  1015. };
  1016. /**
  1017. * Shows a notifications about the passed in camera error.
  1018. *
  1019. * @param {JitsiTrackError} cameraError - An error object related to using or
  1020. * acquiring a video stream.
  1021. * @returns {void}
  1022. */
  1023. UI.showCameraErrorNotification = function(cameraError) {
  1024. if (!cameraError) {
  1025. return;
  1026. }
  1027. const { message, name } = cameraError;
  1028. const cameraJitsiTrackErrorMsg
  1029. = JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[name];
  1030. const cameraErrorMsg = cameraJitsiTrackErrorMsg
  1031. || JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
  1032. .camera[JitsiTrackErrors.GENERAL];
  1033. const additionalCameraErrorMsg = cameraJitsiTrackErrorMsg ? null : message;
  1034. APP.store.dispatch(showWarningNotification({
  1035. description: additionalCameraErrorMsg,
  1036. descriptionKey: cameraErrorMsg,
  1037. titleKey: name === JitsiTrackErrors.PERMISSION_DENIED
  1038. ? 'deviceError.cameraPermission' : 'deviceError.cameraError'
  1039. }));
  1040. };
  1041. /**
  1042. * Shows error dialog that informs the user that no data is received from the
  1043. * device.
  1044. *
  1045. * @param {boolean} isAudioTrack - Whether or not the dialog is for an audio
  1046. * track error.
  1047. * @returns {void}
  1048. */
  1049. UI.showTrackNotWorkingDialog = function(isAudioTrack) {
  1050. messageHandler.showError({
  1051. descriptionKey: isAudioTrack
  1052. ? 'dialog.micNotSendingData' : 'dialog.cameraNotSendingData',
  1053. titleKey: isAudioTrack
  1054. ? 'dialog.micNotSendingDataTitle'
  1055. : 'dialog.cameraNotSendingDataTitle'
  1056. });
  1057. };
  1058. UI.updateDevicesAvailability = function(id, devices) {
  1059. VideoLayout.setDeviceAvailabilityIcons(id, devices);
  1060. };
  1061. /**
  1062. * Show shared video.
  1063. * @param {string} id the id of the sender of the command
  1064. * @param {string} url video url
  1065. * @param {string} attributes
  1066. */
  1067. UI.onSharedVideoStart = function(id, url, attributes) {
  1068. if (sharedVideoManager) {
  1069. sharedVideoManager.onSharedVideoStart(id, url, attributes);
  1070. }
  1071. };
  1072. /**
  1073. * Update shared video.
  1074. * @param {string} id the id of the sender of the command
  1075. * @param {string} url video url
  1076. * @param {string} attributes
  1077. */
  1078. UI.onSharedVideoUpdate = function(id, url, attributes) {
  1079. if (sharedVideoManager) {
  1080. sharedVideoManager.onSharedVideoUpdate(id, url, attributes);
  1081. }
  1082. };
  1083. /**
  1084. * Stop showing shared video.
  1085. * @param {string} id the id of the sender of the command
  1086. * @param {string} attributes
  1087. */
  1088. UI.onSharedVideoStop = function(id, attributes) {
  1089. if (sharedVideoManager) {
  1090. sharedVideoManager.onSharedVideoStop(id, attributes);
  1091. }
  1092. };
  1093. /**
  1094. * Handles user's features changes.
  1095. */
  1096. UI.onUserFeaturesChanged = user => VideoLayout.onUserFeaturesChanged(user);
  1097. /**
  1098. * Returns the number of known remote videos.
  1099. *
  1100. * @returns {number} The number of remote videos.
  1101. */
  1102. UI.getRemoteVideosCount = () => VideoLayout.getRemoteVideosCount();
  1103. /**
  1104. * Sets the remote control active status for a remote participant.
  1105. *
  1106. * @param {string} participantID - The id of the remote participant.
  1107. * @param {boolean} isActive - The new remote control active status.
  1108. * @returns {void}
  1109. */
  1110. UI.setRemoteControlActiveStatus = function(participantID, isActive) {
  1111. VideoLayout.setRemoteControlActiveStatus(participantID, isActive);
  1112. };
  1113. /**
  1114. * Sets the remote control active status for the local participant.
  1115. *
  1116. * @returns {void}
  1117. */
  1118. UI.setLocalRemoteControlActiveChanged = function() {
  1119. VideoLayout.setLocalRemoteControlActiveChanged();
  1120. };
  1121. // TODO: Export every function separately. For now there is no point of doing
  1122. // this because we are importing everything.
  1123. export default UI;