Compare commits

...

24 Commits

Author SHA1 Message Date
Horatiu Muresan
a47cb595db fix(localFlipX) Fix localFlipX for large video (#13728)
- fixed case when localFlipX was taken from store on it`s value update, before the new value was set into store - so always taking the previous value instead of updated one
2023-08-18 17:37:07 +03:00
Calin-Teodor
86ccc176e8 feat(authentication): authentication log in/log out through Profile section 2023-08-18 17:36:16 +03:00
Дамян Минков
b31041f0ce fix: Fixes start A/V muted after going to welcome page. Fixes #11393. (#13726) 2023-08-18 06:29:04 -05:00
damencho
e434a78de9 fix: Fixes i18next trying to fetch non-existing files. Fixes #13716.
Thanks @hamletmun for the solution.
2023-08-18 06:27:42 -05:00
Mihaela Dumitru
6ddb77e03c feat(dial-in) add sip audio only (#13714) 2023-08-18 10:22:03 +03:00
damencho
f6665d79c0 fix: Fixes password authentication on prejoin. Fixes #13721. 2023-08-17 11:36:34 -05:00
Saúl Ibarra Corretgé
75a7b99a42 fix(WaitForOwner) tweak copy 2023-08-17 15:56:38 +02:00
Pontus Fagerström
3858a40c1c fix(doc): fix typo in jsdoc comment 2023-08-17 11:21:20 +02:00
Saúl Ibarra Corretgé
3d6aa8f2b5 fix(auth) move common actions to actions.any 2023-08-17 11:03:14 +02:00
Saúl Ibarra Corretgé
54436f97c1 fix(auth) open token auth URL in a new window on Electron 2023-08-17 11:03:14 +02:00
Saúl Ibarra Corretgé
dca40dc6cb feat(rn,auth) add support for toekn URL auth 2023-08-17 11:03:14 +02:00
Horatiu Muresan
c19d91a373 feat(external-api) add command for setting camera facing mode (#13541)
- added command for setting the camera facing mode remotely
- enhanced toggleVideo command to optionally accept the facing mode
- fix(startSilent) do not create audio track when start silent
2023-08-17 09:47:48 +03:00
Werner Fleischer
840cfd8ab0 feat: use i18next-http-backend and fix ts-ignore
i18next-xhr-backend is not maintained any more and [superseded](https://github.com/i18next/i18next-xhr-backend/issues/348#issuecomment-663060275)
by i18next-http-backend.
2023-08-16 22:04:36 -05:00
Hristo Terezov
8cf6ba88e1 fix(jitsi-local-storage):Is empty after reload
When using useHostPageLocalStorage on the iframe api and local storage
is not throwing error we were writting the passed data to the original
local storage and then switching to the dummy local storage from
jitsiLocalStorage.
2023-08-16 21:22:37 -05:00
Saúl Ibarra Corretgé
4ac81b030e Revert "fix(lang) update Albanian translation"
This reverts commit 03daaa4832.
2023-08-16 15:19:22 +02:00
damencho
e5cd1b29fe fix: Shows login warning for leaving only when in conference.
We were showing the dialog and when waiting for host is shown on prejoin screen.
2023-08-15 14:47:16 -05:00
Gabriel Borlea
ebc932572f feat(rn, settings, rn): update view 2023-08-15 09:32:40 +02:00
Jaya Allamsetty
f0a6c6e67e chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1676.0.0+486efad8...v1678.0.0+77e6803f
2023-08-15 02:13:37 -04:00
JPL
e1454b5c4a fix(rnsdk) copying sounds on iOS
Fixes: #13705
2023-08-14 19:26:29 +02:00
Robert Pintilii
f6e2abdf01 fix(chat) Only display private message dialog for active participants (#13708) 2023-08-14 16:35:13 +03:00
Mihaela Dumitru
e2a02f4b21 fix(toolbar) disable the profile button based on the toolbar logic (#13696) 2023-08-14 09:45:07 +03:00
TTG
934d7db24e fix(lang) update zhCN & zhTW translations 2023-08-13 10:06:38 +02:00
damencho
2b520cbc4c fix: Hides email when gravatar is disabled or avatar is provided.
When avatar is externally provided, set by iframe API or via jwt we hide gravatar setting as it does nothing.
2023-08-11 14:36:04 -05:00
damencho
e37dd73b9e feat: Shows dialog when about to navigate away on login. 2023-08-11 14:36:04 -05:00
66 changed files with 3548 additions and 2592 deletions

View File

@@ -441,7 +441,7 @@ export default {
// Always get a handle on the audio input device so that we have statistics (such as "No audio input" or
// "Are you trying to speak?" ) even if the user joins the conference muted.
const initialDevices = config.disableInitialGUM ? [] : [ MEDIA_TYPE.AUDIO ];
const initialDevices = config.startSilent || config.disableInitialGUM ? [] : [ MEDIA_TYPE.AUDIO ];
const requestedAudio = !config.disableInitialGUM;
let requestedVideo = false;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -250,13 +250,13 @@
"dialog": {
"Back": "Back",
"Cancel": "Cancel",
"IamHost": "I am the host",
"IamHost": "Log-in",
"Ok": "OK",
"Remove": "Remove",
"Share": "Share",
"Submit": "Submit",
"WaitForHostMsg": "The conference has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
"WaitingForHostTitle": "Waiting for the host ...",
"WaitForHostMsg": "The conference has not yet started because no moderators have yet arrived. If you'd like to become a moderator please log-in. Otherwise, please wait.",
"WaitingForHostTitle": "Waiting for a moderator...",
"Yes": "Yes",
"accessibilityLabel": {
"Cancel": "Cancel (leave dialog)",
@@ -269,6 +269,8 @@
"addMeetingNote": "Add a note about this meeting",
"addOptionalNote": "Add a note (optional):",
"allow": "Allow",
"allowToggleCameraDialog": "Do you allow {{initiatorName}} to toggle your camera facing mode?",
"allowToggleCameraTitle": "Allow toggle camera?",
"alreadySharedVideoMsg": "Another participant is already sharing a video. This conference allows only one shared video at a time.",
"alreadySharedVideoTitle": "Only one shared video is allowed at a time",
"applicationWindow": "Application window",
@@ -329,6 +331,7 @@
"lockRoom": "Add meeting $t(lockRoomPassword)",
"lockTitle": "Lock failed",
"login": "Login",
"loginQuestion": "Are you sure you want to login and leave the conference?",
"logoutQuestion": "Are you sure you want to logout and leave the conference?",
"logoutTitle": "Logout",
"maxUsersLimitReached": "The limit for maximum number of participants has been reached. The conference is full. Please contact the meeting owner or try again later!",
@@ -547,6 +550,7 @@
"password": "$t(lockRoomPasswordUppercase): ",
"reachedLimit": "You have reached the limit of your plan.",
"sip": "SIP address",
"sipAudioOnly": "SIP audio only address",
"title": "Share",
"tooltip": "Share link and dial-in info for this meeting",
"upgradeOptions": "Please check the upgrade options on"
@@ -1078,6 +1082,7 @@
"alertOk": "OK",
"alertTitle": "Warning",
"alertURLText": "The entered server URL is invalid",
"apply": "Apply",
"buildInfoSection": "Build Information",
"conferenceSection": "Conference",
"disableCallIntegration": "Disable native call integration",
@@ -1088,6 +1093,7 @@
"displayNamePlaceholderText": "Eg: John Doe",
"email": "Email",
"emailPlaceholderText": "email@example.com",
"gavatarMessage": "If your email is associated with a Gravatar account, we will use it to display your profile picture.",
"goTo": "Go to",
"header": "Settings",
"help": "Help",
@@ -1270,8 +1276,8 @@
"linkToSalesforce": "Link to Salesforce",
"lobbyButtonDisable": "Disable lobby mode",
"lobbyButtonEnable": "Enable lobby mode",
"login": "Login",
"logout": "Logout",
"login": "Log-in",
"logout": "Log-out",
"lowerYourHand": "Lower your hand",
"moreActions": "More actions",
"moreOptions": "More options",

View File

@@ -50,8 +50,8 @@ import {
} from '../../react/features/base/participants/functions';
import { updateSettings } from '../../react/features/base/settings/actions';
import { getDisplayName } from '../../react/features/base/settings/functions.web';
import { toggleCamera } from '../../react/features/base/tracks/actions.any';
import { isToggleCameraEnabled } from '../../react/features/base/tracks/functions';
import { setCameraFacingMode } from '../../react/features/base/tracks/actions.web';
import { CAMERA_FACING_MODE_MESSAGE } from '../../react/features/base/tracks/constants';
import {
autoAssignToBreakoutRooms,
closeBreakoutRoom,
@@ -395,12 +395,8 @@ function initCommands() {
sendAnalytics(createApiEvent('film.strip.resize'));
APP.store.dispatch(resizeFilmStrip(options.width));
},
'toggle-camera': () => {
if (!isToggleCameraEnabled(APP.store.getState())) {
return;
}
APP.store.dispatch(toggleCamera());
'toggle-camera': facingMode => {
APP.store.dispatch(setCameraFacingMode(facingMode));
},
'toggle-camera-mirror': () => {
const state = APP.store.getState();
@@ -529,6 +525,18 @@ function initCommands() {
logger.error('Failed sending endpoint text message', err);
}
},
'send-camera-facing-mode-message': (to, facingMode) => {
if (!to) {
logger.warn('Participant id not set');
return;
}
APP.conference.sendEndpointMessage(to, {
name: CAMERA_FACING_MODE_MESSAGE,
facingMode
});
},
'overwrite-names': participantList => {
logger.debug('Overwrite names command received');

View File

@@ -54,6 +54,7 @@ const commands = {
removeBreakoutRoom: 'remove-breakout-room',
resizeFilmStrip: 'resize-film-strip',
resizeLargeVideo: 'resize-large-video',
sendCameraFacingMode: 'send-camera-facing-mode-message',
sendChatMessage: 'send-chat-message',
sendEndpointTextMessage: 'send-endpoint-text-message',
sendParticipantToRoom: 'send-participant-to-room',
@@ -112,6 +113,7 @@ const events = {
'data-channel-opened': 'dataChannelOpened',
'device-list-changed': 'deviceListChanged',
'display-name-change': 'displayNameChange',
'dominant-speaker-changed': 'dominantSpeakerChanged',
'email-change': 'emailChange',
'error-occurred': 'errorOccurred',
'endpoint-text-message-received': 'endpointTextMessageReceived',
@@ -153,7 +155,6 @@ const events = {
'video-mute-status-changed': 'videoMuteStatusChanged',
'video-quality-changed': 'videoQualityChanged',
'screen-sharing-status-changed': 'screenSharingStatusChanged',
'dominant-speaker-changed': 'dominantSpeakerChanged',
'subject-change': 'subjectChange',
'suspend-detected': 'suspendDetected',
'tile-view-changed': 'tileViewChanged',

View File

@@ -23,11 +23,8 @@ const VideoLayout = {
/**
* Handler for local flip X changed event.
*/
onLocalFlipXChanged() {
onLocalFlipXChanged(localFlipX) {
if (largeVideo) {
const { store } = APP;
const { localFlipX } = store.getState()['features/base/settings'];
largeVideo.onLocalFlipXChange(localFlipX);
}
},

61
package-lock.json generated
View File

@@ -52,14 +52,14 @@
"i18n-iso-countries": "6.8.0",
"i18next": "17.0.6",
"i18next-browser-languagedetector": "3.0.1",
"i18next-xhr-backend": "3.0.0",
"i18next-http-backend": "^2.2.1",
"image-capture": "0.4.0",
"jquery": "3.6.1",
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1676.0.0+486efad8/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1678.0.0+77e6803f/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -8342,6 +8342,14 @@
"@emotion/utils": "0.11.3"
}
},
"node_modules/cross-fetch": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz",
"integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==",
"dependencies": {
"node-fetch": "^2.6.11"
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -11197,13 +11205,12 @@
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-3.0.1.tgz",
"integrity": "sha512-WFjPLNPWl62uu07AHY2g+KsC9qz0tyMq+OZEB/H7N58YKL/JLiCz9U709gaR20Mule/Ppn+uyfVx5REJJjn1HA=="
},
"node_modules/i18next-xhr-backend": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/i18next-xhr-backend/-/i18next-xhr-backend-3.0.0.tgz",
"integrity": "sha512-Pi/X91Zk2nEqdEHTV+FG6VeMHRcMcPKRsYW/A0wlaCfKsoJc3TI7A75Tqse/d5LVGN2Ymzx0FT+R+gLag9Eb2g==",
"deprecated": "replaced by i18next-http-backend",
"node_modules/i18next-http-backend": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.2.1.tgz",
"integrity": "sha512-ZXIdn/8NJIBJ0X4hzXfc3STYxKrCKh1fYjji9HPyIpEJfvTvy8/ZlTl8RuTizzCPj2ZcWrfaecyOMKs6bQ7u5A==",
"dependencies": {
"@babel/runtime": "^7.4.5"
"cross-fetch": "3.1.6"
}
},
"node_modules/iconv-lite": {
@@ -12770,8 +12777,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1676.0.0+486efad8/lib-jitsi-meet.tgz",
"integrity": "sha512-HGkW7sg8LGdNl8d0v8hKdET62C0N2BFHF+SXiJUvfzkJSgJrgtTk6JApZ5EgjLjy5nv+u4gAHuMrI5Fyzm1kJw==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1678.0.0+77e6803f/lib-jitsi-meet.tgz",
"integrity": "sha512-qFLvYCN3+zB18ygJnOXRop3q0n70fw/3zANkry9njvfIlMawQpvF8ZJozXMda9QPZPHzoLglK4C52Ayc+yQmAw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -14091,9 +14098,9 @@
}
},
"node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
"integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
@@ -25945,6 +25952,14 @@
"@emotion/utils": "0.11.3"
}
},
"cross-fetch": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz",
"integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==",
"requires": {
"node-fetch": "^2.6.11"
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -28125,12 +28140,12 @@
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-3.0.1.tgz",
"integrity": "sha512-WFjPLNPWl62uu07AHY2g+KsC9qz0tyMq+OZEB/H7N58YKL/JLiCz9U709gaR20Mule/Ppn+uyfVx5REJJjn1HA=="
},
"i18next-xhr-backend": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/i18next-xhr-backend/-/i18next-xhr-backend-3.0.0.tgz",
"integrity": "sha512-Pi/X91Zk2nEqdEHTV+FG6VeMHRcMcPKRsYW/A0wlaCfKsoJc3TI7A75Tqse/d5LVGN2Ymzx0FT+R+gLag9Eb2g==",
"i18next-http-backend": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-2.2.1.tgz",
"integrity": "sha512-ZXIdn/8NJIBJ0X4hzXfc3STYxKrCKh1fYjji9HPyIpEJfvTvy8/ZlTl8RuTizzCPj2ZcWrfaecyOMKs6bQ7u5A==",
"requires": {
"@babel/runtime": "^7.4.5"
"cross-fetch": "3.1.6"
}
},
"iconv-lite": {
@@ -29276,8 +29291,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1676.0.0+486efad8/lib-jitsi-meet.tgz",
"integrity": "sha512-HGkW7sg8LGdNl8d0v8hKdET62C0N2BFHF+SXiJUvfzkJSgJrgtTk6JApZ5EgjLjy5nv+u4gAHuMrI5Fyzm1kJw==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1678.0.0+77e6803f/lib-jitsi-meet.tgz",
"integrity": "sha512-qFLvYCN3+zB18ygJnOXRop3q0n70fw/3zANkry9njvfIlMawQpvF8ZJozXMda9QPZPHzoLglK4C52Ayc+yQmAw==",
"requires": {
"@jitsi/js-utils": "2.1.2",
"@jitsi/logger": "2.0.0",
@@ -30325,9 +30340,9 @@
}
},
"node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
"integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
"requires": {
"whatwg-url": "^5.0.0"
}

View File

@@ -58,14 +58,14 @@
"i18n-iso-countries": "6.8.0",
"i18next": "17.0.6",
"i18next-browser-languagedetector": "3.0.1",
"i18next-xhr-backend": "3.0.0",
"i18next-http-backend": "^2.2.1",
"image-capture": "0.4.0",
"jquery": "3.6.1",
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1676.0.0+486efad8/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1678.0.0+77e6803f/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",

View File

@@ -26,10 +26,10 @@ Pod::Spec.new do |s|
s.script_phase = {
:name => 'Copy Sound Files',
:script => '
SOURCE_PATH="${PODS_TARGET_SRCROOT}/sounds"
TARGET_PATH="$(dirname ${CONFIGURATION_BUILD_DIR})"
PROJECT_NAME=$(basename $(dirname $(dirname ${PROJECT_DIR}))).app
cp -R "${PODS_TARGET_SRCROOT}/sounds/" "${TARGET_PATH}/${PROJECT_NAME}"
SOURCE_PATH="${PODS_TARGET_SRCROOT}/sounds/"
TARGET_PATH=$(dirname "${CONFIGURATION_BUILD_DIR}")
PROJECT_NAME=$(basename $(dirname $(dirname "${PROJECT_DIR}"))).app
cp -R "${SOURCE_PATH}" "${TARGET_PATH}/${PROJECT_NAME}"
',
}
end

View File

@@ -5,6 +5,7 @@ import { getTokenAuthUrl } from '../authentication/functions';
import { IStateful } from '../base/app/types';
import { isRoomValid } from '../base/conference/functions';
import { isSupportedBrowser } from '../base/environment/environment';
import { browser } from '../base/lib-jitsi-meet';
import { toState } from '../base/redux/functions';
import Conference from '../conference/components/web/Conference';
import { getDeepLinkingPage } from '../deep-linking/functions';
@@ -48,9 +49,9 @@ function _getWebConferenceRoute(state: IReduxState) {
// if we have auto redirect enabled, and we have previously logged in successfully
// let's redirect to the auth url to get the token and login again
if (config.tokenAuthUrl && config.tokenAuthUrlAutoRedirect
&& state['features/authentication'].tokenAuthUrlSuccessful
&& !state['features/base/jwt'].jwt && room) {
if (!browser.isElectron() && config.tokenAuthUrl && config.tokenAuthUrlAutoRedirect
&& state['features/authentication'].tokenAuthUrlSuccessful
&& !state['features/base/jwt'].jwt && room) {
route.href = getTokenAuthUrl(config, room);
return Promise.resolve(route);

View File

@@ -4,6 +4,8 @@ import { IJitsiConference } from '../base/conference/reducer';
import { hideDialog, openDialog } from '../base/dialog/actions';
import {
LOGIN,
LOGOUT,
SET_TOKEN_AUTH_URL_SUCCESS,
STOP_WAIT_FOR_OWNER,
UPGRADE_ROLE_FINISHED,
@@ -137,6 +139,32 @@ export function hideLoginDialog() {
return hideDialog(LoginDialog);
}
/**
* Login.
*
* @returns {{
* type: LOGIN
* }}
*/
export function login() {
return {
type: LOGIN
};
}
/**
* Logout.
*
* @returns {{
* type: LOGOUT
* }}
*/
export function logout() {
return {
type: LOGOUT
};
}
/**
* Opens {@link WaitForOnwerDialog}.
*

View File

@@ -1,8 +1,11 @@
import { Linking, Platform } from 'react-native';
import { appNavigate } from '../app/actions.native';
import { IStore } from '../app/types';
import { conferenceLeft } from '../base/conference/actions';
import { connectionFailed } from '../base/connection/actions.native';
import { set } from '../base/redux/functions';
import { appendURLHashParam } from '../base/util/uri';
import { CANCEL_LOGIN } from './actionTypes';
import { stopWaitForOwner } from './actions.any';
@@ -51,7 +54,7 @@ export function cancelWaitForOwner() {
// recoverable by the feature room-lock and, consequently,
// recoverable-aware features such as mobile's external-api did not
// deliver the CONFERENCE_FAILED to the SDK clients/consumers. Since the
// app/user is going to nativate to WelcomePage, the SDK
// app/user is going to navigate to WelcomePage, the SDK
// clients/consumers need an event.
const { authRequired } = getState()['features/base/conference'];
@@ -65,7 +68,7 @@ export function cancelWaitForOwner() {
};
}
/** .
/**
* Redirect to the default location (e.g. Welcome page).
*
* @returns {Function}
@@ -73,3 +76,21 @@ export function cancelWaitForOwner() {
export function redirectToDefaultLocation() {
return (dispatch: IStore['dispatch']) => dispatch(appNavigate(undefined));
}
/**
* Opens token auth URL page.
*
* @param {string} tokenAuthServiceUrl - Authentication service URL.
*
* @returns {Function}
*/
export function openTokenAuthUrl(tokenAuthServiceUrl: string) {
let url = appendURLHashParam(tokenAuthServiceUrl, 'skipPrejoin', 'true');
// Append ios=true or android=true to the token URL.
url = appendURLHashParam(url, Platform.OS, 'true');
return () => {
Linking.openURL(url);
};
}

View File

@@ -1,11 +1,11 @@
import { maybeRedirectToWelcomePage } from '../app/actions.web';
import { IStore } from '../app/types';
import { openDialog } from '../base/dialog/actions';
import { browser } from '../base/lib-jitsi-meet';
import { appendURLHashParam } from '../base/util/uri';
import {
CANCEL_LOGIN,
LOGIN,
LOGOUT
} from './actionTypes';
import { CANCEL_LOGIN } from './actionTypes';
import LoginQuestionDialog from './components/web/LoginQuestionDialog';
export * from './actions.any';
@@ -38,7 +38,7 @@ export function cancelWaitForOwner() {
};
}
/** .
/**
* Redirect to the default location (e.g. Welcome page).
*
* @returns {Function}
@@ -48,27 +48,36 @@ export function redirectToDefaultLocation() {
}
/**
* Login.
* Opens token auth URL page.
*
* @returns {{
* type: LOGIN
* }}
* @param {string} tokenAuthServiceUrl - Authentication service URL.
*
* @returns {Function}
*/
export function login() {
return {
type: LOGIN
};
}
export function openTokenAuthUrl(tokenAuthServiceUrl: string): any {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const redirect = () => {
// We have already shown the prejoin screen, no need to show it again after obtaining the token.
let url = appendURLHashParam(tokenAuthServiceUrl, 'skipPrejoin', 'true');
/**
* Logout.
*
* @returns {{
* type: LOGOUT
* }}
*/
export function logout() {
return {
type: LOGOUT
if (browser.isElectron()) {
url = appendURLHashParam(url, 'electron', 'true');
window.open(url, '_blank');
} else {
window.location.href = url;
}
};
// Show warning for leaving conference only when in a conference.
if (!browser.isElectron() && getState()['features/base/conference'].conference) {
dispatch(openDialog(LoginQuestionDialog, {
handler: () => {
// Give time for the dialog to close.
setTimeout(() => redirect, 500);
}
}));
} else {
redirect();
}
};
}

View File

@@ -4,7 +4,8 @@ import { connect } from 'react-redux';
import { IStore } from '../../../app/types';
import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog';
import { translate } from '../../../base/i18n/functions';
import { cancelWaitForOwner, openLoginDialog } from '../../actions.native';
import { cancelWaitForOwner, login } from '../../actions.native';
/**
* The type of the React {@code Component} props of {@link WaitForOwnerDialog}.
@@ -77,7 +78,9 @@ class WaitForOwnerDialog extends Component<IProps> {
* @returns {void}
*/
_onLogin() {
this.props.dispatch(openLoginDialog());
this.props.dispatch(login());
return true;
}
}

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Dialog from '../../../base/ui/components/web/Dialog';
/**
* The type of {@link LoginQuestionDialog}'s React {@code Component} props.
*/
interface IProps {
/**
* The handler.
*/
handler: () => void;
}
/**
* Implements the dialog that warns the user that the login will leave the conference.
*
* @param {Object} props - The props of the component.
* @returns {React$Element}.
*/
const LoginQuestionDialog = ({ handler }: IProps) => {
const { t } = useTranslation();
return (
<Dialog
ok = {{ translationKey: 'dialog.Yes' }}
onSubmit = { handler }
titleKey = { t('dialog.login') }>
<div>
{ t('dialog.loginQuestion') }
</div>
</Dialog>
);
};
export default LoginQuestionDialog;

View File

@@ -27,7 +27,7 @@ export const isTokenAuthEnabled = (config: IConfig) =>
* <tt>undefined</tt> if the pattern stored in config is not a string and the URL can not be
* constructed.
*/
export const getTokenAuthUrl = (config: IConfig, roomName: string) => {
export const getTokenAuthUrl = (config: IConfig, roomName: string | undefined) => {
const url = config.tokenAuthUrl;

View File

@@ -7,7 +7,6 @@ import {
} from '../base/conference/actionTypes';
import { isRoomValid } from '../base/conference/functions';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../base/connection/actionTypes';
import { hangup } from '../base/connection/actions';
import { hideDialog } from '../base/dialog/actions';
import { isDialogOpen } from '../base/dialog/functions';
import {
@@ -16,8 +15,6 @@ import {
} from '../base/lib-jitsi-meet';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { getBackendSafeRoomName } from '../base/util/uri';
import { showErrorNotification } from '../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import { openLogoutDialog } from '../settings/actions';
import {
@@ -31,6 +28,7 @@ import {
import {
hideLoginDialog,
openLoginDialog,
openTokenAuthUrl,
openWaitForOwnerDialog,
redirectToDefaultLocation,
setTokenAuthUrlSuccess,
@@ -39,6 +37,8 @@ import {
} from './actions';
import { LoginDialog, WaitForOwnerDialog } from './components';
import { getTokenAuthUrl, isTokenAuthEnabled } from './functions';
import logger from './logger';
/**
* Middleware that captures connection or conference failed errors and controls
@@ -163,33 +163,7 @@ MiddlewareRegistry.register(store => next => action => {
}
case LOGOUT: {
const { dispatch, getState } = store;
const state = getState();
const config = state['features/base/config'];
const { conference } = store.getState()['features/base/conference'];
if (!conference) {
break;
}
dispatch(openLogoutDialog(() => {
const logoutUrl = config.tokenLogoutUrl;
if (isTokenAuthEnabled(config)
&& config.tokenAuthUrlAutoRedirect
&& state['features/base/jwt'].jwt) {
// user is logging out remove auto redirect indication
dispatch(setTokenAuthUrlSuccess(false));
}
if (logoutUrl) {
window.location.href = logoutUrl;
return;
}
conference.room.moderator.logout(() => dispatch(hangup(true)));
}));
_handleLogout(store);
break;
}
@@ -282,31 +256,45 @@ function _handleLogin({ dispatch, getState }: IStore) {
const config = state['features/base/config'];
const room = getBackendSafeRoomName(state['features/base/conference'].room);
if (isTokenAuthEnabled(config)) {
if (typeof APP === 'undefined') {
dispatch(showErrorNotification({
descriptionKey: 'dialog.tokenAuthUnsupported',
titleKey: 'dialog.tokenAuthFailedTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
if (!room) {
logger.warn('Cannot handle login, room is undefined!');
dispatch(redirectToDefaultLocation());
return;
}
if (!room) {
return;
}
// FIXME: This method will not preserve the other URL params that were originally passed.
const tokenAuthServiceUrl = getTokenAuthUrl(config, room);
if (tokenAuthServiceUrl) {
// we have already shown the prejoin screen, so no need to show it again after obtaining the token
window.location.href = `${tokenAuthServiceUrl}${
tokenAuthServiceUrl.includes('#') ? '&' : '#'}skipPrejoin=true`;
}
} else {
dispatch(openLoginDialog());
return;
}
if (!isTokenAuthEnabled(config)) {
dispatch(openLoginDialog());
return;
}
// FIXME: This method will not preserve the other URL params that were originally passed.
const tokenAuthServiceUrl = getTokenAuthUrl(config, room);
if (!tokenAuthServiceUrl) {
logger.warn('Cannot handle login, token service URL is not set');
return;
}
dispatch(openTokenAuthUrl(tokenAuthServiceUrl));
}
/**
* Handles logout challenge. Opens logout dialog and hangs up the conference.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @param {string} logoutUrl - The url for logging out.
* @returns {void}
*/
function _handleLogout({ dispatch, getState }: IStore) {
const state = getState();
const { conference } = state['features/base/conference'];
if (!conference) {
return;
}
dispatch(openLogoutDialog());
}

View File

@@ -1,4 +1,4 @@
type ToolbarButtons = 'camera' |
export type ToolbarButton = 'camera' |
'chat' |
'closedcaptions' |
'desktop' |
@@ -15,6 +15,9 @@ type ToolbarButtons = 'camera' |
'linktosalesforce' |
'livestreaming' |
'microphone' |
'mute-everyone' |
'mute-video-everyone' |
'noisesuppression' |
'participants-pane' |
'profile' |
'raisehand' |
@@ -30,6 +33,7 @@ type ToolbarButtons = 'camera' |
'tileview' |
'toggle-camera' |
'videoquality' |
'whiteboard' |
'__end';
type ButtonsWithNotifyClick = 'camera' |
@@ -579,7 +583,7 @@ export interface IConfig {
tokenAuthUrl?: string;
tokenAuthUrlAutoRedirect?: string;
tokenLogoutUrl?: string;
toolbarButtons?: Array<ToolbarButtons>;
toolbarButtons?: Array<ToolbarButton>;
toolbarConfig?: {
alwaysVisible?: boolean;
autoHideWhileChatIsOpen?: boolean;

View File

@@ -1,3 +1,5 @@
import { ToolbarButton } from './configType';
/**
* The prefix of the {@code localStorage} key into which {@link storeConfig}
* stores and from which {@link restoreConfig} restores.
@@ -13,7 +15,7 @@ export const _CONFIG_STORE_PREFIX = 'config.js';
* @protected
* @type Array<string>
*/
export const TOOLBAR_BUTTONS = [
export const TOOLBAR_BUTTONS: ToolbarButton[] = [
'camera',
'chat',
'closedcaptions',

View File

@@ -7,7 +7,8 @@ import {
IDeeplinkingConfig,
IDeeplinkingMobileConfig,
IDeeplinkingPlatformConfig,
NotifyClickButton
NotifyClickButton,
ToolbarButton
} from './configType';
import { TOOLBAR_BUTTONS } from './constants';
@@ -46,7 +47,7 @@ export function getToolbarButtons(state: IReduxState): Array<string> {
const buttons = Array.isArray(toolbarButtons) ? toolbarButtons : TOOLBAR_BUTTONS;
if (customButtons) {
buttons.push(...customButtons);
buttons.push(...customButtons as ToolbarButton[]);
}
return buttons;

View File

@@ -16,8 +16,10 @@ import {
IDeeplinkingConfig,
IDeeplinkingMobileConfig,
IDeeplinkingPlatformConfig,
IMobileDynamicLink
IMobileDynamicLink,
ToolbarButton
} from './configType';
import { TOOLBAR_BUTTONS } from './constants';
import { _cleanupConfig, _setDeeplinkingDefaults } from './functions';
/**
@@ -546,6 +548,11 @@ function _translateLegacyConfig(oldValue: IConfig) {
};
}
if (oldValue.disableProfile) {
newValue.toolbarButtons = (newValue.toolbarButtons || TOOLBAR_BUTTONS)
.filter((button: ToolbarButton) => button !== 'profile');
}
_setDeeplinkingDefaults(newValue.deeplinking as IDeeplinkingConfig);
return newValue;

View File

@@ -1,6 +1,6 @@
import COUNTRIES_RESOURCES from 'i18n-iso-countries/langs/en.json';
import i18next from 'i18next';
import I18nextXHRBackend from 'i18next-xhr-backend';
import I18nextXHRBackend, { HttpBackendOptions } from 'i18next-http-backend';
import _ from 'lodash';
import LANGUAGES_RESOURCES from '../../../../lang/languages.json';
@@ -61,11 +61,13 @@ export const DEFAULT_LANGUAGE = 'en';
/**
* The options to initialize i18next with.
*
* @type {Object}
* @type {i18next.InitOptions}
*/
const options = {
backend: {
loadPath: 'lang/{{ns}}-{{lng}}.json'
const options: i18next.InitOptions = {
backend: <HttpBackendOptions>{
loadPath: (lng: string[], ns: string[]) =>
// eslint-disable-next-line no-extra-parens
(ns[0] === 'main' ? 'lang/{{ns}}-{{lng}}.json' : 'lang/{{ns}}.json')
},
defaultNS: 'main',
fallbackLng: DEFAULT_LANGUAGE,
@@ -76,6 +78,7 @@ const options = {
ns: [ 'main', 'languages', 'countries', 'translation-languages' ],
react: {
// re-render when a new resource bundle is added
// @ts-expect-error. Fixed in i18next 19.6.1.
bindI18nStore: 'added',
useSuspense: false
},
@@ -89,7 +92,7 @@ const options = {
i18next
.use(navigator.product === 'ReactNative' ? {} : I18nextXHRBackend)
.use(languageDetector) // @ts-ignore
.use(languageDetector)
.init(options);
// Add default language which is preloaded from the source code.

View File

@@ -17,6 +17,8 @@ import logger from './logger';
* @returns {void}
*/
function onFakeLocalStorageChanged() {
console.error(jitsiLocalStorage.serialize([ 'jitsiLocalStorage' ]));
APP.API.notifyLocalStorageChanged(jitsiLocalStorage.serialize([ 'jitsiLocalStorage' ]));
}
@@ -63,6 +65,13 @@ function setupJitsiLocalStorage() {
try {
const localStorageContent = safeJsonParse(urlParams['appData.localStorageContent']);
// We need to disable the local storage before setting the data in case the browser local storage doesn't
// throw exception (in some cases when this happens the local storage may be cleared for every session.
// Example: when loading meet from cross-domain with the IFrame API with Brave with the default
// configuration). Otherwise we will set the data in the browser local storage and then switch to the dummy
// local storage from jitsiLocalStorage and we will loose the data.
jitsiLocalStorage.setLocalStorageDisabled(true);
if (typeof localStorageContent === 'object') {
Object.keys(localStorageContent).forEach(key => {
jitsiLocalStorage.setItem(key, localStorageContent[key]);
@@ -72,7 +81,6 @@ function setupJitsiLocalStorage() {
logger.error('Can\'t parse localStorageContent.', error);
}
jitsiLocalStorage.setLocalStorageDisabled(true);
jitsiLocalStorage.on('changed', onFakeLocalStorageChanged);
}
}

View File

@@ -220,21 +220,22 @@ function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAct
const state = getState();
const { room } = action;
const roomIsValid = isRoomValid(room);
const audioMuted = roomIsValid ? getStartWithAudioMuted(state) : _AUDIO_INITIAL_MEDIA_STATE.muted;
const videoMuted = roomIsValid ? getStartWithVideoMuted(state) : _VIDEO_INITIAL_MEDIA_STATE.muted;
sendAnalytics(
createStartMutedConfigurationEvent('local', audioMuted, Boolean(videoMuted)));
logger.log(
`Start muted: ${audioMuted ? 'audio, ' : ''}${
videoMuted ? 'video' : ''}`);
// when going to welcomepage on web(room is not valid) we want to skip resetting the values of startWithA/V
if (roomIsValid || navigator.product === 'ReactNative') {
const audioMuted = roomIsValid ? getStartWithAudioMuted(state) : _AUDIO_INITIAL_MEDIA_STATE.muted;
const videoMuted = roomIsValid ? getStartWithVideoMuted(state) : _VIDEO_INITIAL_MEDIA_STATE.muted;
// Unconditionally express the desires/expectations/intents of the app and
// the user i.e. the state of base/media. Eventually, practice/reality i.e.
// the state of base/tracks will or will not agree with the desires.
dispatch(setAudioMuted(audioMuted));
dispatch(setCameraFacingMode(CAMERA_FACING_MODE.USER));
dispatch(setVideoMuted(videoMuted));
sendAnalytics(createStartMutedConfigurationEvent('local', audioMuted, Boolean(videoMuted)));
logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${videoMuted ? 'video' : ''}`);
// Unconditionally express the desires/expectations/intents of the app and
// the user i.e. the state of base/media. Eventually, practice/reality i.e.
// the state of base/tracks will or will not agree with the desires.
dispatch(setAudioMuted(audioMuted));
dispatch(setCameraFacingMode(CAMERA_FACING_MODE.USER));
dispatch(setVideoMuted(videoMuted));
}
// startAudioOnly
//

