Compare commits

...

44 Commits

Author SHA1 Message Date
Calinteodor
be06547cc4 feat(notifications/native): ui fixes (#14786)
* feat(notifications/native): ui fixes
2024-05-30 11:38:47 +03:00
Mihaela Dumitru
bd631f5968 fix(subtitles) show subtitles even without reactions enabled (#14784) 2024-05-29 14:55:47 +03:00
Calin-Teodor
bc69a35cd6 feat(react-native-sdk): match any version between react native and metro config 2024-05-28 19:39:40 +03:00
Calinteodor
75e7f64311 feat(polls/web/native): fixed identical answers verification (#14782)
* feat(polls/web/native): fixed identical answers verification
2024-05-28 14:06:11 +03:00
Nitin Kushwaha
cdcb4cdbfb fix(polls): add vertical margin to create poll button (android) 2024-05-27 18:31:40 +03:00
Calinteodor
c31ef2ebc7 feat(polls/web/native): refactoring (#14778)
* feat(polls/web/native): refactoring
2024-05-27 12:14:17 +03:00
zobadaniel
d49a419284 feat(lang) add upper sorbian to transcription languages
* fix showing transcripts for country codes longer than 2 characters
2024-05-24 15:18:38 +02:00
Hristo Terezov
0913554af9 feat(MainToolbar): implement custom order.
As part of the PR, it also fixes:
 - Removes button aliases
 - Unifies the keys in the object returned by getAllToolboxButtons and the button keys
 - Makes sure that the number of buttons displayed are always the same as the number of buttons specified in the thresholds and removes the exception for not filling up the main toolbar with buttons from overflow menu when reactions button is disabled.
 - Introduces a priority for buttons that will be used to fill empty spaces in the main toolbar.
2024-05-23 12:34:15 -05:00
Aaron van Meerten
9af0003c63 feat(invite): move JWT to Authorization header for invite and search endpoints (#14777) 2024-05-23 11:07:33 -05:00
Calinteodor
3ecc16dc87 feat(polls): rework (#14645)
* feat(polls/web/native): rework polls to manage editing and saving before sending to other participants
2024-05-22 15:03:24 +03:00
Jaya Allamsetty
ef387c939a chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1823.0.0+ec98b020...v1830.0.0+5a14bd43
2024-05-21 13:27:01 -04:00
HannesOberreiter
76e3608baf feat(web) add config to hide login button on WaitForOwnerDialog 2024-05-21 13:39:35 +02:00
bgrozev
68384121cd fix: Fix room_jid in room_metadata message when a tenant is set. (#14765) 2024-05-20 13:15:44 -07:00
Aaron van Meerten
2ed0418bd9 fix(jwt): Fixes parsing JWT in hash as JSON instead of a string (#14760) 2024-05-17 11:29:11 -05:00
Saúl Ibarra Corretgé
5eb4064390 feat(gh) use new form based issue and feature request templates 2024-05-17 15:24:45 +02:00
Saúl Ibarra Corretgé
3f943feb4a feat(ci) use a more future-proof way of installing Node in CI
Install the version based on .nvmrc rather than speficying it in the
action.
2024-05-17 15:24:02 +02:00
Mihaela Dumitru
861b32367e fix(recordings) extend notification timeout (#14758) 2024-05-17 13:03:31 +03:00
damencho
cfca0aea4a fix: Drop nodejs build dependency.
Out build process does not actually require nodejs. We assume node/npm is installed on the machine. Dropping it so we can use just nvm to control the versions via .npmrc.
2024-05-16 15:03:59 -05:00
Aaron van Meerten
43ae5915a6 feat: move jwt to URL hash from search parameters (#14749)
* feat: move jwt to URL hash from search parameters

* fix lint

* comments for future work

* comment format

* fix to use ts-ignore like previous parseURLParams case
2024-05-16 13:03:12 -05:00
Horatiu Muresan
7c22bcf827 chore(node): Increment node ver to 20 (#13445)
* chore(node) Update to node 20.x

* fix

* remove accidental file
2024-05-16 12:39:02 -05:00
Calinteodor
1bb9b261dc chore(deps, react-native): update to 0.72.14 (#14741)
* chore(deps, react-native): changes regarding react native update to 0.72.14
2024-05-16 17:16:36 +03:00
Дамян Минков
a95adcdf41 fix(visitors): Drop use of APP, breaks mobile. (#14746) 2024-05-15 08:22:15 -05:00
damencho
6758d13335 fix: Fixes swagger syntax. 2024-05-15 07:42:48 -05:00
Shawn
10ab4df287 fix(premeeting/conference/prejoin): hideConferenceSubject hides subject on prejoin 2024-05-14 07:48:21 -05:00
Emmanuel Pelletier
f3e4a7a9c8 fix(lang) update screen reader related french translation
- settings string contained a string saying "show / hide settings", but
we should match the english string saying only "show settings" as the
button is only used to show the settings (its a dialog you can't go out of, so
there is no way to go back to the settings button to close, the "hide" case
doesn't exist)

- use "Activer" instead of "Rétablir" and "Démarrer" for toggle on states,
as this wording is used accross lots of other buttons, it makes more sense
to use the same wording

- change the unmute/mute audio/video strings to tell the user its *their*
device they toggle. There are a few spots in this app where we label stuff
as "mute microphone", sometimes its for you, sometimes its for other people.
The idea here is to say "Mute your microphone" instead of "Mute microphone"
so that screen reader users fully understand what they do.
2024-05-13 17:58:09 -05:00
Calinteodor
854503aec2 feat(react-native-sdk): Update rnsdk script dependencies (#14739)
* feat(rnsdk): moved metro-config dep to devDeps
2024-05-13 21:43:48 +03:00
damencho
353c3cdd34 fix(visitors): Stops processing pre-join on error reply. 2024-05-13 12:31:15 -05:00
garysmith058
8f7ab33508 feat(external-api) Forward CONFERENCE_CREATED_TIMESTAMP to iframe 2024-05-10 16:49:47 +02:00
damencho
383b534753 feat: Introduce utility for checking sip jigasi stanzas. 2024-05-08 15:47:54 -05:00
damencho
6691f56d0e feat: Checking for visitors in available hosts.
Supports different domains for visitors, including one for jigasi and jigasis to be visitors.
2024-05-08 15:47:54 -05:00
Mihaela Dumitru
50d4b6250c chore(deps) bump excalidraw version (#14641) 2024-05-08 14:54:03 +03:00
Hristo Terezov
daa840564c fix(createAndAddInitialAVTracks): async/await logic. 2024-05-07 18:35:05 -05:00
Hristo Terezov
2cdf77272c feat(createInitialAVTracks): Improve handling/displaying errors. 2024-05-07 18:35:05 -05:00
Hristo Terezov
53299a19c2 fix(visitors): Use single GUM for enabling media on promotion.
Before we were using setAudioMuted and setVideoMuted which was effectively using separate GUM calls for audio and video. This was problematic in the case where GUM permissions prompt was displayed because two separate prompts were displayed.
2024-05-07 18:35:05 -05:00
Aaron van Meerten
e90b270b32 feat: allow client JWT in Authorization header (#14724)
* feat: allow client JWT in Authorization header

* cleaner check for initial header

* better comment for the reason for sub(8

* allow query value to override authorization header
2024-05-07 13:59:35 -05:00
Francois Marier
24ce8c5831 fix(ios) use the correct organization name 2024-05-07 11:30:13 +02:00
Edgars Voroboks
57bd074d00 fix(lang): Make existing phrasing in Latvian language more clear (#14721)
* fix(lang): More updates on existing phrasing in Latvian language

* fix(lang): Update Latvian language translation
2024-05-06 13:48:37 -05:00
Edgars Voroboks
843cdf05f5 fix(lang): Update Latvian language translation (#14720)
* fix(lang): Update Latvian language translation

* fix(lang): Use better wording in Latvian translation
2024-05-06 11:50:57 -05:00
Saúl Ibarra Corretgé
ede8f7ece9 chore(deps) react-native-webview@13.8.7
Fixes: https://github.com/jitsi/jitsi-meet-flutter-sdk/issues/60
2024-05-06 14:28:03 +02:00
Saúl Ibarra Corretgé
e5b736243d chore(deps) run npm audit fix 2024-05-06 14:27:31 +02:00
Saúl Ibarra Corretgé
1d8a9c11c8 feat(ci) run CI also on macOS on arm64
The macOS-13 image runners are x64, but macOS-14 are arm64.
2024-05-06 14:27:18 +02:00
Christoph Settgast
72d05bb969 feat(config) allow overriding desktop deeplinking toggle (#14712) 2024-05-06 11:44:51 +02:00
Christoph Settgast
2a5fc8cc4a lang: update German translation (#14711)
Signed-off-by: Christoph Settgast <csett86_git@quicksands.de>
2024-05-05 20:56:02 +02:00
damencho
aa9bd8c814 fix: Fixes visitors component.
In f279e63 there is some commented code that sneaked in by mistake.
2024-05-03 17:38:06 -05:00
89 changed files with 2370 additions and 2781 deletions

View File

@@ -3,7 +3,7 @@
"image": "mcr.microsoft.com/devcontainers/universal:2",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "16"
"version": "20"
}
},
"hostRequirements": {

View File

@@ -1,48 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
---
<!--
This issue tracker is only for reporting bugs and tracking issues related to the source code.
Before posting, please make sure to check if the same or similar bugs have already been discussed: https://github.com/jitsi/jitsi-meet/issues
General questions regarding usage, installation, etc. should be posted at https://community.jitsi.org. They will be closed if posted here.
-->
### Description:
<!-- Please describe the bug clearly and concisely. -->
### Steps to reproduce:
1. <!-- Open '...' -->
2. <!-- Click on '...' -->
3. <!-- and so on... -->
### Expected behavior:
<!-- Please describe what should happen. -->
### Actual behavior:
<!-- Please describe what actually happens. -->
<!-- Please attach screenshot if possible. -->
### Server information:
- Jitsi Meet version:
- Operating System:
### Client information:
- Browser / app version:
- Operating System:
### Additional information:
<!-- Please provide additional information about the bug, if any. -->

55
.github/ISSUE_TEMPLATE/1-bug.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Bug report
description: File a bug report and help us improve
body:
- type: markdown
attributes:
value: |
This issue tracker is only for reporting bugs and tracking issues related to the source code.
**Before posting, please make sure to check if the same or similar bugs have already been reported.**
⚠️ General questions regarding usage, installation, etc. should be posted in our [community forum](https://community.jitsi.org).
- type: textarea
attributes:
label: What happened?
description: Please describe the problem. Be as detailed as possible.
validations:
required: true
- type: checkboxes
attributes:
label: Platform
description: On what platforms can you reproduce the problem?
options:
- label: Chrome (or Chromium based)
- label: Firefox
- label: Safari
- label: Other desktop browser
- label: Android browser
- label: iOS browser
- label: Electron app
- label: Android mobile app
- label: iOS mobile app
- label: Custom app using a mobile SDK
validations:
required: true
- type: input
attributes:
label: Browser / app / sdk version
description: Please provice the version of the browser / app / sdk where the problem manifests.
validations:
required: true
- type: textarea
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. The browser console JS logs (if applicable) is a good start. This will be automatically formatted into code, so no need for backticks.
render: shell
- type: checkboxes
attributes:
label: Reproducibility
description: Does the problem reproduce on meet.jit.si using Chrome, Firefox or the official mobile apps?
options:
- label: The problem is reproducible on meet.jit.si
- type: textarea
attributes:
label: More details?
description: Please provide more details in case they apply (such as the Jitsi Meet version you are running, if you are hosting your own server).

View File

@@ -1,25 +0,0 @@
---
name: "Feature request"
about: Suggest an idea for this project
title: ''
labels: 'feature-request'
assignees: ''
---
<!--
Thank you for suggesting an idea to make Jitsi Meet better.
Please fill in as much of the template below as you're able.
Note that the ultimate decision for implementing features lies on the Jitsi team, not all feature requests shall be accepted.
-->
**Is your feature request related to a problem you are facing?**
Please describe the problem you are trying to solve.
**Describe the solution you'd like**
Please describe the desired behavior.
**Describe alternatives you've considered**
Please describe alternative solutions or features you have considered.

22
.github/ISSUE_TEMPLATE/2-feature.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Feature request
description: Suggest an idea for Jitsi Meet
labels: ["feature-request"]
body:
- type: markdown
attributes:
value: |
Thank you for suggesting an idea to make Jitsi Meet better.
**Note**: the ultimate decision for implementing features lies on the Jitsi team, not all feature requests shall be accepted.
- type: textarea
attributes:
label: What problem are you trying to solve?
description: Tell us what problem your feature request would solve.
- type: textarea
attributes:
label: What solution would you like to see?
description: Please describe the desired behavior or feature.
- type: textarea
attributes:
label: Is there an alternative?
description: Please describe alternative solutions or features you have considered.

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Need help with Jitsi Meet?
- name: Need help with your Jitsi Meet installation?
url: https://community.jitsi.org
about: Please ask it in our community.
about: Please ask it in our community forum.

View File

@@ -10,8 +10,12 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 16
node-version-file: '.nvmrc'
cache: 'npm'
- name: Check Node / npm versions
run: |
node -v
npm -v
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v41
@@ -37,8 +41,12 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 16
node-version-file: '.nvmrc'
cache: 'npm'
- name: Check Node / npm versions
run: |
node -v
npm -v
- run: npm install
- run: make
macos-ci:
@@ -48,7 +56,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 16
node-version: 20
cache: 'npm'
- run: npm install
- run: make
@@ -59,22 +67,35 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 16
node-version-file: '.nvmrc'
cache: 'npm'
- name: Check Node / npm versions
run: |
node -v
npm -v
- run: npm install
- run: npx react-native bundle --entry-file react/index.native.js --platform android --bundle-output /tmp/android.bundle --reset-cache
ios-build:
name: Build mobile bundle (iOS)
runs-on: macOS-13
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-13, macos-14]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 16
node-version-file: '.nvmrc'
cache: 'npm'
- name: Check Node / npm versions
run: |
node -v
npm -v
- run: npm install
- name: setup Xcode
run: |
uname -a
xcode-select -p
sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer
xcodebuild -version
@@ -97,8 +118,12 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 16
node-version-file: '.nvmrc'
cache: 'npm'
- name: Check Node / npm versions
run: |
node -v
npm -v
- run: npm install
- run: make
- run: sudo apt-get install -y debhelper

2
.nvmrc
View File

@@ -1 +1 @@
16
20

View File

@@ -4,7 +4,6 @@ import { jitsiLocalStorage } from '@jitsi/js-utils';
import Logger from '@jitsi/logger';
import { ENDPOINT_TEXT_MESSAGE_NAME } from './modules/API/constants';
import { AUDIO_ONLY_SCREEN_SHARE_NO_TRACK } from './modules/UI/UIErrors';
import mediaDeviceHelper from './modules/devices/mediaDeviceHelper';
import Recorder from './modules/recorder/Recorder';
import { createTaskQueue } from './modules/util/helpers';
@@ -76,7 +75,6 @@ import {
JitsiConferenceEvents,
JitsiE2ePingEvents,
JitsiMediaDevicesEvents,
JitsiTrackErrors,
JitsiTrackEvents,
browser
} from './react/features/base/lib-jitsi-meet';
@@ -117,8 +115,11 @@ import {
import { updateSettings } from './react/features/base/settings/actions';
import {
addLocalTrack,
createInitialAVTracks,
destroyLocalTracks,
displayErrorsForCreateInitialLocalTracks,
replaceLocalTrack,
setGUMPendingStateOnFailedTracks,
toggleScreensharing as toggleScreensharingA,
trackAdded,
trackRemoved
@@ -383,27 +384,6 @@ function disconnect() {
return APP.connection.disconnect().then(onDisconnected, onDisconnected);
}
/**
* Sets the GUM pending state for the tracks that have failed.
*
* NOTE: Some of the track that we will be setting to GUM pending state NONE may not have failed but they may have
* been requested. This won't be a problem because their current GUM pending state will be NONE anyway.
* @param {JitsiLocalTrack} tracks - The tracks that have been created.
* @returns {void}
*/
function setGUMPendingStateOnFailedTracks(tracks) {
const tracksTypes = tracks.map(track => {
if (track.getVideoType() === VIDEO_TYPE.DESKTOP) {
return MEDIA_TYPE.SCREENSHARE;
}
return track.getType();
});
const nonPendingTracks = [ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ].filter(type => !tracksTypes.includes(type));
APP.store.dispatch(gumPending(nonPendingTracks, IGUMPendingState.NONE));
}
export default {
/**
* Flag used to delay modification of the muted status of local media tracks
@@ -502,57 +482,12 @@ export default {
return [];
});
} else if (requestedAudio || requestedVideo) {
APP.store.dispatch(gumPending(initialDevices, IGUMPendingState.PENDING_UNMUTE));
tryCreateLocalTracks = createLocalTracksF({
tryCreateLocalTracks = APP.store.dispatch(createInitialAVTracks({
devices: initialDevices,
timeout,
firePermissionPromptIsShownEvent: true
})
.catch(async error => {
if (error.name === JitsiTrackErrors.TIMEOUT && !browser.isElectron()) {
errors.audioAndVideoError = error;
return [];
}
// Retry with separate gUM calls.
const gUMPromises = [];
const tracks = [];
if (requestedAudio) {
gUMPromises.push(createLocalTracksF(audioOptions));
}
if (requestedVideo) {
gUMPromises.push(createLocalTracksF({
devices: [ MEDIA_TYPE.VIDEO ],
timeout,
firePermissionPromptIsShownEvent: true
}));
}
const results = await Promise.allSettled(gUMPromises);
let errorMsg;
results.forEach((result, idx) => {
if (result.status === 'fulfilled') {
tracks.push(result.value[0]);
} else {
errorMsg = result.reason;
const isAudio = idx === 0;
logger.error(`${isAudio ? 'Audio' : 'Video'} track creation failed with error ${errorMsg}`);
if (isAudio) {
errors.audioOnlyError = errorMsg;
} else {
errors.videoOnlyError = errorMsg;
}
}
});
if (errors.audioOnlyError && errors.videoOnlyError) {
errors.audioAndVideoError = errorMsg;
}
})).then(({ tracks, errors: pErrors }) => {
Object.assign(errors, pErrors);
return tracks;
});
@@ -573,42 +508,6 @@ export default {
};
},
/**
* Displays error notifications according to the state carried by {@code errors} object returned
* by {@link createInitialLocalTracks}.
* @param {Object} errors - the errors (if any) returned by {@link createInitialLocalTracks}.
*
* @returns {void}
* @private
*/
_displayErrorsForCreateInitialLocalTracks(errors) {
const {
audioAndVideoError,
audioOnlyError,
screenSharingError,
videoOnlyError
} = errors;
// FIXME If there will be microphone error it will cover any screensharing dialog, but it's still better than in
// the reverse order where the screensharing dialog will sometimes be closing the microphone alert
// ($.prompt.close(); is called). Need to figure out dialogs chaining to fix that.
if (screenSharingError) {
this._handleScreenSharingError(screenSharingError);
}
if (audioAndVideoError || audioOnlyError) {
if (audioOnlyError || videoOnlyError) {
// If both requests for 'audio' + 'video' and 'audio' only failed, we assume that there are some
// problems with user's microphone and show corresponding dialog.
APP.store.dispatch(notifyMicError(audioOnlyError));
APP.store.dispatch(notifyCameraError(videoOnlyError));
} else {
// If request for 'audio' + 'video' failed, but request for 'audio' only was OK, we assume that we had
// problems with camera and show corresponding dialog.
APP.store.dispatch(notifyCameraError(audioAndVideoError));
}
}
},
startConference(tracks) {
tracks.forEach(track => {
if ((track.isAudioTrack() && this.isLocalAudioMuted())
@@ -724,11 +623,11 @@ export default {
logger.debug('Prejoin screen no longer displayed at the time when tracks were created');
this._displayErrorsForCreateInitialLocalTracks(errors);
APP.store.dispatch(displayErrorsForCreateInitialLocalTracks(errors));
const tracks = handleInitialTracks(initialOptions, localTracks);
setGUMPendingStateOnFailedTracks(tracks);
setGUMPendingStateOnFailedTracks(tracks, APP.store.dispatch);
return this._setLocalAudioVideoStreams(tracks);
}
@@ -737,7 +636,7 @@ export default {
return Promise.all([
tryCreateLocalTracks.then(tr => {
this._displayErrorsForCreateInitialLocalTracks(errors);
APP.store.dispatch(displayErrorsForCreateInitialLocalTracks(errors));
return tr;
}).then(tr => {
@@ -745,7 +644,7 @@ export default {
const filteredTracks = handleInitialTracks(initialOptions, tr);
setGUMPendingStateOnFailedTracks(filteredTracks);
setGUMPendingStateOnFailedTracks(filteredTracks, APP.store.dispatch);
return filteredTracks;
}),
@@ -1226,7 +1125,7 @@ export default {
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(options);
const localTracks = await tryCreateLocalTracks;
this._displayErrorsForCreateInitialLocalTracks(errors);
APP.store.dispatch(displayErrorsForCreateInitialLocalTracks(errors));
localTracks.forEach(track => {
if ((track.isAudioTrack() && this.isLocalAudioMuted())
|| (track.isVideoTrack() && this.isLocalVideoMuted())) {
@@ -1566,50 +1465,6 @@ export default {
});
},
/**
* Handles {@link JitsiTrackError} returned by the lib-jitsi-meet when
* trying to create screensharing track. It will either do nothing if
* the dialog was canceled on user's request or display an error if
* screensharing couldn't be started.
* @param {JitsiTrackError} error - The error returned by
* {@link _createDesktopTrack} Promise.
* @private
*/
_handleScreenSharingError(error) {
if (error.name === JitsiTrackErrors.SCREENSHARING_USER_CANCELED) {
return;
}
logger.error('failed to share local desktop', error);
// Handling:
// JitsiTrackErrors.CONSTRAINT_FAILED
// JitsiTrackErrors.PERMISSION_DENIED
// JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR
// and any other
let descriptionKey;
let titleKey;
if (error.name === JitsiTrackErrors.PERMISSION_DENIED) {
descriptionKey = 'dialog.screenSharingPermissionDeniedError';
titleKey = 'dialog.screenSharingFailedTitle';
} else if (error.name === JitsiTrackErrors.CONSTRAINT_FAILED) {
descriptionKey = 'dialog.cameraConstraintFailedError';
titleKey = 'deviceError.cameraError';
} else if (error.name === JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR) {
descriptionKey = 'dialog.screenSharingFailed';
titleKey = 'dialog.screenSharingFailedTitle';
} else if (error === AUDIO_ONLY_SCREEN_SHARE_NO_TRACK) {
descriptionKey = 'notify.screenShareNoAudio';
titleKey = 'notify.screenShareNoAudioTitle';
}
APP.store.dispatch(showErrorNotification({
descriptionKey,
titleKey
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
},
/**
* Setup interaction between conference and UI.
*/
@@ -1782,7 +1637,11 @@ export default {
room.on(
JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP,
conferenceTimestamp => APP.store.dispatch(conferenceTimestampChanged(conferenceTimestamp)));
conferenceTimestamp => {
APP.store.dispatch(conferenceTimestampChanged(conferenceTimestamp));
APP.API.notifyConferenceCreatedTimestamp(conferenceTimestamp);
}
);
room.on(
JitsiConferenceEvents.DISPLAY_NAME_CHANGED,

View File

@@ -848,6 +848,22 @@ var config = {
// autoHideWhileChatIsOpen: false,
// },
// Overrides the buttons displayed in the main toolbar. Depending on the screen size the number of displayed
// buttons varies from 2 buttons to 8 buttons. Every array in the mainToolbarButtons array will replace the
// corresponding default buttons configuration matched by the number of buttons specified in the array. Arrays with
// more than 8 buttons or less then 2 buttons will be ignored. When there there isn't an override for a cerain
// configuration (for example when 3 buttons are displayed) the default jitsi-meet configuration will be used.
// The order of the buttons in the array is preserved.
// mainToolbarButtons: [
// [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'reactions', 'participants-pane', 'tileview' ],
// [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants-pane', 'tileview' ],
// [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants-pane' ],
// [ 'microphone', 'camera', 'desktop', 'chat', 'participants-pane' ],
// [ 'microphone', 'camera', 'chat', 'participants-pane' ],
// [ 'microphone', 'camera', 'chat' ],
// [ 'microphone', 'camera' ]
// ],
// Toolbar buttons which have their click/tap event exposed through the API on
// `toolbarButtonClicked`. Passing a string for the button key will
// prevent execution of the click/tap routine; passing an object with `key` and
@@ -1790,6 +1806,9 @@ var config = {
// collectionInterval?: number;
// logGetStats?: boolean;
// },
// Hide login button on auth dialog, you may want to enable this if you are using JWT tokens to authenticate users
// hideLoginButton: true,
};
// Temporary backwards compatibility with old mobile clients.

2
debian/control vendored
View File

@@ -3,7 +3,7 @@ Section: net
Priority: extra
Maintainer: Jitsi Team <dev@jitsi.org>
Uploaders: Emil Ivov <emcho@jitsi.org>, Damian Minkov <damencho@jitsi.org>
Build-Depends: debhelper (>= 8.0.0), nodejs
Build-Depends: debhelper (>= 8.0.0)
Standards-Version: 3.9.6
Homepage: https://jitsi.org/meet

View File

@@ -14,14 +14,14 @@ PODS:
- CocoaLumberjack/Core (= 3.7.2)
- CocoaLumberjack/Core (3.7.2)
- DoubleConversion (1.1.6)
- FBLazyVector (0.72.9)
- FBReactNativeSpec (0.72.9):
- FBLazyVector (0.72.14)
- FBReactNativeSpec (0.72.14):
- RCT-Folly (= 2021.07.22.00)
- RCTRequired (= 0.72.9)
- RCTTypeSafety (= 0.72.9)
- React-Core (= 0.72.9)
- React-jsi (= 0.72.9)
- ReactCommon/turbomodule/core (= 0.72.9)
- RCTRequired (= 0.72.14)
- RCTTypeSafety (= 0.72.14)
- React-Core (= 0.72.14)
- React-jsi (= 0.72.14)
- ReactCommon/turbomodule/core (= 0.72.14)
- Firebase/Analytics (8.15.0):
- Firebase/Core
- Firebase/Core (8.15.0):
@@ -167,26 +167,26 @@ PODS:
- DoubleConversion
- fmt (~> 6.2.1)
- glog
- RCTRequired (0.72.9)
- RCTTypeSafety (0.72.9):
- FBLazyVector (= 0.72.9)
- RCTRequired (= 0.72.9)
- React-Core (= 0.72.9)
- React (0.72.9):
- React-Core (= 0.72.9)
- React-Core/DevSupport (= 0.72.9)
- React-Core/RCTWebSocket (= 0.72.9)
- React-RCTActionSheet (= 0.72.9)
- React-RCTAnimation (= 0.72.9)
- React-RCTBlob (= 0.72.9)
- React-RCTImage (= 0.72.9)
- React-RCTLinking (= 0.72.9)
- React-RCTNetwork (= 0.72.9)
- React-RCTSettings (= 0.72.9)
- React-RCTText (= 0.72.9)
- React-RCTVibration (= 0.72.9)
- React-callinvoker (0.72.9)
- React-Codegen (0.72.9):
- RCTRequired (0.72.14)
- RCTTypeSafety (0.72.14):
- FBLazyVector (= 0.72.14)
- RCTRequired (= 0.72.14)
- React-Core (= 0.72.14)
- React (0.72.14):
- React-Core (= 0.72.14)
- React-Core/DevSupport (= 0.72.14)
- React-Core/RCTWebSocket (= 0.72.14)
- React-RCTActionSheet (= 0.72.14)
- React-RCTAnimation (= 0.72.14)
- React-RCTBlob (= 0.72.14)
- React-RCTImage (= 0.72.14)
- React-RCTLinking (= 0.72.14)
- React-RCTNetwork (= 0.72.14)
- React-RCTSettings (= 0.72.14)
- React-RCTText (= 0.72.14)
- React-RCTVibration (= 0.72.14)
- React-callinvoker (0.72.14)
- React-Codegen (0.72.14):
- DoubleConversion
- FBReactNativeSpec
- glog
@@ -201,10 +201,10 @@ PODS:
- React-rncore
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- React-Core (0.72.9):
- React-Core (0.72.14):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default (= 0.72.9)
- React-Core/Default (= 0.72.14)
- React-cxxreact
- React-jsc
- React-jsi
@@ -214,7 +214,7 @@ PODS:
- React-utils
- SocketRocket (= 0.6.1)
- Yoga
- React-Core/CoreModulesHeaders (0.72.9):
- React-Core/CoreModulesHeaders (0.72.14):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
@@ -227,7 +227,7 @@ PODS:
- React-utils
- SocketRocket (= 0.6.1)
- Yoga
- React-Core/Default (0.72.9):
- React-Core/Default (0.72.14):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-cxxreact
@@ -239,22 +239,22 @@ PODS:
- React-utils
- SocketRocket (= 0.6.1)
- Yoga
- React-Core/DevSupport (0.72.9):
- React-Core/DevSupport (0.72.14):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default (= 0.72.9)
- React-Core/RCTWebSocket (= 0.72.9)
- React-Core/Default (= 0.72.14)
- React-Core/RCTWebSocket (= 0.72.14)
- React-cxxreact
- React-jsc
- React-jsi
- React-jsiexecutor
- React-jsinspector (= 0.72.9)
- React-jsinspector (= 0.72.14)
- React-perflogger
- React-runtimeexecutor
- React-utils
- SocketRocket (= 0.6.1)
- Yoga
- React-Core/RCTActionSheetHeaders (0.72.9):
- React-Core/RCTActionSheetHeaders (0.72.14):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
@@ -267,7 +267,7 @@ PODS:
- React-utils
- SocketRocket (= 0.6.1)
- Yoga
- React-Core/RCTAnimationHeaders (0.72.9):
- React-Core/RCTAnimationHeaders (0.72.14):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
@@ -280,7 +280,7 @@ PODS:
- React-utils
- SocketRocket (= 0.6.1)
- Yoga
- React-Core/RCTBlobHeaders (0.72.9):
- React-Core/RCTBlobHeaders (0.72.14):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
@@ -293,7 +293,7 @@ PODS:
- React-utils
- SocketRocket (= 0.6.1)
- Yoga
- React-Core/RCTImageHeaders (0.72.9):
- React-Core/RCTImageHeaders (0.72.14):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
@@ -306,7 +306,7 @@ PODS:
- React-utils
- SocketRocket (= 0.6.1)
- Yoga
- React-Core/RCTLinkingHeaders (0.72.9):
- React-Core/RCTLinkingHeaders (0.72.14):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
@@ -319,7 +319,7 @@ PODS:
- React-utils
- SocketRocket (= 0.6.1)
- Yoga
- React-Core/RCTNetworkHeaders (0.72.9):
- React-Core/RCTNetworkHeaders (0.72.14):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
@@ -332,7 +332,7 @@ PODS:
- React-utils
- SocketRocket (= 0.6.1)
- Yoga
- React-Core/RCTSettingsHeaders (0.72.9):
- React-Core/RCTSettingsHeaders (0.72.14):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
@@ -345,7 +345,7 @@ PODS:
- React-utils
- SocketRocket (= 0.6.1)
- Yoga
- React-Core/RCTTextHeaders (0.72.9):
- React-Core/RCTTextHeaders (0.72.14):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
@@ -358,7 +358,7 @@ PODS:
- React-utils
- SocketRocket (= 0.6.1)
- Yoga
- React-Core/RCTVibrationHeaders (0.72.9):
- React-Core/RCTVibrationHeaders (0.72.14):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default
@@ -371,10 +371,10 @@ PODS:
- React-utils
- SocketRocket (= 0.6.1)
- Yoga
- React-Core/RCTWebSocket (0.72.9):
- React-Core/RCTWebSocket (0.72.14):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-Core/Default (= 0.72.9)
- React-Core/Default (= 0.72.14)
- React-cxxreact
- React-jsc
- React-jsi
@@ -384,48 +384,48 @@ PODS:
- React-utils
- SocketRocket (= 0.6.1)
- Yoga
- React-CoreModules (0.72.9):
- React-CoreModules (0.72.14):
- RCT-Folly (= 2021.07.22.00)
- RCTTypeSafety (= 0.72.9)
- React-Codegen (= 0.72.9)
- React-Core/CoreModulesHeaders (= 0.72.9)
- React-jsi (= 0.72.9)
- RCTTypeSafety (= 0.72.14)
- React-Codegen (= 0.72.14)
- React-Core/CoreModulesHeaders (= 0.72.14)
- React-jsi (= 0.72.14)
- React-RCTBlob
- React-RCTImage (= 0.72.9)
- ReactCommon/turbomodule/core (= 0.72.9)
- React-RCTImage (= 0.72.14)
- ReactCommon/turbomodule/core (= 0.72.14)
- SocketRocket (= 0.6.1)
- React-cxxreact (0.72.9):
- React-cxxreact (0.72.14):
- boost (= 1.76.0)
- DoubleConversion
- glog
- RCT-Folly (= 2021.07.22.00)
- React-callinvoker (= 0.72.9)
- React-debug (= 0.72.9)
- React-jsi (= 0.72.9)
- React-jsinspector (= 0.72.9)
- React-logger (= 0.72.9)
- React-perflogger (= 0.72.9)
- React-runtimeexecutor (= 0.72.9)
- React-debug (0.72.9)
- React-jsc (0.72.9):
- React-jsc/Fabric (= 0.72.9)
- React-jsi (= 0.72.9)
- React-jsc/Fabric (0.72.9):
- React-jsi (= 0.72.9)
- React-jsi (0.72.9):
- React-callinvoker (= 0.72.14)
- React-debug (= 0.72.14)
- React-jsi (= 0.72.14)
- React-jsinspector (= 0.72.14)
- React-logger (= 0.72.14)
- React-perflogger (= 0.72.14)
- React-runtimeexecutor (= 0.72.14)
- React-debug (0.72.14)
- React-jsc (0.72.14):
- React-jsc/Fabric (= 0.72.14)
- React-jsi (= 0.72.14)
- React-jsc/Fabric (0.72.14):
- React-jsi (= 0.72.14)
- React-jsi (0.72.14):
- boost (= 1.76.0)
- DoubleConversion
- glog
- RCT-Folly (= 2021.07.22.00)
- React-jsiexecutor (0.72.9):
- React-jsiexecutor (0.72.14):
- DoubleConversion
- glog
- RCT-Folly (= 2021.07.22.00)
- React-cxxreact (= 0.72.9)
- React-jsi (= 0.72.9)
- React-perflogger (= 0.72.9)
- React-jsinspector (0.72.9)
- React-logger (0.72.9):
- React-cxxreact (= 0.72.14)
- React-jsi (= 0.72.14)
- React-perflogger (= 0.72.14)
- React-jsinspector (0.72.14)
- React-logger (0.72.14):
- glog
- react-native-background-timer (2.4.1):
- React-Core
@@ -456,9 +456,10 @@ PODS:
- react-native-webrtc (118.0.7):
- JitsiWebRTC (~> 118.0.0)
- React-Core
- react-native-webview (13.5.1):
- react-native-webview (13.8.7):
- RCT-Folly (= 2021.07.22.00)
- React-Core
- React-NativeModulesApple (0.72.9):
- React-NativeModulesApple (0.72.14):
- React-callinvoker
- React-Core
- React-cxxreact
@@ -466,17 +467,17 @@ PODS:
- React-runtimeexecutor
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- React-perflogger (0.72.9)
- React-RCTActionSheet (0.72.9):
- React-Core/RCTActionSheetHeaders (= 0.72.9)
- React-RCTAnimation (0.72.9):
- React-perflogger (0.72.14)
- React-RCTActionSheet (0.72.14):
- React-Core/RCTActionSheetHeaders (= 0.72.14)
- React-RCTAnimation (0.72.14):
- RCT-Folly (= 2021.07.22.00)
- RCTTypeSafety (= 0.72.9)
- React-Codegen (= 0.72.9)
- React-Core/RCTAnimationHeaders (= 0.72.9)
- React-jsi (= 0.72.9)
- ReactCommon/turbomodule/core (= 0.72.9)
- React-RCTAppDelegate (0.72.9):
- RCTTypeSafety (= 0.72.14)
- React-Codegen (= 0.72.14)
- React-Core/RCTAnimationHeaders (= 0.72.14)
- React-jsi (= 0.72.14)
- ReactCommon/turbomodule/core (= 0.72.14)
- React-RCTAppDelegate (0.72.14):
- RCT-Folly
- RCTRequired
- RCTTypeSafety
@@ -488,81 +489,81 @@ PODS:
- React-RCTNetwork
- React-runtimescheduler
- ReactCommon/turbomodule/core
- React-RCTBlob (0.72.9):
- React-RCTBlob (0.72.14):
- RCT-Folly (= 2021.07.22.00)
- React-Codegen (= 0.72.9)
- React-Core/RCTBlobHeaders (= 0.72.9)
- React-Core/RCTWebSocket (= 0.72.9)
- React-jsi (= 0.72.9)
- React-RCTNetwork (= 0.72.9)
- ReactCommon/turbomodule/core (= 0.72.9)
- React-RCTImage (0.72.9):
- React-Codegen (= 0.72.14)
- React-Core/RCTBlobHeaders (= 0.72.14)
- React-Core/RCTWebSocket (= 0.72.14)
- React-jsi (= 0.72.14)
- React-RCTNetwork (= 0.72.14)
- ReactCommon/turbomodule/core (= 0.72.14)
- React-RCTImage (0.72.14):
- RCT-Folly (= 2021.07.22.00)
- RCTTypeSafety (= 0.72.9)
- React-Codegen (= 0.72.9)
- React-Core/RCTImageHeaders (= 0.72.9)
- React-jsi (= 0.72.9)
- React-RCTNetwork (= 0.72.9)
- ReactCommon/turbomodule/core (= 0.72.9)
- React-RCTLinking (0.72.9):
- React-Codegen (= 0.72.9)
- React-Core/RCTLinkingHeaders (= 0.72.9)
- React-jsi (= 0.72.9)
- ReactCommon/turbomodule/core (= 0.72.9)
- React-RCTNetwork (0.72.9):
- RCTTypeSafety (= 0.72.14)
- React-Codegen (= 0.72.14)
- React-Core/RCTImageHeaders (= 0.72.14)
- React-jsi (= 0.72.14)
- React-RCTNetwork (= 0.72.14)
- ReactCommon/turbomodule/core (= 0.72.14)
- React-RCTLinking (0.72.14):
- React-Codegen (= 0.72.14)
- React-Core/RCTLinkingHeaders (= 0.72.14)
- React-jsi (= 0.72.14)
- ReactCommon/turbomodule/core (= 0.72.14)
- React-RCTNetwork (0.72.14):
- RCT-Folly (= 2021.07.22.00)
- RCTTypeSafety (= 0.72.9)
- React-Codegen (= 0.72.9)
- React-Core/RCTNetworkHeaders (= 0.72.9)
- React-jsi (= 0.72.9)
- ReactCommon/turbomodule/core (= 0.72.9)
- React-RCTSettings (0.72.9):
- RCTTypeSafety (= 0.72.14)
- React-Codegen (= 0.72.14)
- React-Core/RCTNetworkHeaders (= 0.72.14)
- React-jsi (= 0.72.14)
- ReactCommon/turbomodule/core (= 0.72.14)
- React-RCTSettings (0.72.14):
- RCT-Folly (= 2021.07.22.00)
- RCTTypeSafety (= 0.72.9)
- React-Codegen (= 0.72.9)
- React-Core/RCTSettingsHeaders (= 0.72.9)
- React-jsi (= 0.72.9)
- ReactCommon/turbomodule/core (= 0.72.9)
- React-RCTText (0.72.9):
- React-Core/RCTTextHeaders (= 0.72.9)
- React-RCTVibration (0.72.9):
- RCTTypeSafety (= 0.72.14)
- React-Codegen (= 0.72.14)
- React-Core/RCTSettingsHeaders (= 0.72.14)
- React-jsi (= 0.72.14)
- ReactCommon/turbomodule/core (= 0.72.14)
- React-RCTText (0.72.14):
- React-Core/RCTTextHeaders (= 0.72.14)
- React-RCTVibration (0.72.14):
- RCT-Folly (= 2021.07.22.00)
- React-Codegen (= 0.72.9)
- React-Core/RCTVibrationHeaders (= 0.72.9)
- React-jsi (= 0.72.9)
- ReactCommon/turbomodule/core (= 0.72.9)
- React-rncore (0.72.9)
- React-runtimeexecutor (0.72.9):
- React-jsi (= 0.72.9)
- React-runtimescheduler (0.72.9):
- React-Codegen (= 0.72.14)
- React-Core/RCTVibrationHeaders (= 0.72.14)
- React-jsi (= 0.72.14)
- ReactCommon/turbomodule/core (= 0.72.14)
- React-rncore (0.72.14)
- React-runtimeexecutor (0.72.14):
- React-jsi (= 0.72.14)
- React-runtimescheduler (0.72.14):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-callinvoker
- React-debug
- React-jsi
- React-runtimeexecutor
- React-utils (0.72.9):
- React-utils (0.72.14):
- glog
- RCT-Folly (= 2021.07.22.00)
- React-debug
- ReactCommon/turbomodule/bridging (0.72.9):
- ReactCommon/turbomodule/bridging (0.72.14):
- DoubleConversion
- glog
- RCT-Folly (= 2021.07.22.00)
- React-callinvoker (= 0.72.9)
- React-cxxreact (= 0.72.9)
- React-jsi (= 0.72.9)
- React-logger (= 0.72.9)
- React-perflogger (= 0.72.9)
- ReactCommon/turbomodule/core (0.72.9):
- React-callinvoker (= 0.72.14)
- React-cxxreact (= 0.72.14)
- React-jsi (= 0.72.14)
- React-logger (= 0.72.14)
- React-perflogger (= 0.72.14)
- ReactCommon/turbomodule/core (0.72.14):
- DoubleConversion
- glog
- RCT-Folly (= 2021.07.22.00)
- React-callinvoker (= 0.72.9)
- React-cxxreact (= 0.72.9)
- React-jsi (= 0.72.9)
- React-logger (= 0.72.9)
- React-perflogger (= 0.72.9)
- React-callinvoker (= 0.72.14)
- React-cxxreact (= 0.72.14)
- React-jsi (= 0.72.14)
- React-logger (= 0.72.14)
- React-perflogger (= 0.72.14)
- RNCalendarEvents (2.2.0):
- React
- RNCAsyncStorage (1.19.4):
@@ -830,8 +831,8 @@ SPEC CHECKSUMS:
boost: 7dcd2de282d72e344012f7d6564d024930a6a440
CocoaLumberjack: b7e05132ff94f6ae4dfa9d5bce9141893a21d9da
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: dc178b8748748c036ef9493a5d59d6d1f91a36ce
FBReactNativeSpec: d0aaae78e93c89dc2d691d8052a4d2aeb1b461ee
FBLazyVector: d98eefb42c5a64cb28ef966bd9096c76770d8f24
FBReactNativeSpec: 53d4eb00e8e1b6e987a3dd5906d2afe131cc54c8
Firebase: 5f8193dff4b5b7c5d5ef72ae54bb76c08e2b841d
FirebaseAnalytics: 7761cbadb00a717d8d0939363eb46041526474fa
FirebaseCore: 5743c5785c074a794d35f2fff7ecc254a91e08b1
@@ -856,20 +857,20 @@ SPEC CHECKSUMS:
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265
RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1
RCTRequired: f30c3213569b1dc43659ecc549a6536e1e11139e
RCTTypeSafety: e1ed3137728804fa98bce30b70e3da0b8e23054e
React: 54070abee263d5773486987f1cf3a3616710ed52
React-callinvoker: 794ea19cc4d8ce25921893141e131b9d6b7d02eb
React-Codegen: d5dc2cae27c93b0fab4241816343c586c58965c1
React-Core: f82477baba3526cf8a4f70055a22a8e878e3ef9c
React-CoreModules: 87cc386c2200862672b76bb02c4574b4b1d11b3c
React-cxxreact: c7163706a8391f373cd513348a8ebbd5a4444626
React-debug: 4dca41301a67ab2916b2c99bef60344a7b653ac5
React-jsc: 44ec1ce4171f4b6776810618da8e3da45899ea77
React-jsi: 799e7004db36b0b6da2d0969ce07bf3a96df8d19
React-jsiexecutor: bb34b280cfad63aedb266571a305b6365affb875
React-jsinspector: 54205b269da20c51417e0fc02c4cde9f29a4bf1a
React-logger: f42d2f2bc4cbb5d19d7c0ce84b8741b1e54e88c8
RCTRequired: 264adaca1d8b1a9c078761891898d4142df05313
RCTTypeSafety: 279a89da7058a69899778a127be73fab38b84499
React: 725b4e11f6ffb43d6f9b14e82879073623db4071
React-callinvoker: c2ba5e7e1187d0f37705b9dcaaf9bbf24d3fe9dc
React-Codegen: ca5bc760044f8472bf2a78053937307fd2021820
React-Core: d0796eb91058542db9e67fa9b1b11c66092de1ac
React-CoreModules: 57ea4ca8627be90b1a29916e0640d879e5684305
React-cxxreact: bbd34bad63b639dbb2c162cce518e8dba33cef57
React-debug: d360c17c84e514b9143e78217072183d4fcfb9c0
React-jsc: 6e1aae704b76f5626c00c87439fe405f34a6446e
React-jsi: 5fe75c2ff9eea1e90ed5437fcbafb64032383d52
React-jsiexecutor: a05da2c08edb1314cc51671c795fd4ed8440c335
React-jsinspector: 275d9f80210f15f0af9a4b7fd5683fab9738e28e
React-logger: 8da4802de77a0eb62512396ad6bb1769904c2f0e
react-native-background-timer: 17ea5e06803401a379ebf1f20505b793ac44d0fe
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
react-native-keep-awake: afad8a51dfef9fe9655a6344771be32c8596d774
@@ -882,24 +883,24 @@ SPEC CHECKSUMS:
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
react-native-video: 472b7c366eaaaa0207e546d9a50410df89790bcf
react-native-webrtc: 8b024c7bb9a005d2b9efeba4c691172dbd00268d
react-native-webview: 8baa0f5c6d336d6ba488e942bcadea5bf51f050a
React-NativeModulesApple: 4225ac31a26696c02c54b471052b3e85e74a9a0c
React-perflogger: cb433f318c6667060fc1f62e26eb58d6eb30a627
React-RCTActionSheet: 0af3f8ac067e8a1dde902810b7ad169d0a0ec31e
React-RCTAnimation: 453a88e76ba6cb49819686acd8b21ce4d9ee4232
React-RCTAppDelegate: 009ede96d00f79460a75ab5071eadb4aaa1e5012
React-RCTBlob: a64134435f331c7cc716dcea6944a1443af49d32
React-RCTImage: 8e059fbdfab18b86127424dc3742532aab960760
React-RCTLinking: 05ae2aa525b21a7f1c5069c14330700f470efd97
React-RCTNetwork: 7ed9d99d028c53e9a23e318f65937f499ba8a6fd
React-RCTSettings: 8b12ebf04d4baa0e259017fcef6cf7abd7d8ac51
React-RCTText: a062ade9ff1591c46bcb6c5055fd4f96c154b8aa
React-RCTVibration: 87c490b6f01746ab8f9b4e555f514cc030c06731
React-rncore: 140bc11b316da7003bf039844aef39e1c242d7ad
React-runtimeexecutor: 226ebef5f625878d3028b196cbecbbdeb6f208e4
React-runtimescheduler: 75f2210cd7a50c4565dfd218e320479013b82f70
React-utils: a3ffbc321572ee91911d7bc30965abe9aa4e16af
ReactCommon: d3522e54560e4d940554aa074a0206bf4d39ae5e
react-native-webview: 95464c1249cae28186d3b7c6f494c6a124b16160
React-NativeModulesApple: 9b58b8f5c1d8ea68d0a63f569a9e786d01281ea6
React-perflogger: daabc494c6328efc1784a4b49b8b74fca305d11c
React-RCTActionSheet: 0e0e64a7cf6c07f1de73d1f0a92d26a70262b256
React-RCTAnimation: faef65b19e73029c4828167985b0a7c01c62756d
React-RCTAppDelegate: 1c4c94324d5dee57c79f4b68dc709b1c290068c1
React-RCTBlob: df6a165ecd97eb5a92d659aa91866ee9ee50bd5e
React-RCTImage: 15e211cbb629210ab9c1fa37e07e7100362b12ed
React-RCTLinking: 50d5faf19b02541cefb78ee5d505029283c8ef95
React-RCTNetwork: dfa9fb4ad2ae459b9193a14204b1d9da907d15a7
React-RCTSettings: 37611fa97d44a9c5a7ea844cfb953d3513f7ace0
React-RCTText: 39ed334f64484d07b85a8159cf117814ff069ff6
React-RCTVibration: 62462803b5fe0842e0be6d9ef86dd74e0df4a614
React-rncore: 25ad3a3c1e0f4edf77913b9af3af9f497b7f99a5
React-runtimeexecutor: e5c2f0a1493d72c61b97465ccfedc339157b3179
React-runtimescheduler: 5b3b73748021c7349f06fcd24181df17860b8fb6
React-utils: 22a77b05da25ce49c744faa82e73856dcae1734e
ReactCommon: 07937a01c967f2023d6f2d07114315f099b4b436
RNCalendarEvents: 7e65eb4a94f53c1744d1e275f7fafcfaa619f7a3
RNCAsyncStorage: 3a8f7145d17cdd9f88e7b77666c94a09e4f759c8
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
@@ -912,8 +913,8 @@ SPEC CHECKSUMS:
RNSVG: ed492aaf3af9ca01bc945f7a149d76d62e73ec82
RNWatch: fd30ca40a5b5ef58dcbc195638e68219bc455236
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Yoga: eddf2bbe4a896454c248a8f23b4355891eb720a6
Yoga: c32e0be1a17f8f1f0e633a3122f7666441f52c82
PODFILE CHECKSUM: ec00682c7062a323dff24a3c3643ca0bbb420d01
COCOAPODS: 1.14.3
COCOAPODS: 1.15.2

37
ios/PrivacyInfo.xcprivacy Normal file
View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
</array>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>

View File

@@ -151,6 +151,7 @@
4EB0603A260E09D000F524C5 /* DarwinNotificationCenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DarwinNotificationCenter.swift; sourceTree = "<group>"; };
4EB0603B260E09D000F524C5 /* SampleUploader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUploader.swift; sourceTree = "<group>"; };
4EC49B8625BED71300E76218 /* ReplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ReplayKit.framework; path = System/Library/Frameworks/ReplayKit.framework; sourceTree = SDKROOT; };
6132EF172BDFF13200BBE14D /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ../PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
756FCE06C08D9B947653C98A /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = "<group>"; };
B3B083EB1D4955FF0069CEE7 /* app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = app.entitlements; sourceTree = "<group>"; };
D6152FF9E9F7B0E86F70A21D /* libPods-JitsiMeet.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-JitsiMeet.a"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -293,6 +294,7 @@
0BEA5C351F7B8F73000D0AB4 /* WatchKit extension */,
4EB06025260E026600F524C5 /* JitsiMeetBroadcast Extension */,
CDD71F5E1157E9F283DF92A8 /* Pods */,
6132EF172BDFF13200BBE14D /* PrivacyInfo.xcprivacy */,
);
indentWidth = 2;
sourceTree = "<group>";
@@ -410,7 +412,7 @@
attributes = {
LastSwiftUpdateCheck = 1240;
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = Facebook;
ORGANIZATIONNAME = Jitsi;
TargetAttributes = {
0BEA5C241F7B8F73000D0AB4 = {
CreatedOnToolsVersion = 9.0;
@@ -1019,11 +1021,7 @@
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
"$(inherited)",
"-Wl",
"-ld_classic",
);
OTHER_LDFLAGS = "$(inherited)";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -1083,11 +1081,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
"$(inherited)",
"-Wl",
"-ld_classic",
);
OTHER_LDFLAGS = "$(inherited)";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;

View File

@@ -145,6 +145,7 @@
4ED4FFF12721B9B90074E620 /* JitsiAudioSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiAudioSession.h; sourceTree = "<group>"; };
4ED4FFF22721B9B90074E620 /* JitsiAudioSession.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiAudioSession.m; sourceTree = "<group>"; };
4ED4FFF52721BAE10074E620 /* JitsiAudioSession+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiAudioSession+Private.h"; sourceTree = "<group>"; };
6132EF172BDFF13200BBE14D /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ../PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
86389F55993FAAF6AEB3FA3E /* Pods-JitsiMeetSDKLite.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeetSDKLite.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeetSDKLite/Pods-JitsiMeetSDKLite.release.xcconfig"; sourceTree = "<group>"; };
891FE43DAD30BC8976683100 /* Pods-JitsiMeetSDK.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeetSDK.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeetSDK/Pods-JitsiMeetSDK.release.xcconfig"; sourceTree = "<group>"; };
8F48C340DE0D91D1012976C5 /* Pods-JitsiMeetSDKLite.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeetSDKLite.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeetSDKLite/Pods-JitsiMeetSDKLite.debug.xcconfig"; sourceTree = "<group>"; };
@@ -230,6 +231,7 @@
0BD906E61EC0C00300C8C18E /* Products */,
0BCA49681EC4BBE500B793EE /* Resources */,
0BD906E71EC0C00300C8C18E /* src */,
6132EF172BDFF13200BBE14D /* PrivacyInfo.xcprivacy */,
);
sourceTree = "<group>";
};
@@ -779,11 +781,7 @@
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
"$(inherited)",
"-Wl",
"-ld_classic",
);
OTHER_LDFLAGS = "$(inherited)";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -846,11 +844,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_CPLUSPLUSFLAGS = "$(inherited)";
OTHER_LDFLAGS = (
"$(inherited)",
"-Wl",
"-ld_classic",
);
OTHER_LDFLAGS = "$(inherited)";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;

View File

@@ -264,6 +264,7 @@
"Share": "Teilen",
"Submit": "OK",
"WaitForHostMsg": "Die Konferenz wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
"WaitForHostNoAuthMsg": "Die Konferenz wurde noch nicht gestartet. Bitte warten Sie, bis die Konferenz gestartet wird.",
"WaitingForHostButton": "Auf Moderation warten",
"WaitingForHostTitle": "Warten auf den Beginn der Konferenz …",
"Yes": "Ja",
@@ -319,6 +320,7 @@
"embedMeeting": "Besprechung einbetten",
"enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
"error": "Fehler",
"errorRoomCreationRestriction": "Sie haben versucht, zu schnell beizutreten, bitte versuchen Sie es gleich noch einmal.",
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
"grantModeratorDialog": "Möchten Sie wirklich Moderationsrechte an diese Person vergeben?",
"grantModeratorTitle": "Moderationsrechte vergeben",
@@ -736,6 +738,8 @@
"connectedTwoMembers": "{{first}} und {{second}} nehmen am Meeting teil",
"dataChannelClosed": "Schlechte Videoqualität",
"dataChannelClosedDescription": "Die Steuerungsverbindung (Bridge Channel) wurde unterbrochen, daher ist die Videoqulität auf die schlechteste Stufe limitiert.",
"dataChannelClosedDescriptionWithAudio": "Die Steuerungsverbindung (Bridge Channel) wurde unterbrochen, daher können Video- und Tonprobleme auftreten.",
"dataChannelClosedWithAudio": "Ton- und Videoqualität können beeinträchtigt sein",
"disabledIframe": "Die Einbettung ist nur für Demo-Zwecke vorgesehen. Diese Konferenz wird in {{timeout}} Minuten beendet.",
"disabledIframeSecondary": "Die Einbettung von {{domain}} ist nur für Demo-Zwecke vorgesehen. Diese Konferenz wird in {{timeout}} Minuten beendet. Bitte nutzen Sie <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a> für produktive Zwecke!",
"disconnected": "getrennt",
@@ -1483,6 +1487,10 @@
"notification": {
"demoteDescription": "Hierhin verschoben von {{actor}}, bitte melden Sie sich um teilzunehmen",
"description": "Bitte melden Sie sich um teilzunehmen",
"noMainParticipantsDescription": "Eine Person muss die Konferenz starten. Bitte versuchen Sie es gleich noch einmal.",
"noMainParticipantsTitle": "Diese Konferenz wurde noch nicht gestartet.",
"noVisitorLobby": "Sie können nicht teilnehmen, solange die Lobby für diese Konferenz aktiviert ist.",
"notAllowedPromotion": "Eine Person muss Ihre Anfrage erst erlauben.",
"title": "Sie sind Gast in der Konferenz"
}
},

View File

@@ -1169,7 +1169,7 @@
"toolbar": {
"Settings": "Paramètres",
"accessibilityLabel": {
"Settings": "Afficher / Masquer le menu des paramètres",
"Settings": "Ouvrir le menu des paramètres",
"audioOnly": "Activer / Désactiver le mode voix uniquement",
"audioRoute": "Sélectionner la source audio",
"boo": "Hou",
@@ -1215,7 +1215,7 @@
"moreActions": "Activer / Désactiver le menu d'actions supplémentaires",
"moreActionsMenu": "Menu d'actions supplémentaires",
"moreOptions": "Voir plus d'options",
"mute": "Activer / Désactiver l'audio",
"mute": "Couper votre micro",
"muteEveryone": "Couper le micro de tout le monde",
"muteEveryoneElse": "Couper le micro de tous les autres",
"muteEveryoneElsesVideoStream": "Couper la caméra de tous les autres",
@@ -1251,11 +1251,11 @@
"tileView": "Activer / Désactiver la vue mosaïque",
"toggleCamera": "Changer de caméra",
"toggleFilmstrip": "Afficher ou masquer les vignettes vidéo",
"unmute": "Rétablir le son",
"unmute": "Activer votre micro",
"videoblur": "Activer / désactiver le floutage",
"videomute": "Activer / Couper la vidéo",
"videomute": "Couper votre vidéo",
"videomuteGUMPending": "Connexion de votre caméra",
"videounmute": "Démarrer la vidéo"
"videounmute": "Activer votre vidéo"
},
"addPeople": "Ajouter des personnes à votre appel",
"audioOnlyOff": "Désactiver le mode bande passante réduite",

View File

@@ -184,6 +184,7 @@
"Share": "Megosztás",
"Submit": "Elküldés",
"WaitForHostMsg": "A konferencia még nem kezdődött meg. Ha Ön a házigazda, akkor hitelesítse magát. Ellenkező esetben, kérjük várjon a házigazda érkezésére.",
"WaitForHostNoAuthMsg": "A konferencia még nem kezdődött el, mert nincs elérhető moderátor. Kérlek várj.",
"WaitingForHost": "Várakozás a házigazdára…",
"Yes": "Igen",
"accessibilityLabel": {

View File

@@ -319,6 +319,7 @@
"embedMeeting": "Iegult sapulci",
"enterDisplayName": "Ievadiet savu vārdu",
"error": "Kļūda",
"errorRoomCreationRestriction": "Jūs mēģinājāt pievienoties pārāk ātri. Lūdzu, atgriezieties vēlāk.",
"gracefulShutdown": "Mūsu serviss pašlaik nedarbojas apkopes dēļ. Lūdzu, pamēģiniet vēlreiz vēlāk.",
"grantModeratorDialog": "Vai tiešām vēlaties piešķirt moderatora tiesības dalībniekam {{participantName}}?",
"grantModeratorTitle": "Piešķirt moderatora tiesības",
@@ -354,28 +355,28 @@
"micPermissionDeniedError": "Nav piekļuves mikrofonam. Jūs varat piedalīties sapulcē, bet citi jūs nedzirdēs. Lai to novērstu, izmantojiet kameras ikonu pārlūkprogrammas adrešu joslā.",
"micTimeoutError": "Nevarēja palaist audio avotu. Iestājās noildze!",
"micUnknownError": "Nevar izmantot mikrofonu nezināma iemesla dēļ.",
"moderationAudioLabel": "Ļaujiet dalībniekiem ieslēgt savu mikrofonu",
"moderationVideoLabel": "Ļaujiet dalībniekiem ieslēgt savu kameru",
"muteEveryoneDialog": "Dalībnieki jebkurā laikā var ieslēgt savu mikrofonu.",
"muteEveryoneDialogModerationOn": "Dalībnieki jebkurā laikā var nosūtīt pieprasījumu runāt.",
"muteEveryoneElseDialog": "Kad skaņa būs izslēgta, jūs nevarēsit to ieslēgt atpakaļ, taču dalībnieki jebkurā laikā to varēs izdarīt paši.",
"moderationAudioLabel": "Atļaut dalībniekiem ieslēgt savu mikrofonu",
"moderationVideoLabel": "Atļaut dalībniekiem ieslēgt savu kameru",
"muteEveryoneDialog": "Dalībnieki paši var ieslēgt savu mikrofonu.",
"muteEveryoneDialogModerationOn": "Dalībnieki var nosūtīt pieprasījumu ieslēgt savu mikrofonu.",
"muteEveryoneElseDialog": "Kad skaņa būs izslēgta, jūs nevarēsiet to ieslēgt atpakaļ, taču dalībnieki to varēs izdarīt paši.",
"muteEveryoneElseTitle": "Vai izslēgt skaņu visiem, izņemot {{whom}}?",
"muteEveryoneElsesVideoDialog": "Kad video būs izslēgts, jūs nevarēsit to ieslēgt atpakaļ, taču dalībnieki jebkurā laikā to varēs izdarīt paši.",
"muteEveryoneElsesVideoDialog": "Kad video būs izslēgts, jūs nevarēsiet to ieslēgt atpakaļ, taču dalībnieki to varēs izdarīt paši.",
"muteEveryoneElsesVideoTitle": "Vai izslēgt video visiem, izņemot {{whom}}?",
"muteEveryoneSelf": "jūs",
"muteEveryoneStartMuted": "No šī brīža visi jauni dalībnieki pieslēdzas ar izslēgt skaņu",
"muteEveryoneTitle": "Vai izslēgt skaņu visiem?",
"muteEveryonesVideoDialog": "Dalībnieki var jebkurā laikā ieslēgt savu video.",
"muteEveryonesVideoDialogModerationOn": "Dalībnieki jebkurā laikā var nosūtīt pieprasījumu ieslēgt viņu video.",
"muteEveryonesVideoDialog": "Dalībnieki var ieslēgt savu video.",
"muteEveryonesVideoDialogModerationOn": "Dalībnieki var nosūtīt pieprasījumu ieslēgt viņu video.",
"muteEveryonesVideoDialogOk": "Atspējot",
"muteEveryonesVideoTitle": "Vai apturēt ikviena video?",
"muteParticipantBody": "Jūs nevarat viņiem ieslēgt skaņu, bet viņi paši to var izdarīt jebkurā laikā.",
"muteParticipantBody": "Jūs nevariet viņiem ieslēgt skaņu, bet viņi paši to var izdarīt jebkurā laikā.",
"muteParticipantButton": "Izslēgt skaņu",
"muteParticipantsVideoBody": "Jūs nevarēsiet atkal ieslēgt kameru, taču viņi var to jebkurā laikā atkal ieslēgt.",
"muteParticipantsVideoBodyModerationOn": "Jūs nevarēsiet atkal ieslēgt kameru, un viņi arī nevarēs.",
"muteParticipantsVideoBody": "Jūs nevarēsiet kameru ieslēgt atpakaļ, taču viņi paši to varēs izdarīt jebkurā laikā.",
"muteParticipantsVideoBodyModerationOn": "Ne Jūs, ne dalībnieki nevarēsiet ieslēgt kameru atpakaļ.",
"muteParticipantsVideoButton": "Pārtraukt video",
"muteParticipantsVideoDialog": "Vai tiešām vēlaties izslēgt šī dalībnieka kameru? Jūs nevarēsiet atkal ieslēgt kameru, taču viņi var to jebkurā laikā atkal ieslēgt.",
"muteParticipantsVideoDialogModerationOn": "Vai tiešām vēlaties izslēgt šī dalībnieka kameru? Jūs nevarēsiet atkal ieslēgt kameru, un viņi arī nevarēs.",
"muteParticipantsVideoDialog": "Vai tiešām vēlaties izslēgt šī dalībnieka kameru? Jūs nevarēsiet to ieslēgt atpakaļ, taču dalībnieks pats to varēs izdarīt jebkurā laikā.",
"muteParticipantsVideoDialogModerationOn": "Vai tiešām vēlaties izslēgt šī dalībnieka kameru? Ne Jūs, ne dalībnieks nevarēsiet to ieslēgt atpakaļ.",
"muteParticipantsVideoTitle": "Vai izslēgt šī dalībnieka video?",
"noDropboxToken": "Nav derīga Dropbox tokena",
"password": "Parole",
@@ -734,8 +735,10 @@
"connectedOneMember": "{{name}} ir pievienojies sapulcei",
"connectedThreePlusMembers": "{{name}} un {{count}} citi ir pievienojušies sapulcei",
"connectedTwoMembers": "{{first}} un {{second}} ir pievienojušies sapulcei",
"dataChannelClosed": "Video kvalitāte ir traucēta",
"dataChannelClosedDescription": "Savienojuma kanāls ir atvienots, un tādējādi video kvalitāte ir ierobežota līdz zemākajam iestatījumam.",
"dataChannelClosed": "Video kvalitāte var būt traucēta",
"dataChannelClosedDescription": "Savienojuma kanāls nedarbojas, tāpēc video kvalitāte var būt ierobežota līdz zemākajam iestatījumam.",
"dataChannelClosedDescriptionWithAudio": "Savienojuma kanāls nedarbojas, tāpēc var rasties audio un video traucējumi.",
"dataChannelClosedWithAudio": "Audio un video kvalitāte var būt traucēta",
"disabledIframe": "Iegulšana ir paredzēta tikai demonstrācijas nolūkiem, tāpēc šis zvans tiks atvienots pēc {{timeout}} minūtēm.",
"disabledIframeSecondary": "{{domain}} iegulšana ir paredzēta tikai demonstrācijas nolūkiem, tāpēc šis zvans tiks atvienots pēc {{timeout}} minūtēm. Lūdzu, izmantojiet <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi kā Pakalpojums</a> produkcijas iegulšanai!",
"disconnected": "savienojums pārtraukts",
@@ -833,7 +836,7 @@
"breakoutRooms": "Grupu istabas",
"invite": "Uzaicināt",
"moreModerationActions": "Vairāk moderēšanas iespēju",
"moreModerationControls": "Vairāk moderēšanas kontroļu",
"moreModerationControls": "Vairāk moderēšanas iespēju",
"moreParticipantOptions": "Vairāk dalībnieku iespēju",
"mute": "Apklusināt",
"muteAll": "Apklusināt visus",
@@ -1483,6 +1486,10 @@
"notification": {
"demoteDescription": "{{actor}} pārveidoja par apmeklētāju, paceliet roku, lai piedalītos",
"description": "Paceliet roku, lai piedalītos",
"noMainParticipantsDescription": "Dalībniekam ir jāsāk sapulce. Lūdzu, pēc brīža mēģiniet vēlreiz.",
"noMainParticipantsTitle": "Šī sapulce vēl nav sākusies.",
"noVisitorLobby": "Jūs nevarat pievienoties, kamēr sapulcei ir iespējots vestibils.",
"notAllowedPromotion": "Dalībniekam vispirms ir jāatļauj jūsu pieprasījums.",
"title": "Jūs esat sapulces apmeklētājs"
}
},

View File

@@ -263,6 +263,7 @@
"Share": "Chia sẻ",
"Submit": "Đăng ký",
"WaitForHostMsg": "Cuộc họp chưa được bắt đầu. Nếu bạn là quản trị viên vui lòng xác thực. Nếu không, vui lòng đợi quản trị viên.",
"WaitingForHost": "Đang đợi quản trị viên...",
"WaitingForHostButton": "Chờ người điều hành",
"WaitingForHostTitle": "Chờ người điều hành ...",
"Yes": "Có",

View File

@@ -264,6 +264,7 @@
"Share": "Share",
"Submit": "Submit",
"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.",
"WaitForHostNoAuthMsg": "The conference has not yet started because no moderators have yet arrived. Please wait.",
"WaitingForHostButton": "Wait for moderator",
"WaitingForHostTitle": "Waiting for a moderator...",
"Yes": "Yes",
@@ -864,6 +865,8 @@
"pinnedParticipant": "The participant is pinned",
"polls": {
"answer": {
"edit": "Edit",
"send": "Send",
"skip": "Skip",
"submit": "Submit"
},
@@ -877,6 +880,7 @@
"pollQuestion": "Poll Question",
"questionPlaceholder": "Ask a question",
"removeOption": "Remove option",
"save": "Save",
"send": "Send"
},
"errors": {

View File

@@ -34,6 +34,7 @@
"hi": "Hindi",
"hmn": "Hmong",
"hr": "Croatian",
"hsb": "Upper Sorbian",
"ht": "Haitian Creole",
"hu": "Hungarian",
"hy": "Armenian",

View File

@@ -2138,6 +2138,21 @@ class API {
}
/**
* Notify external application (if API is enabled) the conference
* start time.
*
* @param {number} timestamp - Timestamp conference was created.
* @returns {void}
*/
notifyConferenceCreatedTimestamp(timestamp) {
this._sendEvent({
name: 'conference-created-timestamp',
timestamp
});
}
/**
* Notify the external application (if API is enabled) if the connection type changed.
*

View File

@@ -108,6 +108,7 @@ const events = {
'camera-error': 'cameraError',
'chat-updated': 'chatUpdated',
'compute-pressure-changed': 'computePressureChanged',
'conference-created-timestamp': 'conferenceCreatedTimestamp',
'content-sharing-participants-changed': 'contentSharingParticipantsChanged',
'data-channel-closed': 'dataChannelClosed',
'data-channel-opened': 'dataChannelOpened',

2842
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
"@giphy/js-fetch-api": "4.7.1",
"@giphy/react-components": "6.8.1",
"@giphy/react-native-sdk": "2.3.0",
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.17/jitsi-excalidraw-0.0.17.tgz",
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.19/jitsi-excalidraw-0.0.19.tgz",
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",
"@jitsi/rnnoise-wasm": "0.1.0",
@@ -36,7 +36,6 @@
"@react-native-community/netinfo": "11.1.0",
"@react-native-community/slider": "4.4.3",
"@react-native-google-signin/google-signin": "10.1.0",
"@react-native/metro-config": "0.72.9",
"@react-navigation/bottom-tabs": "6.5.8",
"@react-navigation/elements": "1.3.18",
"@react-navigation/material-top-tabs": "6.6.3",
@@ -67,7 +66,7 @@
"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/v1823.0.0+ec98b020/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1830.0.0+5a14bd43/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -82,7 +81,7 @@
"react-focus-on": "3.8.1",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"react-native": "0.72.9",
"react-native": "0.72.14",
"react-native-background-timer": "2.4.1",
"react-native-calendar-events": "2.2.0",
"react-native-default-preference": "1.4.4",
@@ -107,7 +106,7 @@
"react-native-video": "6.0.0-alpha.11",
"react-native-watch-connectivity": "1.1.0",
"react-native-webrtc": "118.0.7",
"react-native-webview": "13.5.1",
"react-native-webview": "13.8.7",
"react-native-youtube-iframe": "2.3.0",
"react-redux": "7.2.9",
"react-textarea-autosize": "8.3.0",
@@ -133,6 +132,7 @@
"@babel/preset-env": "7.21.5",
"@babel/preset-react": "7.16.0",
"@jitsi/eslint-config": "4.1.10",
"@react-native/metro-config": "0.72.12",
"@types/amplitude-js": "8.16.5",
"@types/audioworklet": "0.0.29",
"@types/dom-screen-wake-lock": "1.0.1",
@@ -187,8 +187,8 @@
"@xmldom/xmldom": "0.8.7"
},
"engines": {
"node": ">=14.0.0",
"npm": ">=7.0.0"
"node": ">=20.0.0",
"npm": ">=10.0.0"
},
"license": "Apache-2.0",
"scripts": {

View File

@@ -55,7 +55,7 @@
"@amplitude/react-native": "0.0.0",
"@braintree/sanitize-url": "0.0.0",
"@giphy/react-native-sdk": "0.0.0",
"@react-native/metro-config": "0.0.0",
"@react-native/metro-config": "*",
"@react-native-async-storage/async-storage": "0.0.0",
"@react-native-community/clipboard": "0.0.0",
"@react-native-community/netinfo": "0.0.0",

View File

@@ -114,10 +114,6 @@ copyFolderRecursiveSync(
'../react',
'.'
);
copyFolderRecursiveSync(
'../service',
'.'
);
copyFolderRecursiveSync(
'../ios/sdk/sdk.xcodeproj',
'./ios'

View File

@@ -17,6 +17,11 @@ interface IProps extends WithTranslation {
*/
_alternativeCancelText?: boolean;
/**
* Whether to hide the login button.
*/
_hideLoginButton?: boolean;
/**
* Redux store dispatch method.
*/
@@ -80,12 +85,13 @@ class WaitForOwnerDialog extends PureComponent<IProps> {
this.props._alternativeCancelText ? 'dialog.WaitingForHostButton' : 'dialog.Cancel' }}
disableBackdropClose = { true }
hideCloseButton = { true }
ok = {{ translationKey: 'dialog.IamHost' }}
ok = { this.props._hideLoginButton ? { hidden: true,
disabled: true } : { translationKey: 'dialog.IamHost' } }
onCancel = { this._onCancelWaitForOwner }
onSubmit = { this._onIAmHost }
titleKey = { t('dialog.WaitingForHostTitle') }>
<span>
{ t('dialog.WaitForHostMsg') }
{ this.props._hideLoginButton ? t('dialog.WaitForHostNoAuthMsg') : t('dialog.WaitForHostMsg') }
</span>
</Dialog>
);
@@ -102,9 +108,11 @@ class WaitForOwnerDialog extends PureComponent<IProps> {
*/
function mapStateToProps(state: IReduxState) {
const { membersOnly, lobbyWaitingForHost } = state['features/base/conference'];
const { hideLoginButton } = state['features/base/config'];
return {
_alternativeCancelText: membersOnly && lobbyWaitingForHost
_alternativeCancelText: membersOnly && lobbyWaitingForHost,
_hideLoginButton: hideLoginButton
};
}

View File

@@ -10,14 +10,12 @@ import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection/constants';
import { hasAvailableDevices } from '../devices/functions.any';
import { JitsiConferenceEvents, JitsiE2ePingEvents } from '../lib-jitsi-meet';
import {
gumPending,
setAudioMuted,
setAudioUnmutePermissions,
setVideoMuted,
setVideoUnmutePermissions
} from '../media/actions';
import { MEDIA_TYPE, VIDEO_MUTISM_AUTHORITY } from '../media/constants';
import { IGUMPendingState } from '../media/types';
import { MEDIA_TYPE, MediaType } from '../media/constants';
import {
dominantSpeakerChanged,
participantKicked,
@@ -72,6 +70,7 @@ import {
SET_START_REACTIONS_MUTED,
UPDATE_CONFERENCE_METADATA
} from './actionTypes';
import { setupVisitorStartupMedia } from './actions';
import {
AVATAR_URL_COMMAND,
EMAIL_COMMAND,
@@ -1060,9 +1059,7 @@ export function redirect(vnode: string, focusJid: string, username: string) {
.then(() => dispatch(conferenceWillInit()))
.then(() => dispatch(connect()))
.then(() => {
// Clear the gum pending state in case we have set it to pending since we are starting the
// conference without tracks.
dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
const media: Array<MediaType> = [];
if (!vnode) {
const state = getState();
@@ -1076,10 +1073,7 @@ export function redirect(vnode: string, focusJid: string, username: string) {
// do not unmute the user if he was muted before (on the prejoin, the config
// or URL param, etc.)
if (!unmuteBlocked && !muted && !startSilent && available) {
dispatch(setAudioMuted(false, true));
// // FIXME: The old conference logic still relies on this event being emitted.
typeof APP === 'undefined' || APP.conference.muteAudio(false);
media.push(MEDIA_TYPE.AUDIO);
}
}
@@ -1089,18 +1083,13 @@ export function redirect(vnode: string, focusJid: string, username: string) {
// do not unmute the user if he was muted before (on the prejoin, the config, URL param or
// audo only, etc)
if (!unmuteBlocked && !muted && hasAvailableDevices(state, 'videoInput')) {
dispatch(setVideoMuted(false, VIDEO_MUTISM_AUTHORITY.USER, true));
// // FIXME: The old conference logic still relies on this event being emitted.
typeof APP === 'undefined' || APP.conference.muteVideo(false, false);
media.push(MEDIA_TYPE.VIDEO);
}
}
}
// FIXME: Workaround for the web version. To be removed once we get rid of conference.js
if (typeof APP !== 'undefined') {
APP.conference.startConference([]);
}
dispatch(setupVisitorStartupMedia(media));
});
};
}

View File

@@ -0,0 +1,29 @@
import { IStore } from '../../app/types';
import { setAudioMuted, setVideoMuted } from '../media/actions';
import { MEDIA_TYPE, MediaType, VIDEO_MUTISM_AUTHORITY } from '../media/constants';
export * from './actions.any';
/**
* Starts audio and/or video for the visitor.
*
* @param {Array<MediaType>} mediaTypes - The media types that need to be started.
* @returns {Function}
*/
export function setupVisitorStartupMedia(mediaTypes: Array<MediaType>) {
return (dispatch: IStore['dispatch']) => {
if (!mediaTypes || !Array.isArray(mediaTypes)) {
return;
}
mediaTypes.forEach(mediaType => {
switch (mediaType) {
case MEDIA_TYPE.AUDIO:
dispatch(setAudioMuted(false, true));
break;
case MEDIA_TYPE.VIDEO:
dispatch(setVideoMuted(false, VIDEO_MUTISM_AUTHORITY.USER, true));
}
});
};
}

View File

@@ -0,0 +1,31 @@
import { IStore } from '../../app/types';
import { gumPending } from '../media/actions';
import { MEDIA_TYPE, MediaType } from '../media/constants';
import { IGUMPendingState } from '../media/types';
import { createAndAddInitialAVTracks } from '../tracks/actions.web';
export * from './actions.any';
/**
* Starts audio and/or video for the visitor.
*
* @param {Array<MediaType>} media - The media types that need to be started.
* @returns {Function}
*/
export function setupVisitorStartupMedia(media: Array<MediaType>) {
return (dispatch: IStore['dispatch']) => {
// Clear the gum pending state in case we have set it to pending since we are starting the
// conference without tracks.
dispatch(gumPending([ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
if (media && Array.isArray(media) && media.length > 0) {
dispatch(createAndAddInitialAVTracks(media));
}
// FIXME: The name of the function doesn't fit the startConference execution but another PR will removes
// this and calls startConference based on the connection status. This will stay here temporary.
if (typeof APP !== 'undefined') {
APP.conference.startConference([]);
}
};
}

View File

@@ -240,7 +240,7 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
descriptionKey = 'dialog.errorRoomCreationRestriction';
}
APP.store.dispatch(showErrorNotification({
dispatch(showErrorNotification({
descriptionKey,
hideErrorSupportLink: true,
titleKey

View File

@@ -396,6 +396,7 @@ export interface IConfig {
hideDominantSpeakerBadge?: boolean;
hideEmailInSettings?: boolean;
hideLobbyButton?: boolean;
hideLoginButton?: boolean;
hideParticipantsStats?: boolean;
hideRecordingLabel?: boolean;
hosts?: {
@@ -440,6 +441,7 @@ export interface IConfig {
};
localSubject?: string;
locationURL?: URL;
mainToolbarButtons?: Array<Array<string>>;
maxFullResolutionParticipants?: number;
microsoftApiApplicationClientID?: string;
moderatedRoomServiceUrl?: string;

View File

@@ -81,6 +81,7 @@ export default [
'debug',
'debugAudioLevels',
'deeplinking.disabled',
'deeplinking.desktop.enabled',
'defaultLocalDisplayName',
'defaultRemoteDisplayName',
'deploymentUrls',
@@ -184,6 +185,7 @@ export default [
'localRecording',
'localSubject',
'logging',
'mainToolbarButtons',
'maxFullResolutionParticipants',
'mouseMoveCallbackInterval',
'notifications',

View File

@@ -19,7 +19,11 @@ import logger from './logger';
*/
export function parseJWTFromURLParams(url: URL | typeof window.location = window.location) {
// @ts-ignore
return parseURLParams(url, true, 'search').jwt;
const jwt = parseURLParams(url, false, 'hash').jwt;
// TODO: eventually remove the search param and only pull from the hash
// @ts-ignore
return jwt ? jwt : parseURLParams(url, true, 'search').jwt;
}
/**

View File

@@ -4,7 +4,7 @@ import { IReduxState, IStore } from '../../app/types';
import { showModeratedNotification } from '../../av-moderation/actions';
import { shouldShowModeratedNotification } from '../../av-moderation/functions';
import { setNoiseSuppressionEnabled } from '../../noise-suppression/actions';
import { showNotification } from '../../notifications/actions';
import { showErrorNotification, showNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { stopReceiver } from '../../remote-control/actions';
import { setScreenAudioShareState, setScreenshareAudioTrack } from '../../screen-share/actions';
@@ -13,10 +13,12 @@ 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 { notifyCameraError, notifyMicError } from '../devices/actions.web';
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 { JitsiTrackErrors, JitsiTrackEvents, browser } from '../lib-jitsi-meet';
import { gumPending, setScreenshareMuted } from '../media/actions';
import { MEDIA_TYPE, MediaType, VIDEO_TYPE } from '../media/constants';
import { IGUMPendingState } from '../media/types';
import {
addLocalTrack,
@@ -31,7 +33,8 @@ import {
getLocalVideoTrack,
isToggleCameraEnabled
} from './functions';
import { IShareOptions, IToggleScreenSharingOptions } from './types';
import logger from './logger';
import { ICreateInitialTracksOptions, IInitialTracksErrors, IShareOptions, IToggleScreenSharingOptions } from './types';
export * from './actions.any';
@@ -74,33 +77,6 @@ export function toggleScreensharing(
* @param {Object} store - The redux store.
* @returns {void}
*/
function _handleScreensharingError(
error: Error | AUDIO_ONLY_SCREEN_SHARE_NO_TRACK,
{ dispatch }: IStore): void {
if (error.name === JitsiTrackErrors.SCREENSHARING_USER_CANCELED) {
return;
}
let descriptionKey, titleKey;
if (error.name === JitsiTrackErrors.PERMISSION_DENIED) {
descriptionKey = 'dialog.screenSharingPermissionDeniedError';
titleKey = 'dialog.screenSharingFailedTitle';
} else if (error.name === JitsiTrackErrors.CONSTRAINT_FAILED) {
descriptionKey = 'dialog.cameraConstraintFailedError';
titleKey = 'deviceError.cameraError';
} else if (error.name === JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR) {
descriptionKey = 'dialog.screenSharingFailed';
titleKey = 'dialog.screenSharingFailedTitle';
} else if (error === AUDIO_ONLY_SCREEN_SHARE_NO_TRACK) {
descriptionKey = 'notify.screenShareNoAudio';
titleKey = 'notify.screenShareNoAudioTitle';
}
dispatch(showNotification({
titleKey,
descriptionKey
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
}
/**
@@ -128,7 +104,6 @@ async function _maybeApplyAudioMixerEffect(desktopAudioTrack: any, state: IRedux
}
}
/**
* Toggles screen sharing.
*
@@ -182,7 +157,7 @@ async function _toggleScreenSharing(
try {
tracks = await createLocalTracksF(options) as any[];
} catch (error) {
_handleScreensharingError(error as any, store);
dispatch(handleScreenSharingError(error, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
throw error;
}
@@ -196,7 +171,7 @@ async function _toggleScreenSharing(
desktopVideoTrack.dispose();
if (!desktopAudioTrack) {
_handleScreensharingError(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK, store);
dispatch(handleScreenSharingError(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
throw new Error(AUDIO_ONLY_SCREEN_SHARE_NO_TRACK);
}
@@ -317,3 +292,224 @@ export function openAllowToggleCameraDialog(onAllow: Function, initiatorId: stri
initiatorId
});
}
/**
* Sets the GUM pending state for the tracks that have failed.
*
* NOTE: Some of the track that we will be setting to GUM pending state NONE may not have failed but they may have
* been requested. This won't be a problem because their current GUM pending state will be NONE anyway.
*
* @param {JitsiLocalTrack} tracks - The tracks that have been created.
* @param {Function} dispatch - The redux dispatch function.
* @returns {void}
*/
export function setGUMPendingStateOnFailedTracks(tracks: Array<any>, dispatch: IStore['dispatch']) {
const tracksTypes = tracks.map(track => {
if (track.getVideoType() === VIDEO_TYPE.DESKTOP) {
return MEDIA_TYPE.SCREENSHARE;
}
return track.getType();
});
const nonPendingTracks = [ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ].filter(type => !tracksTypes.includes(type));
dispatch(gumPending(nonPendingTracks, IGUMPendingState.NONE));
}
/**
* Creates and adds to the conference the initial audio/video tracks.
*
* @param {Array<MediaType>} devices - Array with devices (audio/video) that will be used.
* @returns {Function}
*/
export function createAndAddInitialAVTracks(devices: Array<MediaType>) {
return async (dispatch: IStore['dispatch']) => {
dispatch(gumPending(devices, IGUMPendingState.PENDING_UNMUTE));
const { tracks, errors } = await dispatch(createInitialAVTracks({ devices }));
setGUMPendingStateOnFailedTracks(tracks, dispatch);
dispatch(displayErrorsForCreateInitialLocalTracks(errors));
await Promise.allSettled(tracks.map((track: any) => {
const legacyConferenceObject = APP.conference;
if (track.isAudioTrack()) {
return legacyConferenceObject.useAudioStream(track);
}
if (track.isVideoTrack()) {
return legacyConferenceObject.useVideoStream(track);
}
return Promise.resolve();
}));
dispatch(gumPending(devices, IGUMPendingState.NONE));
};
}
/**
* Creates the initial audio/video tracks.
*
* @param {ICreateInitialTracksOptions} options - Options for creating the audio/video tracks.
* @returns {Function}
*/
export function createInitialAVTracks(options: ICreateInitialTracksOptions) {
return (dispatch: IStore['dispatch'], _getState: IStore['getState']) => {
const {
devices,
timeout,
firePermissionPromptIsShownEvent
} = options;
dispatch(gumPending(devices, IGUMPendingState.PENDING_UNMUTE));
return createLocalTracksF(options).then(tracks => {
return {
errors: {} as IInitialTracksErrors,
tracks
};
})
.catch(async error => {
const errors = {} as IInitialTracksErrors;
if (error.name === JitsiTrackErrors.TIMEOUT && !browser.isElectron()) {
if (devices.includes(MEDIA_TYPE.AUDIO)) {
errors.audioOnlyError = error;
}
if (devices.includes(MEDIA_TYPE.VIDEO)) {
errors.videoOnlyError = error;
}
if (errors.audioOnlyError && errors.videoOnlyError) {
errors.audioAndVideoError = error;
}
return {
errors,
tracks: []
};
}
// Retry with separate gUM calls.
const gUMPromises = [];
const tracks: any[] | PromiseLike<any[]> = [];
if (devices.includes(MEDIA_TYPE.AUDIO)) {
gUMPromises.push(createLocalTracksF({
devices: [ MEDIA_TYPE.AUDIO ],
timeout,
firePermissionPromptIsShownEvent
}));
}
if (devices.includes(MEDIA_TYPE.VIDEO)) {
gUMPromises.push(createLocalTracksF({
devices: [ MEDIA_TYPE.VIDEO ],
timeout,
firePermissionPromptIsShownEvent
}));
}
const results = await Promise.allSettled(gUMPromises);
let errorMsg;
results.forEach((result, idx) => {
if (result.status === 'fulfilled') {
tracks.push(result.value[0]);
} else {
errorMsg = result.reason;
const isAudio = idx === 0;
logger.error(`${isAudio ? 'Audio' : 'Video'} track creation failed with error ${errorMsg}`);
if (isAudio) {
errors.audioOnlyError = errorMsg;
} else {
errors.videoOnlyError = errorMsg;
}
}
});
if (errors.audioOnlyError && errors.videoOnlyError) {
errors.audioAndVideoError = errorMsg;
}
return {
tracks,
errors
};
});
};
}
/**
* Displays error notifications according to the state carried by the passed {@code errors} object.
*
* @param {InitialTracksErrors} errors - The errors (if any).
* @returns {Function}
* @private
*/
export function displayErrorsForCreateInitialLocalTracks(errors: IInitialTracksErrors) {
return (dispatch: IStore['dispatch']) => {
const {
audioOnlyError,
screenSharingError,
videoOnlyError
} = errors;
if (screenSharingError) {
dispatch(handleScreenSharingError(screenSharingError, NOTIFICATION_TIMEOUT_TYPE.LONG));
}
if (audioOnlyError || videoOnlyError) {
if (audioOnlyError) {
dispatch(notifyMicError(audioOnlyError));
}
if (videoOnlyError) {
dispatch(notifyCameraError(videoOnlyError));
}
}
};
}
/**
* Displays a UI notification for screensharing failure based on the error passed.
*
* @private
* @param {Error | AUDIO_ONLY_SCREEN_SHARE_NO_TRACK} error - The error.
* @param {NOTIFICATION_TIMEOUT_TYPE} timeout - The time for showing the notification.
* @returns {Function}
*/
export function handleScreenSharingError(
error: Error | AUDIO_ONLY_SCREEN_SHARE_NO_TRACK,
timeout: NOTIFICATION_TIMEOUT_TYPE) {
return (dispatch: IStore['dispatch']) => {
logger.error('failed to share local desktop', error);
let descriptionKey;
let titleKey;
if (error.name === JitsiTrackErrors.PERMISSION_DENIED) {
descriptionKey = 'dialog.screenSharingPermissionDeniedError';
titleKey = 'dialog.screenSharingFailedTitle';
} else if (error.name === JitsiTrackErrors.CONSTRAINT_FAILED) {
descriptionKey = 'dialog.cameraConstraintFailedError';
titleKey = 'deviceError.cameraError';
} else if (error.name === JitsiTrackErrors.SCREENSHARING_GENERIC_ERROR) {
descriptionKey = 'dialog.screenSharingFailed';
titleKey = 'dialog.screenSharingFailedTitle';
} else if (error === AUDIO_ONLY_SCREEN_SHARE_NO_TRACK) {
descriptionKey = 'notify.screenShareNoAudio';
titleKey = 'notify.screenShareNoAudioTitle';
} else { // safeguard for not showing notification with empty text. This will also include
// error.name === JitsiTrackErrors.SCREENSHARING_USER_CANCELED
return;
}
dispatch(showErrorNotification({
descriptionKey,
titleKey
}, timeout));
};
}

View File

@@ -72,3 +72,16 @@ export interface IShareOptions {
desktopSharingSources?: string[];
desktopStream?: any;
}
export interface ICreateInitialTracksOptions {
devices: Array<MediaType>;
firePermissionPromptIsShownEvent?: boolean;
timeout?: number;
}
export interface IInitialTracksErrors {
audioAndVideoError?: Error;
audioOnlyError: Error;
screenSharingError: Error;
videoOnlyError: Error;
}

View File

@@ -1,13 +1,16 @@
import React, { forwardRef, useCallback, useState } from 'react';
import {
KeyboardTypeOptions,
NativeSyntheticEvent, ReturnKeyTypeOptions,
NativeSyntheticEvent,
ReturnKeyTypeOptions,
StyleProp,
Text,
TextInput,
TextInputChangeEventData,
TextInputFocusEventData, TextInputKeyPressEventData,
TextInputFocusEventData,
TextInputKeyPressEventData,
TextInputSubmitEditingEventData,
TextStyle,
TouchableOpacity,
View,
ViewStyle
@@ -25,8 +28,16 @@ interface IProps extends IInputProps {
autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters' | undefined;
autoFocus?: boolean;
blurOnSubmit?: boolean | undefined;
bottomLabel?: string;
customStyles?: ICustomStyles;
editable?: boolean | undefined;
/**
* The id to set on the input element.
* This is required because we need it internally to tie the input to its
* info (label, error) so that screen reader users don't get lost.
*/
id?: string;
keyboardType?: KeyboardTypeOptions;
maxLength?: number | undefined;
minHeight?: number | string | undefined;
@@ -52,11 +63,13 @@ const Input = forwardRef<TextInput, IProps>(({
autoCapitalize,
autoFocus,
blurOnSubmit,
bottomLabel,
clearable,
customStyles,
disabled,
error,
icon,
id,
keyboardType,
label,
maxLength,
@@ -106,7 +119,7 @@ const Input = forwardRef<TextInput, IProps>(({
onSubmitEditing?.(text);
}, [ onSubmitEditing ]);
return (<View style = { [ styles.inputContainer, customStyles?.container ] }>
return (<View style = { [ styles.inputContainer, customStyles?.container ] as StyleProp<ViewStyle> }>
{label && <Text style = { styles.label }>{ label }</Text>}
<View style = { styles.fieldContainer as StyleProp<ViewStyle> }>
{icon && <Icon
@@ -121,6 +134,7 @@ const Input = forwardRef<TextInput, IProps>(({
autoFocus = { autoFocus }
blurOnSubmit = { blurOnSubmit }
editable = { !disabled }
id = { id }
keyboardType = { keyboardType }
maxLength = { maxLength }
@@ -145,11 +159,11 @@ const Input = forwardRef<TextInput, IProps>(({
clearable && styles.clearableInput,
customStyles?.input,
disabled && styles.inputDisabled,
error && styles.inputError,
focused && styles.inputFocused,
icon && styles.iconInput,
multiline && styles.inputMultiline
] }
multiline && styles.inputMultiline,
focused && styles.inputFocused,
error && styles.inputError
] as StyleProp<TextStyle> }
textContentType = { textContentType }
value = { typeof value === 'number' ? `${value}` : value } />
{ clearable && !disabled && value !== '' && (
@@ -163,6 +177,20 @@ const Input = forwardRef<TextInput, IProps>(({
</TouchableOpacity>
)}
</View>
{
bottomLabel && (
<View>
<Text
id = { `${id}-description` }
style = { [
styles.bottomLabel,
error && styles.bottomLabelError
] }>
{ bottomLabel }
</Text>
</View>
)
}
</View>);
});

View File

@@ -73,5 +73,15 @@ export default {
clearIcon: {
color: BaseTheme.palette.icon01
},
bottomLabel: {
...BaseTheme.typography.labelRegular,
color: BaseTheme.palette.text02,
marginTop: BaseTheme.spacing[2]
},
bottomLabelError: {
color: BaseTheme.palette.textError
}
};

View File

@@ -537,6 +537,7 @@ export function urlObjectToString(o: { [key: string]: any; }): string | undefine
const search = new URLSearchParams(url.search);
// TODO: once all available versions are updated to support the jwt in the hash, remove this
if (jwt) {
search.set('jwt', jwt);
}
@@ -561,6 +562,14 @@ export function urlObjectToString(o: { [key: string]: any; }): string | undefine
let { hash } = url;
if (jwt) {
if (hash.length) {
hash = `${hash}&jwt=${JSON.stringify(jwt)}`;
} else {
hash = `#jwt=${JSON.stringify(jwt)}`;
}
}
for (const urlPrefix of [ 'config', 'iceServers', 'interfaceConfig', 'devices', 'userInfo', 'appData' ]) {
const urlParamsArray
= _objectToURLParamsArray(

View File

@@ -116,7 +116,7 @@ MiddlewareRegistry.register(store => next => action => {
const state = store.getState();
if (!isReactionsEnabled(state)) {
return;
return next(action);
}
const { participant, data } = action;

View File

@@ -314,6 +314,7 @@ function _calendarNotification({ dispatch, getState }: IStore, eventToShow: any)
customActionType,
description,
icon,
maxLines: 1,
title,
uid
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));

View File

@@ -427,17 +427,20 @@ export function invitePeopleAndChatRooms(
return Promise.resolve();
}
const headers = {
...jwt ? { 'Authorization': `Bearer ${jwt}` } : {},
'Content-Type': 'application/json'
};
return fetch(
`${inviteServiceUrl}?token=${jwt}`,
`${inviteServiceUrl}`,
{
body: JSON.stringify({
'invited': inviteItems,
'url': inviteUrl
}),
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
headers
}
);
}
@@ -544,8 +547,16 @@ export function searchDirectory( // eslint-disable-line max-params
const query = encodeURIComponent(text);
const queryTypesString = encodeURIComponent(JSON.stringify(queryTypes));
const headers = {
...jwt ? { 'Authorization': `Bearer ${jwt}` } : {}
};
return fetch(`${serviceUrl}?query=${query}&queryTypes=${
queryTypesString}&jwt=${jwt}`)
queryTypesString}`,
{
method: 'GET',
headers
})
.then(response => {
const jsonify = response.json();

View File

@@ -152,8 +152,9 @@ const Notification = ({
if (descriptionArray?.length) {
return (
<>
<Text style = { styles.contentTextTitle as TextStyle }>
{titleText}
<Text
style = { styles.contentTextTitle as TextStyle }>
{ titleText }
</Text>
{
descriptionArray.map((line, index) => (
@@ -170,7 +171,7 @@ const Notification = ({
return (
<Text style = { styles.contentTextTitle as TextStyle }>
{titleText}
{ titleText }
</Text>
);
};
@@ -202,7 +203,7 @@ const Notification = ({
{_renderContent()}
</View>
<View style = { styles.btnContainer as ViewStyle }>
{mapAppearanceToButtons()}
{ mapAppearanceToButtons() }
</View>
</View>
<IconButton

View File

@@ -7,13 +7,12 @@ const contentColumn = {
};
const notification = {
display: 'flex',
backgroundColor: BaseTheme.palette.ui10,
borderRadius: BaseTheme.shape.borderRadius,
borderLeftColor: BaseTheme.palette.link01Active,
borderLeftWidth: 4,
borderLeftWidth: BaseTheme.spacing[1],
display: 'flex',
flexDirection: 'row',
maxHeight: 120,
height: 'auto',
marginBottom: BaseTheme.spacing[2],
marginHorizontal: BaseTheme.spacing[2],
@@ -43,20 +42,20 @@ export default {
*/
contentContainer: {
marginTop: BaseTheme.spacing[2]
marginHorizontal: BaseTheme.spacing[2]
},
contentText: {
color: BaseTheme.palette.text04,
marginLeft: BaseTheme.spacing[6],
marginLeft: BaseTheme.spacing[5],
marginTop: BaseTheme.spacing[1]
},
contentTextTitle: {
color: BaseTheme.palette.text04,
marginLeft: BaseTheme.spacing[6],
fontWeight: 'bold',
marginTop: BaseTheme.spacing[1]
marginLeft: BaseTheme.spacing[5],
marginTop: BaseTheme.spacing[2]
},
/**
@@ -102,7 +101,7 @@ export default {
btnContainer: {
display: 'flex',
flexDirection: 'row',
marginLeft: BaseTheme.spacing[1]
marginLeft: BaseTheme.spacing[3]
},
withToolbox: {

View File

@@ -11,12 +11,12 @@ export const NOTIFICATION_TIMEOUT = {
/**
* Notification timeout type.
*/
export const NOTIFICATION_TIMEOUT_TYPE = {
SHORT: 'short',
MEDIUM: 'medium',
LONG: 'long',
STICKY: 'sticky'
};
export enum NOTIFICATION_TIMEOUT_TYPE {
LONG = 'long',
MEDIUM = 'medium',
SHORT = 'short',
STICKY = 'sticky'
}
/**
* The set of possible notification types.

View File

@@ -18,6 +18,17 @@ export const CHANGE_VOTE = 'CHANGE_VOTE';
*/
export const CLEAR_POLLS = 'CLEAR_POLLS';
/**
* The type of the action triggered when the poll is editing.
*
* {
* type: EDIT_POLL,
* pollId: string,
* editing: boolean
* }
*/
export const EDIT_POLL = 'EDIT_POLL';
/**
* The type of the action which signals that a new Poll was received.
*
@@ -71,3 +82,15 @@ export const RETRACT_VOTE = 'RETRACT_VOTE';
* }
*/
export const RESET_NB_UNREAD_POLLS = 'RESET_NB_UNREAD_POLLS';
/**
* The type of the action triggered when the poll is saved.
*
* {
* type: SAVE_POLL,
* poll: Poll,
* pollId: string,
* saved: boolean
* }
*/
export const SAVE_POLL = 'SAVE_POLL';

View File

@@ -1,11 +1,13 @@
import {
CHANGE_VOTE,
CLEAR_POLLS,
EDIT_POLL,
RECEIVE_ANSWER,
RECEIVE_POLL,
REGISTER_VOTE,
RESET_NB_UNREAD_POLLS,
RETRACT_VOTE
RETRACT_VOTE,
SAVE_POLL
} from './actionTypes';
import { IAnswer, IPoll } from './types';
@@ -128,3 +130,46 @@ export function resetNbUnreadPollsMessages() {
type: RESET_NB_UNREAD_POLLS
};
}
/**
* Action to signal saving a poll.
*
* @param {string} pollId - The id of the poll that gets to be saved.
* @param {IPoll} poll - The Poll object that gets to be saved.
* @param {boolean} saved - Whether the poll is saved or not.
* @returns {{
* type: RECEIVE_POLL,
* poll: IPoll,
* pollId: string,
* saved: boolean
* }}
*/
export function savePoll(pollId: string, poll: IPoll, saved: boolean) {
return {
type: SAVE_POLL,
pollId,
poll: {
...poll,
saved
}
};
}
/**
* Action to signal editing a poll.
*
* @param {string} pollId - The id of the poll that gets to be edited.
* @param {boolean} editing - Whether the poll is in edit mode or not.
* @returns {{
* type: RECEIVE_POLL,
* pollId: string,
* editing: boolean
* }}
*/
export function editPoll(pollId: string, editing: boolean) {
return {
type: EDIT_POLL,
pollId,
editing
};
}

View File

@@ -7,8 +7,8 @@ import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { getParticipantDisplayName } from '../../base/participants/functions';
import { useBoundSelector } from '../../base/util/hooks';
import { registerVote, setVoteChanging } from '../actions';
import { COMMAND_ANSWER_POLL } from '../constants';
import { editPoll, registerVote, setVoteChanging } from '../actions';
import { COMMAND_ANSWER_POLL, COMMAND_NEW_POLL } from '../constants';
import { IPoll } from '../types';
/**
@@ -16,6 +16,7 @@ import { IPoll } from '../types';
*/
type InputProps = {
pollId: string;
setCreateMode: (mode: boolean) => void;
};
/*
@@ -26,7 +27,10 @@ export type AbstractProps = {
checkBoxStates: boolean[];
creatorName: string;
poll: IPoll;
pollId: string;
sendPoll: () => void;
setCheckbox: Function;
setCreateMode: (mode: boolean) => void;
skipAnswer: () => void;
skipChangeVote: () => void;
submitAnswer: () => void;
@@ -42,21 +46,23 @@ export type AbstractProps = {
*/
const AbstractPollAnswer = (Component: ComponentType<AbstractProps>) => (props: InputProps) => {
const { pollId } = props;
const { pollId, setCreateMode } = props;
const conference: any = useSelector((state: IReduxState) => state['features/base/conference'].conference);
const poll: IPoll = useSelector((state: IReduxState) => state['features/polls'].polls[pollId]);
const { answers, lastVote, question, senderId } = poll;
const [ checkBoxStates, setCheckBoxState ] = useState(() => {
if (poll.lastVote !== null) {
return [ ...poll.lastVote ];
if (lastVote !== null) {
return [ ...lastVote ];
}
return new Array(poll.answers.length).fill(false);
return new Array(answers.length).fill(false);
});
const participantName = useBoundSelector(getParticipantDisplayName, poll.senderId);
const participantName = useBoundSelector(getParticipantDisplayName, senderId);
const setCheckbox = useCallback((index, state) => {
const newCheckBoxStates = [ ...checkBoxStates ];
@@ -81,10 +87,21 @@ const AbstractPollAnswer = (Component: ComponentType<AbstractProps>) => (props:
return false;
}, [ pollId, checkBoxStates, conference ]);
const sendPoll = useCallback(() => {
conference?.sendMessage({
type: COMMAND_NEW_POLL,
pollId,
question,
answers: answers.map(answer => answer.name)
});
dispatch(editPoll(pollId, false));
}, [ conference, question, answers ]);
const skipAnswer = useCallback(() => {
dispatch(registerVote(pollId, null));
sendAnalytics(createPollEvent('vote.skipped'));
}, [ pollId ]);
const skipChangeVote = useCallback(() => {
@@ -97,7 +114,10 @@ const AbstractPollAnswer = (Component: ComponentType<AbstractProps>) => (props:
checkBoxStates = { checkBoxStates }
creatorName = { participantName }
poll = { poll }
pollId = { pollId }
sendPoll = { sendPoll }
setCheckbox = { setCheckbox }
setCreateMode = { setCreateMode }
skipAnswer = { skipAnswer }
skipChangeVote = { skipChangeVote }
submitAnswer = { submitAnswer }

View File

@@ -1,12 +1,16 @@
import React, { ComponentType, FormEvent, useCallback, useState } from 'react';
/* eslint-disable arrow-body-style */
import React, { ComponentType, FormEvent, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { createPollEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { COMMAND_NEW_POLL } from '../constants';
import { getLocalParticipant } from '../../base/participants/functions';
import { savePoll } from '../actions';
import { hasIdenticalAnswers } from '../functions';
import { IAnswerData, IPoll } from '../types';
/**
* The type of the React {@code Component} props of inheriting component.
@@ -21,12 +25,14 @@ type InputProps = {
**/
export type AbstractProps = InputProps & {
addAnswer: (index?: number) => void;
answers: Array<string>;
answers: Array<IAnswerData>;
editingPoll: IPoll | undefined;
editingPollId: string | undefined;
isSubmitDisabled: boolean;
onSubmit: (event?: FormEvent<HTMLFormElement>) => void;
question: string;
removeAnswer: (index: number) => void;
setAnswer: (index: number, value: string) => void;
setAnswer: (index: number, value: IAnswerData) => void;
setQuestion: (question: string) => void;
t: Function;
};
@@ -44,11 +50,45 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
const { setCreateMode } = props;
const [ question, setQuestion ] = useState('');
const pollState = useSelector((state: IReduxState) => state['features/polls'].polls);
const [ answers, setAnswers ] = useState([ '', '' ]);
const editingPoll: [ string, IPoll ] | null = useMemo(() => {
if (!pollState) {
return null;
}
const setAnswer = useCallback((i, answer) => {
for (const key in pollState) {
if (pollState.hasOwnProperty(key) && pollState[key].editing) {
return [ key, pollState[key] ];
}
}
return null;
}, [ pollState ]);
const answerResults = useMemo(() => {
return editingPoll
? editingPoll[1].answers
: [
{
name: '',
voters: []
},
{
name: '',
voters: []
} ];
}, [ editingPoll ]);
const questionResult = useMemo(() => {
return editingPoll ? editingPoll[1].question : '';
}, [ editingPoll ]);
const [ question, setQuestion ] = useState(questionResult);
const [ answers, setAnswers ] = useState(answerResults);
const setAnswer = useCallback((i: number, answer: IAnswerData) => {
setAnswers(currentAnswers => {
const newAnswers = [ ...currentAnswers ];
@@ -59,10 +99,14 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
}, [ answers ]);
const addAnswer = useCallback((i?: number) => {
const newAnswers = [ ...answers ];
const newAnswers: Array<IAnswerData> = [ ...answers ];
sendAnalytics(createPollEvent('option.added'));
newAnswers.splice(typeof i === 'number' ? i : answers.length, 0, '');
newAnswers.splice(typeof i === 'number'
? i : answers.length, 0, {
name: '',
voters: []
});
setAnswers(newAnswers);
}, [ answers ]);
@@ -79,23 +123,40 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference);
const dispatch = useDispatch();
const pollId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36);
const localParticipant = useSelector(getLocalParticipant);
const onSubmit = useCallback(ev => {
if (ev) {
ev.preventDefault();
}
const filteredAnswers = answers.filter(answer => answer.trim().length > 0);
const filteredAnswers = answers.filter(answer => answer.name.trim().length > 0);
if (filteredAnswers.length < 2) {
return;
}
conference?.sendMessage({
type: COMMAND_NEW_POLL,
pollId: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36),
const poll = {
changingVote: false,
senderId: localParticipant?.id,
showResults: false,
lastVote: null,
question,
answers: filteredAnswers
});
answers: filteredAnswers,
saved: false,
editing: false
};
if (editingPoll) {
dispatch(savePoll(editingPoll[0], poll, true));
} else {
dispatch(savePoll(pollId, poll, true));
}
sendAnalytics(createPollEvent('created'));
setCreateMode(false);
@@ -105,7 +166,7 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
// Check if the poll create form can be submitted i.e. if the send button should be disabled.
const isSubmitDisabled
= question.trim().length <= 0 // If no question is provided
|| answers.filter(answer => answer.trim().length > 0).length < 2 // If not enough options are provided
|| answers.filter(answer => answer.name.trim().length > 0).length < 2 // If not enough options are provided
|| hasIdenticalAnswers(answers); // If duplicate options are provided
const { t } = useTranslation();
@@ -113,6 +174,8 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
return (<Component
addAnswer = { addAnswer }
answers = { answers }
editingPoll = { editingPoll?.[1] }
editingPollId = { editingPoll?.[0] }
isSubmitDisabled = { isSubmitDisabled }
onSubmit = { onSubmit }
question = { question }

View File

@@ -69,9 +69,9 @@ const AbstractPollResults = (Component: ComponentType<AbstractProps>) => (props:
// Getting every voters ID that participates to the poll
for (const answer of pollDetails.answers) {
// checking if the voters is an array for supporting old structure model
const voters = answer.voters?.length ? answer.voters : Object.keys(answer.voters);
const voters: string[] = answer.voters.length ? answer.voters : Object.keys(answer.voters);
voters.forEach(voter => allVoters.add(voter));
voters.forEach((voter: string) => allVoters.add(voter));
}
return pollDetails.answers.map(answer => {

View File

@@ -1,28 +1,34 @@
/* eslint-disable react/jsx-no-bind */
import React from 'react';
import { Text, TextStyle, View, ViewStyle } from 'react-native';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { getLocalParticipant } from '../../../base/participants/functions';
import Button from '../../../base/ui/components/native/Button';
import Switch from '../../../base/ui/components/native/Switch';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { editPoll } from '../../actions';
import { isSubmitAnswerDisabled } from '../../functions';
import AbstractPollAnswer, { AbstractProps } from '../AbstractPollAnswer';
import { chatStyles, dialogStyles } from './styles';
const PollAnswer = (props: AbstractProps) => {
const {
checkBoxStates,
poll,
pollId,
sendPoll,
setCheckbox,
setCreateMode,
skipAnswer,
skipChangeVote,
submitAnswer,
t
} = props;
const { changingVote } = poll;
const { changingVote, saved: pollSaved } = poll;
const dispatch = useDispatch();
const localParticipant = useSelector(getLocalParticipant);
const { PRIMARY, SECONDARY } = BUTTON_TYPES;
@@ -34,33 +40,57 @@ const PollAnswer = (props: AbstractProps) => {
}
</Text>
<View style = { chatStyles.answerContent as ViewStyle }>
{poll.answers.map((answer, index) => (
<View
key = { index }
style = { chatStyles.switchRow as ViewStyle } >
<Switch
checked = { checkBoxStates[index] }
/* eslint-disable-next-line react/jsx-no-bind */
onChange = { state => setCheckbox(index, state) } />
<Text style = { chatStyles.switchLabel as TextStyle }>{answer.name}</Text>
{
poll.answers.map((answer, index: number) => (
<View
key = { index }
style = { chatStyles.switchRow as ViewStyle } >
<Switch
checked = { checkBoxStates[index] }
disabled = { poll.saved }
onChange = { state => setCheckbox(index, state) } />
<Text style = { chatStyles.switchLabel as TextStyle }>
{ answer.name }
</Text>
</View>
))
}
</View>
{
pollSaved
? <View style = { chatStyles.buttonRow as ViewStyle }>
<Button
accessibilityLabel = 'polls.answer.edit'
labelKey = 'polls.answer.edit'
onClick = { () => {
setCreateMode(true);
dispatch(editPoll(pollId, true));
} }
style = { chatStyles.pollCreateButton }
type = { SECONDARY } />
<Button
accessibilityLabel = 'polls.answer.send'
labelKey = 'polls.answer.send'
onClick = { sendPoll }
style = { chatStyles.pollCreateButton }
type = { PRIMARY } />
</View>
))}
</View>
<View style = { chatStyles.buttonRow as ViewStyle }>
<Button
accessibilityLabel = 'polls.answer.skip'
labelKey = 'polls.answer.skip'
onClick = { changingVote ? skipChangeVote : skipAnswer }
style = { chatStyles.pollCreateButton }
type = { SECONDARY } />
<Button
accessibilityLabel = 'polls.answer.submit'
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
labelKey = 'polls.answer.submit'
onClick = { submitAnswer }
style = { chatStyles.pollCreateButton }
type = { PRIMARY } />
</View>
: <View style = { chatStyles.buttonRow as ViewStyle }>
<Button
accessibilityLabel = 'polls.answer.skip'
labelKey = 'polls.answer.skip'
onClick = { changingVote ? skipChangeVote : skipAnswer }
style = { chatStyles.pollCreateButton }
type = { SECONDARY } />
<Button
accessibilityLabel = 'polls.answer.submit'
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
labelKey = 'polls.answer.submit'
onClick = { submitAnswer }
style = { chatStyles.pollCreateButton }
type = { PRIMARY } />
</View>
}
</>
);
};

View File

@@ -2,12 +2,14 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import { FlatList, Platform, View, ViewStyle } from 'react-native';
import { TextInput } from 'react-native-gesture-handler';
import { Divider } from 'react-native-paper';
import { useDispatch } from 'react-redux';
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 styles
from '../../../settings/components/native/styles';
import { editPoll } from '../../actions';
import { ANSWERS_LIMIT, CHAR_LIMIT } from '../../constants';
import AbstractPollCreate, { AbstractProps } from '../AbstractPollCreate';
@@ -17,6 +19,8 @@ const PollCreate = (props: AbstractProps) => {
const {
addAnswer,
answers,
editingPoll,
editingPollId,
isSubmitDisabled,
onSubmit,
question,
@@ -28,6 +32,7 @@ const PollCreate = (props: AbstractProps) => {
} = props;
const answerListRef = useRef<FlatList>(null);
const dispatch = useDispatch();
/*
* This ref stores the Array of answer input fields, allowing us to focus on them.
@@ -74,7 +79,7 @@ const PollCreate = (props: AbstractProps) => {
// Called on keypress in answer fields
const onAnswerKeyDown = useCallback((index: number, ev) => {
const { key } = ev.nativeEvent;
const currentText = answers[index];
const currentText = answers[index].name;
if (key === 'Backspace' && currentText === '' && answers.length > 1) {
removeAnswer(index);
@@ -92,35 +97,48 @@ const PollCreate = (props: AbstractProps) => {
type = { TERTIARY } />
);
const pollCreateButtonsContainerStyles = Platform.OS === 'android'
? chatStyles.pollCreateButtonsContainerAndroid : chatStyles.pollCreateButtonsContainerIos;
/* eslint-disable react/jsx-no-bind */
const renderListItem = ({ index }: { index: number; }) =>
const renderListItem = ({ index }: { index: number; }) => {
// padding to take into account the two default options
(
const isIdenticalAnswer
= answers.slice(0, index).length === 0 ? false : answers.slice(0, index).some(prevAnswer =>
prevAnswer.name === answers[index].name
&& prevAnswer.name !== '' && answers[index].name !== '');
return (
<View
style = { dialogStyles.optionContainer as ViewStyle }>
<Input
blurOnSubmit = { false }
bottomLabel = { (
isIdenticalAnswer ? t('polls.errors.notUniqueOption', { index: index + 1 }) : '') }
error = { isIdenticalAnswer }
id = { `polls-answer-input-${index}` }
label = { t('polls.create.pollOption', { index: index + 1 }) }
maxLength = { CHAR_LIMIT }
multiline = { true }
onChange = { text => setAnswer(index, text) }
onChange = { name => setAnswer(index,
{
name,
voters: []
}) }
onKeyPress = { ev => onAnswerKeyDown(index, ev) }
placeholder = { t('polls.create.answerPlaceholder', { index: index + 1 }) }
// This is set to help the touch event not be propagated to any subviews.
pointerEvents = { 'auto' }
ref = { input => registerFieldRef(index, input) }
value = { answers[index] } />
value = { answers[index].name } />
{
answers.length > 2
&& createRemoveOptionButton(() => removeAnswer(index))
}
</View>
);
const pollCreateButtonsContainerStyles = Platform.OS === 'android'
? chatStyles.pollCreateButtonsContainerAndroid : chatStyles.pollCreateButtonsContainerIos;
};
return (
<View style = { chatStyles.pollCreateContainer as ViewStyle }>
@@ -164,13 +182,18 @@ const PollCreate = (props: AbstractProps) => {
<Button
accessibilityLabel = 'polls.create.cancel'
labelKey = 'polls.create.cancel'
onClick = { () => setCreateMode(false) }
onClick = { () => {
setCreateMode(false);
editingPollId
&& editingPoll?.editing
&& dispatch(editPoll(editingPollId, false));
} }
style = { chatStyles.pollCreateButton }
type = { SECONDARY } />
<Button
accessibilityLabel = 'polls.create.send'
accessibilityLabel = 'polls.create.save'
disabled = { isSubmitDisabled }
labelKey = 'polls.create.send'
labelKey = 'polls.create.save'
onClick = { onSubmit }
style = { chatStyles.pollCreateButton }
type = { PRIMARY } />

View File

@@ -15,9 +15,14 @@ interface IProps {
*/
pollId: string;
/**
* Create mode control.
*/
setCreateMode: (mode: boolean) => void;
}
const PollItem = ({ pollId }: IProps) => {
const PollItem = ({ pollId, setCreateMode }: IProps) => {
const showResults = useSelector(shouldShowResults(pollId));
return (
@@ -28,7 +33,8 @@ const PollItem = ({ pollId }: IProps) => {
key = { pollId }
pollId = { pollId } />
: <PollAnswer
pollId = { pollId } />
pollId = { pollId }
setCreateMode = { setCreateMode } />
}
</View>

View File

@@ -12,7 +12,11 @@ import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import PollItem from './PollItem';
import { chatStyles } from './styles';
const PollsList = () => {
interface IPollListProps {
setCreateMode: (mode: boolean) => void;
}
const PollsList = ({ setCreateMode }: IPollListProps) => {
const polls = useSelector((state: IReduxState) => state['features/polls'].polls);
const { t } = useTranslation();
const listPolls = Object.keys(polls);
@@ -20,7 +24,8 @@ const PollsList = () => {
const renderItem = useCallback(({ item }) => (
<PollItem
key = { item }
pollId = { item } />)
pollId = { item }
setCreateMode = { setCreateMode } />)
, []);
const flatlistRef = useRef<FlatList>(null);

View File

@@ -53,21 +53,20 @@ const PollsPane = (props: AbstractProps) => {
{
createMode
? <PollCreate setCreateMode = { setCreateMode } />
: <PollsList />
}
{
!createMode && <Button
accessibilityLabel = 'polls.create.create'
labelKey = 'polls.create.create'
onClick = { onCreate }
style = { createPollButtonStyles }
type = { BUTTON_TYPES.PRIMARY } />
: <>
<PollsList setCreateMode = { setCreateMode } />
<Button
accessibilityLabel = 'polls.create.create'
labelKey = 'polls.create.create'
onClick = { onCreate }
style = { createPollButtonStyles }
type = { BUTTON_TYPES.PRIMARY } />
</>
}
</JitsiScreen>
);
};
/*
* We apply AbstractPollsPane to fill in the AbstractProps common
* to both the web and native implementations.

View File

@@ -209,7 +209,8 @@ export const chatStyles = createStyleSheet({
},
createPollButtonAndroid: {
marginHorizontal: 20
marginHorizontal: 20,
marginVertical: BaseTheme.spacing[5]
},
pollPane: {

View File

@@ -1,10 +1,14 @@
/* eslint-disable react/jsx-no-bind */
import React from 'react';
import { useDispatch } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Button from '../../../base/ui/components/web/Button';
import Checkbox from '../../../base/ui/components/web/Checkbox';
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
import { editPoll } from '../../actions';
import { isSubmitAnswerDisabled } from '../../functions';
import AbstractPollAnswer, { AbstractProps } from '../AbstractPollAnswer';
@@ -53,13 +57,18 @@ const PollAnswer = ({
creatorName,
checkBoxStates,
poll,
pollId,
setCheckbox,
setCreateMode,
skipAnswer,
skipChangeVote,
sendPoll,
submitAnswer,
t
}: AbstractProps) => {
const { changingVote } = poll;
const { changingVote, saved: pollSaved } = poll;
const dispatch = useDispatch();
const { classes } = useStyles();
return (
@@ -74,32 +83,50 @@ const PollAnswer = ({
</div>
<ul className = { classes.answerList }>
{
poll.answers.map((answer: any, index: number) => (
poll.answers.map((answer, index: number) => (
<li
className = { classes.answer }
key = { index }>
<Checkbox
checked = { checkBoxStates[index] }
disabled = { poll.saved }
key = { index }
label = { answer.name }
// eslint-disable-next-line react/jsx-no-bind
onChange = { ev => setCheckbox(index, ev.target.checked) } />
</li>
))
}
</ul>
<div className = { classes.footer } >
<Button
accessibilityLabel = { t('polls.answer.skip') }
className = { classes.buttonMargin }
labelKey = { 'polls.answer.skip' }
onClick = { changingVote ? skipChangeVote : skipAnswer }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.answer.submit') }
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
labelKey = { 'polls.answer.submit' }
onClick = { submitAnswer } />
{
pollSaved ? <>
<Button
accessibilityLabel = { t('polls.answer.edit') }
className = { classes.buttonMargin }
labelKey = { 'polls.answer.edit' }
onClick = { () => {
setCreateMode(true);
dispatch(editPoll(pollId, true));
} }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.answer.send') }
labelKey = { 'polls.answer.send' }
onClick = { sendPoll } />
</> : <>
<Button
accessibilityLabel = { t('polls.answer.skip') }
className = { classes.buttonMargin }
labelKey = { 'polls.answer.skip' }
onClick = { changingVote ? skipChangeVote : skipAnswer }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.answer.submit') }
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
labelKey = { 'polls.answer.submit' }
onClick = { submitAnswer } />
</>
}
</div>
</div>
);

View File

@@ -1,10 +1,12 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Button from '../../../base/ui/components/web/Button';
import Input from '../../../base/ui/components/web/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
import { editPoll } from '../../actions';
import { ANSWERS_LIMIT, CHAR_LIMIT } from '../../constants';
import AbstractPollCreate, { AbstractProps } from '../AbstractPollCreate';
@@ -64,6 +66,8 @@ const useStyles = makeStyles()(theme => {
const PollCreate = ({
addAnswer,
answers,
editingPoll,
editingPollId,
isSubmitDisabled,
onSubmit,
question,
@@ -74,6 +78,7 @@ const PollCreate = ({
t
}: AbstractProps) => {
const { classes } = useStyles();
const dispatch = useDispatch();
/*
* This ref stores the Array of answer input fields, allowing us to focus on them.
@@ -201,11 +206,12 @@ const PollCreate = ({
value = { question } />
</div>
<ol className = { classes.answerList }>
{answers.map((answer: any, i: number) => {
{answers.map((answer, i: number) => {
const isIdenticalAnswer = answers.slice(0, i).length === 0 ? false
: answers.slice(0, i).some((prevAnswer: string) =>
prevAnswer === answer && prevAnswer !== '' && answer !== '');
: answers.slice(0, i).some(prevAnswer =>
prevAnswer.name === answer.name
&& prevAnswer.name !== '' && answer.name !== '');
return (<li
className = { classes.answer }
@@ -217,12 +223,15 @@ const PollCreate = ({
id = { `polls-answer-input-${i}` }
label = { t('polls.create.pollOption', { index: i + 1 }) }
maxLength = { CHAR_LIMIT }
onChange = { val => setAnswer(i, val) }
onChange = { name => setAnswer(i, {
name,
voters: []
}) }
onKeyPress = { ev => onAnswerKeyDown(i, ev) }
placeholder = { t('polls.create.answerPlaceholder', { index: i + 1 }) }
ref = { r => registerFieldRef(i, r) }
textarea = { true }
value = { answer } />
value = { answer.name } />
{ answers.length > 2
&& <button
@@ -252,13 +261,18 @@ const PollCreate = ({
accessibilityLabel = { t('polls.create.cancel') }
className = { classes.buttonMargin }
labelKey = { 'polls.create.cancel' }
onClick = { () => setCreateMode(false) }
onClick = { () => {
setCreateMode(false);
editingPollId
&& editingPoll?.editing
&& dispatch(editPoll(editingPollId, false));
} }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.create.send') }
accessibilityLabel = { t('polls.create.save') }
disabled = { isSubmitDisabled }
isSubmit = { true }
labelKey = { 'polls.create.send' } />
labelKey = { 'polls.create.save' } />
</div>
</form>);
};

View File

@@ -14,9 +14,14 @@ interface IProps {
*/
pollId: string;
/**
* Create mode control.
*/
setCreateMode: (mode: boolean) => void;
}
const PollItem = React.forwardRef<HTMLDivElement, IProps>(({ pollId }: IProps, ref) => {
const PollItem = React.forwardRef<HTMLDivElement, IProps>(({ pollId, setCreateMode }: IProps, ref) => {
const showResults = useSelector(shouldShowResults(pollId));
return (
@@ -26,7 +31,8 @@ const PollItem = React.forwardRef<HTMLDivElement, IProps>(({ pollId }: IProps, r
key = { pollId }
pollId = { pollId } />
: <PollAnswer
pollId = { pollId } />
pollId = { pollId }
setCreateMode = { setCreateMode } />
}
</div>

View File

@@ -39,7 +39,11 @@ const useStyles = makeStyles()(theme => {
};
});
const PollsList = () => {
interface IPollListProps {
setCreateMode: (mode: boolean) => void;
}
const PollsList = ({ setCreateMode }: IPollListProps) => {
const { t } = useTranslation();
const { classes, theme } = useStyles();
@@ -80,7 +84,8 @@ const PollsList = () => {
<PollItem
key = { id }
pollId = { id }
ref = { listPolls.length - 1 === index ? pollListEndRef : null } />
ref = { listPolls.length - 1 === index ? pollListEndRef : null }
setCreateMode = { setCreateMode } />
))}
</>
);

View File

@@ -35,7 +35,7 @@ const PollsPane = ({ createMode, onCreate, setCreateMode, t }: AbstractProps) =>
? <PollCreate setCreateMode = { setCreateMode } />
: <div className = { classes.container }>
<div className = { classes.listContainer } >
<PollsList />
<PollsList setCreateMode = { setCreateMode } />
</div>
<div className = { classes.footer }>
<Button

View File

@@ -1,5 +1,7 @@
import { IReduxState } from '../app/types';
import { IAnswerData } from './types';
/**
* Selector creator for determining if poll results should be displayed or not.
*
@@ -49,14 +51,14 @@ export function isSubmitAnswerDisabled(checkBoxStates: Array<boolean>) {
/**
* Check if the input array has identical answers.
*
* @param {Array<string>} currentAnswers - The array of current answers to compare.
* @param {Array<IAnswerData>} currentAnswers - The array of current answers to compare.
* @returns {boolean} - Returns true if the answers are identical.
*/
export function hasIdenticalAnswers(currentAnswers: Array<string>): boolean {
export function hasIdenticalAnswers(currentAnswers: Array<IAnswerData>): boolean {
const nonEmptyCurrentAnswers = currentAnswers.filter((answer: string): boolean => answer !== '');
const nonEmptyCurrentAnswers = currentAnswers.filter((answer): boolean => answer.name !== '');
const currentAnswersSet = new Set(nonEmptyCurrentAnswers);
const currentAnswersSet = new Set(nonEmptyCurrentAnswers.map(answer => answer.name));
return currentAnswersSet.size !== nonEmptyCurrentAnswers.length;
}

View File

@@ -50,7 +50,9 @@ const parsePollData = (pollData: IPollData): IPoll | null => {
question,
showResults: true,
lastVote: null,
answers
answers,
saved: false,
editing: false
};
};
@@ -121,7 +123,7 @@ function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], get
switch (data.type) {
case COMMAND_NEW_POLL: {
const { question, answers, pollId, senderId } = data;
const { pollId, answers, senderId, question } = data;
const poll = {
changingVote: false,
@@ -129,12 +131,14 @@ function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], get
showResults: false,
lastVote: null,
question,
answers: answers.map((answer: IAnswer) => {
answers: answers.map((answer: string) => {
return {
name: answer,
voters: []
};
})
}),
saved: false,
editing: false
};
dispatch(receivePoll(pollId, poll, true));

View File

@@ -3,11 +3,13 @@ import ReducerRegistry from '../base/redux/ReducerRegistry';
import {
CHANGE_VOTE,
CLEAR_POLLS,
EDIT_POLL,
RECEIVE_ANSWER,
RECEIVE_POLL,
REGISTER_VOTE,
RESET_NB_UNREAD_POLLS,
RETRACT_VOTE
RETRACT_VOTE,
SAVE_POLL
} from './actionTypes';
import { IAnswer, IPoll } from './types';
@@ -51,20 +53,17 @@ ReducerRegistry.register<IPollsState>('features/polls', (state = INITIAL_STATE,
};
}
// Reducer triggered when a poll is received
case RECEIVE_POLL: {
const newState = {
// Reducer triggered when a poll is received or saved.
case RECEIVE_POLL:
case SAVE_POLL: {
return {
...state,
polls: {
...state.polls,
// The poll is added to the dictionary of received polls
[action.pollId]: action.poll
},
nbUnreadPolls: state.nbUnreadPolls + 1
};
return newState;
}
// Reducer triggered when an answer is received
@@ -162,6 +161,19 @@ ReducerRegistry.register<IPollsState>('features/polls', (state = INITIAL_STATE,
};
}
case EDIT_POLL: {
return {
...state,
polls: {
...state.polls,
[action.pollId]: {
...state.polls[action.pollId],
editing: action.editing
}
}
};
}
default:
return state;
}

View File

@@ -27,13 +27,18 @@ export interface IPoll {
* An array of answers:
* the name of the answer name and a map of ids and names of voters voting for this option.
*/
answers: Array<{ name: string; voters: Array<string>; }>;
answers: Array<IAnswerData>;
/**
* Whether the poll vote is being edited/changed.
*/
changingVote: boolean;
/**
* Whether poll is in edit mode or not?.
*/
editing: boolean;
/**
* The last sent votes for this poll, or null if voting was skipped
* Note: This is reset when voting/skipping, not when clicking "Change vote".
@@ -45,10 +50,15 @@ export interface IPoll {
*/
question: string;
/**
* Whether poll is saved or not?.
*/
saved: boolean;
/**
* ID of the sender of this poll.
*/
senderId: string;
senderId: string | undefined;
/**
* Whether the results should be shown instead of the answer form.
@@ -59,3 +69,16 @@ export interface IPoll {
export interface IPollData extends IPoll {
id: string;
}
export interface IAnswerData {
/**
* The answer name chosen for the poll.
*/
name: string;
/**
* An array of voters.
*/
voters: Array<string>;
}

View File

@@ -207,5 +207,5 @@ export function isRoomNameEnabled(state: IReduxState): boolean {
const { hideConferenceSubject = false } = state['features/base/config'];
return getFeatureFlag(state, MEETING_NAME_ENABLED, true)
|| !hideConferenceSubject;
&& !hideConferenceSubject;
}

View File

@@ -460,6 +460,6 @@ export function showStartRecordingNotificationWithCallback(openRecordingDialog:
dispatch(hideNotification(START_RECORDING_NOTIFICATION_ID));
} ],
appearance: NOTIFICATION_TYPE.NORMAL
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
};
}

View File

@@ -172,8 +172,11 @@ function _endpointMessageReceived(store: IStore, next: Function, action: AnyActi
}
}
// If the suer is not requesting transcriptions just bail.
if (json.language.slice(0, 2) !== language) {
// If the user is not requesting transcriptions just bail.
// Regex to filter out all possible country codes after language code:
// this should catch all notations like 'en-GB' 'en_GB' and 'enGB'
// and be independent of the country code length
if (json.language.replace(/[-_A-Z].*/, '') !== language) {
return next(action);
}

View File

@@ -59,6 +59,16 @@ export const SET_FULL_SCREEN = 'SET_FULL_SCREEN';
*/
export const SET_HANGUP_MENU_VISIBLE = 'SET_HANGUP_MENU_VISIBLE';
/**
* The type of the (redux) action which sets the main toolbar thresholds.
*
* {
* type: SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS,
* mainToolbarButtonsThresholds: IMainToolbarButtonThresholds
* }
*/
export const SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS = 'SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS';
/**
* The type of the redux action that toggles whether the overflow menu(s) should be shown as drawers.
*/

View File

@@ -8,13 +8,16 @@ import {
FULL_SCREEN_CHANGED,
SET_FULL_SCREEN,
SET_HANGUP_MENU_VISIBLE,
SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS,
SET_OVERFLOW_DRAWER,
SET_OVERFLOW_MENU_VISIBLE,
SET_TOOLBAR_HOVERED,
SET_TOOLBOX_TIMEOUT
} from './actionTypes';
import { setToolboxVisible } from './actions.web';
import { THRESHOLDS } from './constants';
import { getToolbarTimeout } from './functions.web';
import { IMainToolbarButtonThresholds } from './types';
export * from './actions.any';
@@ -121,6 +124,56 @@ export function setFullScreen(fullScreen: boolean) {
};
}
/**
* Sets the mainToolbarButtonsThresholds.
*
* @returns {Function}
*/
export function setMainToolbarThresholds() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { mainToolbarButtons } = getState()['features/base/config'];
if (!mainToolbarButtons || !Array.isArray(mainToolbarButtons) || mainToolbarButtons.length === 0) {
return;
}
const mainToolbarButtonsThresholds: IMainToolbarButtonThresholds = [];
const mainToolbarButtonsLenghtMap = new Map();
let orderIsChanged = false;
mainToolbarButtons.forEach(buttons => {
if (!Array.isArray(buttons) || buttons.length === 0) {
return;
}
mainToolbarButtonsLenghtMap.set(buttons.length, buttons);
});
THRESHOLDS.forEach(({ width, order }) => {
let finalOrder = mainToolbarButtonsLenghtMap.get(order.length);
if (finalOrder) {
orderIsChanged = true;
} else {
finalOrder = order;
}
mainToolbarButtonsThresholds.push({
order: finalOrder,
width
});
});
if (orderIsChanged) {
dispatch({
type: SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS,
mainToolbarButtonsThresholds
});
}
};
}
/**
* Shows the toolbox for specified timeout.
*

View File

@@ -15,7 +15,7 @@ import {
setToolbarHovered,
showToolbox
} from '../../actions.web';
import { NOT_APPLICABLE, THRESHOLDS } from '../../constants';
import { MAIN_TOOLBAR_BUTTONS_PRIORITY } from '../../constants';
import {
getAllToolboxButtons,
getJwtDisabledButtons,
@@ -23,7 +23,7 @@ import {
isToolboxVisible
} from '../../functions.web';
import { useKeyboardShortcuts } from '../../hooks.web';
import { IToolboxButton, NOTIFY_CLICK_MODE } from '../../types';
import { IToolboxButton, NOTIFY_CLICK_MODE, ToolbarButton } from '../../types';
import HangupButton from '../HangupButton';
import { EndConferenceButton } from './EndConferenceButton';
@@ -92,6 +92,14 @@ interface IProps extends WithTranslation {
*/
_jwtDisabledButtons: string[];
/**
* The main toolbar buttons thresholds used to determine the visible buttons depending on the current screen size.
*/
_mainToolbarButtonsThresholds: Array<{
order: Array<ToolbarButton | string>;
width: number;
}>;
/**
* Whether or not the overflow menu is displayed in a drawer drawer.
*/
@@ -174,6 +182,7 @@ const Toolbox = ({
_isMobile,
_isNarrowLayout,
_jwtDisabledButtons,
_mainToolbarButtonsThresholds,
_overflowDrawer,
_overflowMenuVisible,
_reactionsButtonEnabled,
@@ -280,35 +289,42 @@ const Toolbox = ({
function getVisibleButtons() {
const buttons = getAllToolboxButtons(_customToolbarButtons);
const filteredButtons = Object.keys(buttons).filter(key =>
typeof key !== 'undefined' // filter invalid buttons that may be comming from config.mainToolbarButtons
// override
&& !_jwtDisabledButtons.includes(key)
&& isButtonEnabled(key, _toolbarButtons));
setButtonsNotifyClickMode(buttons);
const isHangupVisible = isButtonEnabled('hangup', _toolbarButtons);
const { order } = THRESHOLDS.find(({ width }) => _clientWidth > width)
|| THRESHOLDS[THRESHOLDS.length - 1];
const { order } = _mainToolbarButtonsThresholds.find(({ width }) => _clientWidth > width)
|| _mainToolbarButtonsThresholds[_mainToolbarButtonsThresholds.length - 1];
const keys = Object.keys(buttons);
const mainToolbarButtonKeysOrder = [
...order.filter(key => filteredButtons.includes(key)),
...MAIN_TOOLBAR_BUTTONS_PRIORITY.filter(key => !order.includes(key) && filteredButtons.includes(key)),
...filteredButtons.filter(key => !order.includes(key) && !MAIN_TOOLBAR_BUTTONS_PRIORITY.includes(key))
];
const filtered = [
...order.map(key => buttons[key as keyof typeof buttons]),
...Object.values(buttons).filter((button, index) => !order.includes(keys[index]))
].filter(({ key, alias = NOT_APPLICABLE }) =>
!_jwtDisabledButtons.includes(key)
&& (isButtonEnabled(key, _toolbarButtons) || isButtonEnabled(alias, _toolbarButtons))
);
const mainButtonsKeys = mainToolbarButtonKeysOrder.slice(0, order.length);
const overflowMenuButtons = filteredButtons.reduce((acc, key) => {
if (!mainButtonsKeys.includes(key)) {
acc.push(buttons[key]);
}
let sliceIndex = _overflowDrawer || _reactionsButtonEnabled ? order.length + 2 : order.length + 1;
return acc;
}, [] as IToolboxButton[]);
if (isHangupVisible) {
sliceIndex -= 1;
}
// if we have 1 button in the overflow menu it is better to directly display it in the main toolbar by replacing
// the "More" menu button with it.
if (overflowMenuButtons.length === 1) {
const button = overflowMenuButtons.shift()?.key;
// This implies that the overflow button will be displayed, so save some space for it.
if (sliceIndex < filtered.length) {
sliceIndex -= 1;
button && mainButtonsKeys.push(button);
}
return {
mainMenuButtons: filtered.slice(0, sliceIndex),
overflowMenuButtons: filtered.slice(sliceIndex)
mainMenuButtons: mainButtonsKeys.map(key => buttons[key]),
overflowMenuButtons
};
}
@@ -505,6 +521,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
_jwtDisabledButtons: getJwtDisabledButtons(state),
_hangupMenuVisible: hangupMenuVisible,
_isNarrowLayout: isNarrowLayout,
_mainToolbarButtonsThresholds: state['features/toolbox'].mainToolbarButtonsThresholds,
_overflowMenuVisible: overflowMenuVisible,
_overflowDrawer: overflowDrawer,
_reactionsButtonEnabled: isReactionsButtonEnabled(state),

View File

@@ -6,23 +6,23 @@ import { ToolbarButton } from './types';
export const THRESHOLDS = [
{
width: 565,
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'reactions', 'participants', 'tileview' ]
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'reactions', 'participants-pane', 'tileview' ]
},
{
width: 520,
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants', 'tileview' ]
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants-pane', 'tileview' ]
},
{
width: 470,
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants' ]
order: [ 'microphone', 'camera', 'desktop', 'chat', 'raisehand', 'participants-pane' ]
},
{
width: 420,
order: [ 'microphone', 'camera', 'desktop', 'chat', 'participants' ]
order: [ 'microphone', 'camera', 'desktop', 'chat', 'participants-pane' ]
},
{
width: 370,
order: [ 'microphone', 'camera', 'chat', 'participants' ]
order: [ 'microphone', 'camera', 'chat', 'participants-pane' ]
},
{
width: 225,
@@ -34,7 +34,43 @@ export const THRESHOLDS = [
}
];
export const NOT_APPLICABLE = 'N/A';
/**
* Main toolbar buttons priority used to determine which button should be picked to fill empty spaces for disabled
* buttons.
*/
export const MAIN_TOOLBAR_BUTTONS_PRIORITY = [
'microphone',
'camera',
'desktop',
'chat',
'raisehand',
'reactions',
'participants-pane',
'tileview',
'invite',
'toggle-camera',
'videoquality',
'fullscreen',
'security',
'closedcaptions',
'recording',
'livestreaming',
'linktosalesforce',
'sharedvideo',
'shareaudio',
'noisesuppression',
'whiteboard',
'etherpad',
'select-background',
'stats',
'settings',
'shortcuts',
'profile',
'embedmeeting',
'feedback',
'download',
'help'
];
export const TOOLBAR_TIMEOUT = 4000;

View File

@@ -271,7 +271,7 @@ export function getAllToolboxButtons(_customToolbarButtons?: {
group: 2
};
const videoQuality = {
const videoquality = {
key: 'videoquality',
Content: VideoQualityButton,
group: 2
@@ -285,12 +285,11 @@ export function getAllToolboxButtons(_customToolbarButtons?: {
const security = {
key: 'security',
alias: 'info',
Content: SecurityDialogButton,
group: 2
};
const cc = {
const closedcaptions = {
key: 'closedcaptions',
Content: ClosedCaptionButton,
group: 2
@@ -308,25 +307,25 @@ export function getAllToolboxButtons(_customToolbarButtons?: {
group: 2
};
const linkToSalesforce = {
const linktosalesforce = {
key: 'linktosalesforce',
Content: LinkToSalesforceButton,
group: 2
};
const shareVideo = {
const sharedvideo = {
key: 'sharedvideo',
Content: SharedVideoButton,
group: 3
};
const shareAudio = {
const shareaudio = {
key: 'shareaudio',
Content: ShareAudioButton,
group: 3
};
const noiseSuppression = {
const noisesuppression = {
key: 'noisesuppression',
Content: NoiseSuppressionButton,
group: 3
@@ -351,7 +350,7 @@ export function getAllToolboxButtons(_customToolbarButtons?: {
group: 3
};
const speakerStats = {
const stats = {
key: 'stats',
Content: SpeakerStatsButton,
group: 3
@@ -369,7 +368,7 @@ export function getAllToolboxButtons(_customToolbarButtons?: {
group: 4
};
const embed = {
const embedmeeting = {
key: 'embedmeeting',
Content: EmbedMeetingButton,
group: 4
@@ -415,27 +414,27 @@ export function getAllToolboxButtons(_customToolbarButtons?: {
chat,
raisehand,
reactions,
participants,
'participants-pane': participants,
invite,
tileview,
toggleCamera,
videoQuality,
'toggle-camera': toggleCamera,
videoquality,
fullscreen,
security,
cc,
closedcaptions,
recording,
livestreaming,
linkToSalesforce,
shareVideo,
shareAudio,
noiseSuppression,
linktosalesforce,
sharedvideo,
shareaudio,
noisesuppression,
whiteboard,
etherpad,
virtualBackground,
speakerStats,
'select-background': virtualBackground,
stats,
settings,
shortcuts,
embed,
embedmeeting,
feedback,
download,
help,

View File

@@ -16,6 +16,7 @@ import {
SET_TOOLBAR_BUTTONS,
SET_TOOLBOX_TIMEOUT
} from './actionTypes';
import { setMainToolbarThresholds } from './actions.web';
import { TOOLBAR_BUTTONS, VISITORS_MODE_BUTTONS } from './constants';
import { NOTIFY_CLICK_MODE } from './types';
@@ -53,6 +54,9 @@ MiddlewareRegistry.register(store => next => action => {
} = state['features/base/config'];
batch(() => {
if (action.type !== I_AM_VISITOR_MODE) {
dispatch(setMainToolbarThresholds());
}
dispatch({
type: SET_BUTTONS_WITH_NOTIFY_CLICK,
buttonsWithNotifyClick: _buildButtonsArray(buttonsWithNotifyClick, customToolbarButtons)

View File

@@ -6,6 +6,7 @@ import {
FULL_SCREEN_CHANGED,
SET_BUTTONS_WITH_NOTIFY_CLICK,
SET_HANGUP_MENU_VISIBLE,
SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS,
SET_OVERFLOW_DRAWER,
SET_OVERFLOW_MENU_VISIBLE,
SET_PARTICIPANT_MENU_BUTTONS_WITH_NOTIFY_CLICK,
@@ -17,7 +18,8 @@ import {
SET_TOOLBOX_VISIBLE,
TOGGLE_TOOLBOX_VISIBLE
} from './actionTypes';
import { NOTIFY_CLICK_MODE } from './types';
import { THRESHOLDS } from './constants';
import { IMainToolbarButtonThresholds, NOTIFY_CLICK_MODE } from './types';
/**
* Initial state of toolbox's part of Redux store.
@@ -47,6 +49,11 @@ const INITIAL_STATE = {
*/
hovered: false,
/**
* The thresholds for screen size and visible main toolbar buttons.
*/
mainToolbarButtonsThresholds: THRESHOLDS,
participantMenuButtonsWithNotifyClick: new Map(),
/**
@@ -98,6 +105,7 @@ export interface IToolboxState {
fullScreen?: boolean;
hangupMenuVisible: boolean;
hovered: boolean;
mainToolbarButtonsThresholds: IMainToolbarButtonThresholds;
overflowDrawer: boolean;
overflowMenuVisible: boolean;
participantMenuButtonsWithNotifyClick: Map<string, NOTIFY_CLICK_MODE>;
@@ -151,6 +159,12 @@ ReducerRegistry.register<IToolboxState>(
...state,
buttonsWithNotifyClick: action.buttonsWithNotifyClick
};
case SET_MAIN_TOOLBAR_BUTTONS_THRESHOLDS:
return {
...state,
mainToolbarButtonsThresholds: action.mainToolbarButtonsThresholds
};
case SET_TOOLBAR_HOVERED:
return {
...state,

View File

@@ -2,7 +2,6 @@ import { ComponentType } from 'react';
export interface IToolboxButton {
Content: ComponentType<any>;
alias?: string;
group: number;
key: string;
}
@@ -49,3 +48,8 @@ export enum NOTIFY_CLICK_MODE {
ONLY_NOTIFY = 'ONLY_NOTIFY',
PREVENT_AND_NOTIFY = 'PREVENT_AND_NOTIFY'
}
export type IMainToolbarButtonThresholds = Array<{
order: Array<ToolbarButton | string>;
width: number;
}>;

View File

@@ -20,6 +20,7 @@
"hi": "hi-IN",
"mr": "mr-IN",
"hr": "hr-HR",
"hsb": "hsb-DE",
"hu": "hu-HU",
"hy": "hy-AM",
"id": "id-ID",

View File

@@ -45,6 +45,7 @@
"is-IS": "Icelandic (Iceland)",
"it-IT": "Italian (Italy)",
"lt-LT": "Lithuanian (Lithuania)",
"hsb-DE": "Upper Sorbian (Germany)",
"hu-HU": "Hungarian (Hungary)",
"nl-NL": "Dutch (Netherlands)",
"no-NO": "Norwegian Bokmål (Norway)",

View File

@@ -86,7 +86,8 @@ paths:
description: "successful operation"
schema:
type: array
$ref: "#/definitions/PhoneNumberListAnswer"
items:
$ref: "#/definitions/PhoneNumberListAnswer"
securityDefinitions:
token:
type: "apiKey"
@@ -139,8 +140,8 @@ definitions:
tollFree:
type: boolean
example:
{"countryCode":"US","tollFree":false,"formattedNumber":"+1 123-456-7890"}
{"countryCode":"UK","tollFree":true,"formattedNumber":"+44 123 456 7890"}
- {"countryCode":"US","tollFree":false,"formattedNumber":"+1 123-456-7890"}
- {"countryCode":"UK","tollFree":true,"formattedNumber":"+44 123 456 7890"}
externalDocs:
description: "Find out more about the Jitsi Cloud API"

View File

@@ -31,6 +31,15 @@ function init_session(event)
local session, request = event.session, event.request;
local query = request.url.query;
local token = nil;
-- extract token from Authorization header
if request.headers["authorization"] then
-- assumes the header value starts with "Bearer "
token = request.headers["authorization"]:sub(8,#request.headers["authorization"])
end
-- allow override of token via query parameter
if query ~= nil then
local params = formdecode(query);
@@ -39,8 +48,13 @@ function init_session(event)
-- After validating auth_token will be cleaned in case of error and few
-- other fields will be extracted from the token and set in the session
session.auth_token = query and params.token or nil;
if query and params.token then
token = params.token;
end
end
-- in either case set auth_token in the session
session.auth_token = token;
end
module:hook_global("bosh-session", init_session);

View File

@@ -5,6 +5,7 @@ local token_util = module:require "token/util".new(module);
local util = module:require 'util';
local room_jid_match_rewrite = util.room_jid_match_rewrite;
local is_feature_allowed = util.is_feature_allowed;
local is_sip_jigasi = util.is_sip_jigasi;
local get_room_from_jid = util.get_room_from_jid;
local is_healthcheck_room = util.is_healthcheck_room;
local process_host_module = util.process_host_module;
@@ -166,7 +167,7 @@ function get_concurrent_outgoing_count(context_user, context_group)
for _, occupant in room:each_occupant() do
for _, presence in occupant:each_session() do
local initiator = presence:get_child('initiator', 'http://jitsi.org/protocol/jigasi');
local initiator = is_sip_jigasi(presence);
local found_user = false;
local found_group = false;

View File

@@ -61,7 +61,7 @@ module:hook('muc-occupant-pre-join', function (event)
local occupant, room, origin, stanza = event.occupant, event.room, event.origin, event.stanza;
local node, host = jid.split(occupant.bare_jid);
if host == local_domain then
if prosody.hosts[host] and not is_admin(occupant.bare_jid) then
if room._main_room_lobby_enabled then
origin.send(st.error_reply(stanza, 'cancel', 'not-allowed', 'Visitors not allowed while lobby is on!')
:tag('no-visitors-lobby', { xmlns = 'jitsi:visitors' }));
@@ -161,7 +161,7 @@ module:hook('muc-occupant-left', function (event)
local room, occupant = event.room, event.occupant;
local occupant_domain = jid.host(occupant.bare_jid);
if occupant_domain == local_domain then
if prosody.hosts[occupant_domain] and not is_admin(occupant.bare_jid) then
local focus_occupant = get_focus_occupant(room);
if not focus_occupant then
module:log('info', 'No focus found for %s', room.jid);
@@ -428,14 +428,15 @@ module:hook_global('stats-update', function ()
for room in prosody.hosts[module.host].modules.muc.each_room() do
rooms_count = rooms_count + 1;
for _, o in room:each_occupant() do
if jid.host(o.bare_jid) == local_domain then
visitors_count = visitors_count + 1;
else
participants_count = participants_count + 1;
if not is_admin(o.bare_jid) then
local _, host = jid.split(o.bare_jid);
if prosody.hosts[host] then -- local hosts are visitors (including jigasi)
visitors_count = visitors_count + 1;
else
participants_count = participants_count + 1;
end
end
end
-- do not count jicofo
participants_count = participants_count - 1;
end
measure_rooms(rooms_count);

View File

@@ -4,6 +4,7 @@
local util = module:require "util";
local async_handler_wrapper = util.async_handler_wrapper;
local is_sip_jigasi = util.is_sip_jigasi;
local starts_with = util.starts_with;
local formdecode = require "util.http".formdecode;
local urlencode = require "util.http".urlencode;
@@ -135,9 +136,8 @@ function handle_kick_participant (event)
local pr = occupant:get_presence();
local displayName = pr:get_child_text(
'nick', 'http://jabber.org/protocol/nick');
local initiator = pr:get_child('initiator', 'http://jitsi.org/protocol/jigasi');
if initiator and displayName and starts_with(displayName, number) then
if is_sip_jigasi(pr) and displayName and starts_with(displayName, number) then
room:set_role(true, occupant.nick, nil);
module:log('info', 'Occupant kicked %s from %s', occupant.nick, room.jid);
return { status_code = 200; }

View File

@@ -19,6 +19,7 @@ local util = module:require 'util';
local is_healthcheck_room = util.is_healthcheck_room;
local get_room_from_jid = util.get_room_from_jid;
local room_jid_match_rewrite = util.room_jid_match_rewrite;
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
local process_host_module = util.process_host_module;
local COMPONENT_IDENTITY_TYPE = 'room_metadata';
@@ -66,7 +67,7 @@ function broadcastMetadata(room)
local json_msg = getMetadataJSON(room);
for _, occupant in room:each_occupant() do
send_json_msg(occupant.jid, room.jid, json_msg)
send_json_msg(occupant.jid, internal_room_jid_match_rewrite(room.jid), json_msg)
end
end

View File

@@ -4,6 +4,7 @@ local jid = require 'util.jid';
local st = require 'util.stanza';
local util = module:require 'util';
local is_healthcheck_room = util.is_healthcheck_room;
local is_sip_jigasi = util.is_sip_jigasi;
local room_jid_match_rewrite = util.room_jid_match_rewrite;
local get_room_from_jid = util.get_room_from_jid;
local get_focus_occupant = util.get_focus_occupant;
@@ -328,7 +329,7 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
-- we skip any checks when auto-allow is enabled
if auto_allow_promotion
or ignore_list:contains(jid.host(stanza.attr.from)) -- jibri or other domains to ignore
or stanza:get_child('initiator', 'http://jitsi.org/protocol/jigasi')
or is_sip_jigasi(stanza)
or is_sip_jibri_join(stanza) then
return;
end
@@ -344,16 +345,16 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
session.send(st.error_reply(stanza, 'cancel', 'not-allowed', 'Visitor needs to be allowed by a moderator')
:tag('promotion-not-allowed', { xmlns = 'jitsi:visitors' }));
return true;
else
-- if is_vpaas(room) then
-- -- special case for vpaas where if someone with a visitor token tries to join a room, where
-- -- there are no visitors yet, we deny access
-- if session.jitsi_meet_context_user and session.jitsi_meet_context_user.role == 'visitor' then
elseif is_vpaas(room) then
-- special case for vpaas where if someone with a visitor token tries to join a room, where
-- there are no visitors yet, we deny access
if session.jitsi_meet_context_user and session.jitsi_meet_context_user.role == 'visitor' then
session.log('warn', 'Deny user join as visitor in the main meeting, not approved');
session.send(st.error_reply(
stanza, 'cancel', 'not-allowed', 'Visitor tried to join the main room without approval')
:tag('no-main-participants', { xmlns = 'jitsi:visitors' }));
-- end
return true;
end
end
end, 7); -- after muc_meeting_id, the logic for not joining before jicofo

View File

@@ -466,6 +466,11 @@ function is_vpaas(room)
return true;
end
-- Returns the initiator extension if the stanza is coming from a sip jigasi
function is_sip_jigasi(stanza)
return stanza:get_child('initiator', 'http://jitsi.org/protocol/jigasi');
end
function get_sip_jibri_email_prefix(email)
if not email then
return nil;
@@ -538,6 +543,7 @@ return {
is_healthcheck_room = is_healthcheck_room;
is_moderated = is_moderated;
is_sip_jibri_join = is_sip_jibri_join;
is_sip_jigasi = is_sip_jigasi;
is_vpaas = is_vpaas;
get_focus_occupant = get_focus_occupant;
get_room_from_jid = get_room_from_jid;