You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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