View File

@@ -13,18 +13,23 @@ import { toggleScreenshotCaptureSummary } from '../../screenshot-capture/actions
import { isScreenshotCaptureEnabled } from '../../screenshot-capture/functions';
import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect';
import { getCurrentConference } from '../conference/functions';
import { openDialog } from '../dialog/actions';
import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
import { setScreenshareMuted } from '../media/actions';
import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants';
import {
addLocalTrack,
replaceLocalTrack
replaceLocalTrack,
toggleCamera
} from './actions.any';
import AllowToggleCameraDialog from './components/web/AllowToggleCameraDialog';
import {
createLocalTracksF,
getLocalDesktopTrack,
getLocalJitsiAudioTrack
getLocalJitsiAudioTrack,
getLocalVideoTrack,
isToggleCameraEnabled
} from './functions';
import { IShareOptions, IToggleScreenSharingOptions } from './types';
@@ -263,3 +268,52 @@ async function _toggleScreenSharing(
APP.API.notifyScreenSharingStatusChanged(enable, screensharingDetails);
}
}
/**
* Sets the camera facing mode(environment/user). If facing mode not provided, it will do a toggle.
*
* @param {string | undefined} facingMode - The selected facing mode.
* @returns {void}
*/
export function setCameraFacingMode(facingMode: string | undefined) {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
if (!isToggleCameraEnabled(state)) {
return;
}
if (!facingMode) {
dispatch(toggleCamera());
return;
}
const tracks = state['features/base/tracks'];
const localVideoTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
if (!tracks || !localVideoTrack) {
return;
}
const currentFacingMode = localVideoTrack.getCameraFacingMode();
if (currentFacingMode !== facingMode) {
dispatch(toggleCamera());
}
};
}
/**
* Signals to open the permission dialog for toggling camera remotely.
*
* @param {Function} onAllow - Callback to be executed if permission to toggle camera was granted.
* @param {string} initiatorId - The participant id of the requester.
* @returns {Object} - The open dialog action.
*/
export function openAllowToggleCameraDialog(onAllow: Function, initiatorId: string) {
return openDialog(AllowToggleCameraDialog, {
onAllow,
initiatorId
});
}

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { WithTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../../app/types';
import { translate } from '../../../i18n/functions';
import { getParticipantDisplayName } from '../../../participants/functions';
import Dialog from '../../../ui/components/web/Dialog';
interface IProps extends WithTranslation {
/**
* The participant id of the toggle camera requester.
*/
initiatorId: string;
/**
* Function to be invoked after permission to toggle camera granted.
*/
onAllow: () => void;
}
/**
* Dialog to allow toggling camera remotely.
*
* @returns {JSX.Element} - The allow toggle camera dialog.
*/
const AllowToggleCameraDialog = ({ onAllow, t, initiatorId }: IProps): JSX.Element => {
const initiatorName = useSelector((state: IReduxState) => getParticipantDisplayName(state, initiatorId));
return (
<Dialog
ok = {{ translationKey: 'dialog.allow' }}
onSubmit = { onAllow }
titleKey = 'dialog.allowToggleCameraTitle'>
<div>
{ t('dialog.allowToggleCameraDialog', { initiatorName }) }
</div>
</Dialog>
);
};
export default translate(AllowToggleCameraDialog);

View File

@@ -0,0 +1,4 @@
/**
* The payload name for remotely setting the camera facing mode message.
*/
export const CAMERA_FACING_MODE_MESSAGE = 'camera-facing-mode-message';

View File

@@ -2,7 +2,7 @@
* Loads the enabled stream effects.
*
* @param {Object} _store - The Redux store.
* @returns {Promsie} - A Promise which resolves with an array of the loaded effects.
* @returns {Promise} - A Promise which resolves with an array of the loaded effects.
*/
export default function loadEffects(_store: Object): Promise<Array<any>> {
return Promise.resolve([]);

View File

@@ -8,7 +8,7 @@ import logger from './logger';
* Loads the enabled stream effects.
*
* @param {Object} store - The Redux store.
* @returns {Promsie} - A Promise which resolves when all effects are created.
* @returns {Promise} - A Promise which resolves when all effects are created.
*/
export default function loadEffects(store: IStore): Promise<any> {
const state = store.getState();

View File

@@ -631,3 +631,29 @@ export function appendURLParam(url: string, name: string, value: string) {
return newUrl.toString();
}
/**
* Adds new hash param to a url string.
* Checks whether to use '?' or '&' as a separator (checks for already existing params).
*
* @param {string} url - The url to modify.
* @param {string} name - The param name to add.
* @param {string} value - The value for the param.
*
* @returns {string} - The modified url.
*/
export function appendURLHashParam(url: string, name: string, value: string) {
const newUrl = new URL(url);
const dummyUrl = new URL('https://example.com');
// Copy current hash-parameters without the '#' as search-parameters.
dummyUrl.search = newUrl.hash.substring(1);
// Set or update value with the searchParams-API.
dummyUrl.searchParams.append(name, value);
// Write back as hash parameters.
newUrl.hash = dummyUrl.searchParams.toString();
return newUrl.toString();
}

View File

@@ -130,7 +130,10 @@ MiddlewareRegistry.register(store => next => action => {
// recipient. This logic tries to mitigate this risk.
const shouldSendPrivateMessageTo = _shouldSendPrivateMessageTo(state, action);
if (shouldSendPrivateMessageTo) {
const participantExists = shouldSendPrivateMessageTo
&& getParticipantById(state, shouldSendPrivateMessageTo);
if (shouldSendPrivateMessageTo && participantExists) {
dispatch(openDialog(ChatPrivacyDialog, {
message: action.message,
participantID: shouldSendPrivateMessageTo

View File

@@ -0,0 +1 @@
import './middleware.any';

View File

@@ -0,0 +1,45 @@
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
import { IJitsiConference } from '../base/conference/reducer';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { openAllowToggleCameraDialog, setCameraFacingMode } from '../base/tracks/actions.web';
import { CAMERA_FACING_MODE_MESSAGE } from '../base/tracks/constants';
import './middleware.any';
MiddlewareRegistry.register(_store => next => action => {
switch (action.type) {
case CONFERENCE_JOINED: {
_addSetCameraFacingModeListener(action.conference);
break;
}
}
return next(action);
});
/**
* Registers listener for {@link JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED} that
* will perform various chat related activities.
*
* @param {IJitsiConference} conference - The conference.
* @returns {void}
*/
function _addSetCameraFacingModeListener(conference: IJitsiConference) {
conference.on(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
(...args: any) => {
if (args && args.length >= 2) {
const [ sender, eventData ] = args;
if (eventData.name === CAMERA_FACING_MODE_MESSAGE) {
APP.store.dispatch(openAllowToggleCameraDialog(
/* onAllow */ () => APP.store.dispatch(setCameraFacingMode(eventData.facingMode)),
/* initiatorId */ sender._id
));
}
}
}
);
}

View File

@@ -120,7 +120,7 @@ MiddlewareRegistry.register(store => next => action => {
case SETTINGS_UPDATED: {
if (typeof action.settings?.localFlipX === 'boolean') {
// TODO: This needs to be removed once the large video is Reactified.
VideoLayout.onLocalFlipXChanged();
VideoLayout.onLocalFlipXChanged(action.settings.localFlipX);
}
if (action.settings?.disableSelfView) {
const state = store.getState();

View File

@@ -56,7 +56,7 @@ const NumbersList: React.FC<IProps> = ({ t, conferenceID, clickableNumbers, numb
if (countryCode) {
return (
<td className = 'flag-cell'>
{countryCode === 'SIP'
{countryCode === 'SIP' || countryCode === 'SIP_AUDIO_ONLY'
? <Icon src = { IconSip } />
: <i className = { `flag iti-flag ${countryCode}` } />
}
@@ -129,6 +129,8 @@ const NumbersList: React.FC<IProps> = ({ t, conferenceID, clickableNumbers, numb
if (countryCode === 'SIP') {
countryName = t('info.sip');
} else if (countryCode === 'SIP_AUDIO_ONLY') {
countryName = t('info.sipAudioOnly');
} else {
countryName = t(`countries:countries.${countryCode}`);

View File

@@ -11,6 +11,11 @@ import { navigationStyles } from './styles';
interface IProps {
/**
* Icon button color.
*/
color?: string;
/**
* Is the button disabled?
*/
@@ -42,7 +47,7 @@ interface IProps {
twoActions?: boolean;
}
const HeaderNavigationButton = ({ disabled, label, onPress, src, style, twoActions }: IProps) => {
const HeaderNavigationButton = ({ color, disabled, label, onPress, src, style, twoActions }: IProps) => {
let btnStyle;
let labelStyle;
@@ -64,6 +69,7 @@ const HeaderNavigationButton = ({ disabled, label, onPress, src, style, twoActio
{
src ? (
<IconButton
color = { color }
onPress = { onPress }
size = { 24 }
src = { src }

View File

@@ -10,7 +10,7 @@ export const settingsNavigationContainerRef = React.createRef<NavigationContaine
* @param {Object} params - Params to pass to the destination route.
* @returns {Function}
*/
export function navigate(name: string, params: Object) {
export function navigate(name: string, params?: Object) {
return settingsNavigationContainerRef.current?.navigate(name, params);
}

View File

@@ -3,11 +3,15 @@ import { createStackNavigator } from '@react-navigation/stack';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import LanguageSelectView from '../../../../../settings/components/native/LanguageSelectView';
import ProfileView from '../../../../../settings/components/native/ProfileView';
import SettingsView
from '../../../../../settings/components/native/SettingsView';
import { screen } from '../../../routes';
import {
languageSelectScreenOptions,
navigationContainerTheme,
profileSettingsScreenOptions,
settingsScreenOptions,
welcomeScreenOptions
} from '../../../screenOptions';
@@ -37,10 +41,19 @@ const SettingsNavigationContainer = ({ isInWelcomePage }: IProps) => {
const SettingsScreen = useCallback(() =>
(
<SettingsView
addBottomInset = { !isInWelcomePage }
scrollBounces = { isInWelcomePage } />
isInWelcomePage = { isInWelcomePage } />
), []);
const ProfileScreen = useCallback(() =>
(<ProfileView
isInWelcomePage = { isInWelcomePage } />)
, []);
const LanguageSelectScreen = useCallback(() =>
(<LanguageSelectView
isInWelcomePage = { isInWelcomePage } />)
, []);
return (
<NavigationContainer
independent = { true }
@@ -56,6 +69,20 @@ const SettingsNavigationContainer = ({ isInWelcomePage }: IProps) => {
}}>
{ SettingsScreen }
</SettingsStack.Screen>
<SettingsStack.Screen
component = { ProfileScreen }
name = { screen.settings.profile }
options = {{
...profileSettingsScreenOptions,
title: t('settingsView.profileSection')
}} />
<SettingsStack.Screen
component = { LanguageSelectScreen }
name = { screen.settings.language }
options = {{
...languageSelectScreenOptions,
title: t('settings.language')
}} />
</SettingsStack.Navigator>
</NavigationContainer>
);

View File

@@ -49,7 +49,11 @@ export function screenHeaderCloseButton(goBack: (e?: GestureResponderEvent | Rea
* {@code true}; otherwise, {@code false}.
*/
export function isPrejoinPageEnabled(stateful: IStateful) {
return getFeatureFlag(toState(stateful), PREJOIN_PAGE_ENABLED, true);
const state = toState(stateful);
const { prejoinConfig } = state['features/base/config'];
const isPrejoinEnabledInConfig = prejoinConfig?.enabled;
return getFeatureFlag(state, PREJOIN_PAGE_ENABLED, isPrejoinEnabledInConfig ?? true);
}
/**

View File

@@ -44,7 +44,9 @@ export const screen = {
help: 'Help',
privacy: 'Privacy',
terms: 'Terms'
}
},
profile: 'Profile',
language: 'Language'
},
unsafeRoomWarning: 'Unsafe Room Warning'
};

View File

@@ -196,6 +196,24 @@ export const preJoinScreenOptions = {
}
};
/**
* Screen options for profile setting.
*/
export const profileSettingsScreenOptions = {
headerStyle: {
backgroundColor: BaseTheme.palette.ui01
},
headerTitleStyle: {
color: BaseTheme.palette.text01
},
headerBackTitleVisible: false
};
/**
* Screen options for language select screen.
*/
export const languageSelectScreenOptions = profileSettingsScreenOptions;
/**
* Screen options for pre-join screen.
*/

View File

@@ -259,6 +259,9 @@ export function joinConference(options?: Object, ignoreJoiningInProgress = false
const jitsiTracks = localTracks.map((t: any) => t.jitsiTrack);
APP.conference.startConference(jitsiTracks).catch(logger.error);
})
.catch(() => {
// There is nothing to do here. This is handled and dispatched in base/connection/actions.
});
};
}

View File

@@ -2,6 +2,7 @@ import { AnyAction } from 'redux';
import { IStore } from '../app/types';
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../base/conference/actionTypes';
import { CONNECTION_FAILED } from '../base/connection/actionTypes';
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../base/media/actionTypes';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import { updateSettings } from '../base/settings/actions';
@@ -67,6 +68,7 @@ MiddlewareRegistry.register(store => next => async action => {
break;
}
case CONFERENCE_FAILED:
case CONNECTION_FAILED:
store.dispatch(setJoiningInProgress(false));
break;
case CONFERENCE_JOINED:

View File

@@ -1,10 +1,38 @@
import { Linking } from 'react-native';
import { IStore } from '../app/types';
import { isTokenAuthEnabled } from '../authentication/functions';
import { hangup } from '../base/connection/actions.native';
import { openDialog } from '../base/dialog/actions';
import LogoutDialog from './components/native/LogoutDialog';
/**
* Opens {@code LogoutDialog}.
*
* @param {Function} _onLogout - The event in {@code LogoutDialog} that should be
* enabled on click.
* @returns {Function}
*/
export function openLogoutDialog(_onLogout: Function): any {
// this is available only for web at the moment
export function openLogoutDialog() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const { conference } = state['features/base/conference'];
const config = state['features/base/config'];
const logoutUrl = config.tokenLogoutUrl;
dispatch(openDialog(LogoutDialog, {
onLogout() {
if (isTokenAuthEnabled(config)) {
if (logoutUrl) {
Linking.openURL(logoutUrl);
}
dispatch(hangup(true));
} else {
conference?.room.moderator.logout(() => dispatch(hangup(true)));
}
}
}));
};
}

View File

@@ -1,16 +1,21 @@
import { batch } from 'react-redux';
import { IStore } from '../app/types';
import { setTokenAuthUrlSuccess } from '../authentication/actions.web';
import { isTokenAuthEnabled } from '../authentication/functions';
import {
setFollowMe,
setStartMutedPolicy,
setStartReactionsMuted
} from '../base/conference/actions';
import { hangup } from '../base/connection/actions.web';
import { openDialog } from '../base/dialog/actions';
import i18next from '../base/i18n/i18next';
import browser from '../base/lib-jitsi-meet';
import { updateSettings } from '../base/settings/actions';
import { getLocalVideoTrack } from '../base/tracks/functions.any';
import { disableKeyboardShortcuts, enableKeyboardShortcuts } from '../keyboard-shortcuts/actions';
import { getLocalVideoTrack } from '../base/tracks/functions.web';
import { appendURLHashParam } from '../base/util/uri';
import { disableKeyboardShortcuts, enableKeyboardShortcuts } from '../keyboard-shortcuts/actions.web';
import { toggleBackgroundEffect } from '../virtual-background/actions';
import virtualBackgroundLogger from '../virtual-background/logger';
@@ -28,15 +33,45 @@ import {
getShortcutsTabProps
} from './functions.web';
/**
* Opens {@code LogoutDialog}.
*
* @param {Function} onLogout - The event in {@code LogoutDialog} that should be
* enabled on click.
* @returns {Function}
*/
export function openLogoutDialog(onLogout: Function) {
return openDialog(LogoutDialog, { onLogout });
export function openLogoutDialog() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const config = state['features/base/config'];
const logoutUrl = config.tokenLogoutUrl;
const { conference } = state['features/base/conference'];
const { jwt } = state['features/base/jwt'];
dispatch(openDialog(LogoutDialog, {
onLogout() {
if (isTokenAuthEnabled(config) && config.tokenAuthUrlAutoRedirect && jwt) {
// user is logging out remove auto redirect indication
dispatch(setTokenAuthUrlSuccess(false));
}
if (logoutUrl && browser.isElectron()) {
const url = appendURLHashParam(logoutUrl, 'electron', 'true');
window.open(url, '_blank');
dispatch(hangup(true));
} else {
if (logoutUrl) {
window.location.href = logoutUrl;
}
conference?.room.moderator.logout(() => dispatch(hangup(true)));
}
}
}));
};
}
/**

View File

@@ -0,0 +1,114 @@
import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, NativeModules, Platform, Text } from 'react-native';
import { Divider } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { updateSettings } from '../../../base/settings/actions';
import Switch from '../../../base/ui/components/native/Switch';
import FormRow from './FormRow';
import FormSection from './FormSection';
import styles from './styles';
const { AppInfo } = NativeModules;
const AdvancedSection = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const {
disableCrashReporting,
disableCallIntegration,
disableP2P
} = useSelector((state: IReduxState) => state['features/base/settings']);
const onSwitchToggled = useCallback((name: string) => (enabled?: boolean) => {
if (name === 'disableCrashReporting' && enabled === true) {
Alert.alert(
t('settingsView.alertTitle'),
t('settingsView.disableCrashReportingWarning'),
[
{
onPress: () => dispatch(updateSettings({ disableCrashReporting: true })),
text: t('settingsView.alertOk')
},
{
text: t('settingsView.alertCancel')
}
]
);
} else {
dispatch(updateSettings({ [name]: enabled }));
}
}, [ dispatch, updateSettings ]);
const switches = useMemo(() => {
const partialSwitches = [
{
label: 'settingsView.disableCallIntegration',
state: disableCallIntegration,
name: 'disableCallIntegration'
},
{
label: 'settingsView.disableP2P',
state: disableP2P,
name: 'disableP2P'
},
{
label: 'settingsView.disableCrashReporting',
state: disableCrashReporting,
name: 'disableCrashReporting'
}
];
if (Platform.OS !== 'android') {
partialSwitches.shift();
}
if (!AppInfo.GOOGLE_SERVICES_ENABLED) {
partialSwitches.pop();
}
return partialSwitches;
}, [ disableCallIntegration, disableP2P, disableCrashReporting ]);
return (
<>
<FormSection
label = 'settingsView.advanced'>
{
switches.map(({ label, state, name }) => (
<FormRow
key = { label }
label = { label }>
<Switch
checked = { Boolean(state) }
onChange = { onSwitchToggled(name) } />
</FormRow>
))
}
</FormSection>
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
<FormSection
label = 'settingsView.buildInfoSection'>
<FormRow
label = 'settingsView.version'>
<Text style = { styles.text }>
{`${AppInfo.version} build ${AppInfo.buildNumber}`}
</Text>
</FormRow>
<FormRow
label = 'settingsView.sdkVersion'>
<Text style = { styles.text }>
{AppInfo.sdkVersion}
</Text>
</FormRow>
</FormSection>
</>
);
};
export default AdvancedSection;

View File

@@ -0,0 +1,93 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { getDefaultURL } from '../../../app/functions.native';
import { IReduxState } from '../../../app/types';
import { updateSettings } from '../../../base/settings/actions';
import Input from '../../../base/ui/components/native/Input';
import Switch from '../../../base/ui/components/native/Switch';
import { isServerURLChangeEnabled, normalizeUserInputURL } from '../../functions.any';
import FormRow from './FormRow';
import FormSection from './FormSection';
import styles from './styles';
const ConferenceSection = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const defaultServerURL = useSelector((state: IReduxState) => getDefaultURL(state));
const {
serverURL,
startCarMode,
startWithAudioMuted,
startWithVideoMuted
} = useSelector((state: IReduxState) => state['features/base/settings']);
const { serverURLChangeEnabled } = useSelector((state: IReduxState) => isServerURLChangeEnabled(state));
const switches = useMemo(() => [
{
label: 'settingsView.startCarModeInLowBandwidthMode',
state: startCarMode,
name: 'startCarMode'
},
{
label: 'settingsView.startWithAudioMuted',
state: startWithAudioMuted,
name: 'startWithAudioMuted'
},
{
label: 'settingsView.startWithVideoMuted',
state: startWithVideoMuted,
name: 'startWithVideoMuted'
}
], [ startCarMode, startWithAudioMuted, startWithVideoMuted ]);
const onChangeServerURL = useCallback(newServerURL => {
dispatch(updateSettings({ serverURL: newServerURL }));
}, [ updateSettings ]);
const processServerURL = useCallback(() => {
const normalizedURL = normalizeUserInputURL(serverURL ?? '');
onChangeServerURL(normalizedURL);
}, [ serverURL ]);
useEffect(() => () => processServerURL(), []);
const onSwitchToggled = useCallback((name: string) => (enabled?: boolean) => {
dispatch(updateSettings({ [name]: enabled }));
}, [ dispatch, updateSettings ]);
return (
<FormSection
label = 'settingsView.conferenceSection'>
<Input
autoCapitalize = 'none'
customStyles = {{ container: styles.customContainer }}
editable = { serverURLChangeEnabled }
keyboardType = { 'url' }
label = { t('settingsView.serverURL') }
onBlur = { processServerURL }
onChange = { onChangeServerURL }
placeholder = { defaultServerURL }
textContentType = { 'URL' } // iOS only
value = { serverURL ?? '' } />
{
switches.map(({ label, state, name }) => (
<FormRow
key = { label }
label = { label }>
<Switch
checked = { Boolean(state) }
onChange = { onSwitchToggled(name) } />
</FormRow>
))
}
</FormSection>
);
};
export default ConferenceSection;

View File

@@ -59,7 +59,7 @@ class FormRow extends Component<IProps> {
* @returns {ReactElement}
*/
render() {
const { layout, t } = this.props;
const { t } = this.props;
// Some field types need additional props to look good and standardized
// on a form.
@@ -75,8 +75,7 @@ class FormRow extends Component<IProps> {
<Text
style = { [
styles.text,
styles.fieldLabelText,
layout === 'column' ? styles.fieldLabelTextColumn : undefined
styles.fieldLabelText
] } >
{ t(this.props.label) }
</Text>

View File

@@ -20,7 +20,7 @@ interface IProps extends WithTranslation {
/**
* The i18n key of the text label of the section.
*/
label: string;
label?: string;
}
/**
@@ -31,12 +31,9 @@ interface IProps extends WithTranslation {
function FormSection({ children, label, t }: IProps) {
return (
<View>
<View
style = { styles.formSectionTitleContent }>
<Text style = { styles.formSectionTitleText }>
{ t(label) }
</Text>
</View>
{label && <Text style = { styles.formSectionTitleText }>
{ t(label) }
</Text>}
{ children }
</View>
);

View File

@@ -0,0 +1,83 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Text, TouchableHighlight, View, ViewStyle } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import i18next, { DEFAULT_LANGUAGE } from '../../../base/i18n/i18next';
import Icon from '../../../base/icons/components/Icon';
import { IconArrowRight } from '../../../base/icons/svg';
import { updateSettings } from '../../../base/settings/actions';
import Switch from '../../../base/ui/components/native/Switch';
import { navigate } from '../../../mobile/navigation/components/settings/SettingsNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import FormRow from './FormRow';
import FormSection from './FormSection';
import styles from './styles';
const GeneralSection = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const {
disableSelfView,
userSelectedSkipPrejoin
} = useSelector((state: IReduxState) => state['features/base/settings']);
const showPrejoinPage = !userSelectedSkipPrejoin;
let showPrejoinSettings = useSelector(
(state: IReduxState) => state['features/base/config'].prejoinConfig?.enabled);
const { language = DEFAULT_LANGUAGE } = i18next;
const onSelfViewToggled = useCallback((enabled?: boolean) =>
dispatch(updateSettings({ disableSelfView: enabled }))
, [ dispatch, updateSettings ]);
const onShowPejoinToggled = useCallback((enabled?: boolean) => {
dispatch(updateSettings({ userSelectedSkipPrejoin: !enabled }));
}
, [ dispatch, updateSettings ]);
const navigateToLanguageSelect = useCallback(() => {
navigate(screen.settings.language);
}, [ navigate, screen ]);
// TODO:
// Delete this line when prejoin skipping is available on mobile
showPrejoinSettings = false;
return (
<FormSection>
<FormRow label = 'videothumbnail.hideSelfView'>
<Switch
checked = { Boolean(disableSelfView) }
onChange = { onSelfViewToggled } />
</FormRow>
{showPrejoinSettings && <FormRow label = 'prejoin.showScreen'>
<Switch
checked = { showPrejoinPage }
onChange = { onShowPejoinToggled } />
</FormRow>}
<FormRow label = 'settings.language'>
<TouchableHighlight onPress = { navigateToLanguageSelect }>
<View style = { styles.languageButton as ViewStyle }>
<Text
style = { styles.languageText }>{t(`languages:${language}`)}</Text>
<View>
<Icon
size = { 24 }
src = { IconArrowRight } />
</View>
</View>
</TouchableHighlight>
</FormRow>
</FormSection>
);
};
export default GeneralSection;

View File

@@ -0,0 +1,55 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { ScrollView, Text, TouchableHighlight, View, ViewStyle } from 'react-native';
import i18next, { DEFAULT_LANGUAGE, LANGUAGES } from '../../../base/i18n/i18next';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { navigate } from '../../../mobile/navigation/components/settings/SettingsNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import styles from './styles';
const LanguageSelectView = ({ isInWelcomePage }: { isInWelcomePage?: boolean; }) => {
const { t } = useTranslation();
const { language: currentLanguage = DEFAULT_LANGUAGE } = i18next;
const setLanguage = useCallback(language => () => {
i18next.changeLanguage(language);
navigate(screen.settings.main);
}, [ i18next ]);
return (
<JitsiScreen
disableForcedKeyboardDismiss = { true }
// @ts-ignore
safeAreaInsets = { [ !isInWelcomePage && 'bottom', 'left', 'right' ].filter(Boolean) }
style = { styles.settingsViewContainer }>
<ScrollView
bounces = { isInWelcomePage }
contentContainerStyle = { styles.profileView as ViewStyle }>
{
LANGUAGES.map(language => (
<TouchableHighlight
disabled = { currentLanguage === language }
key = { language }
onPress = { setLanguage(language) }>
<View
style = { styles.languageOption as ViewStyle }>
<Text
style = { [
styles.text,
styles.fieldLabelText,
currentLanguage === language && styles.selectedLanguage ] }>
{ t(`languages:${language}`) }
</Text>
</View>
</TouchableHighlight>
))
}
</ScrollView>
</JitsiScreen>
);
};
export default LanguageSelectView;

View File

@@ -0,0 +1,57 @@
import React, { useCallback, useMemo } from 'react';
import { Linking, View, ViewStyle } from 'react-native';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { getLegalUrls } from '../../../base/config/functions.native';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import FormSection from './FormSection';
import styles from './styles';
const LinksSection = () => {
const {
privacy,
helpCentre,
terms
} = useSelector((state: IReduxState) => getLegalUrls(state));
const links = useMemo(() => [
{
label: 'settingsView.help',
link: helpCentre
},
{
label: 'settingsView.terms',
link: terms
},
{
label: 'settingsView.privacy',
link: privacy
}
], [ privacy, helpCentre, terms ]);
const onLinkPress = useCallback(link => () => Linking.openURL(link), [ Linking ]);
return (
<FormSection>
<View style = { styles.linksSection as ViewStyle }>
{
links.map(({ label, link }) => (
<Button
accessibilityLabel = { label }
key = { label }
labelKey = { label }
onClick = { onLinkPress(link) }
style = { styles.linksButton }
type = { BUTTON_TYPES.TERTIARY } />
))
}
</View>
</FormSection>
);
};
export default LinksSection;

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { WithTranslation } from 'react-i18next';
import ConfirmDialog
from '../../../base/dialog/components/native/ConfirmDialog';
interface ILogoutDialogProps extends WithTranslation {
onLogout: Function;
}
const LogoutDialog: React.FC<ILogoutDialogProps> = ({ onLogout }: ILogoutDialogProps) => (
<ConfirmDialog
cancelLabel = 'dialog.Cancel'
confirmLabel = 'dialog.Yes'
descriptionKey = 'dialog.logoutQuestion'
onSubmit = { onLogout } />
);
export default LogoutDialog;

View File

@@ -0,0 +1,96 @@
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { setFollowMe, setStartMutedPolicy, setStartReactionsMuted } from '../../../base/conference/actions';
import { updateSettings } from '../../../base/settings/actions';
import Switch from '../../../base/ui/components/native/Switch';
import { getModeratorTabProps } from '../../functions.native';
import FormRow from './FormRow';
import FormSection from './FormSection';
const ModeratorSection = () => {
const dispatch = useDispatch();
const {
followMeEnabled,
startAudioMuted,
startVideoMuted,
startReactionsMuted
} = useSelector((state: IReduxState) => getModeratorTabProps(state));
const { disableReactionsModeration } = useSelector((state: IReduxState) => state['features/base/config']);
const onStartAudioMutedToggled = useCallback((enabled?: boolean) => {
dispatch(setStartMutedPolicy(
Boolean(enabled), Boolean(startVideoMuted)));
}, [ startVideoMuted, dispatch, setStartMutedPolicy ]);
const onStartVideoMutedToggled = useCallback((enabled?: boolean) => {
dispatch(setStartMutedPolicy(
Boolean(startAudioMuted), Boolean(enabled)));
}, [ startAudioMuted, dispatch, setStartMutedPolicy ]);
const onFollowMeToggled = useCallback((enabled?: boolean) => {
dispatch(setFollowMe(Boolean(enabled)));
}, [ dispatch, setFollowMe ]);
const onStartReactionsMutedToggled = useCallback((enabled?: boolean) => {
dispatch(setStartReactionsMuted(Boolean(enabled), true));
dispatch(updateSettings({ soundsReactions: enabled }));
}, [ dispatch, updateSettings, setStartReactionsMuted ]);
const moderationSettings = useMemo(() => {
const moderation = [
{
label: 'settings.startAudioMuted',
state: startAudioMuted,
onChange: onStartAudioMutedToggled
},
{
label: 'settings.startVideoMuted',
state: startVideoMuted,
onChange: onStartVideoMutedToggled
},
{
label: 'settings.followMe',
state: followMeEnabled,
onChange: onFollowMeToggled
},
{
label: 'settings.startReactionsMuted',
state: startReactionsMuted,
onChange: onStartReactionsMutedToggled
}
];
if (disableReactionsModeration) {
moderation.pop();
}
return moderation;
}, [ startAudioMuted,
startVideoMuted,
followMeEnabled,
disableReactionsModeration,
startReactionsMuted ]);
return (
<FormSection
label = 'settings.playSounds'>
{
moderationSettings.map(({ label, state, onChange }) => (
<FormRow
key = { label }
label = { label }>
<Switch
checked = { Boolean(state) }
onChange = { onChange } />
</FormRow>
))
}
</FormSection>
);
};
export default ModeratorSection;

View File

@@ -0,0 +1,139 @@
import React, { useCallback, useMemo } from 'react';
import { Divider } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { updateSettings } from '../../../base/settings/actions';
import Switch from '../../../base/ui/components/native/Switch';
import { getNotificationsTabProps } from '../../functions.any';
import FormRow from './FormRow';
import FormSection from './FormSection';
import styles from './styles';
const NotificationsSection = () => {
const dispatch = useDispatch();
const {
soundsIncomingMessage,
soundsParticipantJoined,
soundsParticipantKnocking,
soundsParticipantLeft,
soundsReactions,
soundsTalkWhileMuted,
enableReactions,
enabledNotifications,
disabledSounds,
moderatorMutedSoundsReactions
} = useSelector((state: IReduxState) => getNotificationsTabProps(state));
const sounds = useMemo(() => {
const partialSounds = [
{
label: 'settings.reactions',
state: soundsReactions,
name: 'soundsReactions',
disabled: Boolean(moderatorMutedSoundsReactions
|| disabledSounds.includes('REACTION_SOUND'))
},
{
label: 'settings.incomingMessage',
state: soundsIncomingMessage,
name: 'soundsIncomingMessage'
},
{
label: 'settings.participantJoined',
state: soundsParticipantJoined,
name: 'soundsParticipantJoined'
},
{
label: 'settings.participantLeft',
state: soundsParticipantLeft,
name: 'soundsParticipantLeft'
},
{
label: 'settings.talkWhileMuted',
state: soundsTalkWhileMuted,
name: 'soundsTalkWhileMuted'
},
{
label: 'settings.participantKnocking',
state: soundsParticipantKnocking,
name: 'soundsParticipantKnocking'
}
];
if (!enableReactions) {
partialSounds.shift();
}
return partialSounds;
}, [ soundsReactions,
soundsIncomingMessage,
soundsParticipantJoined,
soundsParticipantLeft,
soundsTalkWhileMuted,
soundsParticipantKnocking,
enableReactions ]);
const onSoundToggled = useCallback((name: string) => (enabled?: boolean) => {
dispatch(updateSettings({ [name]: enabled }));
}, [ dispatch, updateSettings ]);
const onNotificationToggled = useCallback((name: string) => (enabled?: boolean) => {
dispatch(updateSettings({
userSelectedNotifications: {
...enabledNotifications,
[name]: Boolean(enabled)
}
}
)
);
}, [ dispatch, updateSettings, enabledNotifications ]);
return (
<>
<FormSection
label = 'settings.playSounds'>
{
sounds.map(({ label, state, name, disabled }) => (
<FormRow
key = { label }
label = { label }>
<Switch
checked = { Boolean(state) }
disabled = { disabled }
onChange = { onSoundToggled(name) } />
</FormRow>
))
}
</FormSection>
{
Object.keys(enabledNotifications).length > 0 && (
<>
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
<FormSection
label = 'notify.displayNotifications'>
{
Object.keys(enabledNotifications).map(name => (
<FormRow
key = { name }
label = { name }>
<Switch
checked = { Boolean(enabledNotifications[name]) }
onChange = { onNotificationToggled(name) } />
</FormRow>)
)
}
</FormSection>
</>
)
}
</>
);
};
export default NotificationsSection;

View File

@@ -0,0 +1,160 @@
import { useNavigation } from '@react-navigation/native';
import React, { useCallback, useLayoutEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ScrollView, Text, View, ViewStyle } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { login, logout } from '../../../authentication/actions.native';
import Avatar from '../../../base/avatar/components/Avatar';
import { IconArrowLeft } from '../../../base/icons/svg';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { getLocalParticipant } from '../../../base/participants/functions';
import { updateSettings } from '../../../base/settings/actions';
import BaseThemeNative from '../../../base/ui/components/BaseTheme.native';
import Button from '../../../base/ui/components/native/Button';
import Input from '../../../base/ui/components/native/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import HeaderNavigationButton
from '../../../mobile/navigation/components/HeaderNavigationButton';
import {
goBack,
navigate
} from '../../../mobile/navigation/components/settings/SettingsNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import FormSection from './FormSection';
import { AVATAR_SIZE } from './constants';
import styles from './styles';
const ProfileView = ({ isInWelcomePage }: {
isInWelcomePage?: boolean;
}) => {
const { t } = useTranslation();
const dispatch = useDispatch();
const navigation = useNavigation();
const { displayName: reduxDisplayName, email: reduxEmail } = useSelector(
(state: IReduxState) => state['features/base/settings']
);
const participant = useSelector((state: IReduxState) => getLocalParticipant(state));
const [ displayName, setDisplayName ] = useState(reduxDisplayName);
const [ email, setEmail ] = useState(reduxEmail);
const { authLogin: isAutenticated } = useSelector((state: IReduxState) => state['features/base/conference']);
const onDisplayNameChanged = useCallback(newDisplayName => {
setDisplayName(newDisplayName);
}, [ setDisplayName ]);
const onEmailChanged = useCallback(newEmail => {
setEmail(newEmail);
}, [ setEmail ]);
const onApplySettings = useCallback(() => {
dispatch(updateSettings({
displayName,
email
}));
navigate(screen.settings.main);
},
[ dispatch, updateSettings, email, displayName ]);
const onLogin = useCallback(() => {
dispatch(login());
}, [ dispatch ]);
const onLogout = useCallback(() => {
dispatch(logout());
}, [ dispatch ]);
const headerLeft = () => (
<HeaderNavigationButton
color = { BaseThemeNative.palette.link01 }
onPress = { goBack }
src = { IconArrowLeft }
style = { styles.backBtn }
twoActions = { true } />
);
const headerRight = () => {
if (isAutenticated) {
return (
<HeaderNavigationButton
label = { t('toolbar.logout') }
onPress = { onLogout }
style = { styles.logBtn }
twoActions = { true } />
);
}
return (
<HeaderNavigationButton
label = { t('toolbar.login') }
onPress = { onLogin }
style = { styles.logBtn }
twoActions = { true } />
);
};
useLayoutEffect(() => {
navigation.setOptions({
headerLeft,
headerRight: !isInWelcomePage && headerRight
});
}, [ navigation ]);
return (
<JitsiScreen
disableForcedKeyboardDismiss = { true }
// @ts-ignore
safeAreaInsets = { [ !isInWelcomePage && 'bottom', 'left', 'right' ].filter(Boolean) }
style = { styles.settingsViewContainer }>
<ScrollView
bounces = { isInWelcomePage }
contentContainerStyle = { styles.profileView as ViewStyle }>
<View>
<View style = { styles.avatarContainer as ViewStyle }>
<Avatar
participantId = { participant?.id }
size = { AVATAR_SIZE } />
</View>
<FormSection>
<Input
customStyles = {{ container: styles.customContainer }}
label = { t('settingsView.displayName') }
onChange = { onDisplayNameChanged }
placeholder = { t('settingsView.displayNamePlaceholderText') }
textContentType = { 'name' } // iOS only
value = { displayName ?? '' } />
<Input
autoCapitalize = 'none'
customStyles = {{ container: styles.customContainer }}
keyboardType = { 'email-address' }
label = { t('settingsView.email') }
onChange = { onEmailChanged }
placeholder = { t('settingsView.emailPlaceholderText') }
textContentType = { 'emailAddress' } // iOS only
value = { email ?? '' } />
<Text style = { styles.gavatarMessageContainer }>
{ t('settingsView.gavatarMessage') }
</Text>
</FormSection>
</View>
<Button
accessibilityLabel = { t('settingsView.apply') }
labelKey = { 'settingsView.apply' }
onClick = { onApplySettings }
style = { styles.applyProfileSettingsButton }
type = { BUTTON_TYPES.PRIMARY } />
</ScrollView>
</JitsiScreen>
);
};
export default ProfileView;

View File

@@ -1,110 +1,42 @@
import _ from 'lodash';
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import {
Alert,
Linking,
NativeModules,
Platform,
ScrollView,
Text,
TouchableHighlight,
View,
ViewStyle
} from 'react-native';
import { Divider } from 'react-native-paper';
import { connect } from 'react-redux';
import { getDefaultURL } from '../../../app/functions.native';
import { IReduxState, IStore } from '../../../app/types';
import Avatar from '../../../base/avatar/components/Avatar';
import { getLegalUrls } from '../../../base/config/functions.native';
import { translate } from '../../../base/i18n/functions';
import Icon from '../../../base/icons/components/Icon';
import { IconArrowRight } from '../../../base/icons/svg';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { getLocalParticipant } from '../../../base/participants/functions';
import { updateSettings } from '../../../base/settings/actions';
import Button from '../../../base/ui/components/native/Button';
import Input from '../../../base/ui/components/native/Input';
import Switch from '../../../base/ui/components/native/Switch';
import { BUTTON_TYPES } from '../../../base/ui/constants.any';
import { AVATAR_SIZE } from '../../../welcome/components/styles';
import { isServerURLChangeEnabled, normalizeUserInputURL } from '../../functions.native';
import { navigate } from '../../../mobile/navigation/components/settings/SettingsNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { shouldShowModeratorSettings } from '../../functions.native';
import FormRow from './FormRow';
import FormSection from './FormSection';
import AdvancedSection from './AdvancedSection';
import ConferenceSection from './ConferenceSection';
import GeneralSection from './GeneralSection';
import LinksSection from './LinksSection';
import ModeratorSection from './ModeratorSection';
import NotificationsSection from './NotificationsSection';
import { AVATAR_SIZE } from './constants';
import styles from './styles';
/**
* Application information module.
*/
const { AppInfo } = NativeModules;
interface IState {
/**
* State variable for the disable call integration switch.
*/
disableCallIntegration?: boolean;
/**
* State variable for the disable crash reporting switch.
*/
disableCrashReporting?: boolean;
/**
* State variable for the disable p2p switch.
*/
disableP2P?: boolean;
/**
* Whether the self view is disabled or not.
*/
disableSelfView?: boolean;
/**
* State variable for the display name field.
*/
displayName?: string;
/**
* State variable for the email field.
*/
email?: string;
/**
* State variable for the server URL field.
*/
serverURL?: string;
/**
* State variable for start car mode.
*/
startCarMode?: boolean;
/**
* State variable for the start with audio muted switch.
*/
startWithAudioMuted?: boolean;
/**
* State variable for the start with video muted switch.
*/
startWithVideoMuted?: boolean;
}
/**
* The type of the React {@code Component} props of
* {@link SettingsView}.
*/
interface IProps extends WithTranslation {
/**
* The legal URL's.
*/
_legalUrls: {
helpCentre: string;
privacy: string;
terms: string;
};
_displayName?: string;
/**
* The ID of the local participant.
@@ -112,34 +44,9 @@ interface IProps extends WithTranslation {
_localParticipantId?: string;
/**
* The default URL for when there is no custom URL set in the settings.
*
* @protected
* Flag indicating whether the moderator settings are available.
*/
_serverURL: string;
/**
* Flag indicating if URL can be changed by user.
*
* @protected
*/
_serverURLChangeEnabled: boolean;
/**
* The current settings object.
*/
_settings: {
disableCallIntegration?: boolean;
disableCrashReporting?: boolean;
disableP2P?: boolean;
disableSelfView?: boolean;
displayName?: string;
email?: string;
serverURL?: string;
startCarMode?: boolean;
startWithAudioMuted?: boolean;
startWithVideoMuted?: boolean;
};
_showModeratorSettings: boolean;
/**
* Whether {@link SettingsView} is visible.
@@ -148,105 +55,35 @@ interface IProps extends WithTranslation {
*/
_visible?: boolean;
/**
* Add bottom padding to the screen.
*/
addBottomInset?: boolean;
/**
* Redux store dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* Flag indicating whether the settings is launched inside welcome page.
*/
isInWelcomePage?: boolean;
/**
* Default prop for navigating between screen components(React Navigation).
*/
navigation?: Object;
/**
* Bounce when scrolling.
*/
scrollBounces?: boolean;
}
/**
* The native container rendering the app settings page.
*/
class SettingsView extends Component<IProps, IState> {
class SettingsView extends Component<IProps> {
_urlField: Object;
/**
* Opens the profile settings screen.
*
* Initializes a new {@code SettingsView} instance.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
const {
disableCallIntegration,
disableCrashReporting,
disableP2P,
disableSelfView,
displayName,
email,
serverURL,
startCarMode,
startWithAudioMuted,
startWithVideoMuted
} = props._settings || {};
this.state = {
disableCallIntegration,
disableCrashReporting,
disableP2P,
disableSelfView,
displayName: displayName || '',
email: email || '',
serverURL: serverURL || '',
startCarMode,
startWithAudioMuted,
startWithVideoMuted
};
// Bind event handlers so they are only bound once per instance.
this._onBlurServerURL = this._onBlurServerURL.bind(this);
this._onChangeDisplayName = this._onChangeDisplayName.bind(this);
this._onChangeEmail = this._onChangeEmail.bind(this);
this._onChangeServerURL = this._onChangeServerURL.bind(this);
this._onClose = this._onClose.bind(this);
this._onDisableCallIntegration = this._onDisableCallIntegration.bind(this);
this._onDisableCrashReporting = this._onDisableCrashReporting.bind(this);
this._onDisableP2P = this._onDisableP2P.bind(this);
this._onDisableSelfView = this._onDisableSelfView.bind(this);
this._onStartAudioMutedChange
= this._onStartAudioMutedChange.bind(this);
this._onStartCarmodeInLowBandwidthMode
= this._onStartCarmodeInLowBandwidthMode.bind(this);
this._onStartVideoMutedChange
= this._onStartVideoMutedChange.bind(this);
this._setURLFieldReference = this._setURLFieldReference.bind(this);
this._onShowHelpPressed = this._onShowHelpPressed.bind(this);
this._onShowPrivacyPressed = this._onShowPrivacyPressed.bind(this);
this._onShowTermsPressed = this._onShowTermsPressed.bind(this);
this._showURLAlert = this._showURLAlert.bind(this);
}
/**
* Updates and syncs settings.
*
* @inheritdoc
* @returns {void}
*/
componentDidUpdate(prevProps: IProps) {
const { _settings } = this.props;
if (!_.isEqual(prevProps._settings, _settings)) {
// @ts-ignore
// eslint-disable-next-line react/no-did-update-set-state
this.setState(_settings);
}
_onPressProfile() {
navigate(screen.settings.profile);
}
/**
@@ -257,24 +94,17 @@ class SettingsView extends Component<IProps, IState> {
*/
render() {
const {
disableCallIntegration,
disableCrashReporting,
disableP2P,
disableSelfView,
displayName,
email,
serverURL,
startCarMode,
startWithAudioMuted,
startWithVideoMuted
} = this.state;
_displayName
} = this.props;
const {
addBottomInset = false,
scrollBounces = false,
t
isInWelcomePage,
_showModeratorSettings
} = this.props;
const addBottomInset = !isInWelcomePage;
const scrollBounces = Boolean(isInWelcomePage);
return (
<JitsiScreen
disableForcedKeyboardDismiss = { true }
@@ -283,484 +113,49 @@ class SettingsView extends Component<IProps, IState> {
safeAreaInsets = { [ addBottomInset && 'bottom', 'left', 'right' ].filter(Boolean) }
style = { styles.settingsViewContainer }>
<ScrollView bounces = { scrollBounces }>
<View style = { styles.avatarContainer as ViewStyle }>
<Avatar
participantId = { this.props._localParticipantId }
size = { AVATAR_SIZE } />
<View style = { styles.profileContainerWrapper }>
<TouchableHighlight onPress = { this._onPressProfile }>
<View
style = { styles.profileContainer as ViewStyle }>
<Avatar
participantId = { this.props._localParticipantId }
size = { AVATAR_SIZE } />
<Text style = { styles.displayName }>
{ _displayName }
</Text>
<Icon
size = { 24 }
src = { IconArrowRight }
style = { styles.profileViewArrow } />
</View>
</TouchableHighlight>
</View>
<FormSection
label = 'settingsView.profileSection'>
<Input
customStyles = {{ container: styles.customContainer }}
label = { t('settingsView.displayName') }
onChange = { this._onChangeDisplayName }
placeholder = { t('settingsView.displayNamePlaceholderText') }
textContentType = { 'name' } // iOS only
value = { displayName ?? '' } />
<GeneralSection />
{ isInWelcomePage && <>
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
<Input
autoCapitalize = 'none'
customStyles = {{ container: styles.customContainer }}
keyboardType = { 'email-address' }
label = { t('settingsView.email') }
onChange = { this._onChangeEmail }
placeholder = { t('settingsView.emailPlaceholderText') }
textContentType = { 'emailAddress' } // iOS only
value = { email ?? '' } />
</FormSection>
<FormSection
label = 'settingsView.conferenceSection'>
<Input
autoCapitalize = 'none'
customStyles = {{ container: styles.customContainer }}
editable = { this.props._serverURLChangeEnabled }
keyboardType = { 'url' }
label = { t('settingsView.serverURL') }
onBlur = { this._onBlurServerURL }
onChange = { this._onChangeServerURL }
placeholder = { this.props._serverURL }
textContentType = { 'URL' } // iOS only
value = { serverURL ?? '' } />
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
<FormRow label = 'settingsView.startCarModeInLowBandwidthMode'>
<Switch
checked = { Boolean(startCarMode) }
onChange = { this._onStartCarmodeInLowBandwidthMode } />
</FormRow>
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
<FormRow
label = 'settingsView.startWithAudioMuted'>
<Switch
checked = { Boolean(startWithAudioMuted) }
onChange = { this._onStartAudioMutedChange } />
</FormRow>
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
<FormRow label = 'settingsView.startWithVideoMuted'>
<Switch
checked = { Boolean(startWithVideoMuted) }
onChange = { this._onStartVideoMutedChange } />
</FormRow>
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
<FormRow label = 'videothumbnail.hideSelfView'>
<Switch
checked = { Boolean(disableSelfView) }
onChange = { this._onDisableSelfView } />
</FormRow>
</FormSection>
<FormSection
label = 'settingsView.links'>
<Button
accessibilityLabel = 'settingsView.help'
labelKey = 'settingsView.help'
onClick = { this._onShowHelpPressed }
style = { styles.linksButton }
type = { BUTTON_TYPES.TERTIARY } />
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
<Button
accessibilityLabel = 'settingsView.terms'
labelKey = 'settingsView.terms'
onClick = { this._onShowTermsPressed }
style = { styles.linksButton }
type = { BUTTON_TYPES.TERTIARY } />
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
<Button
accessibilityLabel = 'settingsView.privacy'
labelKey = 'settingsView.privacy'
onClick = { this._onShowPrivacyPressed }
style = { styles.linksButton }
type = { BUTTON_TYPES.TERTIARY } />
</FormSection>
<FormSection
label = 'settingsView.buildInfoSection'>
<FormRow
label = 'settingsView.version'>
<Text style = { styles.text }>
{`${AppInfo.version} build ${AppInfo.buildNumber}`}
</Text>
</FormRow>
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
<FormRow
label = 'settingsView.sdkVersion'>
<Text style = { styles.text }>
{AppInfo.sdkVersion}
</Text>
</FormRow>
</FormSection>
<FormSection
label = 'settingsView.advanced'>
{ Platform.OS === 'android' && (
<>
<FormRow
label = 'settingsView.disableCallIntegration'>
<Switch
checked = { Boolean(disableCallIntegration) }
onChange = { this._onDisableCallIntegration } />
</FormRow>
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
</>
)}
<FormRow
label = 'settingsView.disableP2P'>
<Switch
checked = { Boolean(disableP2P) }
onChange = { this._onDisableP2P } />
</FormRow>
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
{AppInfo.GOOGLE_SERVICES_ENABLED && (
<FormRow
fieldSeparator = { true }
label = 'settingsView.disableCrashReporting'>
<Switch
checked = { Boolean(disableCrashReporting) }
onChange = { this._onDisableCrashReporting } />
</FormRow>
)}
</FormSection>
<ConferenceSection />
</> }
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
<NotificationsSection />
{ _showModeratorSettings
&& <>
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
<ModeratorSection />
</> }
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
<AdvancedSection />
{/* @ts-ignore */}
<Divider style = { styles.fieldSeparator } />
<LinksSection />
</ScrollView>
</JitsiScreen>
);
}
/**
* Handler the server URL lose focus event. Here we validate the server URL
* and update it to the normalized version, or show an error if incorrect.
*
* @private
* @returns {void}
*/
_onBlurServerURL() {
this._processServerURL(false /* hideOnSuccess */);
}
/**
* Handles the display name field value change.
*
* @param {string} displayName - The value typed in the name field.
* @protected
* @returns {void}
*/
_onChangeDisplayName(displayName: string) {
this.setState({
displayName
});
this._updateSettings({
displayName
});
}
/**
* Handles the email field value change.
*
* @param {string} email - The value typed in the email field.
* @protected
* @returns {void}
*/
_onChangeEmail(email: string) {
this.setState({
email
});
this._updateSettings({
email
});
}
/**
* Handles the server name field value change.
*
* @param {string} serverURL - The server URL typed in the server field.
* @protected
* @returns {void}
*/
_onChangeServerURL(serverURL: string) {
this.setState({
serverURL
});
this._updateSettings({
serverURL
});
}
/**
* Handles the disable call integration change event.
*
* @param {boolean} disableCallIntegration - The new value
* option.
* @private
* @returns {void}
*/
_onDisableCallIntegration(disableCallIntegration?: boolean) {
this.setState({
disableCallIntegration
});
this._updateSettings({
disableCallIntegration
});
}
/**
* Handles the disable P2P change event.
*
* @param {boolean} disableP2P - The new value
* option.
* @private
* @returns {void}
*/
_onDisableP2P(disableP2P?: boolean) {
this.setState({
disableP2P
});
this._updateSettings({
disableP2P
});
}
/** .
* Handles the disable self view change event.
*
* @param {boolean} disableSelfView - The new value.
* @private
* @returns {void}
*/
_onDisableSelfView(disableSelfView?: boolean) {
this.setState({
disableSelfView
});
this._updateSettings({
disableSelfView
});
}
/** .
* Handles car mode in low bandwidth mode.
*
* @param {boolean} startCarMode - The new value.
* @private
* @returns {void}
*/
_onStartCarmodeInLowBandwidthMode(startCarMode?: boolean) {
this.setState({
startCarMode
});
this._updateSettings({
startCarMode
});
}
/**
* Handles the disable crash reporting change event.
*
* @param {boolean} disableCrashReporting - The new value
* option.
* @private
* @returns {void}
*/
_onDisableCrashReporting(disableCrashReporting?: boolean) {
if (disableCrashReporting) {
this._showCrashReportingDisableAlert();
} else {
this._disableCrashReporting(Boolean(disableCrashReporting));
}
}
/**
* Callback to be invoked on closing the modal. Also invokes normalizeUserInputURL to validate
* the URL entered by the user.
*
* @returns {boolean} - True if the modal can be closed.
*/
_onClose() {
return this._processServerURL(true /* hideOnSuccess */);
}
/**
* Handles the start audio muted change event.
*
* @param {boolean} startWithAudioMuted - The new value for the start audio muted
* option.
* @protected
* @returns {void}
*/
_onStartAudioMutedChange(startWithAudioMuted?: boolean) {
this.setState({
startWithAudioMuted
});
this._updateSettings({
startWithAudioMuted
});
}
/**
* Handles the start video muted change event.
*
* @param {boolean} startWithVideoMuted - The new value for the start video muted
* option.
* @protected
* @returns {void}
*/
_onStartVideoMutedChange(startWithVideoMuted?: boolean) {
this.setState({
startWithVideoMuted
});
this._updateSettings({
startWithVideoMuted
});
}
/**
* Processes the server URL. It normalizes it and an error alert is
* displayed in case it's incorrect.
*
* @param {boolean} hideOnSuccess - True if the dialog should be hidden if
* normalization / validation succeeds, false otherwise.
* @private
* @returns {void}
*/
_processServerURL(hideOnSuccess: boolean) {
// @ts-ignore
const { serverURL } = this.props._settings;
const normalizedURL = normalizeUserInputURL(serverURL ?? '');
if (normalizedURL === null) {
this._showURLAlert();
return false;
}
this._onChangeServerURL(normalizedURL);
return hideOnSuccess;
}
/**
* Stores a reference to the URL field for later use.
*
* @param {Object} component - The field component.
* @protected
* @returns {void}
*/
_setURLFieldReference(component: object) {
this._urlField = component;
}
/**
* Shows an alert telling the user that the URL he/she entered was invalid.
*
* @returns {void}
*/
_showURLAlert() {
const { t } = this.props;
Alert.alert(
t('settingsView.alertTitle'),
t('settingsView.alertURLText'),
[
{
// @ts-ignore
onPress: () => this._urlField.focus(),
text: t('settingsView.alertOk')
}
]
);
}
/**
* Opens the help url into the browser.
*
* @returns {void}
*/
_onShowHelpPressed() {
Linking.openURL(this.props._legalUrls.helpCentre);
}
/**
* Opens the privacy url into the browser.
*
* @returns {void}
*/
_onShowPrivacyPressed() {
Linking.openURL(this.props._legalUrls.privacy);
}
/**
* Opens the terms url into the browser.
*
* @returns {void}
*/
_onShowTermsPressed() {
Linking.openURL(this.props._legalUrls.terms);
}
/**
* Shows an alert warning the user about disabling crash reporting.
*
* @returns {void}
*/
_showCrashReportingDisableAlert() {
const { t } = this.props;
Alert.alert(
t('settingsView.alertTitle'),
t('settingsView.disableCrashReportingWarning'),
[
{
onPress: () => this._disableCrashReporting(true),
text: t('settingsView.alertOk')
},
{
text: t('settingsView.alertCancel')
}
]
);
}
/**
* Updates the settings and sets state for disableCrashReporting.
*
* @param {boolean} disableCrashReporting - Whether crash reporting is disabled or not.
* @returns {void}
*/
_disableCrashReporting(disableCrashReporting: boolean) {
this.setState({
disableCrashReporting
});
this._updateSettings({
disableCrashReporting
});
}
/**
* Updates the persisted settings on any change.
*
* @param {Object} updateObject - The partial update object for the
* settings.
* @private
* @returns {void}
*/
_updateSettings(updateObject: Object) {
const { dispatch } = this.props;
dispatch(updateSettings(updateObject));
}
}
/**
@@ -773,12 +168,10 @@ function _mapStateToProps(state: IReduxState) {
const localParticipant = getLocalParticipant(state);
return {
_legalUrls: getLegalUrls(state),
_localParticipantId: localParticipant?.id,
_serverURL: getDefaultURL(state),
_serverURLChangeEnabled: isServerURLChangeEnabled(state),
_settings: state['features/base/settings'],
_visible: state['features/settings'].visible
_displayName: state['features/base/settings'].displayName,
_visible: state['features/settings'].visible,
_showModeratorSettings: shouldShowModeratorSettings(state)
};
}

View File

@@ -0,0 +1 @@
export const AVATAR_SIZE = 64;

View File

@@ -3,19 +3,60 @@ import BaseTheme from '../../../base/ui/components/BaseTheme.native';
export const ANDROID_UNDERLINE_COLOR = 'transparent';
export const PLACEHOLDER_COLOR = BaseTheme.palette.focus01;
const TEXT_SIZE = 14;
/**
* The styles of the native components of the feature {@code settings}.
*/
export default {
profileContainerWrapper: {
margin: BaseTheme.spacing[4]
},
profileContainer: {
backgroundColor: BaseTheme.palette.ui02,
borderRadius: BaseTheme.shape.borderRadius,
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'flex-start',
padding: BaseTheme.spacing[3]
},
profileView: {
flexGrow: 1,
flexDirection: 'column',
justifyContent: 'space-between'
},
applyProfileSettingsButton: {
marginHorizontal: BaseTheme.spacing[4],
marginVertical: BaseTheme.spacing[3]
},
avatarContainer: {
alignItems: 'center',
flexDirection: 'column',
height: 180,
justifyContent: 'center'
flexDirection: 'row',
justifyContent: 'center',
padding: BaseTheme.spacing[3],
margin: BaseTheme.spacing[4]
},
gavatarMessageContainer: {
marginHorizontal: BaseTheme.spacing[4],
color: BaseTheme.palette.text02,
marginTop: -BaseTheme.spacing[2],
...BaseTheme.typography.bodyShortRegular
},
displayName: {
...BaseTheme.typography.bodyLongRegularLarge,
color: BaseTheme.palette.text01,
marginLeft: BaseTheme.spacing[3],
position: 'relative'
},
profileViewArrow: {
position: 'absolute',
right: BaseTheme.spacing[3]
},
/**
@@ -50,29 +91,25 @@ export default {
fieldLabelContainer: {
alignItems: 'center',
flexDirection: 'row',
paddingLeft: 8
flex: 3,
paddingLeft: BaseTheme.spacing[3]
},
/**
* Text of the field labels on the form.
*/
fieldLabelText: {
fontSize: TEXT_SIZE
},
/**
* Appended style for column layout fields.
*/
fieldLabelTextColumn: {
fontSize: 12
...BaseTheme.typography.bodyShortRegularLarge
},
/**
* Field container style for all but last row {@code View}.
*/
fieldSeparator: {
marginHorizontal: BaseTheme.spacing[4],
borderBottomWidth: 1,
borderColor: BaseTheme.palette.ui05
borderColor: BaseTheme.palette.ui05,
marginVertical: BaseTheme.spacing[3]
},
/**
@@ -84,7 +121,7 @@ export default {
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
paddingRight: 8
paddingRight: BaseTheme.spacing[3]
},
/**
@@ -97,10 +134,10 @@ export default {
},
formSectionTitleText: {
...BaseTheme.typography.bodyShortRegular,
color: BaseTheme.palette.text01,
opacity: 0.6,
textAlign: 'center'
...BaseTheme.typography.bodyShortBold,
color: BaseTheme.palette.text02,
marginHorizontal: BaseTheme.spacing[4],
marginVertical: BaseTheme.spacing[3]
},
/**
@@ -115,17 +152,42 @@ export default {
*/
customContainer: {
marginBottom: BaseTheme.spacing[3],
marginHorizontal: BaseTheme.spacing[3],
marginHorizontal: BaseTheme.spacing[4],
marginTop: BaseTheme.spacing[2]
},
languageButton: {
display: 'flex',
flexDirection: 'row'
},
languageOption: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
height: BaseTheme.spacing[6],
marginHorizontal: BaseTheme.spacing[4],
borderBottomWidth: 1,
borderColor: BaseTheme.palette.ui05
},
selectedLanguage: {
color: BaseTheme.palette.text03
},
languageText: {
...BaseTheme.typography.bodyShortRegularLarge,
color: BaseTheme.palette.text01,
marginRight: BaseTheme.spacing[3]
},
/**
* Standard text input field style.
*/
textInputField: {
color: BaseTheme.palette.field01,
flex: 1,
fontSize: TEXT_SIZE,
...BaseTheme.typography.bodyShortRegularLarge,
textAlign: 'right'
},
@@ -147,9 +209,26 @@ export default {
flex: 1
},
linksSection: {
display: 'flex',
flexDirection: 'row',
flex: 1,
marginHorizontal: BaseTheme.spacing[3]
},
linksButton: {
alignSelf: 'center',
maxWidth: 400,
width: 'auto'
width: '33%',
justifyContent: 'center',
flexDirection: 'row',
alignItems: 'center',
...BaseTheme.typography.bodyShortBoldLarge
},
logBtn: {
marginRight: BaseTheme.spacing[2]
},
backBtn: {
marginLeft: BaseTheme.spacing[2]
}
};

View File

@@ -5,29 +5,17 @@ import { SERVER_URL_CHANGE_ENABLED } from '../base/flags/constants';
import { getFeatureFlag } from '../base/flags/functions';
import i18next, { DEFAULT_LANGUAGE, LANGUAGES } from '../base/i18n/i18next';
import {
getLocalParticipant,
isLocalParticipantModerator
getLocalParticipant
} from '../base/participants/functions';
import { toState } from '../base/redux/functions';
import { getHideSelfView } from '../base/settings/functions';
import { parseStandardURIString } from '../base/util/uri';
import { isStageFilmstripEnabled } from '../filmstrip/functions';
import { isFollowMeActive } from '../follow-me/functions';
import { getParticipantsPaneConfig } from '../participants-pane/functions';
import { isReactionsEnabled } from '../reactions/functions.any';
import { iAmVisitor } from '../visitors/functions';
/**
* Used for web. Indicates if the setting section is enabled.
*
* @param {string} settingName - The name of the setting section as defined in
* interface_config.js and SettingsMenu.js.
* @returns {boolean} True to indicate that the given setting section
* is enabled, false otherwise.
*/
export function isSettingEnabled(settingName: string) {
return interfaceConfig.SETTINGS_SECTIONS.includes(settingName);
}
import { shouldShowModeratorSettings } from './functions';
/**
* Returns true if user is allowed to change Server URL.
@@ -84,7 +72,7 @@ export function normalizeUserInputURL(url: string) {
* {@code getState} function to be used to retrieve the state.
* @returns {Object} - The section of notifications to be configured.
*/
export function getNotificationsMap(stateful: IStateful) {
export function getNotificationsMap(stateful: IStateful): { [key: string]: boolean; } {
const state = toState(stateful);
const { notifications } = state['features/base/config'];
const { userSelectedNotifications } = state['features/base/settings'];
@@ -167,21 +155,6 @@ export function getModeratorTabProps(stateful: IStateful) {
};
}
/**
* Returns true if moderator tab in settings should be visible/accessible.
*
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state.
* @returns {boolean} True to indicate that moderator tab should be visible, false otherwise.
*/
export function shouldShowModeratorSettings(stateful: IStateful) {
const state = toState(stateful);
const { hideModeratorSettingsTab } = getParticipantsPaneConfig(state);
const hasModeratorRights = Boolean(isSettingEnabled('moderator') && isLocalParticipantModerator(state));
return hasModeratorRights && !hideModeratorSettingsTab;
}
/**
* Returns the properties for the "Profile" tab from settings dialog from Redux
* state.
@@ -198,9 +171,15 @@ export function getProfileTabProps(stateful: IStateful) {
authLogin,
conference
} = state['features/base/conference'];
const { hideEmailInSettings } = state['features/base/config'];
const config = state['features/base/config'];
let { hideEmailInSettings } = config;
const localParticipant = getLocalParticipant(state);
if (config.gravatar?.disabled
|| (localParticipant?.avatarURL && localParticipant?.avatarURL.length > 0)) {
hideEmailInSettings = true;
}
return {
authEnabled: Boolean(conference && authEnabled),
authLogin,

View File

@@ -1,3 +1,8 @@
import { IStateful } from '../base/app/types';
import { isLocalParticipantModerator } from '../base/participants/functions';
import { toState } from '../base/redux/functions';
import { getParticipantsPaneConfig } from '../participants-pane/functions';
export * from './functions.any';
/**
@@ -16,3 +21,18 @@ export function getShortcutsTabProps(_stateful: any, _isDisplayedOnWelcomePage?:
keyboardShortcutsEnabled: false
};
}
/**
* Returns true if moderator tab in settings should be visible/accessible.
*
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state.
* @returns {boolean} True to indicate that moderator tab should be visible, false otherwise.
*/
export function shouldShowModeratorSettings(stateful: IStateful) {
const state = toState(stateful);
const { hideModeratorSettingsTab } = getParticipantsPaneConfig(state);
const hasModeratorRights = isLocalParticipantModerator(state);
return hasModeratorRights && !hideModeratorSettingsTab;
}

View File

@@ -1,8 +1,10 @@
import { IStateful } from '../base/app/types';
import { createLocalTrack } from '../base/lib-jitsi-meet/functions';
import { isLocalParticipantModerator } from '../base/participants/functions';
import { toState } from '../base/redux/functions';
import { getUserSelectedCameraDeviceId } from '../base/settings/functions.web';
import { areKeyboardShortcutsEnabled, getKeyboardShortcutsHelpDescriptions } from '../keyboard-shortcuts/functions';
import { getParticipantsPaneConfig } from '../participants-pane/functions';
import { isPrejoinPageVisible } from '../prejoin/functions';
export * from './functions.any';
@@ -115,3 +117,31 @@ export function getVirtualBackgroundTabProps(stateful: IStateful, isDisplayedOnW
selectedVideoInputId
};
}
/**
* Used for web. Indicates if the setting section is enabled.
*
* @param {string} settingName - The name of the setting section as defined in
* interface_config.js and SettingsMenu.js.
* @returns {boolean} True to indicate that the given setting section
* is enabled, false otherwise.
*/
export function isSettingEnabled(settingName: string) {
return interfaceConfig.SETTINGS_SECTIONS.includes(settingName);
}
/**
* Returns true if moderator tab in settings should be visible/accessible.
*
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state.
* @returns {boolean} True to indicate that moderator tab should be visible, false otherwise.
*/
export function shouldShowModeratorSettings(stateful: IStateful) {
const state = toState(stateful);
const { hideModeratorSettingsTab } = getParticipantsPaneConfig(state);
const hasModeratorRights = Boolean(isSettingEnabled('moderator') && isLocalParticipantModerator(state));
return hasModeratorRights && !hideModeratorSettingsTab;
}

View File

@@ -103,14 +103,13 @@ class ProfileButton extends AbstractButton<IProps> {
* @returns {Object}
*/
const mapStateToProps = (state: IReduxState) => {
const { defaultLocalDisplayName, disableProfile } = state['features/base/config'];
const { defaultLocalDisplayName } = state['features/base/config'];
return {
_defaultLocalDisplayName: defaultLocalDisplayName ?? '',
_localParticipant: getLocalParticipant(state),
_unclickable: !interfaceConfig.SETTINGS_SECTIONS.includes('profile'),
customClass: 'profile-button-avatar',
visible: !disableProfile
customClass: 'profile-button-avatar'
};
};