Compare commits

...

32 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
384f7d7890 ios: update Podfile.lock 2019-04-02 16:18:09 +02:00
Bettenbuk Zoltan
4d9dcf5d43 [RN] Add InfoDialogButton 2019-04-02 16:18:09 +02:00
Saúl Ibarra Corretgé
e217e10af5 android: update custom version for native dependencies 2019-04-02 14:17:45 +02:00
Saúl Ibarra Corretgé
7feec7c11d deps: react-native-sound€0.10.12 2019-04-02 14:17:45 +02:00
Saúl Ibarra Corretgé
36eb27e233 rn: add build information to SettingsView 2019-04-02 12:40:35 +02:00
Bettenbuk Zoltan
b791fc32fd [RN] Make BaseIndicator render simpler 2019-04-01 21:03:36 +02:00
Bettenbuk Zoltan
b1a70240fc Clear raise hand status on confidence leave 2019-04-01 21:03:36 +02:00
Bettenbuk Zoltan
50d7c1521f Remove legacy web raise hand code 2019-04-01 21:03:36 +02:00
Bettenbuk Zoltan
5d9762b429 Extract notification timeout to a constant 2019-04-01 21:03:36 +02:00
Bettenbuk Zoltan
4e78502c9e Generalize indicators 2019-04-01 21:03:36 +02:00
Bettenbuk Zoltan
6ff733dae0 Platform generic notification for raised hand 2019-04-01 21:03:36 +02:00
Bettenbuk Zoltan
2dc59b9ea0 [RN] Add button to toggle raised hand 2019-04-01 21:03:36 +02:00
Bettenbuk Zoltan
e65918564b [RN] Add UI for raised hand feature 2019-04-01 21:03:36 +02:00
Bettenbuk Zoltan
ce9744b9c3 Move participant event handler to a platform generic location 2019-04-01 21:03:36 +02:00
damencho
b413457a4f Commit from translate.jitsi.org by user damencho.: 262 of 588 strings translated (0 fuzzy). 2019-04-01 12:41:41 +00:00
damencho
75daedf9ab Commit from translate.jitsi.org by user damencho.: 406 of 583 strings translated (30 fuzzy). 2019-04-01 12:40:08 +00:00
damencho
45eeea447a Commit from translate.jitsi.org by user damencho.: 583 of 583 strings translated (0 fuzzy). 2019-04-01 12:40:00 +00:00
damencho
16fcc55ad1 Commit from translate.jitsi.org by user damencho.: 583 of 583 strings translated (0 fuzzy). 2019-04-01 12:39:52 +00:00
damencho
a70009e486 Commit from translate.jitsi.org by user damencho.: 583 of 583 strings translated (0 fuzzy). 2019-04-01 12:39:41 +00:00
damencho
06502e5aac Commit from translate.jitsi.org by user damencho.: 423 of 583 strings translated (24 fuzzy). 2019-04-01 12:39:33 +00:00
damencho
6316447d4b Commit from translate.jitsi.org by user damencho.: 504 of 583 strings translated (0 fuzzy). 2019-04-01 12:39:06 +00:00
jitsi-pootle
df5fa71b92 New files added from translate.jitsi.org based on templates 2019-04-01 12:39:06 +00:00
Bettenbuk Zoltan
10e951c17c Reorg notifications feature files 2019-03-29 18:52:44 +01:00
Hristo Terezov
f12317dc59 docs(api.md): Comply with our coding style. 2019-03-29 16:39:21 +00:00
Hristo Terezov
829e5597d5 fix(iframe-api-devices): Misc small issues. 2019-03-29 15:42:02 +00:00
Hristo Terezov
f2e0704b93 fix(filmstrip-only): DeviceSelectionPopup 2019-03-29 15:42:02 +00:00
Hristo Terezov
a7aaf31c79 feat(iframe-api): Add deviceListChanged event. 2019-03-29 15:42:02 +00:00
Hristo Terezov
4967488e56 ref(iframe-api-devices): Use labels instead of IDs 2019-03-29 15:42:02 +00:00
Hristo Terezov
ed1d3d3df5 fix(api-devices): Initial device function calls 2019-03-29 15:42:02 +00:00
Hristo Terezov
427f49367b feat(iframe-api): Device handling. 2019-03-29 15:42:02 +00:00
Saúl Ibarra Corretgé
659e420005 ios: make sure Fastlane can update the provisioning profile
https://docs.fastlane.tools/codesigning/xcode-project/#xcode-9-and-up
2019-03-29 14:50:59 +01:00
Saúl Ibarra Corretgé
9a46606f0d misc: s/Atlassian/8x8/ 2019-03-29 13:11:36 +01:00
93 changed files with 4418 additions and 1758 deletions

View File

