Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

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