Compare commits
24 Commits
7475
...
jitsi-meet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a47cb595db | ||
|
|
86ccc176e8 | ||
|
|
b31041f0ce | ||
|
|
e434a78de9 | ||
|
|
6ddb77e03c | ||
|
|
f6665d79c0 | ||
|
|
75a7b99a42 | ||
|
|
3858a40c1c | ||
|
|
3d6aa8f2b5 | ||
|
|
54436f97c1 | ||
|
|
dca40dc6cb | ||
|
|
c19d91a373 | ||
|
|
840cfd8ab0 | ||
|
|
8cf6ba88e1 | ||
|
|
4ac81b030e | ||
|
|
e5cd1b29fe | ||
|
|
ebc932572f | ||
|
|
f0a6c6e67e | ||
|
|
e1454b5c4a | ||
|
|
f6e2abdf01 | ||
|
|
e2a02f4b21 | ||
|
|
934d7db24e | ||
|
|
2b520cbc4c | ||
|
|
e37dd73b9e |
@@ -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;
|
||||
|
||||
|
||||
2264
lang/main-sq.json
2264
lang/main-sq.json
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
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
3
modules/API/external/external_api.js
vendored
3
modules/API/external/external_api.js
vendored
@@ -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',
|
||||
|
||||
@@ -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
61
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
4
react/features/base/tracks/constants.ts
Normal file
4
react/features/base/tracks/constants.ts
Normal 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';
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
1
react/features/conference/middleware.native.ts
Normal file
1
react/features/conference/middleware.native.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './middleware.any';
|
||||
45
react/features/conference/middleware.web.ts
Normal file
45
react/features/conference/middleware.web.ts
Normal 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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,7 +44,9 @@ export const screen = {
|
||||
help: 'Help',
|
||||
privacy: 'Privacy',
|
||||
terms: 'Terms'
|
||||
}
|
||||
},
|
||||
profile: 'Profile',
|
||||
language: 'Language'
|
||||
},
|
||||
unsafeRoomWarning: 'Unsafe Room Warning'
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
114
react/features/settings/components/native/AdvancedSection.tsx
Normal file
114
react/features/settings/components/native/AdvancedSection.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
83
react/features/settings/components/native/GeneralSection.tsx
Normal file
83
react/features/settings/components/native/GeneralSection.tsx
Normal 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;
|
||||
@@ -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;
|
||||
57
react/features/settings/components/native/LinksSection.tsx
Normal file
57
react/features/settings/components/native/LinksSection.tsx
Normal 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;
|
||||
21
react/features/settings/components/native/LogoutDialog.tsx
Normal file
21
react/features/settings/components/native/LogoutDialog.tsx
Normal 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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
160
react/features/settings/components/native/ProfileView.tsx
Normal file
160
react/features/settings/components/native/ProfileView.tsx
Normal 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;
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
1
react/features/settings/components/native/constants.ts
Normal file
1
react/features/settings/components/native/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const AVATAR_SIZE = 64;
|
||||
@@ -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]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user