@@ -13,7 +13,7 @@ Found a bug and know how to fix it? Great! Please read on.
## Contributor License Agreement
While the Jitsi projects are released under the
[Apache License 2.0](https://github.com/jitsi/jitsi-meet/blob/master/LICENSE), the copyright
holder and principal creator is [Atlassian](https://www.atlassian.com/). To
holder and principal creator is [8x8](https://www.8x8.com/). To
ensure that we can continue making these projects available under an Open Source license,
we need you to sign our Apache-based contributor
license agreement as either a [corporation](https://jitsi.org/ccla) or an

View File

@@ -73,25 +73,25 @@ allprojects {
// consequently, we should qualify their version.
def versionQualifier = '-jitsi-1'
if ('react-native-background-timer' == project.name)
versionQualifier = '-jitsi-3' // 2.0.0 + react-native 0.57
versionQualifier = '-jitsi-4' // 2.1.1 + react-native 0.59
else if ('react-native-calendar-events' == project.name)
versionQualifier = '-jitsi-2' // 1.6.4 + react-native 0.57
versionQualifier = '-jitsi-3' // 1.6.4 + react-native 0.59
else if ('react-native-fast-image' == project.name)
versionQualifier = '-jitsi-2' // 5.1.1 + react-native 0.57
versionQualifier = '-jitsi-3' // 5.1.1 + react-native 0.59
else if ('react-native-google-signin' == project.name)
versionQualifier = '-jitsi-2' // 1.0.2 + react-native 0.57
versionQualifier = '-jitsi-3' // 1.0.2 + react-native 0.59
else if ('react-native-immersive' == project.name)
versionQualifier = '-jitsi-5' // 2.0.0 + react-native 0.57
versionQualifier = '-jitsi-6' // 2.0.0 + react-native 0.59
else if ('react-native-keep-awake' == project.name)
versionQualifier = '-jitsi-4' // 4.0.0 + react-native 0.57
versionQualifier = '-jitsi-5' // 4.0.0 + react-native 0.59
else if ('react-native-linear-gradient' == project.name)
versionQualifier = '-jitsi-1' // 2.4.0 + react-native 0.57
versionQualifier = '-jitsi-2' // 2.5.3 + react-native 0.59
else if ('react-native-sound' == project.name)
versionQualifier = '-jitsi-2' // 0.10.9 + react-native 0.57
versionQualifier = '-jitsi-3' // 0.10.12 + react-native 0.59
else if ('react-native-vector-icons' == project.name)
versionQualifier = '-jitsi-3' // 6.0.2 + react-native 0.57
versionQualifier = '-jitsi-4' // 6.0.2 + react-native 0.59
else if ('react-native-webrtc' == project.name)
versionQualifier = '-jitsi-9' // 6322a9b5a38ce590cfaea4041072ea87c8dbf558 + react-native 0.57
versionQualifier = '-jitsi-10' // 032ee5c90e2c5ff27ab2f952217104772fcbd155 + react-native 0.59
project.version = "${json.version}${versionQualifier}"

View File

@@ -64,6 +64,9 @@ class AppInfoModule
Map<String, Object> constants = new HashMap<>();
constants.put(
"buildNumber",
packageInfo == null ? "" : String.valueOf(packageInfo.versionCode));
constants.put(
"name",
applicationInfo == null

View File

@@ -1919,21 +1919,6 @@ export default {
JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
(participant, name, oldValue, newValue) => {
switch (name) {
case 'features_screen-sharing': {
APP.store.dispatch(participantUpdated({
conference: room,
id: participant.getId(),
features: { 'screen-sharing': true }
}));
break;
}
case 'raisedHand':
APP.store.dispatch(participantUpdated({
conference: room,
id: participant.getId(),
raisedHand: newValue === 'true'
}));
break;
case 'remoteControlSessionStatus':
APP.UI.setRemoteControlActiveStatus(
participant.getId(),
@@ -2417,7 +2402,7 @@ export default {
*/
updateAudioIconEnabled() {
const audioMediaDevices
= APP.store.getState()['features/base/devices'].audioInput;
= APP.store.getState()['features/base/devices'].availableDevices.audioInput;
const audioDeviceCount
= audioMediaDevices ? audioMediaDevices.length : 0;
@@ -2440,7 +2425,7 @@ export default {
*/
updateVideoIconEnabled() {
const videoMediaDevices
= APP.store.getState()['features/base/devices'].videoInput;
= APP.store.getState()['features/base/devices'].availableDevices.videoInput;
const videoDeviceCount
= videoMediaDevices ? videoMediaDevices.length : 0;

View File

@@ -7,7 +7,7 @@ You can use the Jitsi Meet API to embed Jitsi Meet in to your application. You a
To embed Jitsi Meet in your application you need to add the Jitsi Meet API library:
```javascript
<script src="https://meet.jit.si/external_api.js"></script>
<script src='https://meet.jit.si/external_api.js'></script>
```
## API
@@ -16,7 +16,7 @@ To embed Jitsi Meet in your application you need to add the Jitsi Meet API libra
The next step for embedding Jitsi Meet is to create the Jitsi Meet API object.
Its constructor gets a number of options:
* **domain**: domain used to build the conference URL, "meet.jit.si" for
* **domain**: domain used to build the conference URL, 'meet.jit.si' for
example.
* **options**: object with properties - the optional arguments:
* **roomName**: (optional) name of the room to join.
@@ -29,46 +29,160 @@ Its constructor gets a number of options:
* **jwt**: (optional) [JWT](https://jwt.io/) token.
* **onload**: (optional) handler for the iframe onload event.
* **invitees**: (optional) Array of objects containing information about new participants that will be invited in the call.
* **devices**: (optional) A map containing information about the initial devices that will be used in the call.
Example:
```javascript
var domain = "meet.jit.si";
var options = {
roomName: "JitsiMeetAPIExample",
const domain = 'meet.jit.si';
const options = {
roomName: 'JitsiMeetAPIExample',
width: 700,
height: 700,
parentNode: document.querySelector('#meet')
}
var api = new JitsiMeetExternalAPI(domain, options);
};
const api = new JitsiMeetExternalAPI(domain, options);
```
You can set the initial media devices for the call:
```javascript
const domain = 'meet.jit.si';
const options = {
...
devices: {
audioInput: '<deviceLabel>',
audioOutput: '<deviceLabel>',
videoInput: '<deviceLabel>'
},
...
};
const api = new JitsiMeetExternalAPI(domain, options);
```
You can overwrite options set in [config.js] and [interface_config.js].
For example, to enable the filmstrip-only interface mode, you can use:
```javascript
var options = {
interfaceConfigOverwrite: {filmStripOnly: true}
const options = {
...
interfaceConfigOverwrite: { filmStripOnly: true },
...
};
var api = new JitsiMeetExternalAPI(domain, options);
const api = new JitsiMeetExternalAPI(domain, options);
```
You can also pass a jwt token to Jitsi Meet:
```javascript
var options = {
jwt: "<jwt_token>",
noSsl: false
const options = {
...
jwt: '<jwt_token>',
noSsl: false,
...
};
var api = new JitsiMeetExternalAPI(domain, options);
const api = new JitsiMeetExternalAPI(domain, options);
```
### Controlling the embedded Jitsi Meet Conference
Device management `JitsiMeetExternalAPI` methods:
* **getAvailableDevices** - Retrieve a list of available devices.
```javascript
api.getAvailableDevices().then(devices => {
// devices = {
// audioInput: [{
// deviceId: 'ID'
// groupId: 'grpID'
// kind: 'audioinput'
// label: 'label'
// },....],
// audioOutput: [{
// deviceId: 'ID'
// groupId: 'grpID'
// kind: 'audioOutput'
// label: 'label'
// },....],
// videoInput: [{
// deviceId: 'ID'
// groupId: 'grpID'
// kind: 'videoInput'
// label: 'label'
// },....]
// }
...
});
```
* **getCurrentDevices** - Retrieve a list with the devices that are currently selected.
```javascript
api.getCurrentDevices().then(devices => {
// devices = {
// audioInput: {
// deviceId: 'ID'
// groupId: 'grpID'
// kind: 'videoInput'
// label: 'label'
// },
// audioOutput: {
// deviceId: 'ID'
// groupId: 'grpID'
// kind: 'videoInput'
// label: 'label'
// },
// videoInput: {
// deviceId: 'ID'
// groupId: 'grpID'
// kind: 'videoInput'
// label: 'label'
// }
// }
...
});
```
* **isDeviceChangeAvailable** - Resolves with true if the device change is available and with false if not.
```javascript
// The accepted deviceType values are - 'output', 'input' or undefined.
api.isDeviceChangeAvailable(deviceType).then(isDeviceChangeAvailable => {
...
});
```
* **isDeviceListAvailable** - Resolves with true if the device list is available and with false if not.
```javascript
api.isDeviceListAvailable().then(isDeviceListAvailable => {
...
});
```
* **isMultipleAudioInputSupported** - Resolves with true if multiple audio input is supported and with false if not.
```javascript
api.isMultipleAudioInputSupported().then(isMultipleAudioInputSupported => {
...
});
```
* **setAudioInputDevice** - Sets the audio input device to the one with the label or id that is passed.
```javascript
api.setAudioInputDevice(deviceLabel, deviceId);
```
* **setAudioOutputDevice** - Sets the audio output device to the one with the label or id that is passed.
```javascript
api.setAudioOutputDevice(deviceLabel, deviceId);
```
* **setVideoInputDevice** - Sets the video input device to the one with the label or id that is passed.
```javascript
api.setVideoInputDevice(deviceLabel, deviceId);
```
You can control the embedded Jitsi Meet conference using the `JitsiMeetExternalAPI` object by using `executeCommand`:
```javascript
api.executeCommand(command, ...arguments)
api.executeCommand(command, ...arguments);
```
The `command` parameter is String object with the name of the command. The following commands are currently supported:
@@ -85,57 +199,60 @@ api.executeCommand('subject', 'New Conference Subject');
* **toggleAudio** - Mutes / unmutes the audio for the local participant. No arguments are required.
```javascript
api.executeCommand('toggleAudio')
api.executeCommand('toggleAudio');
```
* **toggleVideo** - Mutes / unmutes the video for the local participant. No arguments are required.
```javascript
api.executeCommand('toggleVideo')
api.executeCommand('toggleVideo');
```
* **toggleFilmStrip** - Hides / shows the filmstrip. No arguments are required.
```javascript
api.executeCommand('toggleFilmStrip')
api.executeCommand('toggleFilmStrip');
```
* **toggleChat** - Hides / shows the chat. No arguments are required.
```javascript
api.executeCommand('toggleChat')
api.executeCommand('toggleChat');
```
* **toggleShareScreen** - Starts / stops screen sharing. No arguments are required.
```javascript
api.executeCommand('toggleShareScreen')
api.executeCommand('toggleShareScreen');
```
* **hangup** - Hangups the call. No arguments are required.
```javascript
api.executeCommand('hangup')
api.executeCommand('hangup');
```
* **email** - Changes the local email address. This command requires one argument - the new email address to be set.
```javascript
api.executeCommand('email', 'example@example.com')
api.executeCommand('email', 'example@example.com');
```
* **avatarUrl** - Changes the local avatar URL. This command requires one argument - the new avatar URL to be set.
```javascript
api.executeCommand('avatarUrl', 'https://avatars0.githubusercontent.com/u/3671647')
api.executeCommand('avatarUrl', 'https://avatars0.githubusercontent.com/u/3671647');
```
You can also execute multiple commands using the `executeCommands` method:
```javascript
api.executeCommands(commands)
api.executeCommands(commands);
```
The `commands` parameter is an object with the names of the commands as keys and the arguments for the commands as values:
```javascript
api.executeCommands({displayName: ['nickname'], toggleAudio: []});
api.executeCommands({
displayName: [ 'nickname' ],
toggleAudio: []
});
```
You can add event listeners to the embedded Jitsi Meet using the `addEventListener` method.
**NOTE: This method still exists but it is deprecated. JitsiMeetExternalAPI class extends [EventEmitter]. Use [EventEmitter] methods (`addListener` or `on`).**
```javascript
api.addEventListener(event, listener)
api.addEventListener(event, listener);
```
The `event` parameter is a String object with the name of the event.
@@ -146,36 +263,36 @@ The following events are currently supported:
changes. The listener will receive an object with the following structure:
```javascript
{
"id": id, // the id of the participant that changed his avatar.
"avatarURL": avatarURL // the new avatar URL.
id: string, // the id of the participant that changed his avatar.
avatarURL: string // the new avatar URL.
}
```
* **audioAvailabilityChanged** - event notifications about audio availability status changes. The listener will receive an object with the following structure:
```javascript
{
"available": available // new available status - boolean
available: boolean // new available status - boolean
}
```
* **audioMuteStatusChanged** - event notifications about audio mute status changes. The listener will receive an object with the following structure:
```javascript
{
"muted": muted // new muted status - boolean
muted: boolean // new muted status - boolean
}
```
* **screenSharingStatusChanged** - receives event notifications about turning on/off the local user screen sharing. The listener will receive object with the following structure:
```javascript
{
"on": on, //whether screen sharing is on
"details": {
on: boolean, //whether screen sharing is on
details: {
// From where the screen sharing is capturing, if known. Values which are
// passed include "window", "screen", "proxy", "device". The value undefined
// will be passed if the source type is unknown or screen share is off.
sourceType: sourceType
}
// From where the screen sharing is capturing, if known. Values which are
// passed include 'window', 'screen', 'proxy', 'device'. The value undefined
// will be passed if the source type is unknown or screen share is off.
sourceType: string|undefined
}
}
```
@@ -183,9 +300,9 @@ changes. The listener will receive an object with the following structure:
messages. The listener will receive an object with the following structure:
```javascript
{
"from": from, // The id of the user that sent the message
"nick": nick, // the nickname of the user that sent the message
"message": txt // the text of the message
from: string, // The id of the user that sent the message
nick: string, // the nickname of the user that sent the message
message: string // the text of the message
}
```
@@ -193,7 +310,7 @@ messages. The listener will receive an object with the following structure:
messages. The listener will receive an object with the following structure:
```javascript
{
"message": txt // the text of the message
message: string // the text of the message
}
```
@@ -201,69 +318,77 @@ messages. The listener will receive an object with the following structure:
changes. The listener will receive an object with the following structure:
```javascript
{
"id": id, // the id of the participant that changed his display name
"displayname": displayName // the new display name
id: string, // the id of the participant that changed his display name
displayname: string // the new display name
}
```
* **deviceListChanged** - event notifications about device list changes. The listener will receive an object with the following structure:
```javascript
{
devices: Object // the new list of available devices.
}
```
NOTE: The devices object has the same format as the getAvailableDevices result format.
* **emailChange** - event notifications about email
changes. The listener will receive an object with the following structure:
```javascript
{
"id": id, // the id of the participant that changed his email
"email": email // the new email
id: string, // the id of the participant that changed his email
email: string // the new email
}
```
* **filmstripDisplayChanged** - event notifications about the visibility of the filmstrip being updated.
```javascript
{
"visible": visible, // Whether or not the filmstrip is displayed or hidden.
visible: boolean // Whether or not the filmstrip is displayed or hidden.
}
```
* **participantJoined** - event notifications about new participants who join the room. The listener will receive an object with the following structure:
```javascript
{
"id": id, // the id of the participant
"displayName": displayName // the display name of the participant
id: string, // the id of the participant
displayName: string // the display name of the participant
}
```
* **participantLeft** - event notifications about participants that leave the room. The listener will receive an object with the following structure:
```javascript
{
"id": id // the id of the participant
id: string // the id of the participant
}
```
* **videoConferenceJoined** - event notifications fired when the local user has joined the video conference. The listener will receive an object with the following structure:
```javascript
{
"roomName": room, // the room name of the conference
"id": id, // the id of the local participant
"displayName": displayName, // the display name of the local participant
"avatarURL": avatarURL // the avatar URL of the local participant
roomName: string, // the room name of the conference
id: string, // the id of the local participant
displayName: string, // the display name of the local participant
avatarURL: string // the avatar URL of the local participant
}
```
* **videoConferenceLeft** - event notifications fired when the local user has left the video conference. The listener will receive an object with the following structure:
```javascript
{
"roomName": room // the room name of the conference
roomName: string // the room name of the conference
}
```
* **videoAvailabilityChanged** - event notifications about video availability status changes. The listener will receive an object with the following structure:
```javascript
{
"available": available // new available status - boolean
available: boolean // new available status - boolean
}
```
* **videoMuteStatusChanged** - event notifications about video mute status changes. The listener will receive an object with the following structure:
```javascript
{
"muted": muted // new muted status - boolean
muted: boolean // new muted status - boolean
}
```
@@ -273,7 +398,7 @@ changes. The listener will receive an object with the following structure:
The listener will receive an object with the following structure:
```javascript
{
"subject": subject // the new subject
subject: string // the new subject
}
```
@@ -295,79 +420,80 @@ function outgoingMessageListener(object)
api.addEventListeners({
incomingMessage: incomingMessageListener,
outgoingMessage: outgoingMessageListener})
outgoingMessage: outgoingMessageListener
});
```
If you want to remove a listener you can use `removeEventListener` method with argument the name of the event.
**NOTE: This method still exists but it is deprecated. JitsiMeetExternalAPI class extends [EventEmitter]. Use [EventEmitter] methods( `removeListener`).**
```javascript
api.removeEventListener("incomingMessage");
api.removeEventListener('incomingMessage');
```
If you want to remove more than one event you can use `removeEventListeners` method with an Array with the names of the events as an argument.
**NOTE: This method still exists but it is deprecated. JitsiMeetExternalAPI class extends [EventEmitter]. Use [EventEmitter] methods.**
```javascript
api.removeEventListeners(["incomingMessage", "outgoingMessageListener"]);
api.removeEventListeners([ 'incomingMessage', 'outgoingMessageListener' ]);
```
You can get the number of participants in the conference with the following API function:
```javascript
var numberOfParticipants = api.getNumberOfParticipants();
const numberOfParticipants = api.getNumberOfParticipants();
```
You can get the avatar URL of a participant in the conference with the following API function:
```javascript
var avatarURL = api.getAvatarURL(participantId);
const avatarURL = api.getAvatarURL(participantId);
```
You can get the display name of a participant in the conference with the following API function:
```javascript
var displayName = api.getDisplayName(participantId);
const displayName = api.getDisplayName(participantId);
```
You can get the email of a participant in the conference with the following API function:
```javascript
var email = api.getEmail(participantId);
const email = api.getEmail(participantId);
```
You can get the iframe HTML element where Jitsi Meet is loaded with the following API function:
```javascript
var iframe = api.getIFrame();
const iframe = api.getIFrame();
```
You can check whether the audio is muted with the following API function:
```javascript
api.isAudioMuted().then(function(muted) {
api.isAudioMuted().then(muted => {
...
});
```
You can check whether the video is muted with the following API function:
```javascript
api.isVideoMuted().then(function(muted) {
api.isVideoMuted().then(muted => {
...
});
```
You can check whether the audio is available with the following API function:
```javascript
api.isAudioAvailable().then(function(available) {
api.isAudioAvailable().then(available => {
...
});
```
You can check whether the video is available with the following API function:
```javascript
api.isVideoAvailable().then(function(available) {
api.isVideoAvailable().then(available => {
...
});
```
You can invite new participants to the call with the following API function:
```javascript
api.invite([{...}, {...}, {...}]).then(function() {
api.invite([ {...}, {...}, {...} ]).then(() => {
// success
}).catch(function() {
}).catch(() => {
// failure
});
```
@@ -375,7 +501,7 @@ api.invite([{...}, {...}, {...}]).then(function() {
You can remove the embedded Jitsi Meet Conference with the following API function:
```javascript
api.dispose()
api.dispose();
```
NOTE: It's a good practice to remove the conference before the page is unloaded.

View File

@@ -149,10 +149,10 @@ PODS:
- RNGoogleSignin (1.0.2):
- GoogleSignIn
- React
- RNSound (0.10.9):
- RNSound (0.10.12):
- React/Core
- RNSound/Core (= 0.10.9)
- RNSound/Core (0.10.9):
- RNSound/Core (= 0.10.12)
- RNSound/Core (0.10.12):
- React/Core
- RNVectorIcons (6.0.2):
- React
@@ -275,7 +275,7 @@ SPEC CHECKSUMS:
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
react-native-webrtc: a14197fefe96ab462dc098b79c428fc5a7f68216
RNGoogleSignin: 361174d9a3090d295b06257162b560d8efc8a6ed
RNSound: 53d2fc9c6589bd68daba530262b7560393def3ac
RNSound: e157320f503bdd4f4ee6d8542e948d54f90c3c3a
RNVectorIcons: d819334932bcda3332deb3d2c8ea4d069e0b98f9
SDWebImage: 3f3f0c02f09798048c47a5ed0a13f17b063572d8
yoga: 3768a3026ade0fb46a68f3a31a917cf86bc34fc4

View File

@@ -23,7 +23,11 @@ platform :ios do
)
# Actually build the app
build_app(scheme: "jitsi-meet", include_bitcode: false)
build_app(
scheme: "jitsi-meet",
include_bitcode: false,
export_xcargs: "-allowProvisioningUpdates"
)
# Upload the build to TestFlight (but don't distribute it)
upload_to_testflight(skip_submission: true, skip_waiting_for_build_processing: true)

View File

@@ -61,8 +61,15 @@ RCT_EXPORT_MODULE();
}
}
// build number
NSString *buildNumber = infoDictionary[@"CFBundleVersion"];
if (buildNumber == nil) {
buildNumber = @"";
}
return @{
@"calendarEnabled": [NSNumber numberWithBool:calendarEnabled],
@"buildNumber": buildNumber,
@"name": name,
@"sdkBundlePath": sdkBundlePath,
@"version": version

1
lang/languages-ca.json Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -1,11 +1,18 @@
{
"en": "Inglese",
"az": "Azero",
"bg": "Bulgaro",
"cs": "Ceco",
"de": "Tedesco",
"el": "Greco",
"eo": "Esperanto",
"es": "Spagnolo",
"fr": "Francese",
"hy": "Armeno",
"it": "Italiano",
"ja": "Giapponese",
"ko": "Coreano",
"nb": "Norvegese bokmal",
"oc": "Occitano",
"pl": "Polacco",
"ptBR": "Portoghese (Brasile)",
@@ -14,7 +21,6 @@
"sl": "Sloveno",
"sv": "Svedese",
"tr": "Turco",
"zhCN": "Cinese (Cina)",
"nb": "",
"eo": ""
"vi": "Vietnamita",
"zhCN": "Cinese (Cina)"
}

View File

@@ -1,20 +1,26 @@
{
"en": "",
"bg": "",
"de": "",
"es": "",
"fr": "",
"hy": "",
"it": "",
"oc": "",
"pl": "",
"ptBR": "",
"ru": "",
"sk": "",
"sl": "",
"sv": "",
"tr": "",
"zhCN": "",
"nb": "",
"eo": ""
"en": "英語",
"az": "アゼルバイジャン語",
"bg": "ブルガリア語",
"cs": "チェコ語",
"de": "ドイツ語",
"el": "ギリシア語",
"eo": "エスペラント語",
"es": "スペイン語",
"fr": "フランス語",
"hy": "アルメニア語",
"it": "イタリア語",
"ja": "日本語",
"ko": "韓国語",
"nb": "ノルウェー語 (ブークモール)",
"oc": "オック語",
"pl": "ポーランド語",
"ptBR": "ポルトガル語 (ブラジル)",
"ru": "ロシア語",
"sk": "スロバキア語",
"sl": "スロベニア語",
"sv": "スウェーデン語",
"tr": "トルコ語",
"vi": "ベトナム語",
"zhCN": "中国語 (中国)"
}

View File

@@ -1,11 +1,18 @@
{
"en": "英语",
"az": "",
"bg": "保加利亚语",
"cs": "",
"de": "德语",
"el": "",
"eo": "世界语",
"es": "西班牙语",
"fr": "法语",
"hy": "亚美尼亚语",
"it": "意大利语",
"ja": "",
"ko": "",
"nb": "挪威布克摩尔语",
"oc": "欧西坦语",
"pl": "波兰语",
"ptBR": "葡萄牙语(巴西)",
@@ -14,7 +21,6 @@
"sl": "斯洛文尼亚语",
"sv": "瑞典语",
"tr": "土耳其语",
"zhCN": "中文(中国)",
"nb": "挪威布克摩尔语",
"eo": "世界语"
"vi": "",
"zhCN": "中文(中国)"
}

View File

@@ -1,11 +1,18 @@
{
"en": "English",
"az": "Azerbaijani",
"bg": "Bulgarian",
"cs": "Czech",
"de": "German",
"el": "Greek",
"eo": "Esperanto",
"es": "Spanish",
"fr": "French",
"hy": "Armenian",
"it": "Italian",
"ja": "日本語",
"ko": "韓文",
"nb": "Norwegian Bokmal",
"oc": "Occitan",
"pl": "Polish",
"ptBR": "Portuguese (Brazil)",
@@ -14,7 +21,6 @@
"sl": "Slovenian",
"sv": "Swedish",
"tr": "Turkish",
"zhCN": "中文 简体 (中国)",
"nb": "Norwegian Bokmal",
"eo": "Esperanto"
"vi": "Vietnamese",
"zhCN": "中文 简体 (中国)"
}

View File

@@ -65,7 +65,7 @@
"privacy": "Privaatheid",
"recentList": "Onlangs",
"recentListDelete": "Skrap",
"recentListEmpty": "Die lys van onlangse gesprekke is leeg. Gesels met u span al u onlangse gesprekke sal hier wees.",
"recentListEmpty": "Die lys van onlangse gesprekke is leeg. Gesels met u span en al u onlangse gesprekke sal hier wys.",
"roomname": "Gee kamernaam",
"roomnameHint": "Gee die naam of URL van die kamer waar u wil aansluit. Dink gerus enige naam uit. Laat weet net die mense wat u ontmoet wat dit is sodat hulle die selfde naam gee.",
"sendFeedback": "Stuur terugvoer",
@@ -241,7 +241,7 @@
},
"notify": {
"disconnected": "ontkoppel",
"moderator": "",
"moderator": "U is ou moderator!",
"connectedOneMember": "__name__ het gekoppel",
"connectedTwoMembers": "__first__ en __second__ het gekoppel",
"connectedThreePlusMembers": "__name__ en __count__ ander het gekoppel",
@@ -357,7 +357,7 @@
"permissionDenied": "Toestemming gewyer",
"screenSharingFailedToInstall": "Oeps! Die uitbreiding vir skermdeling kon nie installeer nie.",
"screenSharingFailedToInstallTitle": "Uitbreiding vir skermdeling kon nie installeer nie",
"screenSharingFirefoxPermissionDeniedError": "",
"screenSharingFirefoxPermissionDeniedError": "Iets het skeefgeloop toe ons die skerm probeer deel het. Maak seker dat ons dei toestemming gegee word om dit te doen. ",
"screenSharingFirefoxPermissionDeniedTitle": "Oeps! Ons kon nie skermdeling begin nie!",
"screenSharingPermissionDeniedError": "",
"cameraUnsupportedResolutionError": "Die kamera ondersteun nie die nodige videoresolusie nie.",
@@ -483,13 +483,13 @@
"unavailableTitle": ""
},
"transcribing": {
"pending": "",
"pending": "Berei tans voor om die vergadering te transkribeer...",
"off": "",
"error": "",
"expandedLabel": "",
"failedToStart": "",
"tr": "",
"labelToolTip": "",
"labelToolTip": "Die vergadering word getranskribeer",
"ccButtonTooltip": "",
"start": "",
"stop": ""

746
lang/main-ca.json Normal file
View File

@@ -0,0 +1,746 @@
{
"addPeople": {
"add": "",
"countryNotSupported": "Encara no és possible usar aquesta destinació.",
"countryReminder": "Truqueu des de fora els EUA? Assegureu-vos que comenceu b el codi de país!",
"disabled": "",
"failedToAdd": "No s'hi ha pogut afegir cap membre",
"footerText": "",
"invite": "",
"loading": "",
"loadingNumber": "S'està validant el número de telèfon",
"loadingPeople": "S'estan cercant les persones a convidar",
"noResults": "No s'ha trobat cap resultat coincident",
"notAvailable": "No podeu convidar-hi persones.",
"noValidNumbers": "Introduïu un número de telèfon",
"searchNumbers": "Afegeix-hi números de telèfon",
"searchPeople": "Cerca-hi persones",
"searchPeopleAndNumbers": "Cerca persones o n'afegeix els números de telèfon",
"telephone": "Telèfon: __number__",
"title": "Convida persones a aquesta reunió"
},
"audioDevices": {
"bluetooth": "Bluetooth",
"headphones": "Auriculars",
"phone": "Telèfon",
"speaker": ""
},
"audioOnly": {
"audioOnly": "Només l'àudio",
"featureToggleDisabled": ""
},
"calendarSync": {
"addMeetingURL": "Afegeix un enllaç de reunió",
"confirmAddLink": "",
"confirmAddLinkTitle": "",
"error": {
"appConfiguration": "La integració de l'agenda no està configurada correctament.",
"generic": "S'ha produït un error. Comproveu la configuració de l'agenda o intenteu d'actualitzar-la.",
"notSignedIn": ""
},
"join": "Afegeix-m'hi",
"joinTooltip": "Uniu-vos a la reunió",
"nextMeeting": "reunió següent",
"noEvents": "No hi ha cap esdeveniment previst a l'agenda.",
"ongoingMeeting": "reunió en curs",
"permissionButton": "Obre la configuració",
"permissionMessage": "",
"refresh": "Actualitza l'agenda",
"today": ""
},
"chat": {
"error": "",
"messagebox": "Introduïu text...",
"nickname": {
"popover": "Trieu un sobrenom",
"title": "Introduïu un sobrenom per a usar el xat"
},
"title": "Xat"
},
"connection": {
"ATTACHED": "Adjunt",
"AUTHENTICATING": "S'està autenticant",
"AUTHFAIL": "",
"CONNECTED": "",
"CONNECTING": "",
"CONNFAIL": "La connexió ha fallat",
"DISCONNECTED": "",
"DISCONNECTING": "S'està desconnectant",
"ERROR": "",
"RECONNECTING": "S'ha produït un problema de xarxa. S'esta tornant a connectar..."
},
"connectionindicator": {
"address": "Adreça:",
"bandwidth": "Ample de banda estimat:",
"bitrate": "",
"bridgeCount": "Nombre de servidors: ",
"connectedTo": "Connectat a:",
"framerate": "",
"header": "Dades de connexió",
"less": "Menys informació",
"localaddress": "Adreça local:",
"localaddress_plural": "Adreces locals:",
"localport": "Port local:",
"localport_plural": "Ports locals:",
"more": "Més informació",
"na": "",
"packetloss": "Pèrdua de paquets:",
"quality": {
"good": "",
"inactive": "Inactiva",
"lost": "Perduda",
"nonoptimal": "No òptima",
"poor": "Pobra"
},
"remoteaddress": "Adreça remota:",
"remoteaddress_plural": "Adreces remotes:",
"remoteport": "Port remot:",
"remoteport_plural": "Ports remots:",
"resolution": "Resolució:",
"status": "Connexió:",
"transport": "Transport:",
"transport_plural": "Transports:",
"turn": " (torn)"
},
"contactlist_plural": "__count__ membres",
"dateUtils": {
"earlier": "",
"today": "Avui",
"yesterday": "Ahir"
},
"deepLinking": {
"appNotInstalled": "",
"description": "No ha passat res? Hem intentat iniciar la reunió en l'aplicació d'escriptori _app_. Torneu a intentar-ho en l'aplicació web _app_.",
"downloadApp": "Baixa l'aplicació",
"launchWebButton": "Inicia al web",
"openApp": "Continua en l'aplicació",
"title": "S'està iniciant la reunió en _app_....",
"tryAgainButton": "Torna-ho a intentar en l'escriptori"
},
"defaultLink": "p. ex. __url__",
"defaultNickname": "p. ex. Pere Cullera",
"deviceError": {
"cameraError": "No s'ha pogut accedir a la càmera",
"cameraPermission": "",
"microphoneError": "",
"microphonePermission": ""
},
"deviceSelection": {
"deviceSettings": "Configuració de l'aparell",
"noPermission": "",
"previewUnavailable": "",
"selectADevice": "Seleccioneu un aparell",
"testAudio": "Reprodueix un so de prova"
},
"dialog": {
"accessibilityLabel": {
"liveStreaming": "Transmissió en directe"
},
"allow": "Permet",
"alreadySharedVideoMsg": "Un altre membre ja està compartint un vídeo. Aquesta conferència només permet un vídeo compartit a la vegada.",
"alreadySharedVideoTitle": "Només es permet un vídeo compartit a la vegada",
"applicationWindow": "Finestra de l'aplicació",
"Back": "Enrere",
"cameraConstraintFailedError": "La càmera no satisfà algun dels requeriments.",
"cameraNotFoundError": "",
"cameraNotSendingData": "No podem accedir a la càmera. Comproveu si alguna altra aplicació l'està usant, seleccioneu un altre aparell al menú de configuració o intenteu de recarregar l'aplicació.",
"cameraNotSendingDataTitle": "",
"cameraPermissionDeniedError": "",
"cameraUnknownError": "",
"cameraUnsupportedResolutionError": "",
"Cancel": "Cancel·la",
"close": "Tanca",
"conferenceDisconnectMsg": "",
"conferenceDisconnectTitle": "",
"conferenceReloadMsg": "Estem intentat de corregir-ho. Tornem a connectar en __seconds__ segons...",
"conferenceReloadTitle": "Malauradament, alguna cosa no ha anat bé.",
"confirm": "Confirmo",
"confirmNo": "",
"confirmYes": "",
"connectError": "Vaja! Alguna cosa no ha anat bé i no podem connectar a la conferència.",
"connectErrorWithMsg": "Vaja! Alguna cosa no ha anat bé i no podem connectar a la conferència: _msg_",
"connecting": "S'està connectant",
"contactSupport": "Contacte amb l'assistència",
"copy": "Copia",
"currentPassword": "La contrasenya actual és",
"defaultError": "S'ha produït algun tipus d'error",
"detectext": "S'ha produït un error en intentar detectar l'extensió de compartició d'escriptori.",
"dismiss": "",
"displayNameRequired": "",
"done": "",
"doNotShowMessageAgain": "",
"enterDisplayName": "",
"error": "Error",
"externalInstallationMsg": "",
"externalInstallationTitle": "",
"failedpermissions": "",
"feedbackHelp": "Els vostres comentaris ens ajuden a millorar la nostra experiència de vídeo.",
"feedbackQuestion": "",
"goToStore": "",
"gracefulShutdown": "",
"hungUp": "Heu penjat",
"IamHost": "Sóc l'amfitrió",
"incorrectPassword": "El nom o la contrasenya no són correctes",
"inlineInstallationMsg": "",
"inlineInstallExtension": "Instal·la-ho ara",
"internalError": "",
"internalErrorTitle": "",
"joinAgain": "",
"kickMessage": "",
"kickParticipantButton": "",
"kickParticipantDialog": "",
"kickParticipantTitle": "",
"kickTitle": "",
"liveStreaming": "",
"liveStreamingDisabledForGuestTooltip": "",
"liveStreamingDisabledTooltip": "",
"lockMessage": "No s'ha pogut blocar la conferència.",
"lockRoom": "Bloca la sala",
"lockTitle": "El bloqueig ha fallat",
"logoutQuestion": "Esteu segur de voler tancar la sessió i aturar la conferència?",
"logoutTitle": "",
"maxUsersLimitReached": "",
"maxUsersLimitReachedTitle": "",
"micConstraintFailedError": "",
"micNotFoundError": "No s'ha trobat cap micròfon.",
"micNotSendingData": "No podem accedir al micròfon. Seleccioneu un altre aparell al menú de configuració o intenteu de recarregar l'aplicació.",
"micNotSendingDataTitle": "",
"micPermissionDeniedError": "",
"micUnknownError": "",
"muteParticipantBody": "",
"muteParticipantButton": "",
"muteParticipantDialog": "",
"muteParticipantTitle": "Voleu silenciar aquest membre?",
"Ok": "",
"oops": "Vaja!",
"password": "Introduïu una contrasenya",
"passwordError": "",
"passwordError2": "",
"passwordErrorTitle": "",
"passwordLabel": "Contrasenya",
"passwordNotSupported": "",
"passwordNotSupportedTitle": "",
"passwordRequired": "",
"permissionDenied": "S'ha denegat el permís",
"popupError": "El vostre navegador bloca les finestres emergents d'aquest lloc. Habiliteu les finestres emergents a la configuració de seguretat del navegador i torneu-ho a intentar.",
"popupErrorTitle": "Finestres emergents blocades",
"recording": "",
"recordingDisabledForGuestTooltip": "Els convidats no poden iniciar enregistraments.",
"recordingDisabledTooltip": "",
"recordingToken": "",
"rejoinNow": "",
"remoteControlAllowedMessage": "",
"remoteControlDeniedMessage": "",
"remoteControlErrorMessage": "",
"remoteControlRequestMessage": "",
"remoteControlShareScreenWarning": "Tingueu present que si pitgeu \"Permet\" compartireu la vostra pantalla!",
"remoteControlStopMessage": "",
"remoteControlTitle": "Control d'escriptori remot",
"Remove": "",
"removePassword": "",
"removeSharedVideoMsg": "",
"removeSharedVideoTitle": "",
"reservationError": "",
"reservationErrorMsg": "",
"retry": "",
"Save": "",
"screenSharingFailedToInstall": "",
"screenSharingFailedToInstallTitle": "",
"screenSharingFirefoxPermissionDeniedError": "",
"screenSharingFirefoxPermissionDeniedTitle": "",
"screenSharingPermissionDeniedError": "",
"serviceUnavailable": "El servei no és disponible",
"sessTerminated": "La trucada ha finalitzat",
"Share": "",
"shareVideoLinkError": "Proporcioneu un enllaç de YouTube correcte.",
"shareVideoTitle": "Comparteix un vídeo",
"shareYourScreen": "Comparteix la pantalla",
"shareYourScreenDisabled": "",
"shareYourScreenDisabledForGuest": "Els convidats no poden compartir la pantalla.",
"SLDFailure": "",
"sorryFeedback": "",
"SRDFailure": "",
"startLiveStreaming": "Inicia la transmissió en directe",
"startRecording": "Inicia l'enregistrament",
"startRemoteControlErrorMessage": "",
"stopLiveStreaming": "",
"stopRecording": "",
"stopRecordingWarning": "",
"stopStreamingWarning": "",
"streamKey": "",
"Submit": "",
"thankYou": "",
"token": "",
"tokenAuthFailed": "",
"tokenAuthFailedTitle": "L'autenticació ha fallat",
"transcribing": "",
"unableToSwitch": "",
"unlockRoom": "",
"userPassword": "",
"WaitForHostMsg": "",
"WaitForHostMsgWOk": "",
"WaitingForHost": "",
"warning": "",
"Yes": "",
"yourEntireScreen": ""
},
"dialOut": {
"statusMessage": ""
},
"email": {
"and": "",
"body": "",
"sharedKey": "",
"subject": ""
},
"feedback": {
"average": "",
"bad": "",
"detailsLabel": "",
"good": "",
"rateExperience": "",
"veryBad": "",
"veryGood": ""
},
"incomingCall": {
"answer": "",
"audioCallTitle": "",
"decline": "",
"productLabel": "",
"videoCallTitle": ""
},
"info": {
"accessibilityLabel": "",
"addPassword": "",
"cancelPassword": "",
"conferenceURL": "",
"country": "",
"dialANumber": "",
"dialInConferenceID": "",
"dialInNotSupported": "",
"dialInNumber": "",
"dialInTollFree": "",
"genericError": "",
"inviteLiveStream": "",
"invitePhone": "",
"invitePhoneAlternatives": "",
"inviteURL": "",
"liveStreamURL": "",
"moreNumbers": "",
"noNumbers": "",
"noPassword": "",
"noRoom": "",
"numbers": "",
"password": "",
"title": "Comparteix",
"tooltip": "",
"label": ""
},
"inviteDialog": {
"alertOk": "D'acord",
"alertText": "",
"alertTitle": "",
"header": "Convida",
"searchCallOnlyPlaceholder": "",
"searchPeopleOnlyPlaceholder": "",
"searchPlaceholder": "",
"send": ""
},
"inlineDialogFailure": {
"msg": "",
"retry": "",
"support": "",
"supportMsg": ""
},
"inviteUrlDefaultMsg": "",
"keyboardShortcuts": {
"focusLocal": "",
"focusRemote": "",
"fullScreen": "",
"keyboardShortcuts": "",
"localRecording": "",
"mute": "",
"pushToTalk": "",
"raiseHand": "",
"showSpeakerStats": "",
"toggleChat": "",
"toggleFilmstrip": "",
"toggleScreensharing": "",
"toggleShortcuts": "",
"videoMute": ""
},
"liveStreaming": {
"busy": "",
"busyTitle": "",
"buttonTooltip": "",
"changeSignIn": "",
"choose": "",
"chooseCTA": "",
"enterStreamKey": "",
"error": "",
"errorAPI": "",
"errorLiveStreamNotEnabled": "",
"expandedOff": "",
"expandedOn": "",
"expandedPending": "",
"failedToStart": "",
"getStreamKeyManually": "",
"invalidStreamKey": "",
"off": "",
"on": "",
"pending": "",
"serviceName": "",
"signedInAs": "",
"signIn": "",
"signInCTA": "",
"signOut": "",
"start": "",
"streamIdHelp": "Què és això?",
"unavailableTitle": ""
},
"localRecording": {
"clientState": {
"off": "Inactiu",
"on": "Actiu",
"unknown": "Desconegut"
},
"dialogTitle": "Controls d'enregistrament local",
"duration": "Durada",
"durationNA": "N/D",
"encoding": "Codificació",
"label": "",
"labelToolTip": "",
"localRecording": "",
"me": "",
"messages": {
"engaged": "",
"finished": "",
"finishedModerator": "",
"notModerator": "No sou el moderador. No podeu iniciar ni aturar un enregistrament local."
},
"moderator": "",
"no": "No",
"participant": "",
"participantStats": "",
"sessionToken": "",
"start": "",
"stop": "",
"yes": "Sí"
},
"me": "",
"notify": {
"connectedOneMember": "",
"connectedThreePlusMembers": "",
"connectedTwoMembers": "",
"disconnected": "desconnectat",
"focus": "",
"focusFail": "",
"grantedTo": "",
"me": "Jo",
"moderator": "",
"muted": "Heu iniciat una conversa silenciada.",
"mutedTitle": "Esteu silenciat!",
"raisedHand": "Vull parlar.",
"somebody": "Algú",
"suboptimalExperienceDescription": "",
"suboptimalExperienceTitle": ""
},
"passwordSetRemotely": "",
"poweredby": "",
"presenceStatus": {
"busy": "Ocupat",
"calling": "",
"connected": "Connectat",
"connecting": "Està connectant...",
"connecting2": "Està connectant*...",
"disconnected": "Desconnectat",
"expired": "",
"ignored": "Ignorat",
"initializingCall": "S'està inicialitzant la trucada...",
"invited": "Convidat",
"rejected": "Rebutjat",
"ringing": ""
},
"profile": {
"setDisplayNameLabel": "",
"setEmailInput": "",
"setEmailLabel": "",
"title": ""
},
"raisedHand": "Vull parlar",
"recentList": {
"joinPastMeeting": ""
},
"recording": {
"authDropboxText": "Puja a Dropbox",
"availableSpace": "",
"beta": "BETA",
"busy": "",
"busyTitle": "",
"buttonTooltip": "Inicia o atura l'enregistrament",
"error": "L'enregistrament ha fallat. Torneu-ho a intentar més tard.",
"expandedOff": "",
"expandedOn": "",
"expandedPending": "",
"failedToStart": "",
"live": "",
"loggedIn": "",
"off": "",
"on": "Enregistrament",
"pending": "S'està preparant per a enregistrar la reunió...",
"rec": "ENREG",
"serviceDescription": "",
"serviceName": "Servei d'enregistrament",
"signIn": "Inicia sessió",
"signOut": "Tanca la sessió",
"startRecordingBody": "Esteu segur de voler iniciar l'enregistrament?",
"unavailable": "",
"unavailableTitle": "L'enregistrament no és disponible"
},
"sectionList": {
"pullToRefresh": ""
},
"settings": {
"audioVideo": "",
"calendar": {
"about": "",
"disconnect": "",
"microsoftSignIn": "",
"signedIn": "",
"title": ""
},
"cameraAndMic": "Càmera i micròfon",
"devices": "Aparells",
"followMe": "Tothom em segueix",
"language": "Llengua",
"loggedIn": "",
"moderator": "",
"more": "Més",
"name": "",
"noDevice": "",
"password": "",
"selectAudioOutput": "Sortida d'àudio",
"selectCamera": "",
"selectMic": "",
"startAudioMuted": "",
"startVideoMuted": "",
"title": "",
"update": ""
},
"settingsView": {
"alertOk": "D'acord",
"alertTitle": "",
"alertURLText": "L'URL introduït no és vàlid",
"conferenceSection": "Conferència",
"displayName": "Nom visible",
"email": "Adreça electrònica",
"header": "",
"profileSection": "",
"serverURL": "URL del servidor",
"startWithAudioMuted": "Inicia amb l'àudio silenciat",
"startWithVideoMuted": "Inicia amb el vídeo silenciat"
},
"share": {
"dialInfoText": "",
"mainText": ""
},
"speaker": "Altaveu",
"speakerStats": {
"hours": "",
"minutes": "",
"name": "Nom",
"seconds": "",
"speakerStats": "",
"speakerTime": ""
},
"startupoverlay": {
"policyText": "",
"title": ""
},
"suspendedoverlay": {
"rejoinKeyTitle": "",
"text": "",
"title": ""
},
"toolbar": {
"accessibilityLabel": {
"audioOnly": "Canvia només l'àudio",
"audioRoute": "",
"callQuality": "",
"cc": "Canvia l'estat dels subtítols",
"chat": "Canvia l'estat de la finestra de xats",
"document": "Canvia el document compartit",
"feedback": "",
"fullScreen": "Activa o desactiva la pantalla completa",
"hangup": "Abandona la trucada",
"invite": "",
"kick": "",
"localRecording": "Activa o desactiva les controls d'enregistrament local",
"lockRoom": "Activa o desactiva el bloqueig de la sala",
"moreActions": "Canvia el menú d'accions addicionals",
"moreActionsMenu": "Menú d'accions addicionals",
"mute": "Activa o desactiva el silenci de l'àudio",
"pip": "",
"profile": "",
"raiseHand": "Aixeca o abaixa la mà",
"recording": "Activa o desactiva l'enregistrament",
"remoteMute": "Silencia el participant",
"Settings": "Canvia la configuració",
"sharedvideo": "Activa o desactiva la compartició de vídeo",
"shareRoom": "",
"shareYourScreen": "",
"shortcuts": "",
"speakerStats": "",
"tileView": "",
"toggleCamera": "",
"videomute": ""
},
"addPeople": "Afegeix persones a la trucada",
"audioonly": "Activa o desactiva el mode de només àudio",
"audioOnlyOff": "Desactiva el mode de només àudio",
"audioOnlyOn": "Activa el mode de només àudio",
"audioRoute": "Seleccioneu l'aparell de so",
"authenticate": "Autentica",
"callQuality": "Gestiona la qualitat de la trucada",
"cameraDisabled": "La càmera no és disponible",
"chat": "Obre o tanca el xat",
"closeChat": "Tanca el xat",
"documentClose": "Tanca el document compartit",
"documentOpen": "Obre el document compartit",
"enterFullScreen": "Mostra en pantalla completa",
"enterTileView": "",
"etherpad": "Obre o tanca el document compartit",
"exitFullScreen": "Surt de la pantalla completa",
"exitTileView": "",
"feedback": "Deixa comentaris",
"filmstrip": "Mostra o amaga vídeos",
"fullscreen": "",
"hangup": "Surt",
"invite": "Convida-hi persones",
"lock": "",
"login": "",
"logout": "Tanca la sessió",
"lowerYourHand": "",
"micDisabled": "",
"micMutedPopup": "",
"moreActions": "Accions addionals",
"mute": "",
"openChat": "Obre el xat",
"pip": "",
"profile": "Edita el perfil",
"raiseHand": "Aixeca o abaixa la mà",
"raiseYourHand": "Aixeca la mà",
"Settings": "Configuració",
"sharedvideo": "Comparteix un vídeo de YouTube",
"sharedVideoMutedPopup": "",
"shareRoom": "Convida-hi algú",
"shortcuts": "Mostra les dreceres",
"sip": "",
"speakerStats": "Estadístiques de l'interlocutor",
"startScreenSharing": "",
"startSubtitles": "Inicia els subtítols",
"stopScreenSharing": "Atura la compartició de la pantalla",
"stopSubtitles": "Atura els subtítols",
"stopSharedVideo": "Atura el vídeo de YouTube",
"talkWhileMutedPopup": "Intenteu parlar? Esteu silenciat.",
"tileViewToggle": "",
"toggleCamera": "",
"unableToUnmutePopup": "",
"videomute": "Inicia o atura la càmera"
},
"transcribing": {
"ccButtonTooltip": "Inicia o atura els subtítols",
"error": "La transcripció ha fallat. Torneu-ho a intentar més tard.",
"expandedLabel": "",
"failedToStart": "",
"labelToolTip": "",
"off": "",
"pending": "",
"start": "",
"stop": "",
"tr": "TR"
},
"userMedia": {
"androidGrantPermissions": "",
"chromeGrantPermissions": "",
"edgeGrantPermissions": "",
"electronGrantPermissions": "",
"firefoxGrantPermissions": "",
"iexplorerGrantPermissions": "",
"nwjsGrantPermissions": "",
"operaGrantPermissions": "",
"react-nativeGrantPermissions": "",
"safariGrantPermissions": ""
},
"videoSIPGW": {
"busy": "",
"busyTitle": "",
"errorAlreadyInvited": "",
"errorInvite": "",
"errorInviteFailed": "",
"errorInviteFailedTitle": "",
"errorInviteTitle": "",
"pending": "",
"serviceName": "Servei de sales",
"unavailableTitle": ""
},
"videoStatus": {
"audioOnly": "AUD",
"audioOnlyExpanded": "",
"callQuality": "Qualitat de la trucada",
"hd": "HD",
"hdTooltip": "",
"highDefinition": "Alta definició",
"labelTooiltipNoVideo": "No hi ha vídeo",
"labelTooltipAudioOnly": "",
"labelTooltipVideo": "Qualitat de vídeo actual",
"ld": "LD",
"ldTooltip": "",
"lowDefinition": "Baixa definició",
"onlyAudioAvailable": "Només hi ha disponible l'àudio",
"onlyAudioSupported": "",
"p2pEnabled": "",
"p2pVideoQualityDescription": "",
"qualityButtonTip": "Canvia la qualitat de vídeo rebut",
"recHighDefinitionOnly": "",
"sd": "",
"sdTooltip": "",
"standardDefinition": ""
},
"videothumbnail": {
"domute": "",
"flip": "",
"kick": "",
"moderator": "Moderador",
"mute": "El membre està silenciat",
"muted": "Silenciat",
"remoteControl": "Control remot",
"videomute": "El membre ha aturat la càmera"
},
"welcomepage": {
"accessibilityLabel": {
"join": "Toqueu per a unir-vos-hi",
"roomname": ""
},
"appDescription": "",
"audioVideoSwitch": {
"audio": "Veu",
"video": ""
},
"calendar": "Agenda",
"connectCalendarButton": "",
"connectCalendarText": "",
"enterRoomTitle": "Inicia una reunió nova",
"go": "",
"join": "",
"privacy": "Privadesa",
"recentList": "Recents",
"recentListDelete": "Suprimeix",
"recentListEmpty": "",
"roomname": "Introduïu el nom de la sala",
"roomnameHint": "",
"sendFeedback": "",
"terms": "Condicions",
"title": "Videoconferència segura, plena de funcionalitats i completament gratuïta i lliure"
}
}

View File

@@ -57,7 +57,7 @@
"video": "Vidéo"
},
"calendar": "Calendrier",
"connectCalendarText": "Connectez votre calendrier pour voir toutes vos réunion dans __app__. Ajoutez les réunions __app__ dans votre calendrier pour les lancer en un seul clic.",
"connectCalendarText": "Connectez votre calendrier pour voir toutes vos réunions dans __app__. Ajoutez les réunions __app__ dans votre calendrier pour les lancer en un seul clic.",
"connectCalendarButton": "Connecter votre calendrier",
"enterRoomTitle": "Démarrer une nouvelle réunion",
"go": "Créer",
@@ -239,6 +239,7 @@
},
"status": "Connexion:"
},
"\u0005connectionindicator": {},
"notify": {
"disconnected": "déconnecté",
"moderator": "Droits modérateur accordés !",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,19 @@
{
"contactlist": "",
"passwordSetRemotely": "",
"connectionsettings": "连接设置",
"contactlist_plural": "__count__ 位成员",
"passwordSetRemotely": "被其他用户设置",
"poweredby": "技术支持",
"feedback": {
"average": "",
"bad": "差",
"good": "好",
"rateExperience": "请评价您的会议体验。",
"veryBad": "非常差",
"veryGood": "非常好"
},
"inviteUrlDefaultMsg": "您的会议正在被创建。。。",
"me": "我",
"speaker": "发言人",
"raisedHand": "请求发言",
"defaultNickname": "例如 星视通",
"defaultLink": "例如 __url__",
"callingName": "__name__",
"audioDevices": {
"bluetooth": "蓝牙",
"headphones": "耳机",
"phone": "电话",
"speaker": "发言人"
},
"audioOnly": {
"audioOnly": "只有音频",
"featureToggleDisabled": "在仅使用音频模式下切换功能无效"
@@ -26,6 +22,7 @@
"react-nativeGrantPermissions": "当浏览器要求权限许可时选择 <b><i>允许</i></b>",
"chromeGrantPermissions": "当浏览器要求权限许可时选择 <b><i>允许</i></b>",
"androidGrantPermissions": "当浏览器要求权限许可时选择 <b><i>允许</i></b>",
"electronGrantPermissions": "请授权使用您的摄像头和麦克风",
"firefoxGrantPermissions": "当浏览器要求权限许可时选择<b><i>共享设备</i></b> ",
"operaGrantPermissions": "当浏览器要求权限许可时选择 <b><i>允许</i></b>",
"iexplorerGrantPermissions": "当浏览器要求权限许可时选择 <b><i>可以</i></b>",
@@ -38,117 +35,146 @@
"raiseHand": "申请或取消发言",
"pushToTalk": "按住说话",
"toggleScreensharing": "在摄像头和屏幕共享之间切换",
"toggleFilmstrip": "显示隐藏视频",
"toggleShortcuts": "显示隐藏帮助菜单",
"toggleFilmstrip": "显示/隐藏 视频缩略图",
"toggleShortcuts": "显示/隐藏 快捷键",
"focusLocal": "切换到本地视频上",
"focusRemote": "切换到远端视频",
"focusRemote": "显示对方的视频",
"toggleChat": "打开或关闭聊天",
"mute": "静音或取消静音",
"fullScreen": "全屏或退出全屏",
"fullScreen": "开启 / 关闭 全屏",
"videoMute": "开启或关闭视频",
"showSpeakerStats": "查看扬声器状态"
"showSpeakerStats": "查看扬声器状态",
"localRecording": "显示 / 隐藏 本地录制选项"
},
"\u0005keyboardShortcuts": {},
"welcomepage": {
"disable": "不再显示该页",
"feature1": {
"content": "无需下载. __app__ 直接通过浏览器使用。 分享您的会议链接给其他人即可参与会议。",
"title": "简单易用"
"accessibilityLabel": {
"join": "点击加入",
"roomname": "请输入房间名"
},
"feature2": {
"content": "多方视频会议所需带宽仅需128Kbps。 屏幕共享和语音会议所需的带宽更少。",
"title": "低带宽"
},
"feature3": {
"content": "__app__ 有Apache许可. 在此许可下,您可以免费下载,使用,修改和分享该代码",
"title": "开源"
},
"feature4": {
"content": "",
"title": "不限用户数"
},
"feature5": {
"content": "和他人共享屏幕非常简单。 __app__ 对于在线演示、讲座和技术支持会议再合适不过了。",
"title": "屏幕共享"
},
"feature6": {
"content": "是否担心隐私安全? __app__ 可以设定会议室密码防止他人进入会议。",
"title": "安全"
},
"feature7": {
"content": "__app__ 的一大特色是Etherpad——一个完美适用于会议、写作等场景可实时协作的文本编辑器。",
"title": "共享笔记"
},
"feature8": {
"content": "通过简单地整合Piwik, Google Analytics或者其他使用监控和统计系统来了解您的使用者。",
"title": "使用统计"
"appDescription": "快来使用全队视频通话。您可以邀请任何您认识的人。__app__ 是一个完全加密100% 开源的视频会议解决方案。无需注册帐号,无限时免费使用。",
"audioVideoSwitch": {
"audio": "语音",
"video": "视频"
},
"calendar": "日历",
"connectCalendarText": "",
"connectCalendarButton": "",
"enterRoomTitle": "",
"go": "开始",
"join": "加入",
"privacy": "隐私",
"recentList": "最近",
"recentListDelete": "",
"recentListEmpty": "",
"roomname": "请输入房间名",
"roomnamePlaceHolder": "房间名",
"roomnameHint": "请输入您想加入房间的 URL 地址或者房间名。您也可以想个房名创建房间,只要其他人输入和您一样的名称就能加入您的房间。",
"sendFeedback": "发送反馈",
"terms": "条款"
"terms": "条款",
"title": ""
},
"startupoverlay": {
"policyText": "~",
"policyText": " ",
"title": "__app__ 需要使用您的麦克风和摄像头。"
},
"suspendedoverlay": {
"title": "由于您的电脑休眠,视频通话已经中断。",
"text": "重新连接",
"title": "由于您的电脑进入休眠模式,视频通话已经中断。",
"text": "按下 <i>重新加入</i> 按钮重新连接",
"rejoinKeyTitle": "重新加入"
},
"\u0005suspendedoverlay": {},
"toolbar": {
"addPeople": "",
"accessibilityLabel": {
"audioOnly": "",
"audioRoute": "",
"callQuality": "",
"chat": "显示 / 隐藏 聊天窗口",
"cc": "",
"document": "开启 / 关闭 文档共享",
"feedback": "",
"fullScreen": "进入 / 退出 全屏模式",
"hangup": "退出聊天室",
"invite": "",
"localRecording": "",
"lockRoom": "",
"moreActions": "显示 / 隐藏 更多选择",
"moreActionsMenu": "",
"mute": "静音 / 取消静音",
"pip": "",
"profile": "编辑您的简介",
"raiseHand": "举手 / 取消举手",
"recording": "开启 / 停止 视频录制",
"Settings": "显示 / 隐藏 设置",
"sharedvideo": "",
"shareRoom": "",
"shareYourScreen": "",
"shortcuts": "",
"speakerStats": "显示 / 隐藏 演说者资料",
"toggleCamera": "",
"tileView": "",
"videomute": "静音 / 取消静音"
},
"addPeople": "添加成员到您的通话中",
"audioonly": "启用/禁用仅音频模式(节省带宽)",
"audioOnlyOn": "启用/禁用仅音频模式(节省带宽)",
"audioOnlyOff": "",
"audioRoute": "",
"callQuality": "",
"enterFullScreen": "",
"exitFullScreen": "",
"feedback": "",
"moreActions": "",
"mute": "静音 / 解除静音",
"videomute": "开启 / 关闭 摄像头",
"authenticate": "认证",
"lock": "锁定 / 解锁 房间",
"invite": "分享链接",
"chat": "开启 / 关闭 聊天",
"etherpad": "开启 / 关闭 共享文档",
"documentOpen": "开启 / 关闭 共享文档",
"documentClose": "开启 / 关闭 共享文档",
"shareRoom": "",
"sharedvideo": "分享YouTube视频",
"sharescreen": "开启 / 关闭 屏幕共享",
"stopSharedVideo": "",
"fullscreen": "开启 / 关闭 全屏",
"sip": "呼叫SIP号码",
"Settings": "设置",
"hangup": "离开",
"login": "登录",
"logout": "登出",
"dialpad": "开启 / 关闭 拨号盘",
"sharedVideoMutedPopup": "",
"micMutedPopup": "",
"sharedVideoMutedPopup": "您共享的视频已经禁音,现在可以和其他成员交谈了。",
"toggleCamera": "",
"micMutedPopup": "您可以欣赏您共享的视频,因为您的麦克风已被静音。",
"talkWhileMutedPopup": "您在尝试发言吗? 当前您已被静音。",
"unableToUnmutePopup": "正在共享视频的时候您不能解除静音。",
"cameraDisabled": "摄像头不可用",
"micDisabled": "麦克风不可用",
"filmstrip": "显示 / 隐藏 视频",
"pip": "",
"profile": "编辑您的简介",
"raiseHand": "请求 / 取消 发言"
"raiseHand": "请求 / 取消 发言",
"shortcuts": "",
"speakerStats": "发言者状态",
"tileViewToggle": "画面模式",
"invite": "邀请"
},
"unsupportedBrowser": {
"appInstalled": "或者如果您已经安装了<br /><strong>那么</strong>",
"appNotInstalled": "您需要在您的移动设备上安装 <strong>__app__</strong> 来参与会议",
"downloadApp": "下载应用",
"joinConversation": "加入会议",
"startConference": "发起会议"
},
"bottomtoolbar": {
"chat": "开启 / 关闭 聊天",
"filmstrip": "显示 / 隐藏 视频",
"contactlist": ""
"\u0005toolbar": {
"accessibilityLabel": {}
},
"chat": {
"nickname": {
"title": "请在下面的方框内输入昵称",
"popover": "选择一个昵称"
},
"error": "",
"messagebox": "请输入文本..."
},
"settings": {
"calendar": {
"about": "",
"disconnect": "已断开连接",
"microsoftSignIn": "",
"signedIn": "",
"title": "日历"
},
"title": "设置",
"update": "更新",
"name": "名称",
@@ -158,11 +184,18 @@
"selectMic": "麦克风",
"selectAudioOutput": "音频输出",
"followMe": "所有人跟随我",
"language": "语言",
"loggedIn": "",
"noDevice": "未发现设备",
"cameraAndMic": "摄像头和麦克风",
"moderator": "主持人",
"moderator": "管理员",
"more": "更多",
"password": "设定密码",
"audioVideo": "音频和视频"
"audioVideo": "音频和视频",
"devices": "设备"
},
"\u0005settings": {
"calendar": {}
},
"profile": {
"title": "简介",
@@ -171,9 +204,9 @@
"setEmailInput": "输入您的邮箱"
},
"videothumbnail": {
"moderator": "",
"videomute": "",
"mute": "",
"moderator": "管理员",
"videomute": "成员关闭了他的摄像头",
"mute": "成员已静音",
"kick": "踢出",
"muted": "已静音",
"domute": "静音",
@@ -182,20 +215,18 @@
},
"connectionindicator": {
"header": "连接数据",
"bitrate": "比特率",
"packetloss": "丢包",
"resolution": "分辨率",
"connectedTo": "连接到:",
"bitrate": "比特率:",
"bridgeCount": "服务器数量:",
"packetloss": "丢包:",
"resolution": "分辨率:",
"framerate": "帧率:",
"less": "显示更少",
"more": "显示更多",
"address": "地址",
"remoteport_plural": "远程端口:",
"address": "地址",
"remoteport": "远程端口:",
"localport_plural": "本地端口:",
"localport": "本地端口:",
"localaddress_plural": "本地地址:",
"localaddress": "本地地址:",
"remoteaddress_plural": "远程地址:",
"remoteaddress": "远程地址:",
"transport": "传输:",
"bandwidth": "估计带宽:",
@@ -204,61 +235,67 @@
"quality": {
"good": "好",
"inactive": "未激活",
"lost": "",
"nonoptimal": "",
"poor": ""
"lost": "掉线",
"nonoptimal": "",
"poor": ""
},
"status": "连接"
"status": "连接"
},
"\u0005connectionindicator": {},
"notify": {
"disconnected": "已断开连接",
"moderator": "已授权主持人权限!",
"connected": "已连接",
"connectedOneMember": "__name__ 已连接",
"connectedTwoMembers": "__first__ 以及 __second__ 已连接",
"connectedThreePlusMembers": "__name__ 还有 __count__ 位其他成员已接入",
"somebody": "某人",
"me": "自己",
"focus": "会议聚焦",
"focusFail": "__component__ 不可用 - 在__ms__秒后重试",
"grantedTo": "主持权限已授予__to__",
"grantedToUnknown": "主持权限已授予$t(somebody)",
"muted": "您已经开始了通话,并处于静音状态。",
"mutedTitle": "您已被静音!",
"raisedHand": "请求发言"
"raisedHand": "请求发言",
"suboptimalExperienceTitle": "浏览器警告",
"suboptimalExperienceDescription": "呃…恐怕您对 __appName__ 的体验会很不好。我们正在尝试优化对此浏览器的支持。眼下,请尝试使用 <a href='static/recommendedBrowsers.html' target='_blank'>已知体验很好的浏览器</a>。"
},
"dialog": {
"add": "添加",
"accessibilityLabel": {
"liveStreaming": "流媒体直播中"
},
"allow": "允许",
"confirm": "",
"kickMessage": "您已被踢出会议!",
"popupErrorTitle": "",
"popupError": "",
"kickTitle": "",
"popupErrorTitle": "弹出窗口被拦截",
"popupError": "您的浏览器在此网站上阻止了弹出式窗口。请在浏览器的安全设置中打开它并再试一次。",
"passwordErrorTitle": "密码错误",
"passwordError": "此会议现在受密码保护。只有会议的拥有者可以设定密码。",
"passwordError2": "此会议现在受密码保护。只有会议的拥有者可以设定密码。",
"connectError": "发生错误,无法连接至会议!",
"connectErrorWithMsg": "发生错误,无法连接至会议: __msg__",
"incorrectPassword": "",
"incorrectPassword": "错误的用户名或者密码",
"connecting": "连接中",
"copy": "复制",
"contactSupport": "",
"contactSupport": "联系支持",
"error": "错误",
"createPassword": "创建密码",
"detectext": "尝试检测桌面共享扩展时发生错误",
"failedpermissions": "未能获取使用本地麦克风或摄像头的权限。",
"conferenceReloadTitle": "不好意思,出错了。",
"conferenceReloadMsg": "我们试着修复它。重连中__秒",
"conferenceReloadMsg": "不好意思,出错了。",
"conferenceDisconnectTitle": "你已经断开。",
"conferenceDisconnectMsg": "需要检查你的网络连接。重新连接中 __秒",
"dismiss": "",
"conferenceDisconnectMsg": "检查你的网络连接。将会在 __seconds__ 秒后重新连接…",
"dismiss": "解除,离开",
"rejoinNow": "马上重新加入",
"maxUsersLimitReachedTitle": "",
"maxUsersLimitReached": "",
"maxUsersLimitReachedTitle": "成员数量达到上限",
"maxUsersLimitReached": "由于会议已达到人数上限,不能加入。请联系会议组织者或者再次尝试!",
"lockRoom": "",
"lockTitle": "锁定失败",
"lockMessage": "锁定会议失败。",
"warning": "警告",
"passwordNotSupportedTitle": "",
"passwordNotSupported": "",
"passwordNotSupportedTitle": "不支持密码",
"passwordNotSupported": "不支持设置会议密码",
"internalErrorTitle": "内部错误",
"internalError": "",
"internalError": "哎呀!出现了点问题。错误: __error__",
"unableToSwitch": "无法转换视频流。",
"SLDFailure": "发生错误,无法静音! (SLD故障)",
"SRDFailure": "发生错误,无法关闭视频! (SRD故障)",
@@ -275,8 +312,8 @@
"shareVideoLinkError": "请提供正确的youtube链接。",
"removeSharedVideoTitle": "移除共享的视频",
"removeSharedVideoMsg": "您确定要移除共享的视频吗?",
"alreadySharedVideoMsg": "",
"alreadySharedVideoTitle": "",
"alreadySharedVideoMsg": "另一个成员正在共享视频。此会议一次只能共享一个视频。",
"alreadySharedVideoTitle": "只能同时分享 1 个视频",
"WaitingForHost": "等待主持人。。。",
"WaitForHostMsg": "会议<b>__room__ </b>还没有开始。如果您是主持人请授权开始,否则请等待主持人。",
"IamHost": "我是主持人。",
@@ -284,18 +321,14 @@
"Submit": "提交",
"retry": "重试",
"logoutTitle": "登出",
"logoutQuestion": "你确定要登出并停止会议吗",
"sessTerminated": "",
"logoutQuestion": "你确定要登出并停止会议吗",
"sessTerminated": "通话已终止",
"hungUp": "挂断",
"joinAgain": "重新加入",
"Share": "分享",
"Save": "保存",
"recording": "录制中",
"recordingToken": "输入记录标识",
"passwordCheck": "确定要移除密码吗?",
"passwordMsg": "设定密码来锁定房间",
"shareLink": "分享此会议的链接",
"yourPassword": "输入新的密码",
"Back": "返回",
"serviceUnavailable": "服务不可用",
"gracefulShutdown": "服务器正在维护,请稍后再试。",
@@ -303,66 +336,75 @@
"reservationError": "预定系统错误",
"reservationErrorMsg": "错误代号: __code__, 提示信息: __msg__",
"password": "输入密码",
"unlockRoom": "",
"userPassword": "用户密码",
"token": "标识",
"tokenAuthFailedTitle": "认证失败",
"tokenAuthFailed": "对不起,您未被允许参加此会议。",
"displayNameRequired": "需要显示名称",
"enterDisplayName": "请输入您的显示名称",
"extensionRequired": "需要扩展程序",
"firefoxExtensionPrompt": "您需要安装Firefox的扩展才能使用屏幕共享功能。请从<a href='__url__'>这里获取后</a>!重试。",
"feedbackHelp": "您的反馈将帮助我们提高我们的视频体验。",
"feedbackQuestion": "告诉我们您的联系方式。",
"feedbackQuestion": "告诉我们您的通话体验!",
"thankYou": "感谢使用__appName__",
"sorryFeedback": "很抱歉听到这些,能告诉我们更多详细情况吗?",
"liveStreaming": "流媒体直播中",
"streamKey": "流 名称/关键字",
"startLiveStreaming": "开始流媒体直播",
"streamKey": "流媒体直播密钥",
"startLiveStreaming": "开始直播",
"startRecording": "停止录制",
"stopStreamingWarning": "确定要停止流媒体直播吗?",
"stopRecordingWarning": "确定要停止录制吗",
"stopLiveStreaming": "停止流媒体直播",
"stopRecording": "停止录制",
"doNotShowWarningAgain": "不再显示此警告",
"doNotShowMessageAgain": "不再显示此信息",
"permissionDenied": "许可禁止",
"screenSharingFailedToInstall": "",
"screenSharingFailedToInstallTitle": "",
"screenSharingPermissionDeniedError": "",
"micErrorPresent": "连接到麦克风时发生错误。",
"cameraErrorPresent": "连接到摄像头时发生错误。",
"screenSharingFailedToInstall": "哎呀!屏幕共享插件安装失败。",
"screenSharingFailedToInstallTitle": "屏幕共享插件安装失败",
"screenSharingFirefoxPermissionDeniedError": "尝试进行屏幕共享时遇到了问题。请确认给予了相应的权限。",
"screenSharingFirefoxPermissionDeniedTitle": "哎呀!我们无法启动屏幕共享!",
"screenSharingPermissionDeniedError": "哎呀!您的视频共享插件发生了一点问题。请刷新重试。",
"cameraUnsupportedResolutionError": "您的摄像头不支持所需分辨率。",
"cameraUnknownError": "由于不可预知的错误,无法使用摄像头。",
"cameraUnknownError": "由于未知错误,无法使用摄像头。",
"cameraPermissionDeniedError": "您未授权使用您的摄像头。您仍可参加会议但是其他人无法看到,使用地址栏里的摄像头按钮来启动摄像头。",
"cameraNotFoundError": "未发现摄像头",
"cameraConstraintFailedError": "你的摄像头不满足要求。",
"micUnknownError": "未知错误,麦克风不可用",
"micPermissionDeniedError": "您未授权使用麦克风,您仍可参加会议但是其他人无法听到,使用地址栏里的摄像头按钮来启动麦克风",
"micUnknownError": "未知错误,麦克风不可用",
"micPermissionDeniedError": "您未授权使用麦克风,您仍可参加会议但是其他人无法听到,使用地址栏里的摄像头按钮来启动麦克风",
"micNotFoundError": "未发现麦克风",
"micConstraintFailedError": "你的麦克风不满足要求。",
"micNotSendingDataTitle": "",
"micNotSendingData": "",
"cameraNotSendingDataTitle": "",
"cameraNotSendingData": "",
"micNotSendingDataTitle": "无法访问麦克风",
"micNotSendingData": "我们无法访问您的麦克风。请从设定菜单里选择其他设备或者重新加载。",
"cameraNotSendingDataTitle": "无法访问摄像头",
"cameraNotSendingData": "我们无法访问您的摄像头。请检查是否有其他程序正在使用这个设备,否则请从设定菜单里选择其他设备或者重新加载。",
"goToStore": "跳转至应用商店",
"externalInstallationTitle": "需要扩展程序",
"externalInstallationMsg": "您需要安装桌面共享扩展",
"inlineInstallationMsg": "您需要安装桌面共享扩展",
"inlineInstallExtension": "",
"muteParticipantTitle": "",
"inlineInstallExtension": "立刻安装",
"muteParticipantTitle": "静音该成员?",
"muteParticipantBody": "您无法对他们解除静音,但是他们自己可以随时解除静音。",
"muteParticipantButton": "静音",
"liveStreamingDisabledTooltip": "",
"liveStreamingDisabledForGuestTooltip": "",
"recordingDisabledTooltip": "",
"recordingDisabledForGuestTooltip": "",
"remoteControlTitle": "远程桌面控制",
"remoteControlRequestMessage": "你允许 __用户__ 远程控制你的桌面吗?",
"remoteControlShareScreenWarning": "注意:如果按下“允许”你将共享你的屏幕!",
"remoteControlDeniedMessage": "__user__ 拒绝了您的远程控制请求",
"remoteControlAllowedMessage": "__user__ 接受了您的远程控制请求",
"remoteControlErrorMessage": "在尝试向__user__请求远程控制权限时发生了一个错误",
"startRemoteControlErrorMessage": "",
"remoteControlErrorMessage": "在尝试向__user__请求远程控制权限时发生了一个错误",
"startRemoteControlErrorMessage": "尝试开始远程控制会话时发生了一个错误!",
"remoteControlStopMessage": "远程控制结束!",
"close": "关闭",
"shareYourScreen": "共享你的屏幕",
"shareYourScreenDisabled": "",
"shareYourScreenDisabledForGuest": "",
"yourEntireScreen": "你的整个屏幕",
"applicationWindow": "应用窗口"
"applicationWindow": "应用窗口",
"transcribing": ""
},
"\u0005dialog": {
"accessibilityLabel": {}
},
"email": {
"sharedKey": [
@@ -392,6 +434,10 @@
],
"and": "添加"
},
"share": {
"mainText": "",
"dialInfoText": ""
},
"connection": {
"ERROR": "错误",
"CONNECTING": "连接中",
@@ -405,31 +451,83 @@
"ATTACHED": "已接入"
},
"recording": {
"busy": "",
"busyTitle": "",
"beta": "",
"busy": "我们正在释放录制资源。请几分钟之后再试。",
"busyTitle": "所有的录制设备正忙",
"buttonTooltip": "开始 / 结束录制",
"error": "录制失败。请重新尝试。",
"expandedOff": "录制已停止",
"expandedOn": "",
"expandedPending": "",
"failedToStart": "录制启动失败",
"live": "",
"off": "录制已停止",
"on": "录制中",
"pending": "录制中,等待一位与会者加入",
"unavailable": "",
"unavailableTitle": ""
"pending": "",
"rec": "",
"authDropboxText": "",
"serviceName": "录像服务",
"signOut": "",
"signIn": "",
"loggedIn": "",
"availableSpace": "",
"startRecordingBody": "确定要停止录制吗",
"unavailable": "噢__serviceName__ 暂时无法使用。我们正在解决此问题。请稍后再试。",
"unavailableTitle": "录制不可用"
},
"\u0005recording": {},
"liveStreaming": {
"busy": "",
"busyTitle": "",
"buttonTooltip": "",
"error": "",
"failedToStart": "",
"transcribing": {
"pending": "",
"off": "",
"error": "录制失败。请重新尝试。",
"expandedLabel": "",
"failedToStart": "",
"tr": "",
"labelToolTip": "",
"ccButtonTooltip": "",
"start": "",
"stop": ""
},
"\u0005transcribing": {},
"liveStreaming": {
"busy": "我们正在释放串流资源。请几分钟后再试。",
"busyTitle": "所有的串流设备正忙",
"buttonTooltip": "开始 / 停止直播",
"changeSignIn": "切换帐号",
"choose": "选择一个直播流",
"chooseCTA": "请选择直播选项。您现在以 __email__ 身份登录。",
"enterStreamKey": "在此输入您的 YouTube 串流密钥。",
"error": "流媒体直播失败。请重试。",
"errorAPI": "在访问您的 YouTube 直播服务时发生问题。请重新登录。",
"errorLiveStreamNotEnabled": "",
"expandedOff": "",
"expandedOn": "",
"expandedPending": "",
"failedToStart": "直播服务启动失败",
"off": "流媒体直播已停止",
"on": "流媒体直播中",
"pending": "启动流媒体。。。",
"streamIdRequired": "",
"streamIdHelp": "在哪里找到这个",
"unavailable": "",
"unavailableTitle": ""
"serviceName": "直播服务",
"signedInAs": "",
"signIn": "使用谷歌登录",
"signOut": "",
"signInCTA": "输入 YouTube 串流密钥或者登录 YouTube 帐号。",
"start": "开始直播",
"streamIdHelp": "这是什么?",
"unavailableTitle": "流媒体直播不可用"
},
"\u0005liveStreaming": {},
"videoSIPGW": {
"busy": "我们正在清理和释放资源。请过几分钟后再试。",
"busyTitle": "房间服务正忙",
"errorInvite": "会议还未开始。请稍后再来。",
"errorInviteTitle": "",
"errorAlreadyInvited": "__displayName__ 已被邀请过了",
"errorInviteFailedTitle": "邀请 __displayName__ 失败",
"errorInviteFailed": "我们正在解决问题。请稍后再试。",
"pending": "__displayName__ 已被邀请",
"serviceName": "房间服务",
"unavailableTitle": "房间服务不可用。"
},
"speakerStats": {
"hours": "__count__h",
@@ -441,69 +539,203 @@
},
"deviceSelection": {
"deviceSettings": "设备设置",
"noPermission": "未授权限 ",
"noPermission": "未授权限",
"previewUnavailable": "预览不可用",
"selectADevice": "选择设备",
"testAudio": "测试声音"
},
"invite": {
"addPassword": "添加密码",
"callNumber": "呼叫__号码__",
"enterID": "在电话终端输入会议ID:__conferenceID__和#号拨打会议电话",
"howToDialIn": "用以下号码和会议编号拨号。",
"hidePassword": "隐藏密码",
"inviteTo": "邀请人到 __会议名称__",
"invitedYouTo": "",
"invitePeople": "",
"locked": "本次呼叫被锁定,新的呼叫必须有链接并输入口令后加入。",
"showPassword": "查看口令",
"unlocked": "本次呼叫已被锁定,用这个链接可以加入。"
"testAudio": ""
},
"videoStatus": {
"callQuality": "",
"audioOnly": "",
"audioOnlyExpanded": "",
"callQuality": "通话质量",
"hd": "高清",
"highDefinition": "",
"labelTooltipVideo": "",
"labelTooltipAudioOnly": "",
"ld": "",
"lowDefinition": "",
"p2pEnabled": "",
"p2pVideoQualityDescription": "",
"recHighDefinitionOnly": "",
"hdTooltip": "",
"highDefinition": "高清",
"labelTooltipAudioOnly": "已启用仅音频模式",
"labelTooiltipNoVideo": "",
"labelTooltipVideo": "当前视频质量",
"ld": "低清",
"ldTooltip": "",
"lowDefinition": "低清",
"onlyAudioAvailable": "只能使用音频",
"onlyAudioSupported": "我们只支持此浏览器的音频功能。",
"p2pEnabled": "点对点已启用",
"p2pVideoQualityDescription": "在点对点模式下,通话质量只有高清和仅音频两个选项。其他选项只有在点对点模式退出后才可用。",
"recHighDefinitionOnly": "将会首选高清模式。",
"sd": "标清",
"standardDefinition": "",
"qualityButtonTip": ""
"sdTooltip": "",
"standardDefinition": "标清",
"qualityButtonTip": "修改接收视频质量"
},
"dialOut": {
"dial": "拨号",
"dialOut": "",
"statusMessage": "",
"enterPhone": "输入电话号码",
"phoneNotAllowed": "还不支持这个目标!抱歉!"
"statusMessage": "现在状态为 __status__"
},
"addPeople": {
"add": "添加",
"noResults": "",
"searchPlaceholder": "",
"title": "",
"failedToAdd": ""
"add": "邀请",
"countryNotSupported": "目的国家暂时未被支持。",
"countryReminder": "尝试在美国之外通话?请检查国家代码!",
"disabled": "您不能邀请成员",
"footerText": "",
"invite": "邀请",
"loading": "查找联系人或者电话号码",
"loadingNumber": "验证电话号码",
"loadingPeople": "正在搜索需要邀请的成员",
"noResults": "没有符合要求的搜索结果",
"noValidNumbers": "请输入一个电话号码",
"notAvailable": "您不能邀请成员",
"searchNumbers": "",
"searchPeople": "",
"searchPeopleAndNumbers": "",
"telephone": "电话号码: __number__",
"title": "邀请成员参加您的会议",
"failedToAdd": "无法添加成员"
},
"\u0005addPeople": {},
"inlineDialogFailure": {
"msg": "",
"retry": "",
"support": "",
"supportMsg": ""
"msg": "貌似出了点问题。",
"retry": "重试",
"support": "支持",
"supportMsg": "如果此事多次发生,请联系"
},
"deviceError": {
"cameraError": "",
"microphoneError": "",
"cameraPermission": "",
"microphonePermission": ""
"cameraError": "无法访问您的摄像头",
"microphoneError": "无法访问您的麦克风",
"cameraPermission": "无法获得摄像头访问权限",
"microphonePermission": "无法获得麦克风访问权限"
},
"feedback": {
"average": "平均",
"bad": "差",
"good": "好",
"detailsLabel": "",
"rateExperience": "请评价您的会议体验。",
"veryBad": "非常差",
"veryGood": "非常好"
},
"\u0005feedback": {},
"info": {
"copy": "",
"invite": "",
"title": "",
"accessibilityLabel": "",
"addPassword": "添加密码",
"cancelPassword": "取消密码",
"conferenceURL": "",
"country": "国家",
"dialANumber": "要加入您的会议请拨打其中之一的电话号码并输入PIN__conferenceID__#",
"dialInNumber": "",
"dialInConferenceID": "",
"dialInNotSupported": "抱歉,不支持电话呼入。",
"genericError": "糟糕!出错了。",
"inviteLiveStream": "",
"invitePhone": "要想使用电话加入会议,请拨打 __number__ 并输入 PIN __conferenceID__#",
"invitePhoneAlternatives": "点击此链接查看更多电话号码: __url__",
"inviteURL": "请点击此链接 __url__ 加入视频会议",
"liveStreamURL": "流媒体直播中",
"moreNumbers": "更多成员",
"noNumbers": "无呼入号码。",
"noPassword": "未发现设备",
"noRoom": "没有指定要呼入的房间。",
"numbers": "呼入号码",
"password": "密码:",
"title": "分享",
"tooltip": ""
}
},
"\u0005info": {},
"settingsView": {
"alertOk": "确认",
"alertTitle": "警告",
"alertURLText": "服务器 URL 无效",
"conferenceSection": "会议",
"displayName": "显示名称",
"email": "电子邮件",
"header": "设置",
"profileSection": "简介",
"serverURL": "服务器 URL",
"startWithAudioMuted": "启动并关闭音频",
"startWithVideoMuted": "启动并关闭视频"
},
"calendarSync": {
"addMeetingURL": "",
"confirmAddLink": "",
"confirmAddLinkTitle": "日历",
"join": "",
"joinTooltip": "",
"nextMeeting": "下次会议",
"noEvents": "",
"ongoingMeeting": "",
"permissionButton": "",
"permissionMessage": "",
"refresh": "",
"today": "今日"
},
"recentList": {
"joinPastMeeting": ""
},
"sectionList": {
"pullToRefresh": ""
},
"deepLinking": {
"title": "",
"description": "",
"tryAgainButton": "",
"launchWebButton": "",
"appNotInstalled": "",
"downloadApp": "下载应用",
"openApp": ""
},
"presenceStatus": {
"invited": "邀请",
"ringing": "",
"calling": "",
"initializingCall": "",
"connected": "已连接",
"connecting": "连接中",
"connecting2": "连接中",
"disconnected": "已断开连接",
"busy": "",
"rejected": "",
"ignored": "",
"expired": ""
},
"\u0005presenceStatus": {},
"dateUtils": {
"today": "今日",
"yesterday": "昨天",
"earlier": "更早的"
},
"incomingCall": {
"answer": "",
"audioCallTitle": "",
"decline": "解除,离开",
"productLabel": "",
"videoCallTitle": ""
},
"localRecording": {
"localRecording": "",
"dialogTitle": "",
"start": "停止录制",
"stop": "停止录制",
"moderator": "管理员",
"me": "自己",
"duration": "",
"durationNA": "",
"encoding": "",
"participantStats": "",
"participant": "与会者",
"sessionToken": "",
"clientState": {
"on": "开",
"off": "关",
"unknown": "未知"
},
"messages": {
"engaged": "",
"finished": "",
"finishedModerator": "",
"notModerator": "你不是主持人.你不能开启或关闭本地录音"
},
"yes": "是",
"no": "否",
"label": "录音",
"labelToolTip": "本地录音被占用"
},
"\u0005localRecording": {}
}

View File

@@ -4,7 +4,7 @@
"poweredby": "技術支援",
"inviteUrlDefaultMsg": "您的會議正在建立起來………",
"me": "我",
"speaker": "",
"speaker": "發言者",
"raisedHand": "請求發言",
"defaultNickname": "例如 阿美 志明",
"defaultLink": "例如 __url__",
@@ -19,13 +19,13 @@
"featureToggleDisabled": "在僅用音訊模式下,開關 __feature__ 功能是停用的"
},
"userMedia": {
"react-nativeGrantPermissions": "",
"chromeGrantPermissions": "",
"androidGrantPermissions": "",
"electronGrantPermissions": "",
"react-nativeGrantPermissions": "當瀏覽器要求權限允許時,請選擇 <b><i>允許</i></b>",
"chromeGrantPermissions": "當瀏覽器要求權限允許時,請選擇 <b><i>允許</i></b>",
"androidGrantPermissions": "當瀏覽器要求權限允許時,請選擇 <b><i>允許</i></b>",
"electronGrantPermissions": "請允許權限使用您的攝影裝置和麥克風",
"firefoxGrantPermissions": "當瀏覽器要求權限允許時,請選擇<b><i>分享設備</i></b> ",
"operaGrantPermissions": "當瀏覽器要求權限允許時,請選擇 <b><i>允許</i></b>",
"iexplorerGrantPermissions": "",
"iexplorerGrantPermissions": "當瀏覽器要求權限允許時,請選擇 <b><i>OK</i></b>",
"safariGrantPermissions": "當瀏覽器要求權限允許時,請選擇 <b><i>OK</i></b>",
"nwjsGrantPermissions": "請允許權限使用您的攝影裝置和麥克風",
"edgeGrantPermissions": "當瀏覽器要求權限允許時,請選擇 <b><i>是的</i></b>"
@@ -35,31 +35,42 @@
"raiseHand": "舉手發言或不作發言",
"pushToTalk": "按鍵通話",
"toggleScreensharing": "在攝影鏡頭和螢幕分享之間進行切換",
"toggleFilmstrip": "顯示或隱藏視訊",
"toggleShortcuts": "顯示或隱藏說明選單",
"toggleFilmstrip": "顯示或隱藏視訊影片縮圖",
"toggleShortcuts": "顯示或顯示鍵盤快捷鍵",
"focusLocal": "聚焦於自己的視訊",
"focusRemote": "聚焦於另一位通話者的視訊",
"focusRemote": "聚焦於另一的視訊",
"toggleChat": "開啟或關閉聊天",
"mute": "靜音或解除靜音",
"fullScreen": "進入或退出全螢幕",
"fullScreen": "觀看 或 離開 全螢幕",
"videoMute": "啟動或停止自己的攝影裝置",
"showSpeakerStats": "顯示發言者數據"
"showSpeakerStats": "顯示發言者數據",
"localRecording": "顯示或顯示本地端錄影控制"
},
"welcomepage": {
"accessibilityLabel": {
"join": "輕觸即可參加",
"roomname": "輸入會議室名稱"
},
"appDescription": "快來使用吧,團隊全部成員使用視訊通話,可以邀請任何您所認識的人。 __app__ 是一套完全加密、100% 開放源碼的視訊會議解決方案。無需註冊帳號,無時無刻不分日夜均可免費使用。",
"audioVideoSwitch": {
"audio": "語音",
"video": "視訊"
},
"calendar": "日曆",
"connectCalendarText": "連接你的行事曆,在 __app__ 查看你的會議,此外,增加 __app__ 會議到你的行事曆中,只要按一下就可以啟動。",
"connectCalendarButton": "連接你的行事曆",
"enterRoomTitle": "啟動新的會議",
"go": "開始",
"join": "加入",
"privacy": "隱私",
"recentList": "最近使用",
"recentListDelete": "刪除",
"recentListEmpty": "目前最近使用是空白的。與你的團隊成員聊天,即會在此處找到最近的會議。",
"roomname": "輸入會議室名稱",
"roomnameHint": "請輸入您想加入的會議室 URL 網址或名稱。您可以用個名稱來建立會議室,只要其他人輸入相同的名稱就能加入會議室喔。",
"sendFeedback": "發送回報",
"terms": "條款",
"title": "更加安全、更具彈性、又完全免費的視訊會議系統"
"title": "安全、全功能、完全免費的視訊會議"
},
"startupoverlay": {
"policyText": " ",
@@ -71,8 +82,41 @@
"rejoinKeyTitle": "重新加入"
},
"toolbar": {
"accessibilityLabel": {
"audioOnly": "切換僅有聲音",
"audioRoute": "",
"callQuality": "管理通話品質",
"chat": "切換聊天視窗",
"cc": "切換字幕",
"document": "切換分享的文件",
"feedback": "留言回報",
"fullScreen": "切換全螢幕",
"hangup": "離開來電",
"invite": "",
"localRecording": "切換本地端錄影控制",
"lockRoom": "切換會議室鎖定",
"moreActions": "切換更多動作功能表",
"moreActionsMenu": "更多動作功能表",
"mute": "切換靜音",
"pip": "切換子母畫面模式",
"profile": "編輯您的簡介",
"raiseHand": "切換舉手",
"recording": "切換錄影",
"Settings": "切換設置",
"sharedvideo": "切換 Youtube 影片分享",
"shareRoom": "邀請某人",
"shareYourScreen": "切換螢幕分享",
"shortcuts": "切換快捷鍵",
"speakerStats": "切換發言人統計",
"toggleCamera": "",
"tileView": "",
"videomute": "切換靜音視訊"
},
"addPeople": "新增人員到您的通話中",
"audioonly": "啟用/停用 僅用音訊模式(節省頻寬)",
"audioOnlyOn": "啟用僅用音訊模式(節省頻寬)",
"audioOnlyOff": "關閉僅用音訊模式",
"audioRoute": "選擇聲音裝置",
"callQuality": "管理通話品質",
"enterFullScreen": "觀看全螢幕",
"exitFullScreen": "跳出全螢幕",
@@ -86,52 +130,68 @@
"etherpad": "開啟/關閉 分享文件檔案",
"documentOpen": "開啟分享的文件檔案",
"documentClose": "關閉分享的文件檔案",
"shareRoom": "分享室",
"sharedvideo": "分享 YouTube 視訊",
"sharescreen": "螢幕分享",
"stopSharedVideo": "停止 YouTube 視訊",
"fullscreen": "觀看/跳出 全螢幕",
"sip": "播打 SIP 號碼",
"Settings": "",
"Settings": "設置",
"hangup": "留言",
"login": "登入",
"logout": "",
"logout": "登出",
"sharedVideoMutedPopup": "您分享的視訊已經靜音,現在可以和其他成員交談了。",
"toggleCamera": "切換攝影機",
"micMutedPopup": "您的麥克風已經處於靜音,可以觀看分享視訊了。",
"talkWhileMutedPopup": "您要發言嗎? 目前您處於靜音。",
"unableToUnmutePopup": "當分享視訊正在使用時,您不能解除靜音。",
"cameraDisabled": "攝影裝置無法使用",
"micDisabled": "麥克風無法使用",
"filmstrip": "顯示/隱藏 視訊",
"pip": "進入子母畫模式",
"profile": "編輯您的簡介",
"raiseHand": "舉手/取消 請求發言",
"shortcuts": "查看快捷鍵",
"speakerStats": "發言者數據"
"speakerStats": "發言者數據",
"tileViewToggle": "切換平鋪檢視",
"invite": "邀請人員"
},
"chat": {
"nickname": {
"title": "請在下面欄位輸入暱稱",
"popover": "選擇暱稱"
},
"error": "錯誤:你的訊息 \"__originalText__\" 未被送出。原因: __error__",
"messagebox": "請輸入文字..."
},
"settings": {
"title": "",
"calendar": {
"about": "此 __appName__ 行事曆整合是安全存取你的行事曆,所以可以讀取即將發生的事件。",
"disconnect": "中斷連接",
"microsoftSignIn": "使用 Microsoft 帳戶登入",
"signedIn": "目前是以 __email__ 來存取行事曆事件。點按下方取消連接鈕可以停止存取行事曆事件。",
"title": "日曆"
},
"title": "設置",
"update": "更新",
"name": "",
"name": "名稱",
"startAudioMuted": "全部人啟動時處於靜音",
"startVideoMuted": "全部人啟動時隱藏視訊畫面",
"selectCamera": "攝影裝置",
"selectMic": "麥克風",
"selectAudioOutput": "音訊輸出",
"followMe": "全部人跟隨仿照我",
"noDevice": "",
"language": "語言",
"loggedIn": "以 __name__ 登入",
"noDevice": "無",
"cameraAndMic": "攝影裝置和麥克風",
"moderator": "主持人",
"more": "更多",
"password": "設定密碼",
"audioVideo": "音訊和視訊"
"audioVideo": "音訊和視訊",
"devices": "裝置"
},
"profile": {
"title": "",
"title": "簡介",
"setDisplayNameLabel": "設定您的顯示名稱",
"setEmailLabel": "設置您的大頭人像電子信箱",
"setEmailInput": "輸入您的電子信箱"
@@ -142,13 +202,15 @@
"mute": "成員處於靜音",
"kick": "踢出",
"muted": "處於靜音",
"domute": "",
"domute": "靜音",
"flip": "翻轉",
"remoteControl": "遠端控制"
},
"connectionindicator": {
"header": "連接資料",
"connectedTo": "已連接至:",
"bitrate": "比特率:",
"bridgeCount": "伺服器計數:",
"packetloss": "丟包:",
"resolution": "解析度:",
"framerate": "影格率:",
@@ -164,7 +226,7 @@
"na": "一旦會議啟動,即可回到此處查看連接資訊",
"turn": " (轉)",
"quality": {
"good": "",
"good": "很好",
"inactive": "未啟用",
"lost": "漏失",
"nonoptimal": "不甚理想",
@@ -183,7 +245,6 @@
"focus": "會議焦點",
"focusFail": "__component__ 無法使用 - 請在 __ms__ 秒後重試",
"grantedTo": "主持人權限已授予 __to__!",
"grantedToUnknown": "主持人權限已經授予 $t(somebody) ",
"muted": "您已經啟動通話,並處於靜音狀態。",
"mutedTitle": "您目前處於靜音!",
"raisedHand": "請求發言。",
@@ -191,8 +252,13 @@
"suboptimalExperienceDescription": "呃……恐怕您對 __appName__ 的體驗不是很好,我們正在嘗試找方法改進對此瀏覽器的支援。現下敬請選用 <a href='static/recommendedBrowsers.html' target='_blank'>全力支援的瀏覽器</a> 來進行。"
},
"dialog": {
"accessibilityLabel": {
"liveStreaming": "直播串流"
},
"allow": "允許",
"confirm": "確認",
"kickMessage": "您已經被踢出會議!",
"kickTitle": "從會議踢出",
"popupErrorTitle": "彈出視窗遭到阻攔",
"popupError": "您的瀏覽器在此網站上阻攔彈出視窗。請在瀏覽器的安全設置中開啟它並再試一次。",
"passwordErrorTitle": "密碼錯誤",
@@ -201,10 +267,10 @@
"connectError": "喔哦!發生錯誤,無法連接至會議。",
"connectErrorWithMsg": "喔哦!發生錯誤,無法連接至會議: __msg__",
"incorrectPassword": "錯誤的用戶名稱或密碼",
"connecting": "",
"connecting": "連接中",
"copy": "複製",
"contactSupport": "聯絡支援",
"error": "",
"error": "錯誤",
"detectext": "嘗試偵測桌面分享擴充應用程式時發生錯誤。",
"failedpermissions": "未能取得使用本地麥克風或攝影裝置的權限。",
"conferenceReloadTitle": "不好意思,出錯了。",
@@ -215,9 +281,10 @@
"rejoinNow": "立即重新加入",
"maxUsersLimitReachedTitle": "成員人數已經達到上限",
"maxUsersLimitReached": "由於會議已達到人數上限,額滿不能加入。請聯絡會議發起人,或是稍後再次嘗試!",
"lockRoom": "鎖定會議室",
"lockTitle": "鎖定失敗",
"lockMessage": "鎖定會議失敗。",
"warning": "",
"warning": "警告",
"passwordNotSupportedTitle": "不支援密碼",
"passwordNotSupported": "不支援設置會議密碼。",
"internalErrorTitle": "內部錯誤",
@@ -251,9 +318,9 @@
"sessTerminated": "通話已經終止",
"hungUp": "我方掛斷",
"joinAgain": "再次加入",
"Share": "",
"Share": "分享",
"Save": "儲存",
"recording": "",
"recording": "錄製作業中",
"recordingToken": "輸入錄製標記",
"Back": "返回",
"serviceUnavailable": "服務無法使用",
@@ -262,9 +329,10 @@
"reservationError": "預約系統錯誤",
"reservationErrorMsg": "錯誤碼: __code__, 訊息: __msg__",
"password": "輸入密碼",
"unlockRoom": "解鎖會議室",
"userPassword": "用戶密碼",
"token": "標記",
"tokenAuthFailedTitle": "",
"tokenAuthFailedTitle": "驗證失敗",
"tokenAuthFailed": "對不起,您未被允許加入此會議。",
"displayNameRequired": "顯示名稱是必須的",
"enterDisplayName": "請輸入您的顯示名稱",
@@ -272,9 +340,9 @@
"feedbackQuestion": "請告訴我們本次通話體驗!",
"thankYou": "感謝您使用 __appName__",
"sorryFeedback": "很抱歉聽到這些,能告訴我們更多詳情嗎?",
"liveStreaming": "",
"liveStreaming": "直播串流中",
"streamKey": "直播串流密鑰",
"startLiveStreaming": "立即開始直播",
"startLiveStreaming": "啟動直播串流",
"startRecording": "啟動錄製作業",
"stopStreamingWarning": "確定要停止直播串流嗎?",
"stopRecordingWarning": "確定要停止錄製作業嗎?",
@@ -302,12 +370,16 @@
"cameraNotSendingData": "我們無法取用您的攝影裝置。請檢查是否有其他程序正在使用這個設備,否則請從設置選單裡選擇其他設備或者重新裝載。",
"goToStore": "前往應用商店",
"externalInstallationTitle": "需要擴充應用程式",
"externalInstallationMsg": "",
"externalInstallationMsg": "您需要安裝桌面分享擴充應用程式。",
"inlineInstallationMsg": "您需要安裝桌面分享擴充應用程式。",
"inlineInstallExtension": "立即安裝",
"muteParticipantTitle": "靜音這位成員?",
"muteParticipantBody": "您無法對他們解除靜音,但是他們自己隨時可以解除靜音。",
"muteParticipantButton": "靜音",
"liveStreamingDisabledTooltip": "啟動直播串流已關閉。",
"liveStreamingDisabledForGuestTooltip": "訪客無法啟動直播串流。",
"recordingDisabledTooltip": "啟動錄影已關閉。",
"recordingDisabledForGuestTooltip": "訪客無法啟動錄影。",
"remoteControlTitle": "遠端桌面控制",
"remoteControlRequestMessage": "您要允許 __user__ 遠端控制您的桌面嗎?",
"remoteControlShareScreenWarning": "注意:如果按下 \"允許\" 您將分享自己的螢幕!",
@@ -318,8 +390,11 @@
"remoteControlStopMessage": "遠端控制階段結束!",
"close": "關閉",
"shareYourScreen": "分享自己的螢幕",
"shareYourScreenDisabled": "螢幕分享已關閉。",
"shareYourScreenDisabledForGuest": "訪客無法螢幕分享。",
"yourEntireScreen": "自己的全螢幕",
"applicationWindow": "應用程式視窗"
"applicationWindow": "應用程式視窗",
"transcribing": "轉錄中"
},
"email": {
"sharedKey": [
@@ -349,6 +424,22 @@
],
"and": "與"
},
"share": {
"mainText": [
"點按以下連結參加會議:",
"__roomUrl__"
],
"dialInfoText": [
"",
"",
"=====",
"",
"僅是想要由電話播入嗎?",
"",
"點按以下連結查看此會議的電話播入號碼",
"__dialInfoPageUrl__"
]
},
"connection": {
"ERROR": "錯誤",
"CONNECTING": "連接中",
@@ -362,18 +453,42 @@
"ATTACHED": "已經附加"
},
"recording": {
"beta": "BETA",
"busy": "我們正在釋放錄製資源。請過幾分鐘後再試。",
"busyTitle": "全部錄製設備正在忙碌",
"buttonTooltip": "啟動/停止 錄製作業",
"error": "錄製作業失敗。請再次重試。",
"expandedOff": "錄影已經停止",
"expandedOn": "此會議目前正在錄影。",
"expandedPending": "錄影正在啟動…",
"failedToStart": "錄製啟動失敗",
"live": "直播",
"off": "錄製作業已經停止",
"on": "錄製作業中",
"pending": "錄製作業正在等待成員加入……",
"pending": "準備錄影此會議…",
"rec": "REC 錄影",
"authDropboxText": "上傳至 Dropbox",
"serviceName": "錄製作業服務",
"signOut": "jde eei ",
"signIn": "jde bp ",
"loggedIn": "以 __userName__ 登入",
"availableSpace": "可用空間: __spaceLeft__ MB (大約錄影時間 __duration__ 分鐘)",
"startRecordingBody": "確定要停止錄影作業嗎?",
"unavailable": "喔哦__serviceName__ 目前無法使用。我們正在解決此問題,請稍後再試。",
"unavailableTitle": "錄製作業無法使用"
},
"transcribing": {
"pending": "正在準備轉錄會議…",
"off": "轉錄已停止",
"error": "錄影作業失敗。請重試。",
"expandedLabel": "轉錄目前開啟",
"failedToStart": "轉錄啟動失敗",
"tr": "TR 轉錄",
"labelToolTip": "此會議正被轉錄",
"ccButtonTooltip": "啟動 / 停止顯示字幕",
"start": "啟動顯示字幕",
"stop": "停止顯示字幕"
},
"liveStreaming": {
"busy": "我們正在釋放串流資源。請過幾分鐘後再試。",
"busyTitle": "全部串流設備正在忙碌",
@@ -384,12 +499,18 @@
"enterStreamKey": "在此輸入您的 YouTube 直播串流密鑰。",
"error": "直播串流失敗。請重試。",
"errorAPI": "取用您的 YouTube 播出時發生錯誤。請重新登入。",
"errorLiveStreamNotEnabled": "直播串流在 __email__ 尚未啟用。請開啟直播串流或登入有啟用直播串流的帳戶。",
"expandedOff": "直播串流已停止",
"expandedOn": "會議串流目前送至 YouTube 。",
"expandedPending": "直播串流正被啟動…",
"failedToStart": "直播串流啟動失敗",
"off": "直播串流已經停止",
"on": "直播串流中",
"pending": "啟動直播串流………",
"serviceName": "直播串流服務",
"signedInAs": "你目前登入名為:",
"signIn": "使用 Google 帳戶登入",
"signOut": "登出",
"signInCTA": "輸入 YouTube 直播串流密鑰,或登入 YouTube 帳號。",
"start": "啟動直播串流",
"streamIdHelp": "這是什麼?",
@@ -420,9 +541,11 @@
"noPermission": "未取得權限",
"previewUnavailable": "預覽無法使用",
"selectADevice": "選擇設備",
"testAudio": "測試聲音"
"testAudio": "播放測試聲音"
},
"videoStatus": {
"audioOnly": "AUD 聲音",
"audioOnlyExpanded": "你處於僅用音訊模式。這個模式節省頻寬,但無法看見他人影像。",
"callQuality": "通話品質",
"hd": "HD 高清",
"hdTooltip": "觀看高清視訊 HD",
@@ -447,10 +570,11 @@
"statusMessage": "現在狀態為 __status__"
},
"addPeople": {
"add": "",
"add": "邀請",
"countryNotSupported": "此目標區域尚未支援。",
"countryReminder": "嘗試在美國外地通話?請確認開頭使用的國家代碼!",
"disabled": "",
"disabled": "您不可以邀請人員。",
"footerText": "播打已關閉。",
"invite": "邀請",
"loading": "尋找聯絡人及電話號碼",
"loadingNumber": "驗證電話號碼",
@@ -487,6 +611,7 @@
"veryGood": "極好"
},
"info": {
"accessibilityLabel": "顯示資訊",
"addPassword": "新增密碼",
"cancelPassword": "取消密碼",
"conferenceURL": "連結:",
@@ -508,7 +633,7 @@
"numbers": "播入號碼",
"password": "密碼:",
"title": "分享",
"tooltip": "取得關於會議的連接使用資訊"
"tooltip": "顯示此會議的連結及電話播入號碼"
},
"settingsView": {
"alertOk": "確認",
@@ -524,17 +649,21 @@
"startWithVideoMuted": "啟動並視訊靜音"
},
"calendarSync": {
"later": "稍後",
"next": "即將推出",
"addMeetingURL": "增加會議連結",
"confirmAddLink": "你要加上 Jitsi 連結於此事件嗎?",
"confirmAddLinkTitle": "日曆",
"join": "參加",
"joinTooltip": "參加會議",
"nextMeeting": "下次會議",
"now": "現在",
"noEvents": "沒有預定事件排入行程。",
"ongoingMeeting": "即將進行會議",
"permissionButton": "開啟設定",
"permissionMessage": "日曆允許權限是必須的,以列入您的會議於應用程式中。"
"permissionMessage": "日曆允許權限是必須的,以查看你的會議於應用程式中。",
"refresh": "重新整理行事曆",
"today": "今日"
},
"recentList": {
"today": "今日",
"yesterday": "昨天",
"earlier": "稍早"
"joinPastMeeting": "參加過往會議"
},
"sectionList": {
"pullToRefresh": "下滑以重新整理"
@@ -547,5 +676,60 @@
"appNotInstalled": "在您的手機上需要 __app__ 行動應用程式去加入這場會議。",
"downloadApp": "下載應用 APP",
"openApp": "繼續前往此應用程式"
},
"presenceStatus": {
"invited": "被邀請的",
"ringing": "鈴鈴鈴……",
"calling": "來電…",
"initializingCall": "播打電話…",
"connected": "已經連接",
"connecting": "連線中...",
"connecting2": "通話中*...",
"disconnected": "已經中斷連接",
"busy": "忙線",
"rejected": "拒接",
"ignored": "忽略",
"expired": "未接"
},
"dateUtils": {
"today": "今日",
"yesterday": "昨天",
"earlier": "稍早"
},
"incomingCall": {
"answer": "接通",
"audioCallTitle": "來電",
"decline": "解除",
"productLabel": "來自 Jitsi Meet",
"videoCallTitle": "視訊來電"
},
"localRecording": {
"localRecording": "本地端錄影中",
"dialogTitle": "本地端錄影控制",
"start": "啟動錄影作業",
"stop": "停止錄影作業",
"moderator": "主持人",
"me": "自己",
"duration": "期間",
"durationNA": "N/A",
"encoding": "解碼中",
"participantStats": "參與者狀態",
"participant": "參與者",
"sessionToken": "階段標記",
"clientState": {
"on": "開",
"off": "關",
"unknown": "不明"
},
"messages": {
"engaged": "本地端錄影已使用。",
"finished": "錄影階段 __token__ 已完成。請傳送錄影檔案至主持人。",
"finishedModerator": "錄影階段 __token__ 已完成。本地端錄影追蹤已存檔。請要求各參與者提交其錄影檔案。",
"notModerator": "你不是主持人,無法啟動或停止本地端錄影。"
},
"yes": "是的",
"no": "沒有",
"label": "LOR",
"labelToolTip": "本地端錄影使用中"
}
}

View File

@@ -469,7 +469,7 @@
"moderator": "Moderator rights granted!",
"muted": "You have started the conversation muted.",
"mutedTitle": "You're muted!",
"raisedHand": "Would like to speak.",
"raisedHand": "__name__ would like to speak.",
"somebody": "Somebody",
"suboptimalExperienceDescription": "Eer... we are afraid your experience with __appName__ isn't going to be that great here. We are looking for ways to improve this but, until then, please try using one of the <a href='static/recommendedBrowsers.html' target='_blank'>fully supported browsers</a>.",
"suboptimalExperienceTitle": "Browser Warning"
@@ -560,6 +560,7 @@
"alertOk": "OK",
"alertTitle": "Warning",
"alertURLText": "The entered server URL is invalid",
"buildInfoSection": "Build Information",
"conferenceSection": "Conference",
"displayName": "Display name",
"email": "Email",
@@ -567,7 +568,8 @@
"profileSection": "Profile",
"serverURL": "Server URL",
"startWithAudioMuted": "Start with audio muted",
"startWithVideoMuted": "Start with video muted"
"startWithVideoMuted": "Start with video muted",
"version": "Version"
},
"share": {
"dialInfoText": "\n\n=====\n\nJust want to dial in on your phone?\n\n__defaultDialInNumber__Click this link to see the dial in phone numbers for this meeting\n__dialInfoPageUrl__",

View File

@@ -11,6 +11,9 @@ import { invite } from '../../react/features/invite';
import { getJitsiMeetTransport } from '../transport';
import { API_ID } from './constants';
import {
processExternalDeviceRequest
} from '../../react/features/device-selection/functions';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -117,6 +120,12 @@ function initCommands() {
return false;
});
transport.on('request', (request, callback) => {
const { dispatch, getState } = APP.store;
if (processExternalDeviceRequest(dispatch, getState, request, callback)) {
return true;
}
const { name } = request;
switch (name) {
@@ -377,6 +386,19 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that the device list has
* changed.
*
* @param {Object} devices - The new device list.
* @returns {void}
*/
notifyDeviceListChanged(devices: Object) {
this._sendEvent({
name: 'device-list-changed',
devices });
}
/**
* Notify external application (if API is enabled) that user changed their
* nickname.

View File

@@ -7,6 +7,16 @@ import {
} from '../../transport';
import electronPopupsConfig from './electronPopupsConfig.json';
import {
getAvailableDevices,
getCurrentDevices,
isDeviceChangeAvailable,
isDeviceListAvailable,
isMultipleAudioInputSupported,
setAudioInputDevice,
setAudioOutputDevice,
setVideoInputDevice
} from './functions';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -40,6 +50,7 @@ const events = {
'avatar-changed': 'avatarChanged',
'audio-availability-changed': 'audioAvailabilityChanged',
'audio-mute-status-changed': 'audioMuteStatusChanged',
'device-list-changed': 'deviceListChanged',
'display-name-change': 'displayNameChange',
'email-change': 'emailChange',
'feedback-submitted': 'feedbackSubmitted',
@@ -211,6 +222,8 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* for iframe onload event.
* @param {Array<Object>} [options.invitees] - Array of objects containing
* information about new participants that will be invited in the call.
* @param {Array<Object>} [options.devices] - Array of objects containing
* information about the initial devices that will be used in the call.
*/
constructor(domain, ...args) {
super();
@@ -224,7 +237,8 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
noSSL = false,
jwt = undefined,
onload = undefined,
invitees
invitees,
devices
} = parseArguments(args);
this._parentNode = parentNode;
@@ -233,7 +247,8 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
interfaceConfigOverwrite,
jwt,
noSSL,
roomName
roomName,
devices
});
this._createIFrame(height, width, onload);
this._transport = new Transport({
@@ -593,6 +608,24 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
}
}
/**
* Returns Promise that resolves with a list of available devices.
*
* @returns {Promise}
*/
getAvailableDevices() {
return getAvailableDevices(this._transport);
}
/**
* Returns Promise that resolves with current selected devices.
*
* @returns {Promise}
*/
getCurrentDevices() {
return getCurrentDevices(this._transport);
}
/**
* Check if the audio is available.
*
@@ -605,6 +638,38 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
});
}
/**
* Returns Promise that resolves with true if the device change is available
* and with false if not.
*
* @param {string} [deviceType] - Values - 'output', 'input' or undefined.
* Default - 'input'.
* @returns {Promise}
*/
isDeviceChangeAvailable(deviceType) {
return isDeviceChangeAvailable(this._transport, deviceType);
}
/**
* Returns Promise that resolves with true if the device list is available
* and with false if not.
*
* @returns {Promise}
*/
isDeviceListAvailable() {
return isDeviceListAvailable(this._transport);
}
/**
* Returns Promise that resolves with true if multiple audio input is supported
* and with false if not.
*
* @returns {Promise}
*/
isMultipleAudioInputSupported() {
return isMultipleAudioInputSupported(this._transport);
}
/**
* Invite people to the call.
*
@@ -771,6 +836,42 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
});
}
/**
* Sets the audio input device to the one with the label or id that is
* passed.
*
* @param {string} label - The label of the new device.
* @param {string} deviceId - The id of the new device.
* @returns {Promise}
*/
setAudioInputDevice(label, deviceId) {
return setAudioInputDevice(this._transport, label, deviceId);
}
/**
* Sets the audio output device to the one with the label or id that is
* passed.
*
* @param {string} label - The label of the new device.
* @param {string} deviceId - The id of the new device.
* @returns {Promise}
*/
setAudioOutputDevice(label, deviceId) {
return setAudioOutputDevice(this._transport, label, deviceId);
}
/**
* Sets the video input device to the one with the label or id that is
* passed.
*
* @param {string} label - The label of the new device.
* @param {string} deviceId - The id of the new device.
* @returns {Promise}
*/
setVideoInputDevice(label, deviceId) {
return setVideoInputDevice(this._transport, label, deviceId);
}
/**
* Returns the configuration for electron for the windows that are open
* from Jitsi Meet.

156
modules/API/external/functions.js vendored Normal file
View File

@@ -0,0 +1,156 @@
// @flow
import Logger from 'jitsi-meet-logger';
const logger = Logger.getLogger(__filename);
/**
* Returns Promise that resolves with result an list of available devices.
*
* @param {Transport} transport - The @code{Transport} instance responsible for
* the external communication.
* @returns {Promise}
*/
export function getAvailableDevices(transport: Object) {
return transport.sendRequest({
type: 'devices',
name: 'getAvailableDevices'
}).catch(e => {
logger.error(e);
return {};
});
}
/**
* Returns Promise that resolves with current selected devices.
*
* @param {Transport} transport - The @code{Transport} instance responsible for
* the external communication.
* @returns {Promise}
*/
export function getCurrentDevices(transport: Object) {
return transport.sendRequest({
type: 'devices',
name: 'getCurrentDevices'
}).catch(e => {
logger.error(e);
return {};
});
}
/**
* Returns Promise that resolves with true if the device change is available
* and with false if not.
*
* @param {Transport} transport - The @code{Transport} instance responsible for
* the external communication.
* @param {string} [deviceType] - Values - 'output', 'input' or undefined.
* Default - 'input'.
* @returns {Promise}
*/
export function isDeviceChangeAvailable(transport: Object, deviceType: string) {
return transport.sendRequest({
deviceType,
type: 'devices',
name: 'isDeviceChangeAvailable'
});
}
/**
* Returns Promise that resolves with true if the device list is available
* and with false if not.
*
* @param {Transport} transport - The @code{Transport} instance responsible for
* the external communication.
* @returns {Promise}
*/
export function isDeviceListAvailable(transport: Object) {
return transport.sendRequest({
type: 'devices',
name: 'isDeviceListAvailable'
});
}
/**
* Returns Promise that resolves with true if multiple audio input is supported
* and with false if not.
*
* @param {Transport} transport - The @code{Transport} instance responsible for
* the external communication.
* @returns {Promise}
*/
export function isMultipleAudioInputSupported(transport: Object) {
return transport.sendRequest({
type: 'devices',
name: 'isMultipleAudioInputSupported'
});
}
/**
* Sets the audio input device to the one with the label or id that is passed.
*
* @param {Transport} transport - The @code{Transport} instance responsible for
* the external communication.
* @param {string} label - The label of the new device.
* @param {string} id - The id of the new device.
* @returns {Promise}
*/
export function setAudioInputDevice(transport: Object, label: string, id: string) {
return _setDevice(transport, {
id,
kind: 'audioinput',
label
});
}
/**
* Sets the audio output device to the one with the label or id that is passed.
*
* @param {Transport} transport - The @code{Transport} instance responsible for
* the external communication.
* @param {string} label - The label of the new device.
* @param {string} id - The id of the new device.
* @returns {Promise}
*/
export function setAudioOutputDevice(transport: Object, label: string, id: string) {
return _setDevice(transport, {
id,
kind: 'audiooutput',
label
});
}
/**
* Sets the currently used device to the one that is passed.
*
* @param {Transport} transport - The @code{Transport} instance responsible for
* the external communication.
* @param {Object} device - The new device to be used.
* @returns {Promise}
*/
function _setDevice(transport: Object, device) {
return transport.sendRequest({
type: 'devices',
name: 'setDevice',
device
});
}
/**
* Sets the video input device to the one with the label or id that is passed.
*
* @param {Transport} transport - The @code{Transport} instance responsible for
* the external communication.
* @param {string} label - The label of the new device.
* @param {string} id - The id of the new device.
* @returns {Promise}
*/
export function setVideoInputDevice(transport: Object, label: string, id: string) {
return _setDevice(transport, {
id,
kind: 'videoinput',
label
});
}

View File

@@ -175,37 +175,6 @@ UI.changeDisplayName = function(id, displayName) {
VideoLayout.onDisplayNameChanged(id, displayName);
};
/**
* Sets the "raised hand" status for a participant.
*
* @param {string} id - The id of the participant whose raised hand UI should
* be updated.
* @param {string} name - The name of the participant with the raised hand
* update.
* @param {boolean} raisedHandStatus - Whether the participant's hand is raised
* or not.
* @returns {void}
*/
UI.setRaisedHandStatus = (id, name, raisedHandStatus) => {
VideoLayout.setRaisedHandStatus(id, raisedHandStatus);
if (raisedHandStatus) {
messageHandler.participantNotification(
name,
'notify.somebody',
'connected',
'notify.raisedHand');
}
};
/**
* Sets the local "raised hand" status.
*/
UI.setLocalRaisedHandStatus
= raisedHandStatus =>
VideoLayout.setRaisedHandStatus(
APP.conference.getMyUserId(),
raisedHandStatus);
/**
* Initialize conference UI.
*/

View File

@@ -4,6 +4,7 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
import jitsiLocalStorage from '../../util/JitsiLocalStorage';
import {
NOTIFICATION_TIMEOUT,
showErrorNotification,
showNotification,
showWarningNotification
@@ -454,7 +455,7 @@ const messageHandler = {
cls,
messageKey,
messageArguments,
timeout = 2500) {
timeout = NOTIFICATION_TIMEOUT) {
APP.store.dispatch(showNotification({
descriptionArguments: messageArguments,
descriptionKey: messageKey,

View File

@@ -824,6 +824,7 @@ SmallVideo.prototype.updateIndicators = function() {
}
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<div>
<AtlasKitThemeProvider mode = 'dark'>
@@ -839,11 +840,10 @@ SmallVideo.prototype.updateIndicators = function() {
= { statsPopoverPosition }
userID = { this.id } />
: null }
{ this._showRaisedHand
? <RaisedHandIndicator
iconSize = { iconSize }
tooltipPosition = { tooltipPosition } />
: null }
<RaisedHandIndicator
iconSize = { iconSize }
participantId = { this.id }
tooltipPosition = { tooltipPosition } />
{ this._showDominantSpeaker
? <DominantSpeakerIndicator
iconSize = { iconSize }
@@ -851,7 +851,8 @@ SmallVideo.prototype.updateIndicators = function() {
: null }
</AtlasKitThemeProvider>
</div>
</I18nextProvider>,
</I18nextProvider>
</Provider>,
indicatorToolbar
);
};

View File

@@ -672,22 +672,6 @@ const VideoLayout = {
}
},
/**
* Sets the "raised hand" status for a participant identified by 'id'.
*/
setRaisedHandStatus(id, raisedHandStatus) {
const video
= APP.conference.isLocalId(id)
? localVideoThumbnail : remoteVideos[id];
if (video) {
video.showRaisedHandIndicator(raisedHandStatus);
if (raisedHandStatus) {
video.showDominantSpeakerIndicator(false);
}
}
},
/**
* On dominant speaker changed event.
*

5
package-lock.json generated
View File

@@ -11955,8 +11955,9 @@
}
},
"react-native-sound": {
"version": "github:jitsi/react-native-sound#e4260ed7f641eeb0377d76eac7987aba72e1cf08",
"from": "github:jitsi/react-native-sound#e4260ed7f641eeb0377d76eac7987aba72e1cf08"
"version": "0.10.12",
"resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.10.12.tgz",
"integrity": "sha512-lhvzZ+ekNSHW9s4FjmxxGAQmd7yDT9FnHgJNP824cMyHRrTES/LgHnSOxES/1FWVoaQ8FdfbEHkWd2rN0G/L9A=="
},
"react-native-swipeout": {
"version": "2.3.6",

View File

@@ -72,7 +72,7 @@
"react-native-immersive": "2.0.0",
"react-native-keep-awake": "4.0.0",
"react-native-linear-gradient": "2.5.3",
"react-native-sound": "github:jitsi/react-native-sound#e4260ed7f641eeb0377d76eac7987aba72e1cf08",
"react-native-sound": "0.10.12",
"react-native-swipeout": "2.3.6",
"react-native-vector-icons": "6.0.2",
"react-native-webrtc": "github:jitsi/react-native-webrtc#032ee5c90e2c5ff27ab2f952217104772fcbd155",

View File

@@ -7,6 +7,8 @@ declare var config: Object;
const logger = require('jitsi-meet-logger').getLogger(__filename);
import { configureInitialDevices } from '../devices';
export {
connectionEstablished,
connectionFailed,
@@ -25,12 +27,13 @@ export function connect() {
// XXX For web based version we use conference initialization logic
// from the old app (at the moment of writing).
return APP.conference.init({
roomName: room
}).catch(error => {
APP.API.notifyConferenceLeft(APP.conference.roomName);
logger.error(error);
});
return dispatch(configureInitialDevices()).then(
() => APP.conference.init({
roomName: room
}).catch(error => {
APP.API.notifyConferenceLeft(APP.conference.roomName);
logger.error(error);
}));
};
}

View File

@@ -30,3 +30,23 @@ export const SET_VIDEO_INPUT_DEVICE = 'SET_VIDEO_INPUT_DEVICE';
* }
*/
export const UPDATE_DEVICE_LIST = 'UPDATE_DEVICE_LIST';
/**
* The type of Redux action which will add a pending device requests that will
* be executed later when it is possible (when the conference is joined).
*
* {
* type: ADD_PENDING_DEVICE_REQUEST,
* request: Object
* }
*/
export const ADD_PENDING_DEVICE_REQUEST = 'ADD_PENDING_DEVICE_REQUEST';
/**
* The type of Redux action which will remove all pending device requests.
*
* {
* type: REMOVE_PENDING_DEVICE_REQUESTS
* }
*/
export const REMOVE_PENDING_DEVICE_REQUESTS = 'REMOVE_PENDING_DEVICE_REQUESTS';

View File

@@ -1,10 +1,93 @@
import JitsiMeetJS from '../lib-jitsi-meet';
import { updateSettings } from '../settings';
import {
ADD_PENDING_DEVICE_REQUEST,
REMOVE_PENDING_DEVICE_REQUESTS,
SET_AUDIO_INPUT_DEVICE,
SET_VIDEO_INPUT_DEVICE,
UPDATE_DEVICE_LIST
} from './actionTypes';
import {
areDeviceLabelsInitialized,
getDeviceIdByLabel,
getDevicesFromURL
} from './functions';
/**
* Adds a pending device request.
*
* @param {Object} request - The request to be added.
* @returns {{
* type: ADD_PENDING_DEVICE_REQUEST,
* request: Object
* }}
*/
export function addPendingDeviceRequest(request) {
return {
type: ADD_PENDING_DEVICE_REQUEST,
request
};
}
/**
* Configures the initial A/V devices before the conference has started.
*
* @returns {Function}
*/
export function configureInitialDevices() {
return (dispatch, getState) => new Promise(resolve => {
const deviceLabels = getDevicesFromURL(getState());
if (deviceLabels) {
dispatch(getAvailableDevices()).then(() => {
const state = getState();
if (!areDeviceLabelsInitialized(state)) {
// The labels are not available if the A/V permissions are
// not yet granted.
Object.keys(deviceLabels).forEach(key => {
dispatch(addPendingDeviceRequest({
type: 'devices',
name: 'setDevice',
device: {
kind: key.toLowerCase(),
label: deviceLabels[key]
},
// eslint-disable-next-line no-empty-function
responseCallback() {}
}));
});
resolve();
return;
}
const newSettings = {};
const devicesKeysToSettingsKeys = {
audioInput: 'micDeviceId',
audioOutput: 'audioOutputDeviceId',
videoInput: 'cameraDeviceId'
};
Object.keys(deviceLabels).forEach(key => {
const label = deviceLabels[key];
const deviceId = getDeviceIdByLabel(state, label);
if (deviceId) {
newSettings[devicesKeysToSettingsKeys[key]] = deviceId;
}
});
dispatch(updateSettings(newSettings));
resolve();
});
} else {
resolve();
}
});
}
/**
* Queries for connected A/V input and output devices and updates the redux
@@ -29,6 +112,20 @@ export function getAvailableDevices() {
});
}
/**
* Remove all pending device requests.
*
* @returns {{
* type: REMOVE_PENDING_DEVICE_REQUESTS
* }}
*/
export function removePendingDeviceRequests() {
return {
type: REMOVE_PENDING_DEVICE_REQUESTS
};
}
/**
* Signals to update the currently used audio input device.
*
@@ -77,3 +174,4 @@ export function updateDeviceList(devices) {
devices
};
}

View File

@@ -1,8 +1,34 @@
// @flow
import { parseURLParams } from '../config';
import JitsiMeetJS from '../lib-jitsi-meet';
import { updateSettings } from '../settings';
declare var APP: Object;
/**
* Detects the use case when the labels are not available if the A/V permissions
* are not yet granted.
*
* @param {Object} state - The redux state.
* @returns {boolean} - True if the labels are already initialized and false
* otherwise.
*/
export function areDeviceLabelsInitialized(state: Object) {
// TODO: Replace with something that doesn't use APP when the conference.js logic is reactified.
if (APP.conference._localTracksInitialized) {
return true;
}
for (const type of [ 'audioInput', 'audioOutput', 'videoInput' ]) {
if ((state['features/base/devices'].availableDevices[type] || []).find(d => Boolean(d.label))) {
return true;
}
}
return false;
}
/**
* Get device id of the audio output device which is currently in use.
* Empty string stands for default device.
@@ -13,6 +39,71 @@ export function getAudioOutputDeviceId() {
return JitsiMeetJS.mediaDevices.getAudioOutputDevice();
}
/**
* Finds a device with a label that matches the passed label and returns its id.
*
* @param {Object} state - The redux state.
* @param {string} label - The label.
* @returns {string|undefined}
*/
export function getDeviceIdByLabel(state: Object, label: string) {
const types = [ 'audioInput', 'audioOutput', 'videoInput' ];
for (const type of types) {
const device
= (state['features/base/devices'].availableDevices[type] || [])
.find(d => d.label === label);
if (device) {
return device.deviceId;
}
}
}
/**
* Returns the devices set in the URL.
*
* @param {Object} state - The redux state.
* @returns {Object|undefined}
*/
export function getDevicesFromURL(state: Object) {
const urlParams
= parseURLParams(state['features/base/connection'].locationURL);
const audioOutput = urlParams['devices.audioOutput'];
const videoInput = urlParams['devices.videoInput'];
const audioInput = urlParams['devices.audioInput'];
if (!audioOutput && !videoInput && !audioInput) {
return undefined;
}
const devices = {};
audioOutput && (devices.audioOutput = audioOutput);
videoInput && (devices.videoInput = videoInput);
audioInput && (devices.audioInput = audioInput);
return devices;
}
/**
* Converts an array of media devices into an object organized by device kind.
*
* @param {Array<MediaDeviceInfo>} devices - Available media devices.
* @private
* @returns {Object} An object with the media devices split by type. The keys
* are device type and the values are arrays with devices matching the device
* type.
*/
export function groupDevicesByKind(devices: Object[]): Object {
return {
audioInput: devices.filter(device => device.kind === 'audioinput'),
audioOutput: devices.filter(device => device.kind === 'audiooutput'),
videoInput: devices.filter(device => device.kind === 'videoinput')
};
}
/**
* Set device id of the audio output device which is currently in use.
* Empty string stands for default device.

View File

@@ -1,9 +1,11 @@
/* global APP */
import { CONFERENCE_JOINED } from '../conference';
import { processExternalDeviceRequest } from '../../device-selection';
import { MiddlewareRegistry } from '../redux';
import UIEvents from '../../../../service/UI/UIEvents';
import { MiddlewareRegistry } from '../redux';
import { removePendingDeviceRequests } from './actions';
import {
SET_AUDIO_INPUT_DEVICE,
SET_VIDEO_INPUT_DEVICE
@@ -18,6 +20,8 @@ import {
// eslint-disable-next-line no-unused-vars
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CONFERENCE_JOINED:
return _conferenceJoined(store, next, action);
case SET_AUDIO_INPUT_DEVICE:
APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
break;
@@ -28,3 +32,34 @@ MiddlewareRegistry.register(store => next => action => {
return next(action);
});
/**
* Does extra sync up on properties that may need to be updated after the
* conference was joined.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
* specified {@code action} to the specified {@code store}.
* @param {Action} action - The redux action {@code CONFERENCE_JOINED} which is
* being dispatched in the specified {@code store}.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _conferenceJoined({ dispatch, getState }, next, action) {
const result = next(action);
const state = getState();
const { pendingRequests } = state['features/base/devices'];
pendingRequests.forEach(request => {
processExternalDeviceRequest(
dispatch,
getState,
request,
request.responseCallback);
});
dispatch(removePendingDeviceRequests());
return result;
}

View File

@@ -1,15 +1,21 @@
import {
ADD_PENDING_DEVICE_REQUEST,
REMOVE_PENDING_DEVICE_REQUESTS,
SET_AUDIO_INPUT_DEVICE,
SET_VIDEO_INPUT_DEVICE,
UPDATE_DEVICE_LIST
} from './actionTypes';
import { groupDevicesByKind } from './functions';
import { ReducerRegistry } from '../redux';
const DEFAULT_STATE = {
audioInput: [],
audioOutput: [],
videoInput: []
availableDevices: {
audioInput: [],
audioOutput: [],
videoInput: []
},
pendingRequests: []
};
/**
@@ -27,13 +33,29 @@ ReducerRegistry.register(
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case UPDATE_DEVICE_LIST: {
const deviceList = _groupDevicesByKind(action.devices);
const deviceList = groupDevicesByKind(action.devices);
return {
...deviceList
...state,
availableDevices: deviceList
};
}
case ADD_PENDING_DEVICE_REQUEST:
return {
...state,
pendingRequests: [
...state.pendingRequests,
action.request
]
};
case REMOVE_PENDING_DEVICE_REQUESTS:
return {
...state,
pendingRequests: [ ]
};
// TODO: Changing of current audio and video device id is currently
// handled outside of react/redux. Fall through to default logic for
// now.
@@ -44,19 +66,3 @@ ReducerRegistry.register(
}
});
/**
* Converts an array of media devices into an object organized by device kind.
*
* @param {Array<MediaDeviceInfo>} devices - Available media devices.
* @private
* @returns {Object} An object with the media devices split by type. The keys
* are device type and the values are arrays with devices matching the device
* type.
*/
function _groupDevicesByKind(devices) {
return {
audioInput: devices.filter(device => device.kind === 'audioinput'),
audioOutput: devices.filter(device => device.kind === 'audiooutput'),
videoInput: devices.filter(device => device.kind === 'videoinput')
};
}

View File

@@ -3,7 +3,7 @@
import throttle from 'lodash/throttle';
import { set } from '../redux';
import { showNotification } from '../../notifications';
import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
import {
DOMINANT_SPEAKER_CHANGED,
@@ -452,7 +452,7 @@ const _throttledNotifyParticipantConnected = throttle(dispatch => {
if (notificationProps) {
dispatch(
showNotification(notificationProps, 2500));
showNotification(notificationProps, NOTIFICATION_TIMEOUT));
}
joinedParticipantsNames = [];

View File

@@ -1,14 +1,18 @@
// @flow
import UIEvents from '../../../../service/UI/UIEvents';
import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
import { CALLING, INVITED } from '../../presence-status';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
import {
CONFERENCE_WILL_JOIN,
forEachConference,
getCurrentConference
} from '../conference';
import { CALLING, INVITED } from '../../presence-status';
import { JitsiConferenceEvents } from '../lib-jitsi-meet';
import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
import UIEvents from '../../../../service/UI/UIEvents';
import { playSound, registerSound, unregisterSound } from '../sounds';
import {
@@ -36,8 +40,8 @@ import {
import {
getAvatarURLByParticipantId,
getLocalParticipant,
getParticipantById,
getParticipantCount
getParticipantCount,
getParticipantDisplayName
} from './functions';
import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
@@ -185,6 +189,44 @@ StateListenerRegistry.register(
localParticipantIdChanged(LOCAL_PARTICIPANT_DEFAULT_ID));
});
/**
* Registers listeners for participant change events.
*/
StateListenerRegistry.register(
state => state['features/base/conference'].conference,
(conference, store) => {
if (conference) {
// We joined a conference
conference.on(
JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
(participant, propertyName, oldValue, newValue) => {
switch (propertyName) {
case 'features_screen-sharing':
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
features: { 'screen-sharing': true }
}));
break;
case 'raisedHand': {
_raiseHandUpdated(
store, conference, participant.getId(), newValue);
break;
}
default:
// Ignore for now.
}
});
} else {
// We left the conference, raise hand of the local participant must be updated.
_raiseHandUpdated(
store, conference, undefined, false);
}
}
);
/**
* Initializes the local participant and signals that it joined.
*
@@ -293,21 +335,6 @@ function _participantJoinedOrUpdated({ getState }, next, action) {
'raisedHand',
raisedHand);
}
if (typeof APP === 'object') {
if (local) {
APP.UI.onLocalRaiseHandChanged(raisedHand);
APP.UI.setLocalRaisedHandStatus(raisedHand);
} else {
const remoteParticipant = getParticipantById(getState(), id);
remoteParticipant
&& APP.UI.setRaisedHandStatus(
remoteParticipant.id,
remoteParticipant.name,
raisedHand);
}
}
}
// Notify external listeners of potential avatarURL changes.
@@ -332,6 +359,36 @@ function _participantJoinedOrUpdated({ getState }, next, action) {
return next(action);
}
/**
* Handles a raise hand status update.
*
* @param {Function} dispatch - The Redux dispatch function.
* @param {Object} conference - The conference for which we got an update.
* @param {string?} participantId - The ID of the participant from which we got an update. If undefined,
* we update the local participant.
* @param {boolean} newValue - The new value of the raise hand status.
* @returns {void}
*/
function _raiseHandUpdated({ dispatch, getState }, conference, participantId, newValue) {
const raisedHand = newValue === 'true';
const pid = participantId || getLocalParticipant(getState()).id;
dispatch(participantUpdated({
conference,
id: pid,
raisedHand
}));
if (raisedHand) {
dispatch(showNotification({
titleArguments: {
name: getParticipantDisplayName(getState, pid)
},
titleKey: 'notify.raisedHand'
}, NOTIFICATION_TIMEOUT));
}
}
/**
* Registers sounds related with the participants feature.
*

View File

@@ -469,16 +469,16 @@ export function urlObjectToString(o: Object): ?string {
let { hash } = url;
for (const configName of [ 'config', 'interfaceConfig' ]) {
for (const urlPrefix of [ 'config', 'interfaceConfig', 'devices' ]) {
const urlParamsArray
= _objectToURLParamsArray(
o[`${configName}Overwrite`]
|| o[configName]
|| o[`${configName}Override`]);
o[`${urlPrefix}Overwrite`]
|| o[urlPrefix]
|| o[`${urlPrefix}Override`]);
if (urlParamsArray.length) {
let urlParamsString
= `${configName}.${urlParamsArray.join(`&${configName}.`)}`;
= `${urlPrefix}.${urlParamsArray.join(`&${urlPrefix}.`)}`;
if (hash.length) {
urlParamsString = `&${urlParamsString}`;

View File

@@ -6,17 +6,15 @@ import {
import { createDeviceChangedEvent, sendAnalytics } from '../analytics';
import {
getAudioOutputDeviceId,
setAudioInputDevice,
setAudioOutputDeviceId,
setVideoInputDevice
} from '../base/devices';
import { i18next } from '../base/i18n';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { updateSettings } from '../base/settings';
import { SET_DEVICE_SELECTION_POPUP_DATA } from './actionTypes';
import { getDeviceSelectionDialogProps } from './functions';
import { getDeviceSelectionDialogProps, processExternalDeviceRequest } from './functions';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -60,7 +58,7 @@ export function openDeviceSelectionPopup() {
});
transport.on('request',
_processRequest.bind(undefined, dispatch, getState));
processExternalDeviceRequest.bind(undefined, dispatch, getState));
transport.on('event', event => {
if (event.type === 'devices-dialog' && event.name === 'close') {
popup.close();
@@ -80,75 +78,6 @@ export function openDeviceSelectionPopup() {
};
}
/**
* Processes device requests from external applications.
*
* @param {Dispatch} dispatch - The redux {@code dispatch} function.
* @param {Function} getState - The redux function that gets/retrieves the redux
* state.
* @param {Object} request - The request to be processed.
* @param {Function} responseCallback - The callback that will send the
* response.
* @returns {boolean}
*/
function _processRequest(dispatch, getState, request, responseCallback) { // eslint-disable-line max-len, max-params
if (request.type === 'devices') {
const state = getState();
const settings = state['features/base/settings'];
switch (request.name) {
case 'isDeviceListAvailable':
responseCallback(JitsiMeetJS.mediaDevices.isDeviceListAvailable());
break;
case 'isDeviceChangeAvailable':
responseCallback(
JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(
request.deviceType));
break;
case 'isMultipleAudioInputSupported':
responseCallback(JitsiMeetJS.isMultipleAudioInputSupported());
break;
case 'getCurrentDevices':
responseCallback({
audioInput: settings.micDeviceId,
audioOutput: getAudioOutputDeviceId(),
videoInput: settings.cameraDeviceId
});
break;
case 'getAvailableDevices':
responseCallback(getState()['features/base/devices']);
break;
case 'setDevice': {
const { device } = request;
switch (device.kind) {
case 'audioinput':
dispatch(setAudioInputDevice(device.id));
break;
case 'audiooutput':
setAudioOutputDeviceId(device.id, dispatch);
break;
case 'videoinput':
dispatch(setVideoInputDevice(device.id));
break;
default:
}
responseCallback(true);
break;
}
default:
return false;
}
return true;
}
return false;
}
/**
* Sets information about device selection popup in the store.
*

View File

@@ -1,5 +1,18 @@
// @flow
import { getAudioOutputDeviceId } from '../base/devices';
import type { Dispatch } from 'redux';
import {
addPendingDeviceRequest,
areDeviceLabelsInitialized,
getAudioOutputDeviceId,
getAvailableDevices,
getDeviceIdByLabel,
groupDevicesByKind,
setAudioInputDevice,
setAudioOutputDeviceId,
setVideoInputDevice
} from '../base/devices';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { toState } from '../base/redux';
@@ -15,7 +28,7 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) {
const settings = state['features/base/settings'];
return {
availableDevices: state['features/base/devices'],
availableDevices: state['features/base/devices'].availableDevices,
disableAudioInputChange:
!JitsiMeetJS.isMultipleAudioInputSupported(),
disableDeviceChange:
@@ -29,3 +42,142 @@ export function getDeviceSelectionDialogProps(stateful: Object | Function) {
selectedVideoInputId: settings.cameraDeviceId
};
}
/**
* Processes device requests from external applications.
*
* @param {Dispatch} dispatch - The redux {@code dispatch} function.
* @param {Function} getState - The redux function that gets/retrieves the redux
* state.
* @param {Object} request - The request to be processed.
* @param {Function} responseCallback - The callback that will send the
* response.
* @returns {boolean} - True if the request has been processed and false otherwise.
*/
export function processExternalDeviceRequest( // eslint-disable-line max-params
dispatch: Dispatch<any>,
getState: Function,
request: Object,
responseCallback: Function) {
if (request.type !== 'devices') {
return false;
}
const state = getState();
const settings = state['features/base/settings'];
const { conference } = state['features/base/conference'];
let result = true;
switch (request.name) {
case 'isDeviceListAvailable':
responseCallback(JitsiMeetJS.mediaDevices.isDeviceListAvailable());
break;
case 'isDeviceChangeAvailable':
responseCallback(
JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(
request.deviceType));
break;
case 'isMultipleAudioInputSupported':
responseCallback(JitsiMeetJS.isMultipleAudioInputSupported());
break;
case 'getCurrentDevices':
dispatch(getAvailableDevices()).then(devices => {
if (areDeviceLabelsInitialized(state)) {
let audioInput, audioOutput, videoInput;
const audioOutputDeviceId = getAudioOutputDeviceId();
const { cameraDeviceId, micDeviceId } = settings;
devices.forEach(device => {
const { deviceId } = device;
switch (deviceId) {
case micDeviceId:
audioInput = device;
break;
case audioOutputDeviceId:
audioOutput = device;
break;
case cameraDeviceId:
videoInput = device;
break;
}
});
responseCallback({
audioInput,
audioOutput,
videoInput
});
} else {
// The labels are not available if the A/V permissions are
// not yet granted.
dispatch(addPendingDeviceRequest({
type: 'devices',
name: 'getCurrentDevices',
responseCallback
}));
}
});
break;
case 'getAvailableDevices':
dispatch(getAvailableDevices()).then(devices => {
if (areDeviceLabelsInitialized(state)) {
responseCallback(groupDevicesByKind(devices));
} else {
// The labels are not available if the A/V permissions are
// not yet granted.
dispatch(addPendingDeviceRequest({
type: 'devices',
name: 'getAvailableDevices',
responseCallback
}));
}
});
break;
case 'setDevice': {
const { device } = request;
if (!conference) {
dispatch(addPendingDeviceRequest({
type: 'devices',
name: 'setDevice',
device,
responseCallback
}));
return true;
}
const { label, id } = device;
const deviceId = label ? getDeviceIdByLabel(state, device.label) : id;
if (deviceId) {
switch (device.kind) {
case 'audioinput': {
dispatch(setAudioInputDevice(deviceId));
break;
}
case 'audiooutput':
setAudioOutputDeviceId(deviceId, dispatch);
break;
case 'videoinput':
dispatch(setVideoInputDevice(deviceId));
break;
default:
result = false;
}
} else {
result = false;
}
responseCallback(result);
break;
}
default:
return false;
}
return true;
}

View File

@@ -1,6 +1,10 @@
// @flow
import { UPDATE_DEVICE_LIST } from '../base/devices';
import { MiddlewareRegistry } from '../base/redux';
declare var APP: Object;
/**
* Implements the middleware of the feature device-selection.
*
@@ -12,11 +16,19 @@ MiddlewareRegistry.register(store => next => action => {
const result = next(action);
if (action.type === UPDATE_DEVICE_LIST) {
const { popupDialogData }
= store.getState()['features/device-selection'];
const state = store.getState();
const { popupDialogData } = state['features/device-selection'];
const { availableDevices } = state['features/base/devices'];
if (popupDialogData) {
popupDialogData.transport.sendEvent({ name: 'deviceListChanged' });
popupDialogData.transport.sendEvent({
name: 'deviceListChanged',
devices: availableDevices
});
}
if (typeof APP !== 'undefined') {
APP.API.notifyDeviceListChanged(availableDevices);
}
}

View File

@@ -0,0 +1,62 @@
// @flow
import { Component } from 'react';
import { getParticipantById } from '../../base/participants';
export type Props = {
/**
* The participant id who we want to render the raised hand indicator
* for.
*/
participantId: string,
/**
* True if the hand is raised for this participant.
*/
_raisedHand?: boolean
}
/**
* Implements an abstract class for the RaisedHandIndicator component.
*/
export default class AbstractRaisedHandIndicator<P: Props>
extends Component<P> {
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
if (!this.props._raisedHand) {
return null;
}
return this._renderIndicator();
}
/**
* Renders the platform specific indicator element.
*
* @returns {React$Element<*>}
*/
_renderIndicator: () => React$Element<*>
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {Object}
*/
export function _mapStateToProps(state: Object, ownProps: Props): Object {
const participant = getParticipantById(state, ownProps.participantId);
return {
_raisedHand: participant && participant.raisedHand
};
}

View File

@@ -1,13 +1,13 @@
// @flow
import React, { Component } from 'react';
import { Icon } from '../../../base/font-icons';
import styles from './styles';
import BaseIndicator from './BaseIndicator';
/**
* Thumbnail badge for displaying the audio mute status of a participant.
*/
export default class AudioMutedIndicator extends Component {
export default class AudioMutedIndicator extends Component<{}> {
/**
* Implements React's {@link Component#render()}.
*
@@ -15,9 +15,9 @@ export default class AudioMutedIndicator extends Component {
*/
render() {
return (
<Icon
name = 'mic-disabled'
style = { styles.thumbnailIndicator } />
<BaseIndicator
highlight = { false }
icon = 'mic-disabled' />
);
}
}

View File

@@ -0,0 +1,44 @@
// @flow
import React, { Component } from 'react';
import { View } from 'react-native';
import { Icon } from '../../../base/font-icons';
import styles from './styles';
type Props = {
/**
* True if a highlighted background has to be applied.
*/
highlight: boolean,
/**
* The name of the icon to be used as the indicator.
*/
icon: string
};
/**
* Implements a base indicator to be reused across all native indicators on
* the filmstrip.
*/
export default class BaseIndicator extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
const { highlight, icon } = this.props;
return (
<View style = { highlight ? styles.highlightedIndicator : null }>
<Icon
name = { icon }
style = { styles.indicator } />
</View>
);
}
}

View File

@@ -1,27 +1,24 @@
// @flow
import React, { Component } from 'react';
import { View } from 'react-native';
import { Icon } from '../../../base/font-icons';
import styles from './styles';
import BaseIndicator from './BaseIndicator';
/**
* Thumbnail badge showing that the participant is the dominant speaker in
* the conference.
*/
export default class DominantSpeakerIndicator extends Component {
export default class DominantSpeakerIndicator extends Component<{}> {
/**
* Implements React's {@link Component#render()}.
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
return (
<View style = { styles.dominantSpeakerIndicatorBackground }>
<Icon
name = 'dominant-speaker'
style = { styles.dominantSpeakerIndicator } />
</View>
<BaseIndicator
highlight = { true }
icon = 'dominant-speaker' />
);
}
}

View File

@@ -6,7 +6,7 @@ import { View } from 'react-native';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import styles from '../styles';
import styles from './styles';
import Thumbnail from './Thumbnail';
type Props = {

View File

@@ -1,13 +1,13 @@
// @flow
import React, { Component } from 'react';
import { Icon } from '../../../base/font-icons';
import styles from './styles';
import BaseIndicator from './BaseIndicator';
/**
* Thumbnail badge showing that the participant is a conference moderator.
*/
export default class ModeratorIndicator extends Component {
export default class ModeratorIndicator extends Component<{}> {
/**
* Implements React's {@link Component#render()}.
*
@@ -15,9 +15,9 @@ export default class ModeratorIndicator extends Component {
*/
render() {
return (
<Icon
name = 'star'
style = { styles.moderatorIndicator } />
<BaseIndicator
highlight = { false }
icon = 'star' />
);
}
}

View File

@@ -0,0 +1,34 @@
// @flow
import React from 'react';
import { connect } from '../../../base/redux';
import AbstractRaisedHandIndicator, {
type Props,
_mapStateToProps
} from '../AbstractRaisedHandIndicator';
import BaseIndicator from './BaseIndicator';
/**
* Thumbnail badge showing that the participant would like to speak.
*
* @extends Component
*/
class RaisedHandIndicator extends AbstractRaisedHandIndicator<Props> {
/**
* Renders the platform specific indicator element.
*
* @returns {React$Element<*>}
*/
_renderIndicator() {
return (
<BaseIndicator
highlight = { true }
icon = 'raised-hand' />
);
}
}
export default connect(_mapStateToProps)(RaisedHandIndicator);

View File

@@ -1,6 +1,7 @@
// @flow
import React, { Component } from 'react';
import { View } from 'react-native';
import type { Dispatch } from 'redux';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
@@ -22,8 +23,8 @@ import { RemoteVideoMenu } from '../../../remote-video-menu';
import AudioMutedIndicator from './AudioMutedIndicator';
import DominantSpeakerIndicator from './DominantSpeakerIndicator';
import ModeratorIndicator from './ModeratorIndicator';
import { AVATAR_SIZE } from '../styles';
import styles from './styles';
import RaisedHandIndicator from './RaisedHandIndicator';
import styles, { AVATAR_SIZE } from './styles';
import VideoMutedIndicator from './VideoMutedIndicator';
/**
@@ -163,10 +164,15 @@ class Thumbnail extends Component<Props> {
zOrder = { 1 } />
{ participant.role === PARTICIPANT_ROLE.MODERATOR
&& <ModeratorIndicator /> }
&& <View style = { styles.moderatorIndicatorContainer }>
<ModeratorIndicator />
</View> }
{ participant.dominantSpeaker
&& <DominantSpeakerIndicator /> }
<View style = { styles.thumbnailTopIndicatorContainer }>
<RaisedHandIndicator participantId = { participant.id } />
{ participant.dominantSpeaker
&& <DominantSpeakerIndicator /> }
</View>
<Container style = { styles.thumbnailIndicatorContainer }>
{ audioMuted

View File

@@ -1,13 +1,13 @@
// @flow
import React, { Component } from 'react';
import { Icon } from '../../../base/font-icons';
import styles from './styles';
import BaseIndicator from './BaseIndicator';
/**
* Thumbnail badge for displaying the video mute status of a participant.
*/
export default class VideoMutedIndicator extends Component {
export default class VideoMutedIndicator extends Component<{}> {
/**
* Implements React's {@link Component#render()}.
*
@@ -15,9 +15,9 @@ export default class VideoMutedIndicator extends Component {
*/
render() {
return (
<Icon
name = 'camera-disabled'
style = { styles.thumbnailIndicator } />
<BaseIndicator
highlight = { false }
icon = 'camera-disabled' />
);
}
}

View File

@@ -1,50 +1,175 @@
import { ColorPalette, createStyleSheet } from '../../../base/styles';
// @flow
import { default as platformIndependentStyles } from '../styles';
import { ColorSchemeRegistry, schemeColor } from '../../../base/color-scheme';
import { ColorPalette } from '../../../base/styles';
import { FILMSTRIP_SIZE } from '../../constants';
/**
* The base/default style of indicators such as audioMutedIndicator,
* moderatorIndicator, and videoMutedIndicator.
* Size for the Avatar.
*/
const indicator = {
textShadowColor: ColorPalette.black,
textShadowOffset: {
height: -1,
width: 0
}
};
export const AVATAR_SIZE = 50;
/**
* The styles of the feature filmstrip.
*/
export default createStyleSheet(platformIndependentStyles, {
dominantSpeakerIndicator: {
fontSize: 12
},
export default {
/**
* Dominant speaker indicator background style.
* Highlighted indicator additional style.
*/
dominantSpeakerIndicatorBackground: {
highlightedIndicator: {
backgroundColor: ColorPalette.blue,
borderRadius: 16,
padding: 4
},
/**
* Moderator indicator style.
* Dominant speaker indicator style.
*/
moderatorIndicator: indicator,
indicator: {
backgroundColor: ColorPalette.transparent,
color: ColorPalette.white,
fontSize: 12,
textShadowColor: ColorPalette.black,
textShadowOffset: {
height: -1,
width: 0
}
},
/**
* Video thumbnail style.
* The style of the narrow {@link Filmstrip} version which displays
* thumbnails in a row at the bottom of the screen.
*/
filmstripNarrow: {
flexDirection: 'row',
flexGrow: 0,
justifyContent: 'flex-end',
height: FILMSTRIP_SIZE
},
/**
* The style of the wide {@link Filmstrip} version which displays thumbnails
* in a column on the short size of the screen.
*
* NOTE: width is calculated based on the children, but it should also align
* to {@code FILMSTRIP_SIZE}.
*/
filmstripWide: {
bottom: 0,
flexDirection: 'column',
flexGrow: 0,
position: 'absolute',
right: 0,
top: 0
},
/**
* Container of the {@link LocalThumbnail}.
*/
localThumbnail: {
alignContent: 'stretch',
alignSelf: 'stretch',
aspectRatio: 1,
flexShrink: 0,
flexDirection: 'row'
},
moderatorIndicatorContainer: {
bottom: 4,
position: 'absolute',
right: 4
},
/**
* The style of the scrollview containing the remote thumbnails.
*/
scrollView: {
flexGrow: 0
},
/**
* The style of a participant's Thumbnail which renders either the video or
* the avatar of the associated participant.
*/
thumbnail: {
alignItems: 'stretch',
backgroundColor: ColorPalette.appBackground,
borderColor: '#424242',
borderRadius: 3,
borderStyle: 'solid',
borderWidth: 1,
flex: 1,
height: 80,
justifyContent: 'center',
margin: 2,
overflow: 'hidden',
position: 'relative',
width: 80
},
/**
* Audio muted indicator style.
* The thumbnails indicator container.
*/
thumbnailIndicator: indicator
thumbnailIndicatorContainer: {
alignSelf: 'stretch',
bottom: 4,
flex: 1,
flexDirection: 'row',
left: 4,
position: 'absolute'
},
thumbnailTopIndicatorContainer: {
left: 0,
padding: 4,
position: 'absolute',
top: 0
},
tileView: {
alignSelf: 'center'
},
tileViewRows: {
justifyContent: 'center'
},
tileViewRow: {
flexDirection: 'row',
justifyContent: 'center'
}
};
/**
* Color schemed styles for the @{code Thumbnail} component.
*/
ColorSchemeRegistry.register('Thumbnail', {
/**
* Tinting style of the on-stage participant thumbnail.
*/
activeThumbnailTint: {
backgroundColor: schemeColor('activeParticipantTint')
},
/**
* Coloring if the thumbnail background.
*/
participantViewStyle: {
backgroundColor: schemeColor('background')
},
/**
* Pinned video thumbnail style.
*/
thumbnailPinned: {
borderColor: schemeColor('activeParticipantHighlight'),
shadowColor: schemeColor('activeParticipantHighlight'),
shadowOffset: {
height: 5,
width: 5
},
shadowRadius: 5
}
});

View File

@@ -1,185 +0,0 @@
// @flow
import { ColorSchemeRegistry, schemeColor } from '../../base/color-scheme';
import { ColorPalette } from '../../base/styles';
import { FILMSTRIP_SIZE } from '../constants';
/**
* Size for the Avatar.
*/
export const AVATAR_SIZE = 50;
/**
* The base style of {@link Filmstrip} shared between narrow and wide versions.
*/
const filmstrip = {
flexGrow: 0
};
/**
* The styles of the feature filmstrip common to both Web and native.
*/
export default {
/**
* Dominant speaker indicator style.
*/
dominantSpeakerIndicator: {
color: ColorPalette.white,
fontSize: 15
},
/**
* Dominant speaker indicator background style.
*/
dominantSpeakerIndicatorBackground: {
backgroundColor: ColorPalette.blue,
borderRadius: 15,
left: 4,
padding: 5,
position: 'absolute',
top: 4
},
/**
* The style of the narrow {@link Filmstrip} version which displays
* thumbnails in a row at the bottom of the screen.
*/
filmstripNarrow: {
...filmstrip,
flexDirection: 'row',
justifyContent: 'flex-end',
height: FILMSTRIP_SIZE
},
/**
* The style of the wide {@link Filmstrip} version which displays thumbnails
* in a column on the short size of the screen.
*
* NOTE: width is calculated based on the children, but it should also align
* to {@code FILMSTRIP_SIZE}.
*/
filmstripWide: {
...filmstrip,
bottom: 0,
flexDirection: 'column',
position: 'absolute',
right: 0,
top: 0
},
/**
* Container of the {@link LocalThumbnail}.
*/
localThumbnail: {
alignContent: 'stretch',
alignSelf: 'stretch',
aspectRatio: 1,
flexShrink: 0,
flexDirection: 'row'
},
/**
* Moderator indicator style.
*/
moderatorIndicator: {
backgroundColor: 'transparent',
bottom: 4,
color: ColorPalette.white,
position: 'absolute',
right: 4
},
/**
* The style of the scrollview containing the remote thumbnails.
*/
scrollView: {
flexGrow: 0
},
/**
* The style of a participant's Thumbnail which renders either the video or
* the avatar of the associated participant.
*/
thumbnail: {
alignItems: 'stretch',
backgroundColor: ColorPalette.appBackground,
borderColor: '#424242',
borderRadius: 3,
borderStyle: 'solid',
borderWidth: 1,
flex: 1,
justifyContent: 'center',
margin: 2,
overflow: 'hidden',
position: 'relative'
},
/**
* The thumbnail audio and video muted indicator style.
*/
thumbnailIndicator: {
backgroundColor: 'transparent',
color: ColorPalette.white,
paddingLeft: 1,
paddingRight: 1,
position: 'relative'
},
/**
* The thumbnails indicator container.
*/
thumbnailIndicatorContainer: {
alignSelf: 'stretch',
bottom: 4,
flex: 1,
flexDirection: 'row',
left: 4,
position: 'absolute'
},
tileView: {
alignSelf: 'center'
},
tileViewRows: {
justifyContent: 'center'
},
tileViewRow: {
flexDirection: 'row',
justifyContent: 'center'
}
};
/**
* Color schemed styles for the @{code Thumbnail} component.
*/
ColorSchemeRegistry.register('Thumbnail', {
/**
* Tinting style of the on-stage participant thumbnail.
*/
activeThumbnailTint: {
backgroundColor: schemeColor('activeParticipantTint')
},
/**
* Coloring if the thumbnail background.
*/
participantViewStyle: {
backgroundColor: schemeColor('background')
},
/**
* Pinned video thumbnail style.
*/
thumbnailPinned: {
borderColor: schemeColor('activeParticipantHighlight'),
shadowColor: schemeColor('activeParticipantHighlight'),
shadowOffset: {
height: 5,
width: 5
},
shadowRadius: 5
}
});

View File

@@ -1,13 +1,20 @@
/* @flow */
import React, { Component } from 'react';
import React from 'react';
import { connect } from '../../../base/redux';
import AbstractRaisedHandIndicator, {
type Props as AbstractProps,
_mapStateToProps
} from '../AbstractRaisedHandIndicator';
import BaseIndicator from './BaseIndicator';
/**
* The type of the React {@code Component} props of {@link RaisedHandIndicator}.
*/
type Props = {
type Props = AbstractProps & {
/**
* The font-size for the icon.
@@ -25,13 +32,13 @@ type Props = {
*
* @extends Component
*/
class RaisedHandIndicator extends Component<Props> {
class RaisedHandIndicator extends AbstractRaisedHandIndicator<Props> {
/**
* Implements React's {@link Component#render()}.
* Renders the platform specific indicator element.
*
* @inheritdoc
* @returns {React$Element<*>}
*/
render() {
_renderIndicator() {
return (
<BaseIndicator
className = 'raisehandindicator indicator show-inline'
@@ -43,4 +50,4 @@ class RaisedHandIndicator extends Component<Props> {
}
}
export default RaisedHandIndicator;
export default connect(_mapStateToProps)(RaisedHandIndicator);

View File

@@ -1,118 +0,0 @@
// @flow
import type { Dispatch } from 'redux';
import { translate } from '../../base/i18n';
import { connect } from '../../base/redux';
import { AbstractButton } from '../../base/toolbox';
import type { AbstractButtonProps } from '../../base/toolbox';
import { beginShareRoom } from '../../share-room';
import { setAddPeopleDialogVisible } from '../actions';
import { isAddPeopleEnabled, isDialOutEnabled } from '../functions';
type Props = AbstractButtonProps & {
/**
* Whether or not the feature to invite people to join the
* conference is available.
*/
_addPeopleEnabled: boolean,
/**
* Opens the add people dialog.
*/
_onOpenAddPeopleDialog: Function,
/**
* Begins the UI procedure to share the conference/room URL.
*/
_onShareRoom: Function
};
/**
* Implements an {@link AbstractButton} to enter add/invite people to the
* current call/conference/meeting.
*/
class InviteButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.shareRoom';
iconName = 'icon-link';
label = 'toolbar.shareRoom';
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
const {
_addPeopleEnabled,
_onOpenAddPeopleDialog,
_onShareRoom
} = this.props;
if (_addPeopleEnabled) {
_onOpenAddPeopleDialog();
} else {
_onShareRoom();
}
}
}
/**
* Maps redux actions to {@link InviteButton}'s React
* {@code Component} props.
*
* @param {Function} dispatch - The redux action {@code dispatch} function.
* @returns {{
* _onOpenAddPeopleDialog,
* _onShareRoom
* }}
* @private
*/
function _mapDispatchToProps(dispatch: Dispatch<any>) {
return {
/**
* Opens the add people dialog.
*
* @private
* @returns {void}
* @type {Function}
*/
_onOpenAddPeopleDialog() {
dispatch(setAddPeopleDialogVisible(true));
},
/**
* Begins the UI procedure to share the conference/room URL.
*
* @private
* @returns {void}
* @type {Function}
*/
_onShareRoom() {
dispatch(beginShareRoom());
}
};
}
/**
* Maps (parts of) the redux state to {@link Toolbox}'s React {@code Component}
* props.
*
* @param {Object} state - The redux store/state.
* @private
* @returns {{
* _addPeopleEnabled: boolean
* }}
*/
function _mapStateToProps(state) {
return {
_addPeopleEnabled: isAddPeopleEnabled(state) || isDialOutEnabled(state)
};
}
export default translate(
connect(_mapStateToProps, _mapDispatchToProps)(InviteButton));

View File

@@ -0,0 +1,73 @@
// @flow
import type { Dispatch } from 'redux';
import { translate } from '../../../../base/i18n';
import { connect } from '../../../../base/redux';
import { AbstractButton } from '../../../../base/toolbox';
import type { AbstractButtonProps } from '../../../../base/toolbox';
import { setAddPeopleDialogVisible } from '../../../actions';
import { isAddPeopleEnabled, isDialOutEnabled } from '../../../functions';
type Props = AbstractButtonProps & {
/**
* Whether or not the feature to invite people to join the
* conference is available.
*/
_addPeopleEnabled: boolean,
/**
* The Redux dispatch function.
*/
dispatch: Dispatch<any>
};
/**
* Implements an {@link AbstractButton} to enter add/invite people to the
* current call/conference/meeting.
*/
class InviteButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.shareRoom';
iconName = 'icon-link';
label = 'toolbar.shareRoom';
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
this.props.dispatch(setAddPeopleDialogVisible(true));
}
/**
* Returns true if none of the invite methods are available.
*
* @protected
* @returns {boolean}
*/
_isDisabled() {
return !this.props._addPeopleEnabled;
}
}
/**
* Maps (parts of) the redux state to {@link InviteButton}'s React {@code Component}
* props.
*
* @param {Object} state - The redux store/state.
* @private
* @returns {{
* _addPeopleEnabled: boolean
* }}
*/
function _mapStateToProps(state) {
return {
_addPeopleEnabled: isAddPeopleEnabled(state) || isDialOutEnabled(state)
};
}
export default translate(connect(_mapStateToProps)(InviteButton));

View File

@@ -1,3 +1,4 @@
// @flow
export { default as AddPeopleDialog } from './AddPeopleDialog';
export { default as InviteButton } from './InviteButton';

View File

@@ -2,6 +2,5 @@
export * from './add-people-dialog';
export { DialInSummary } from './dial-in-summary';
export { default as InfoDialogButton } from './InfoDialogButton';
export { default as InviteButton } from './InviteButton';
export * from './info-dialog';
export * from './callee-info';

View File

@@ -1 +0,0 @@
export { default as InfoDialog } from './InfoDialog';

View File

@@ -0,0 +1,3 @@
// @flow
export * from './native';

View File

@@ -0,0 +1,3 @@
// @flow
export * from './web';

View File

@@ -0,0 +1,38 @@
// @flow
import type { Dispatch } from 'redux';
import { translate } from '../../../../base/i18n';
import { connect } from '../../../../base/redux';
import { AbstractButton } from '../../../../base/toolbox';
import type { AbstractButtonProps } from '../../../../base/toolbox';
import { beginShareRoom } from '../../../../share-room';
type Props = AbstractButtonProps & {
/**
* The Redux dispatch function.
*/
dispatch: Dispatch<any>
};
/**
* Implements an {@link AbstractButton} to open the info dialog of the meeting.
*/
class InfoDialogButton extends AbstractButton<Props, *> {
accessibilityLabel = 'info.accessibilityLabel';
iconName = 'icon-info';
label = 'info.label';
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
this.props.dispatch(beginShareRoom());
}
}
export default translate(connect()(InfoDialogButton));

View File

@@ -0,0 +1,3 @@
// @flow
export { default as InfoDialogButton } from './InfoDialogButton';

View File

@@ -1,8 +1,8 @@
/* @flow */
// @flow
import React, { Component } from 'react';
import { translate } from '../../../base/i18n';
import { translate } from '../../../../base/i18n';
/**
* The type of the React {@code Component} props of {@link DialInNumber}.

View File

@@ -1,16 +1,16 @@
/* @flow */
// @flow
import React, { Component } from 'react';
import type { Dispatch } from 'redux';
import { setPassword } from '../../../base/conference';
import { getInviteURL } from '../../../base/connection';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { isLocalParticipantModerator } from '../../../base/participants';
import { setPassword } from '../../../../base/conference';
import { getInviteURL } from '../../../../base/connection';
import { Dialog } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { connect } from '../../../../base/redux';
import { isLocalParticipantModerator } from '../../../../base/participants';
import { _getDefaultPhoneNumber, getDialInfoPageURL } from '../../functions';
import { _getDefaultPhoneNumber, getDialInfoPageURL } from '../../../functions';
import DialInNumber from './DialInNumber';
import PasswordForm from './PasswordForm';

View File

@@ -4,18 +4,18 @@ import InlineDialog from '@atlaskit/inline-dialog';
import React, { Component } from 'react';
import type { Dispatch } from 'redux';
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { openDialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
import { getParticipantCount } from '../../base/participants';
import { OverflowMenuItem } from '../../base/toolbox';
import { connect } from '../../base/redux';
import { getActiveSession } from '../../recording';
import { ToolbarButton } from '../../toolbox';
import { updateDialInNumbers } from '../actions';
import { createToolbarEvent, sendAnalytics } from '../../../../analytics';
import { openDialog } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
import { getParticipantCount } from '../../../../base/participants';
import { OverflowMenuItem } from '../../../../base/toolbox';
import { connect } from '../../../../base/redux';
import { getActiveSession } from '../../../../recording';
import { ToolbarButton } from '../../../../toolbox';
import { updateDialInNumbers } from '../../../actions';
import { InfoDialog } from './info-dialog';
import InfoDialog from './InfoDialog';
/**
* The type of the React {@code Component} props of {@link InfoDialogButton}.

View File

@@ -1,9 +1,9 @@
/* @flow */
// @flow
import React, { Component } from 'react';
import { translate } from '../../../base/i18n';
import { LOCKED_LOCALLY } from '../../../room-lock';
import { translate } from '../../../../base/i18n';
import { LOCKED_LOCALLY } from '../../../../room-lock';
/**
* The type of the React {@code Component} props of {@link PasswordForm}.

View File

@@ -0,0 +1,4 @@
// @flow
export { default as InfoDialog } from './InfoDialog';
export { default as InfoDialogButton } from './InfoDialogButton';

View File

@@ -1,3 +1,5 @@
// @flow
/**
* The type of (redux) action which signals that all the stored notifications
* need to be cleared.

View File

@@ -0,0 +1,3 @@
// @flow
export * from './native';

View File

@@ -0,0 +1,3 @@
// @flow
export * from './web';

View File

@@ -3,12 +3,13 @@
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { Icon } from '../../base/font-icons';
import { translate } from '../../base/i18n';
import { Icon } from '../../../base/font-icons';
import { translate } from '../../../base/i18n';
import AbstractNotification, {
type Props
} from './AbstractNotification';
} from '../AbstractNotification';
import styles from './styles';
/**

View File

@@ -3,12 +3,13 @@
import React from 'react';
import { View } from 'react-native';
import { connect } from '../../base/redux';
import { connect } from '../../../base/redux';
import AbstractNotificationsContainer, {
_abstractMapStateToProps,
type Props as AbstractProps
} from './AbstractNotificationsContainer';
} from '../AbstractNotificationsContainer';
import Notification from './Notification';
import styles from './styles';

View File

@@ -1,2 +1,4 @@
// @flow
export { default as Notification } from './Notification';
export { default as NotificationsContainer } from './NotificationsContainer';

View File

@@ -1,11 +1,11 @@
// @flow
import { BoxModel, createStyleSheet, ColorPalette } from '../../base/styles';
import { BoxModel, ColorPalette } from '../../../base/styles';
/**
* The styles of the React {@code Components} of the feature notifications.
*/
export default createStyleSheet({
export default {
/**
* The content (left) column of the notification.
@@ -58,4 +58,4 @@ export default createStyleSheet({
notificationContent: {
flexDirection: 'column'
}
});
};

View File

@@ -7,13 +7,13 @@ import WarningIcon from '@atlaskit/icon/glyph/warning';
import { colors } from '@atlaskit/theme';
import React from 'react';
import { translate } from '../../base/i18n';
import { translate } from '../../../base/i18n';
import { NOTIFICATION_TYPE } from '../constants';
import { NOTIFICATION_TYPE } from '../../constants';
import AbstractNotification, {
type Props
} from './AbstractNotification';
} from '../AbstractNotification';
declare var interfaceConfig: Object;

View File

@@ -3,12 +3,13 @@
import { FlagGroup } from '@atlaskit/flag';
import React from 'react';
import { connect } from '../../base/redux';
import { connect } from '../../../base/redux';
import AbstractNotificationsContainer, {
_abstractMapStateToProps as _mapStateToProps,
type Props
} from './AbstractNotificationsContainer';
} from '../AbstractNotificationsContainer';
import Notification from './Notification';
/**

View File

@@ -0,0 +1,4 @@
// @flow
export { default as Notification } from './Notification';
export { default as NotificationsContainer } from './NotificationsContainer';

View File

@@ -1,3 +1,10 @@
// @flow
/**
* The standard time when auto-disappearing notifications should disappear.
*/
export const NOTIFICATION_TIMEOUT = 2500;
/**
* The set of possible notification types.
*

View File

@@ -1,3 +1,5 @@
// @flow
import { toState } from '../base/redux';
/**
@@ -7,7 +9,7 @@ import { toState } from '../base/redux';
* @param {Object|Function} stateful - The redux store state.
* @returns {boolean}
*/
export function areThereNotifications(stateful) {
export function areThereNotifications(stateful: Object | Function) {
const state = toState(stateful);
const { enabled, notifications } = state['features/notifications'];

View File

@@ -1,6 +1,9 @@
// @flow
export * from './actions';
export * from './actionTypes';
export * from './components';
export * from './constants';
export * from './functions';
import './middleware';

View File

@@ -1,3 +1,5 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import {

View File

@@ -2,6 +2,7 @@
import JitsiMeetJS, { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import {
NOTIFICATION_TIMEOUT,
hideNotification,
showErrorNotification,
showNotification
@@ -125,7 +126,7 @@ export function showStoppedRecordingNotification(streamType: string) {
titleKey: 'dialog.recording'
};
return showNotification(dialogProps, 2500);
return showNotification(dialogProps, NOTIFICATION_TIMEOUT);
}
/**

View File

@@ -1,14 +1,7 @@
// @flow
import React from 'react';
import {
Alert,
SafeAreaView,
ScrollView,
Switch,
TextInput,
View
} from 'react-native';
import { Alert, NativeModules, SafeAreaView, ScrollView, Switch, Text, TextInput, View } from 'react-native';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { translate } from '../../../base/i18n';
@@ -27,6 +20,11 @@ import { normalizeUserInputURL } from '../../functions';
import styles from './styles';
import { HeaderLabel } from '../../../base/react/components/native';
/**
* Application information module.
*/
const { AppInfo } = NativeModules;
type Props = AbstractProps & {
/**
@@ -193,6 +191,15 @@ class SettingsView extends AbstractSettingsView<Props> {
onValueChange = { this._onStartVideoMutedChange }
value = { _settings.startWithVideoMuted } />
</FormRow>
<FormSectionHeader
label = 'settingsView.buildInfoSection' />
<FormRow
fieldSeparator = { true }
label = 'settingsView.version'>
<Text>
{ `${AppInfo.version} build ${AppInfo.buildNumber}` }
</Text>
</FormRow>
</ScrollView>
</SafeAreaView>
);

View File

@@ -1,7 +1,6 @@
/* global JitsiMeetJS */
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import Logger from 'jitsi-meet-logger';
import React from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
@@ -10,13 +9,21 @@ import {
PostMessageTransportBackend,
Transport
} from '../../../../../modules/transport';
import {
getAvailableDevices,
getCurrentDevices,
isDeviceChangeAvailable,
isDeviceListAvailable,
isMultipleAudioInputSupported,
setAudioInputDevice,
setAudioOutputDevice,
setVideoInputDevice
} from '../../../../../modules/API/external/functions';
import { parseURLParams } from '../../../base/config';
import { DialogWithTabs } from '../../../base/dialog';
import { DeviceSelection } from '../../../device-selection';
const logger = Logger.getLogger(__filename);
/**
* Implements a class that renders the React components for the device selection
* popup page and handles the communication between the components and Jitsi
@@ -102,14 +109,7 @@ export default class DeviceSelectionPopup {
* @returns {Promise}
*/
_getAvailableDevices() {
return this._transport.sendRequest({
type: 'devices',
name: 'getAvailableDevices'
}).catch(e => {
logger.error(e);
return {};
});
return getAvailableDevices(this._transport);
}
/**
@@ -118,13 +118,18 @@ export default class DeviceSelectionPopup {
* @returns {Promise}
*/
_getCurrentDevices() {
return this._transport.sendRequest({
type: 'devices',
name: 'getCurrentDevices'
}).catch(e => {
logger.error(e);
return getCurrentDevices(this._transport).then(currentDevices => {
const {
audioInput = {},
audioOutput = {},
videoInput = {}
} = currentDevices;
return {};
return {
audioInput: audioInput.deviceId,
audioOutput: audioOutput.deviceId,
videoInput: videoInput.deviceId
};
});
}
@@ -170,15 +175,7 @@ export default class DeviceSelectionPopup {
* @returns {Promise}
*/
_isDeviceChangeAvailable(deviceType) {
return this._transport.sendRequest({
deviceType,
type: 'devices',
name: 'isDeviceChangeAvailable'
}).catch(e => {
logger.error(e);
return false;
});
return isDeviceChangeAvailable(this._transport, deviceType).catch(() => false);
}
/**
@@ -188,31 +185,17 @@ export default class DeviceSelectionPopup {
* @returns {Promise}
*/
_isDeviceListAvailable() {
return this._transport.sendRequest({
type: 'devices',
name: 'isDeviceListAvailable'
}).catch(e => {
logger.error(e);
return false;
});
return isDeviceListAvailable(this._transport).catch(() => false);
}
/**
* Returns Promise that resolves with true if the device list is available
* Returns Promise that resolves with true if multiple audio input is supported
* and with false if not.
*
* @returns {Promise}
*/
_isMultipleAudioInputSupported() {
return this._transport.sendRequest({
type: 'devices',
name: 'isMultipleAudioInputSupported'
}).catch(e => {
logger.error(e);
return false;
});
return isMultipleAudioInputSupported(this._transport).catch(() => false);
}
/**
@@ -281,10 +264,7 @@ export default class DeviceSelectionPopup {
* @returns {Promise}
*/
_setAudioInputDevice(id) {
return this._setDevice({
id,
kind: 'audioinput'
});
return setAudioInputDevice(this._transport, undefined, id);
}
/**
@@ -294,24 +274,7 @@ export default class DeviceSelectionPopup {
* @returns {Promise}
*/
_setAudioOutputDevice(id) {
return this._setDevice({
id,
kind: 'audiooutput'
});
}
/**
* Sets the currently used device to the one that is passed.
*
* @param {Object} device - The new device to be used.
* @returns {Promise}
*/
_setDevice(device) {
return this._transport.sendRequest({
type: 'devices',
name: 'setDevice',
device
});
return setAudioOutputDevice(this._transport, undefined, id);
}
/**
@@ -321,10 +284,7 @@ export default class DeviceSelectionPopup {
* @returns {Promise}
*/
_setVideoInputDevice(id) {
return this._setDevice({
id,
kind: 'videoinput'
});
return setVideoInputDevice(this._transport, undefined, id);
}
/**

View File

@@ -10,7 +10,7 @@ import {
} from '../../../base/dialog';
import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
import { InviteButton } from '../../../invite';
import { InfoDialogButton, InviteButton } from '../../../invite';
import { AudioRouteButton } from '../../../mobile/audio-mode';
import { LiveStreamButton, RecordButton } from '../../../recording';
import { RoomLockButton } from '../../../room-lock';
@@ -18,6 +18,7 @@ import { ClosedCaptionButton } from '../../../subtitles';
import { TileViewButton } from '../../../video-layout';
import AudioOnlyButton from './AudioOnlyButton';
import RaiseHandButton from './RaiseHandButton';
import ToggleCameraButton from './ToggleCameraButton';
declare var __DEV__;
@@ -95,6 +96,8 @@ class OverflowMenu extends Component<Props> {
<LiveStreamButton { ...buttonProps } />
<TileViewButton { ...buttonProps } />
<InviteButton { ...buttonProps } />
<InfoDialogButton { ...buttonProps } />
<RaiseHandButton { ...buttonProps } />
</BottomSheet>
);
}

View File

@@ -0,0 +1,113 @@
// @flow
import { type Dispatch } from 'redux';
import {
createToolbarEvent,
sendAnalytics
} from '../../../analytics';
import { translate } from '../../../base/i18n';
import {
getLocalParticipant,
participantUpdated
} from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton } from '../../../base/toolbox';
import type { AbstractButtonProps } from '../../../base/toolbox';
/**
* The type of the React {@code Component} props of {@link RaiseHandButton}.
*/
type Props = AbstractButtonProps & {
/**
* The local participant.
*/
_localParticipant: Object,
/**
* Whether the participant raused their hand or not.
*/
_raisedHand: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Dispatch<any>
};
/**
* An implementation of a button to raise or lower hand.
*/
class RaiseHandButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.raiseHand';
iconName = 'raised-hand';
label = 'toolbar.raiseYourHand';
toggledIconName = 'raised-hand';
toggledLabel = 'toolbar.lowerYourHand';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
_handleClick() {
this._toggleRaisedHand();
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isToggled() {
return this.props._raisedHand;
}
/**
* Toggles the rased hand status of the local participant.
*
* @returns {void}
*/
_toggleRaisedHand() {
const enable = !this.props._raisedHand;
sendAnalytics(createToolbarEvent('raise.hand', { enable }));
this.props.dispatch(participantUpdated({
// XXX Only the local participant is allowed to update without
// stating the JitsiConference instance (i.e. participant property
// `conference` for a remote participant) because the local
// participant is uniquely identified by the very fact that there is
// only one local participant.
id: this.props._localParticipant.id,
local: true,
raisedHand: enable
}));
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _raisedHand: boolean
* }}
*/
function _mapStateToProps(state): Object {
const _localParticipant = getLocalParticipant(state);
return {
_localParticipant,
_raisedHand: _localParticipant.raisedHand
};
}
export default translate(connect(_mapStateToProps)(RaiseHandButton));

View File

@@ -10,6 +10,7 @@ import {
STOP_TRANSCRIBING
} from './actionTypes';
import {
NOTIFICATION_TIMEOUT,
hideNotification,
showErrorNotification,
showNotification
@@ -161,7 +162,7 @@ export function hidePendingTranscribingNotification() {
/**
* Signals that the stopped transcribing notification should be shown on the
* screen for a 2500 ms.
* screen.
*
* @returns {showNotification}
*/
@@ -169,7 +170,7 @@ export function showStoppedTranscribingNotification() {
return showNotification({
descriptionKey: 'transcribing.off',
titleKey: 'dialog.transcribing'
}, 2500);
}, NOTIFICATION_TIMEOUT);
}