mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-02-16 19:00:19 +00:00
Compare commits
45 Commits
enable-deb
...
5243
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e4a22bdbf | ||
|
|
ddbf334930 | ||
|
|
cd5f2b483f | ||
|
|
cf34b0a783 | ||
|
|
10cc3b2b31 | ||
|
|
27e4e862fd | ||
|
|
4b3d92dcbd | ||
|
|
5c1e2b4bd2 | ||
|
|
a4d516ca86 | ||
|
|
d41c597446 | ||
|
|
4788720edd | ||
|
|
40099e97ec | ||
|
|
a7a44902ec | ||
|
|
7af23f35ba | ||
|
|
cf49af1a9f | ||
|
|
186125607f | ||
|
|
508decc3f3 | ||
|
|
6d3a4b920b | ||
|
|
595df524b0 | ||
|
|
9914a5d14a | ||
|
|
fbf9d489f0 | ||
|
|
8106fb06e4 | ||
|
|
036ef0f387 | ||
|
|
307699a34c | ||
|
|
c23375793e | ||
|
|
4dc642fb4d | ||
|
|
d47e67c28c | ||
|
|
db0b861353 | ||
|
|
e40d4a48b8 | ||
|
|
8c82c0f56e | ||
|
|
c123ff9e15 | ||
|
|
ae33755913 | ||
|
|
bcc870daa7 | ||
|
|
a6a95ad13b | ||
|
|
195508ea60 | ||
|
|
5ce4b82c2c | ||
|
|
ab1c016108 | ||
|
|
29dd0cf726 | ||
|
|
1e0c25d816 | ||
|
|
dfd28c501b | ||
|
|
ed98eca326 | ||
|
|
c5c4449fbc | ||
|
|
e45cab9a80 | ||
|
|
6f44368647 | ||
|
|
a31a10ba38 |
@@ -40,10 +40,6 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
* Room name.
|
||||
*/
|
||||
private String room;
|
||||
/**
|
||||
* Conference subject.
|
||||
*/
|
||||
private String subject;
|
||||
/**
|
||||
* JWT token used for authentication.
|
||||
*/
|
||||
@@ -54,19 +50,16 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
*/
|
||||
private Bundle colorScheme;
|
||||
|
||||
/**
|
||||
* Config. See: https://github.com/jitsi/jitsi-meet/blob/master/config.js
|
||||
*/
|
||||
private Bundle config;
|
||||
|
||||
/**
|
||||
* Feature flags. See: https://github.com/jitsi/jitsi-meet/blob/master/react/features/base/flags/constants.js
|
||||
*/
|
||||
private Bundle featureFlags;
|
||||
|
||||
/**
|
||||
* Set to {@code true} to join the conference with audio / video muted or to start in audio
|
||||
* only mode respectively.
|
||||
*/
|
||||
private Boolean audioMuted;
|
||||
private Boolean audioOnly;
|
||||
private Boolean videoMuted;
|
||||
|
||||
/**
|
||||
* USer information, to be used when no token is specified.
|
||||
*/
|
||||
@@ -80,10 +73,6 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
return room;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
@@ -96,18 +85,6 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
return featureFlags;
|
||||
}
|
||||
|
||||
public boolean getAudioMuted() {
|
||||
return audioMuted;
|
||||
}
|
||||
|
||||
public boolean getAudioOnly() {
|
||||
return audioOnly;
|
||||
}
|
||||
|
||||
public boolean getVideoMuted() {
|
||||
return videoMuted;
|
||||
}
|
||||
|
||||
public JitsiMeetUserInfo getUserInfo() {
|
||||
return userInfo;
|
||||
}
|
||||
@@ -118,19 +95,16 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
public static class Builder {
|
||||
private URL serverURL;
|
||||
private String room;
|
||||
private String subject;
|
||||
private String token;
|
||||
|
||||
private Bundle colorScheme;
|
||||
private Bundle config;
|
||||
private Bundle featureFlags;
|
||||
|
||||
private Boolean audioMuted;
|
||||
private Boolean audioOnly;
|
||||
private Boolean videoMuted;
|
||||
|
||||
private JitsiMeetUserInfo userInfo;
|
||||
|
||||
public Builder() {
|
||||
config = new Bundle();
|
||||
featureFlags = new Bundle();
|
||||
}
|
||||
|
||||
@@ -162,7 +136,7 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
* @return - The {@link Builder} object itself so the method calls can be chained.
|
||||
*/
|
||||
public Builder setSubject(String subject) {
|
||||
this.subject = subject;
|
||||
setConfigOverride("subject", subject);
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -193,11 +167,11 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
|
||||
/**
|
||||
* Indicates the conference will be joined with the microphone muted.
|
||||
* @param muted - Muted indication.
|
||||
* @param audioMuted - Muted indication.
|
||||
* @return - The {@link Builder} object itself so the method calls can be chained.
|
||||
*/
|
||||
public Builder setAudioMuted(boolean muted) {
|
||||
this.audioMuted = muted;
|
||||
public Builder setAudioMuted(boolean audioMuted) {
|
||||
setConfigOverride("startWithAudioMuted", audioMuted);
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -209,7 +183,7 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
* @return - The {@link Builder} object itself so the method calls can be chained.
|
||||
*/
|
||||
public Builder setAudioOnly(boolean audioOnly) {
|
||||
this.audioOnly = audioOnly;
|
||||
setConfigOverride("startAudioOnly", audioOnly);
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -219,7 +193,7 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
* @return - The {@link Builder} object itself so the method calls can be chained.
|
||||
*/
|
||||
public Builder setVideoMuted(boolean videoMuted) {
|
||||
this.videoMuted = videoMuted;
|
||||
setConfigOverride("startWithVideoMuted", videoMuted);
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -261,6 +235,36 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setConfigOverride(String config, String value) {
|
||||
this.config.putString(config, value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setConfigOverride(String config, int value) {
|
||||
this.config.putInt(config, value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setConfigOverride(String config, boolean value) {
|
||||
this.config.putBoolean(config, value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setConfigOverride(String config, Bundle bundle) {
|
||||
this.config.putBundle(config, bundle);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setConfigOverride(String config, String[] list) {
|
||||
this.config.putStringArray(config, list);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the immutable {@link JitsiMeetConferenceOptions} object with the configuration
|
||||
* that this {@link Builder} instance specified.
|
||||
@@ -271,13 +275,10 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
|
||||
options.serverURL = this.serverURL;
|
||||
options.room = this.room;
|
||||
options.subject = this.subject;
|
||||
options.token = this.token;
|
||||
options.colorScheme = this.colorScheme;
|
||||
options.config = this.config;
|
||||
options.featureFlags = this.featureFlags;
|
||||
options.audioMuted = this.audioMuted;
|
||||
options.audioOnly = this.audioOnly;
|
||||
options.videoMuted = this.videoMuted;
|
||||
options.userInfo = this.userInfo;
|
||||
|
||||
return options;
|
||||
@@ -290,17 +291,12 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
private JitsiMeetConferenceOptions(Parcel in) {
|
||||
serverURL = (URL) in.readSerializable();
|
||||
room = in.readString();
|
||||
subject = in.readString();
|
||||
token = in.readString();
|
||||
colorScheme = in.readBundle();
|
||||
config = in.readBundle();
|
||||
featureFlags = in.readBundle();
|
||||
userInfo = new JitsiMeetUserInfo(in.readBundle());
|
||||
byte tmpAudioMuted = in.readByte();
|
||||
audioMuted = tmpAudioMuted == 0 ? null : tmpAudioMuted == 1;
|
||||
byte tmpAudioOnly = in.readByte();
|
||||
audioOnly = tmpAudioOnly == 0 ? null : tmpAudioOnly == 1;
|
||||
byte tmpVideoMuted = in.readByte();
|
||||
videoMuted = tmpVideoMuted == 0 ? null : tmpVideoMuted == 1;
|
||||
}
|
||||
|
||||
Bundle asProps() {
|
||||
@@ -317,21 +313,6 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
props.putBundle("colorScheme", colorScheme);
|
||||
}
|
||||
|
||||
Bundle config = new Bundle();
|
||||
|
||||
if (audioMuted != null) {
|
||||
config.putBoolean("startWithAudioMuted", audioMuted);
|
||||
}
|
||||
if (audioOnly != null) {
|
||||
config.putBoolean("startAudioOnly", audioOnly);
|
||||
}
|
||||
if (videoMuted != null) {
|
||||
config.putBoolean("startWithVideoMuted", videoMuted);
|
||||
}
|
||||
if (subject != null) {
|
||||
config.putString("subject", subject);
|
||||
}
|
||||
|
||||
Bundle urlProps = new Bundle();
|
||||
|
||||
// The room is fully qualified
|
||||
@@ -379,14 +360,11 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeSerializable(serverURL);
|
||||
dest.writeString(room);
|
||||
dest.writeString(subject);
|
||||
dest.writeString(token);
|
||||
dest.writeBundle(colorScheme);
|
||||
dest.writeBundle(config);
|
||||
dest.writeBundle(featureFlags);
|
||||
dest.writeBundle(userInfo != null ? userInfo.asBundle() : new Bundle());
|
||||
dest.writeByte((byte) (audioMuted == null ? 0 : audioMuted ? 1 : 2));
|
||||
dest.writeByte((byte) (audioOnly == null ? 0 : audioOnly ? 1 : 2));
|
||||
dest.writeByte((byte) (videoMuted == null ? 0 : videoMuted ? 1 : 2));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2106,7 +2106,7 @@ export default {
|
||||
|
||||
room.on(
|
||||
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
|
||||
id => APP.store.dispatch(dominantSpeakerChanged(id, room)));
|
||||
(dominant, previous) => APP.store.dispatch(dominantSpeakerChanged(dominant, previous, room)));
|
||||
|
||||
room.on(
|
||||
JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP,
|
||||
|
||||
45
config.js
45
config.js
@@ -27,9 +27,6 @@ var config = {
|
||||
// Websocket URL
|
||||
// websocket: 'wss://jitsi-meet.example.com/xmpp-websocket',
|
||||
|
||||
// The name of client node advertised in XEP-0115 'c' stanza
|
||||
clientNode: 'http://jitsi.org/jitsimeet',
|
||||
|
||||
// The real JID of focus participant - can be overridden here
|
||||
// Do not change username - FIXME: Make focus username configurable
|
||||
// https://github.com/jitsi/jitsi-meet/issues/7376
|
||||
@@ -73,6 +70,9 @@ var config = {
|
||||
// Enables reactions feature.
|
||||
// enableReactions: false,
|
||||
|
||||
// Disables polls feature.
|
||||
// disablePolls: false,
|
||||
|
||||
// Disables ICE/UDP by filtering out local and remote UDP candidates in
|
||||
// signalling.
|
||||
// webrtcIceUdpDisable: false,
|
||||
@@ -144,6 +144,9 @@ var config = {
|
||||
// Sets the preferred resolution (height) for local video. Defaults to 720.
|
||||
// resolution: 720,
|
||||
|
||||
// Specifies whether there will be a search field in speaker stats or not
|
||||
// disableSpeakerStatsSearch: false,
|
||||
|
||||
// How many participants while in the tile view mode, before the receiving video quality is reduced from HD to SD.
|
||||
// Use -1 to disable.
|
||||
// maxFullResolutionParticipants: 2,
|
||||
@@ -230,6 +233,17 @@ var config = {
|
||||
// subtitles and buttons can be configured)
|
||||
// transcribingEnabled: false,
|
||||
|
||||
// If true transcriber will use the application language.
|
||||
// The application language is either explicitly set by participants in their settings or automatically
|
||||
// detected based on the environment, e.g. if the app is opened in a chrome instance which is using french as its
|
||||
// default language then transcriptions for that participant will be in french.
|
||||
// Defaults to true.
|
||||
// transcribeWithAppLanguage: true,
|
||||
|
||||
// Transcriber language. This settings will only work if "transcribeWithAppLanguage" is explicitly set to false.
|
||||
// Available languages can be found in lang/language.json.
|
||||
// preferredTranscribeLanguage: 'en',
|
||||
|
||||
// Enables automatic turning on captions when recording is started
|
||||
// autoCaptionOnRecord: false,
|
||||
|
||||
@@ -513,6 +527,28 @@ var config = {
|
||||
// callStatsID: '',
|
||||
// callStatsSecret: '',
|
||||
|
||||
// The callstats initialize config params as described in the API:
|
||||
// https://docs.callstats.io/docs/javascript#callstatsinitialize-with-app-secret
|
||||
// callStatsConfigParams: {
|
||||
// disableBeforeUnloadHandler: true, // disables callstats.js's window.onbeforeunload parameter.
|
||||
// applicationVersion: "app_version", // Application version specified by the developer.
|
||||
// disablePrecalltest: true, // disables the pre-call test, it is enabled by default.
|
||||
// siteID: "siteID", // The name/ID of the site/campus from where the call/pre-call test is made.
|
||||
// additionalIDs: { // additionalIDs object, contains application related IDs.
|
||||
// customerID: "Customer Identifier. Example, walmart.",
|
||||
// tenantID: "Tenant Identifier. Example, monster.",
|
||||
// productName: "Product Name. Example, Jitsi.",
|
||||
// meetingsName: "Meeting Name. Example, Jitsi loves callstats.",
|
||||
// serverName: "Server/MiddleBox Name. Example, jvb-prod-us-east-mlkncws12.",
|
||||
// pbxID: "PBX Identifier. Example, walmart.",
|
||||
// pbxExtensionID: "PBX Extension Identifier. Example, 5625.",
|
||||
// fqExtensionID: "Fully qualified Extension Identifier. Example, +71 (US) +5625.",
|
||||
// sessionID: "Session Identifier. Example, session-12-34"
|
||||
// },
|
||||
// collectLegacyStats: true, //enables the collection of legacy stats in chrome browser
|
||||
// collectIP: true //enables the collection localIP address
|
||||
// },
|
||||
|
||||
// Enables sending participants' display names to callstats
|
||||
// enableDisplayNameInStats: false,
|
||||
|
||||
@@ -583,6 +619,9 @@ var config = {
|
||||
},
|
||||
|
||||
analytics: {
|
||||
// True if the analytics should be disabled
|
||||
// disabled: false,
|
||||
|
||||
// The Google Analytics Tracking ID:
|
||||
// googleAnalyticsTrackingId: 'your-tracking-id-UA-123456-1'
|
||||
|
||||
|
||||
@@ -51,8 +51,10 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.jitsi-icon svg {
|
||||
fill: white;
|
||||
.jitsi-icon {
|
||||
&-default svg {
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled .jitsi-icon svg {
|
||||
|
||||
@@ -574,3 +574,41 @@
|
||||
background: #36383C;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-tabs-container {
|
||||
width: 100%;
|
||||
border-bottom: thin solid #292929;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.chat-tab {
|
||||
font-size: 1.2em;
|
||||
padding-bottom: 0.5em;
|
||||
width: 50%;
|
||||
text-align: center;
|
||||
color: #8B8B8B;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-tab-focus {
|
||||
border-bottom-style: solid;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.chat-tab-title {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.chat-tab-badge {
|
||||
background-color: #165ecc;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
padding: 0 4px;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
@@ -5,14 +5,6 @@
|
||||
.description {
|
||||
font-size: 13px;
|
||||
margin: 15px 0;
|
||||
|
||||
.read-more {
|
||||
cursor: pointer;
|
||||
opacity: .9;
|
||||
color: #fff;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.control-row {
|
||||
|
||||
@@ -51,6 +51,10 @@
|
||||
top: 30px;
|
||||
z-index: $toolbarZ + 1;
|
||||
|
||||
&:empty {
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.toolbox-visible {
|
||||
// Same as toolbox subject position
|
||||
top: 120px;
|
||||
|
||||
@@ -48,4 +48,11 @@
|
||||
.participants_pane-content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.jitsi-icon {
|
||||
&-dominant-speaker {
|
||||
background-color: #1EC26A;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
448
css/_polls.scss
Normal file
448
css/_polls.scss
Normal file
@@ -0,0 +1,448 @@
|
||||
.poll-dialog {
|
||||
font-size: 1rem;
|
||||
|
||||
h1, span, li, strong {
|
||||
color: #bce;
|
||||
}
|
||||
ol {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-question-field {
|
||||
padding: 8px 16px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #525252;
|
||||
}
|
||||
|
||||
.poll-header {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.poll-answer-container{
|
||||
padding: 8px;
|
||||
background: #3D3D3D;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.poll-answer-field-list, .poll-answer-list, .poll-result-list {
|
||||
list-style-type: none;
|
||||
padding: 0 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ol.poll-result-list {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.poll-result-list > li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.poll-answer-field {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
margin-bottom: 16;
|
||||
|
||||
}
|
||||
|
||||
.poll-answer-field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.poll-create-option-row {
|
||||
display: 'flex';
|
||||
margin-bottom: 4;
|
||||
}
|
||||
|
||||
// Needeed to override atlaskit default blue color
|
||||
.poll-create-container .jsYMHu {
|
||||
background: #292929;
|
||||
border-color: #808090;
|
||||
color: white // #808090
|
||||
}
|
||||
|
||||
.poll-add-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.poll-remove-option-button {
|
||||
background: 0 0;
|
||||
border: none;
|
||||
color: #8B8B8B;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.poll-create-add-option {
|
||||
border: none;
|
||||
background-color: #292929;
|
||||
padding: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.poll-icon-button, .poll-drag-handle {
|
||||
.jitsi-icon svg {
|
||||
fill: #bce;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-drag-handle {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: grab;
|
||||
padding-left: 8;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.poll-dragged {
|
||||
opacity: 0.5;
|
||||
* {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-question {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.poll-answer-voters {
|
||||
font-size: 1em;
|
||||
font-weight: lighter;
|
||||
list-style-type: none;
|
||||
border: #616161 solid 1px;
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
margin: 4px 0px 12px;
|
||||
background-color: #616161;
|
||||
}
|
||||
|
||||
.poll-answer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.poll-answer-vote-name {
|
||||
flex-shrink: 1;
|
||||
overflow-wrap: anywhere
|
||||
}
|
||||
|
||||
.poll-answer-vote-count-container{
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.poll-answer-vote-count {
|
||||
margin-left: 10px;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.poll-answer-short-results{
|
||||
display: flex;
|
||||
min-width: 10em;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.poll-bar-container, .poll-bar {
|
||||
border-radius: 3px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.poll-bar-container {
|
||||
background-color: #616161;
|
||||
max-width: 160px;
|
||||
margin-top: 3px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.poll-bar {
|
||||
background-color: #246FE5;
|
||||
}
|
||||
|
||||
.poll-message-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.poll-notice {
|
||||
font-weight: 100;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.poll-show-details {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-result-links {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
a.poll-detail-link, a.poll-change-vote-link {
|
||||
color: #246FE5;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.polls-pane-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-weight: 600;
|
||||
height: 85%;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.pane-content{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.empty-pane-icon {
|
||||
width: 50%;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty-pane-icon svg {
|
||||
fill: #3D3D3D;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.empty-pane-message {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.poll-results {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.poll-answer {
|
||||
h1, strong ,span {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-results, .poll-answer {
|
||||
margin-bottom: 16px;
|
||||
background: #292929;
|
||||
border-radius: 8px;
|
||||
padding: 12px 8px;
|
||||
border-width: thin;
|
||||
border-style: solid;
|
||||
border-color: #616161;
|
||||
}
|
||||
|
||||
.poll-create-label {
|
||||
color: white;
|
||||
margin-bottom: 4;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.expandable-input{
|
||||
resize: none;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
border: 1px solid #666666;
|
||||
background-color: #141414;
|
||||
color: #FFF;
|
||||
border-radius: 6px;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.poll-container {
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
padding: 16px;
|
||||
|
||||
& > * + *:not(.ignore-child) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-create-header {
|
||||
font-size: 20px;
|
||||
margin: 20px 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.poll-create-container {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.poll-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 8px 16px;
|
||||
height: 40px;
|
||||
align-items: stretch;
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-primary-button {
|
||||
align-items: center;
|
||||
background-color: #0056E0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
font-weight: unset;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
flex: 1;
|
||||
|
||||
&:hover {
|
||||
background-color: #246FE5;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #0045B3;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: #0045B3;
|
||||
border: 3px solid #99BBF3;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #00225A;
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-secondary-button {
|
||||
align-items: center;
|
||||
background-color: #3D3D3D;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
font-weight: unset;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
height: 40px;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: #525252;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #292929;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: #292929;
|
||||
border: 3px solid #858585;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #141414;
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-small-primary-button {
|
||||
align-items: center;
|
||||
background-color: #0056E0;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
font-weight: unset;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
height: 40px;
|
||||
width: 50%;
|
||||
|
||||
&:hover {
|
||||
background-color: #246FE5;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #0045B3;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: #0045B3;
|
||||
border: 3px solid #99BBF3;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #00225A;
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-small-secondary-button {
|
||||
align-items: center;
|
||||
background-color: #3D3D3D;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
font-weight: unset;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
height: 40px;
|
||||
width: 50%;
|
||||
|
||||
&:hover {
|
||||
background-color: #525252;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #292929;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: #292929;
|
||||
border: 3px solid #858585;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #141414;
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
@@ -108,5 +108,6 @@ $flagsImagePath: "../images/";
|
||||
@import 'participants-pane';
|
||||
@import 'reactions-menu';
|
||||
@import 'plan-limit';
|
||||
@import 'polls';
|
||||
|
||||
/* Modules END */
|
||||
|
||||
@@ -65,6 +65,7 @@ Component "conference.jitmeet.example.com" "muc"
|
||||
modules_enabled = {
|
||||
"muc_meeting_id";
|
||||
"muc_domain_mapper";
|
||||
"polls";
|
||||
--"token_verification";
|
||||
}
|
||||
admins = { "focusUser@auth.jitmeet.example.com" }
|
||||
|
||||
@@ -125,12 +125,19 @@ RCT_EXPORT_MODULE();
|
||||
return _workerQueue;
|
||||
}
|
||||
|
||||
- (BOOL)setConfigWithoutLock:(RTCAudioSessionConfiguration *)config
|
||||
error:(NSError * _Nullable *)outError {
|
||||
RTCAudioSession *session = [RTCAudioSession sharedInstance];
|
||||
|
||||
return [session setConfiguration:config error:outError];
|
||||
}
|
||||
|
||||
- (BOOL)setConfig:(RTCAudioSessionConfiguration *)config
|
||||
error:(NSError * _Nullable *)outError {
|
||||
|
||||
RTCAudioSession *session = [RTCAudioSession sharedInstance];
|
||||
[session lockForConfiguration];
|
||||
BOOL success = [session setConfiguration:config error:outError];
|
||||
BOOL success = [self setConfigWithoutLock:config error:outError];
|
||||
[session unlockForConfiguration];
|
||||
|
||||
return success;
|
||||
@@ -196,7 +203,7 @@ RCT_EXPORT_METHOD(setAudioDevice:(NSString *)device
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (port != nil) {
|
||||
// First remove the override if we are going to select a different device.
|
||||
if (isSpeakerOn) {
|
||||
@@ -206,11 +213,11 @@ RCT_EXPORT_METHOD(setAudioDevice:(NSString *)device
|
||||
// Special case for the earpiece.
|
||||
if ([port.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
|
||||
forceEarpiece = YES;
|
||||
[self setConfig:earpieceConfig error:nil];
|
||||
[self setConfigWithoutLock:earpieceConfig error:nil];
|
||||
} else if (isEarpieceOn) {
|
||||
// Reset the config.
|
||||
RTCAudioSessionConfiguration *config = [self configForMode:activeMode];
|
||||
[self setConfig:config error:nil];
|
||||
[self setConfigWithoutLock:config error:nil];
|
||||
}
|
||||
|
||||
// Select our preferred input.
|
||||
|
||||
@@ -29,10 +29,6 @@
|
||||
* Room name.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSString *room;
|
||||
/**
|
||||
* Conference subject.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSString *subject;
|
||||
/**
|
||||
* JWT token used for authentication.
|
||||
*/
|
||||
@@ -49,13 +45,7 @@
|
||||
*/
|
||||
@property (nonatomic, readonly, nonnull) NSDictionary *featureFlags;
|
||||
|
||||
/**
|
||||
* Set to YES to join the conference with audio / video muted or to start in audio
|
||||
* only mode respectively.
|
||||
*/
|
||||
@property (nonatomic) BOOL audioOnly;
|
||||
@property (nonatomic) BOOL audioMuted;
|
||||
@property (nonatomic) BOOL videoMuted;
|
||||
@property (nonatomic, readonly, nonnull) NSDictionary *config;
|
||||
|
||||
/**
|
||||
* Set to YES to enable the welcome page. Typically SDK users won't need this enabled
|
||||
@@ -71,15 +61,17 @@
|
||||
- (void)setFeatureFlag:(NSString *_Nonnull)flag withBoolean:(BOOL)value;
|
||||
- (void)setFeatureFlag:(NSString *_Nonnull)flag withValue:(id _Nonnull)value;
|
||||
|
||||
/**
|
||||
* CallKit call handle, to be used when implementing incoming calls.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSString *callHandle;
|
||||
- (void)setConfigOverride:(NSString *_Nonnull)config withBoolean:(BOOL)value;
|
||||
- (void)setConfigOverride:(NSString *_Nonnull)config withValue:(id _Nonnull)value;
|
||||
- (void)setConfigOverride:(NSString *_Nonnull)config withDictionary:(NSDictionary * _Nonnull)dictionary;
|
||||
- (void)setConfigOverride:(NSString *_Nonnull)config withArray:( NSArray * _Nonnull)array;
|
||||
|
||||
/**
|
||||
* CallKit call UUID, to be used when implementing incoming calls.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSUUID *callUUID;
|
||||
- (void)setAudioOnly:(BOOL)audioOnly;
|
||||
- (void)setAudioMuted:(BOOL)audioMuted;
|
||||
- (void)setVideoMuted:(BOOL)videoMuted;
|
||||
- (void)setCallHandle:(NSString *_Nonnull)callHandle;
|
||||
- (void)setCallUUID:(NSUUID *_Nonnull)callUUID;
|
||||
- (void)setSubject:(NSString *_Nonnull)subject;
|
||||
|
||||
@end
|
||||
|
||||
@@ -88,23 +80,15 @@
|
||||
@property (nonatomic, copy, nullable, readonly) NSURL *serverURL;
|
||||
|
||||
@property (nonatomic, copy, nullable, readonly) NSString *room;
|
||||
@property (nonatomic, copy, nullable, readonly) NSString *subject;
|
||||
@property (nonatomic, copy, nullable, readonly) NSString *token;
|
||||
|
||||
@property (nonatomic, copy, nullable) NSDictionary *colorScheme;
|
||||
@property (nonatomic, readonly, nonnull) NSDictionary *featureFlags;
|
||||
|
||||
@property (nonatomic, readonly) BOOL audioOnly;
|
||||
@property (nonatomic, readonly) BOOL audioMuted;
|
||||
@property (nonatomic, readonly) BOOL videoMuted;
|
||||
|
||||
@property (nonatomic, readonly) BOOL welcomePageEnabled;
|
||||
|
||||
@property (nonatomic, nullable) JitsiMeetUserInfo *userInfo;
|
||||
|
||||
@property (nonatomic, copy, nullable, readonly) NSString *callHandle;
|
||||
@property (nonatomic, copy, nullable, readonly) NSUUID *callUUID;
|
||||
|
||||
+ (instancetype _Nonnull)fromBuilder:(void (^_Nonnull)(JitsiMeetConferenceOptionsBuilder *_Nonnull))initBlock;
|
||||
- (instancetype _Nonnull)init NS_UNAVAILABLE;
|
||||
|
||||
|
||||
@@ -26,35 +26,23 @@ static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
|
||||
|
||||
|
||||
@implementation JitsiMeetConferenceOptionsBuilder {
|
||||
NSNumber *_audioOnly;
|
||||
NSNumber *_audioMuted;
|
||||
NSNumber *_videoMuted;
|
||||
NSMutableDictionary *_featureFlags;
|
||||
NSMutableDictionary *_config;
|
||||
}
|
||||
|
||||
@dynamic audioOnly;
|
||||
@dynamic audioMuted;
|
||||
@dynamic videoMuted;
|
||||
@dynamic welcomePageEnabled;
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
_serverURL = nil;
|
||||
_room = nil;
|
||||
_subject = nil;
|
||||
_token = nil;
|
||||
|
||||
_colorScheme = nil;
|
||||
_config = [[NSMutableDictionary alloc] init];
|
||||
_featureFlags = [[NSMutableDictionary alloc] init];
|
||||
|
||||
_audioOnly = nil;
|
||||
_audioMuted = nil;
|
||||
_videoMuted = nil;
|
||||
|
||||
_userInfo = nil;
|
||||
|
||||
_callHandle = nil;
|
||||
_callUUID = nil;
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -68,32 +56,48 @@ static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
|
||||
_featureFlags[flag] = value;
|
||||
}
|
||||
|
||||
#pragma mark - Dynamic properties
|
||||
|
||||
- (void)setAudioOnly:(BOOL)audioOnly {
|
||||
_audioOnly = [NSNumber numberWithBool:audioOnly];
|
||||
}
|
||||
|
||||
- (BOOL)audioOnly {
|
||||
return _audioOnly && [_audioOnly boolValue];
|
||||
[self setConfigOverride:@"startAudioOnly" withBoolean:audioOnly];
|
||||
}
|
||||
|
||||
- (void)setAudioMuted:(BOOL)audioMuted {
|
||||
_audioMuted = [NSNumber numberWithBool:audioMuted];
|
||||
}
|
||||
|
||||
- (BOOL)audioMuted {
|
||||
return _audioMuted && [_audioMuted boolValue];
|
||||
[self setConfigOverride:@"startWithAudioMuted" withBoolean:audioMuted];
|
||||
}
|
||||
|
||||
- (void)setVideoMuted:(BOOL)videoMuted {
|
||||
_videoMuted = [NSNumber numberWithBool:videoMuted];
|
||||
[self setConfigOverride:@"startWithVideoMuted" withBoolean:videoMuted];
|
||||
}
|
||||
|
||||
- (BOOL)videoMuted {
|
||||
return _videoMuted && [_videoMuted boolValue];
|
||||
- (void)setCallHandle:(NSString *_Nonnull)callHandle {
|
||||
[self setConfigOverride:@"callHandle" withValue:callHandle];
|
||||
}
|
||||
|
||||
- (void)setCallUUID:(NSUUID *_Nonnull)callUUID {
|
||||
[self setConfigOverride:@"callUUID" withValue:[callUUID UUIDString]];
|
||||
}
|
||||
|
||||
- (void)setSubject:(NSString *_Nonnull)subject {
|
||||
[self setConfigOverride:@"subject" withValue:subject];
|
||||
}
|
||||
|
||||
- (void)setConfigOverride:(NSString *_Nonnull)config withBoolean:(BOOL)value {
|
||||
[self setConfigOverride:config withValue:[NSNumber numberWithBool:value]];
|
||||
}
|
||||
|
||||
- (void)setConfigOverride:(NSString *_Nonnull)config withDictionary:(NSDictionary*)dictionary {
|
||||
_config[config] = dictionary;
|
||||
}
|
||||
|
||||
- (void)setConfigOverride:(NSString *_Nonnull)config withArray:( NSArray * _Nonnull)array {
|
||||
_config[config] = array;
|
||||
}
|
||||
|
||||
- (void)setConfigOverride:(NSString *_Nonnull)config withValue:(id _Nonnull)value {
|
||||
_config[config] = value;
|
||||
}
|
||||
|
||||
#pragma mark - Dynamic properties
|
||||
|
||||
- (void)setWelcomePageEnabled:(BOOL)welcomePageEnabled {
|
||||
[self setFeatureFlag:WelcomePageEnabledFeatureFlag
|
||||
withBoolean:welcomePageEnabled];
|
||||
@@ -105,48 +109,17 @@ static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
|
||||
return n != nil ? [n boolValue] : NO;
|
||||
}
|
||||
|
||||
#pragma mark - Private API
|
||||
|
||||
- (NSNumber *)getAudioOnly {
|
||||
return _audioOnly;
|
||||
}
|
||||
|
||||
- (NSNumber *)getAudioMuted {
|
||||
return _audioMuted;
|
||||
}
|
||||
|
||||
- (NSNumber *)getVideoMuted {
|
||||
return _videoMuted;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation JitsiMeetConferenceOptions {
|
||||
NSNumber *_audioOnly;
|
||||
NSNumber *_audioMuted;
|
||||
NSNumber *_videoMuted;
|
||||
NSDictionary *_featureFlags;
|
||||
NSDictionary *_config;
|
||||
}
|
||||
|
||||
@dynamic audioOnly;
|
||||
@dynamic audioMuted;
|
||||
@dynamic videoMuted;
|
||||
@dynamic welcomePageEnabled;
|
||||
|
||||
#pragma mark - Dynamic properties
|
||||
|
||||
- (BOOL)audioOnly {
|
||||
return _audioOnly && [_audioOnly boolValue];
|
||||
}
|
||||
|
||||
- (BOOL)audioMuted {
|
||||
return _audioMuted && [_audioMuted boolValue];
|
||||
}
|
||||
|
||||
- (BOOL)videoMuted {
|
||||
return _videoMuted && [_videoMuted boolValue];
|
||||
}
|
||||
|
||||
- (BOOL)welcomePageEnabled {
|
||||
NSNumber *n = _featureFlags[WelcomePageEnabledFeatureFlag];
|
||||
|
||||
@@ -159,21 +132,15 @@ static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
|
||||
if (self = [super init]) {
|
||||
_serverURL = builder.serverURL;
|
||||
_room = builder.room;
|
||||
_subject = builder.subject;
|
||||
_token = builder.token;
|
||||
|
||||
_colorScheme = builder.colorScheme;
|
||||
|
||||
_audioOnly = [builder getAudioOnly];
|
||||
_audioMuted = [builder getAudioMuted];
|
||||
_videoMuted = [builder getVideoMuted];
|
||||
_config = builder.config;
|
||||
|
||||
_featureFlags = [NSDictionary dictionaryWithDictionary:builder.featureFlags];
|
||||
|
||||
_userInfo = builder.userInfo;
|
||||
|
||||
_callHandle = builder.callHandle;
|
||||
_callUUID = builder.callUUID;
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -198,26 +165,6 @@ static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
|
||||
props[@"colorScheme"] = self.colorScheme;
|
||||
}
|
||||
|
||||
NSMutableDictionary *config = [[NSMutableDictionary alloc] init];
|
||||
if (_audioOnly != nil) {
|
||||
config[@"startAudioOnly"] = @(self.audioOnly);
|
||||
}
|
||||
if (_audioMuted != nil) {
|
||||
config[@"startWithAudioMuted"] = @(self.audioMuted);
|
||||
}
|
||||
if (_videoMuted != nil) {
|
||||
config[@"startWithVideoMuted"] = @(self.videoMuted);
|
||||
}
|
||||
if (_subject != nil) {
|
||||
config[@"subject"] = self.subject;
|
||||
}
|
||||
if (_callHandle != nil) {
|
||||
config[@"callHandle"] = self.callHandle;
|
||||
}
|
||||
if (_callUUID != nil) {
|
||||
config[@"callUUID"] = [self.callUUID UUIDString];
|
||||
}
|
||||
|
||||
NSMutableDictionary *urlProps = [[NSMutableDictionary alloc] init];
|
||||
|
||||
// The room is fully qualified.
|
||||
@@ -241,7 +188,7 @@ static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
|
||||
props[@"userInfo"] = [self.userInfo asDict];
|
||||
}
|
||||
|
||||
urlProps[@"config"] = config;
|
||||
urlProps[@"config"] = _config;
|
||||
props[@"url"] = urlProps;
|
||||
|
||||
return props;
|
||||
|
||||
@@ -144,7 +144,6 @@ public class PiPViewCoordinator {
|
||||
/// screen size changes
|
||||
public func resetBounds(bounds: CGRect) {
|
||||
currentBounds = bounds
|
||||
exitPictureInPicture()
|
||||
}
|
||||
|
||||
/// Stop the dragging gesture of the root view
|
||||
|
||||
@@ -586,6 +586,7 @@
|
||||
},
|
||||
"speaker": "Говорещ",
|
||||
"speakerStats": {
|
||||
"search": "Търсене",
|
||||
"hours": "{{count}}ч",
|
||||
"minutes": "{{count}}мин",
|
||||
"name": "Име",
|
||||
|
||||
@@ -802,6 +802,7 @@
|
||||
},
|
||||
"speaker": "Sprecher/-in",
|
||||
"speakerStats": {
|
||||
"search": "Suche",
|
||||
"hours": "{{count}} Std. ",
|
||||
"minutes": "{{count}} Min. ",
|
||||
"name": "Name",
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
"e2eeDescription": "El cifrado de extremo a extremo es actualmente EXPERIMENTAL. Tenga en cuenta que activarlo puede deshabilitar servicios como: grabación, transmisión en vivo y participación telefónica. Además, esta reunión solo funcionará con personas que se unan con un navegador.",
|
||||
"e2eeWarning": "ADVERTENCIA: No todos los participantes de esta reunión soportan el cifrado de extremo a extremo. Si usted habilita esta opción, ellos no podrán verlo ni oírlo.",
|
||||
"enterDisplayName": "Por favor ingresa tu nombre aquí",
|
||||
"enterDisplayNameToJoin": "Por favor ingresa tu nombre para unirte",
|
||||
"error": "Error",
|
||||
"externalInstallationMsg": "Necesita instalar nuestra extensión para compartir pantalla.",
|
||||
"externalInstallationTitle": "Extensión requerida",
|
||||
@@ -952,4 +953,4 @@
|
||||
"reject": "Rechazar",
|
||||
"toggleLabel": "Activar sala de espera"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"today": "Aujourd'hui"
|
||||
},
|
||||
"chat": {
|
||||
"enter": "Entrez dans le salon de chat",
|
||||
"enter": "Entrez dans le salon",
|
||||
"error": "Erreur : votre message n'a pas été envoyé. Raison : {{error}}",
|
||||
"fieldPlaceHolder": "Tapez votre message ici",
|
||||
"messagebox": "Saisissez un message",
|
||||
@@ -66,15 +66,19 @@
|
||||
"noMessagesMessage": "Il n'y a pas encore de messages dans cette réunion. Démarrez une conversation ici !",
|
||||
"nickname": {
|
||||
"popover": "Choisissez un pseudonyme",
|
||||
"title": "Entrez un pseudonyme pour utiliser le chat"
|
||||
"title": "Entrez un pseudonyme pour utiliser le chat et les sondages"
|
||||
},
|
||||
"privateNotice": "Message privé à {{recipient}}",
|
||||
"title": "Chat",
|
||||
"you": "vous",
|
||||
"message": "Message",
|
||||
"messageAccessibleTitle": "{{user}} dit: ",
|
||||
"messageAccessibleTitleMe": "Je dis: ",
|
||||
"smileysPanel": "Panneaux des Émojis"
|
||||
"smileysPanel": "Panneaux des Émojis",
|
||||
"tabs": {
|
||||
"chat": "Chat",
|
||||
"polls": "Sondages"
|
||||
},
|
||||
"title": "Chat et Sondages",
|
||||
"you": "vous"
|
||||
},
|
||||
"chromeExtensionBanner": {
|
||||
"installExtensionText": "Installer l'extension pour l'intégration de Google Calendar et Office 365",
|
||||
@@ -568,6 +572,34 @@
|
||||
},
|
||||
"passwordSetRemotely": "défini par un autre participant",
|
||||
"passwordDigitsOnly": "Jusqu'à {{number}} chiffres",
|
||||
"polls": {
|
||||
"create": {
|
||||
"addOption": "Ajouter une option",
|
||||
"answerPlaceholder": "Option {{index}}",
|
||||
"create": "Créer un sondage",
|
||||
"cancel": "Annuler",
|
||||
"pollOption" : "Option {{index}}",
|
||||
"pollQuestion" : "Question du sondage",
|
||||
"questionPlaceholder": "Poser une question",
|
||||
"removeOption": "Supprimer l'option",
|
||||
"send": "Envoyer"
|
||||
},
|
||||
"answer": {
|
||||
"skip": "Passer",
|
||||
"submit": "Envoyer"
|
||||
},
|
||||
"results": {
|
||||
"vote": "Voter",
|
||||
"changeVote": "Changer le vote",
|
||||
"empty": "Il n'y a pas encore de sondages dans cette réunion. Démarrez un sondage ici !",
|
||||
"hideDetailedResults": "Cacher les détails",
|
||||
"showDetailedResults": "Montrer les détails"
|
||||
},
|
||||
"notification": {
|
||||
"title": "Un nouveau sondage a été ajouté à la réunion",
|
||||
"description": "Ouvrez l'onget des sondages pour voter"
|
||||
}
|
||||
},
|
||||
"poweredby": "produit par",
|
||||
"prejoin": {
|
||||
"audioAndVideoError": "Erreur audio et video:",
|
||||
|
||||
@@ -58,17 +58,26 @@
|
||||
"today": "Hoje"
|
||||
},
|
||||
"chat": {
|
||||
"enter": "Entrar na sala",
|
||||
"error": "Erro: a sua mensagem não foi enviada. Motivo: {{error}}",
|
||||
"fieldPlaceHolder": "",
|
||||
"messagebox": "Digite uma mensagem",
|
||||
"fieldPlaceHolder": "Escreva aqui a sua mensagem",
|
||||
"messagebox": "Escreva uma mensagem",
|
||||
"messageTo": "Mensagem privada para {{recipient}}",
|
||||
"noMessagesMessage": "",
|
||||
"noMessagesMessage": "Ainda não há mensagens na reunião. Comece aqui uma conversa!",
|
||||
"nickname": {
|
||||
"popover": "Escolha um apelido",
|
||||
"title": "Digite um apelido para usar na conversação"
|
||||
"title": "Introduza um apelido para usar no chat e nas sondagens"
|
||||
},
|
||||
"privateNotice": "Mensagem privada para {{recipient}}",
|
||||
"title": "Conversação",
|
||||
"message": "Mensagem",
|
||||
"messageAccessibleTitle": "{{user}} disse:",
|
||||
"messageAccessibleTitleMe": "Você disse:",
|
||||
"smileysPanel": "Painel de Emojis",
|
||||
"tabs": {
|
||||
"chat": "Chat",
|
||||
"polls": "Sondagens"
|
||||
},
|
||||
"title": "Chat e Sondagens",
|
||||
"you": "você"
|
||||
},
|
||||
"connectingOverlay": {
|
||||
@@ -125,9 +134,11 @@
|
||||
"appNotInstalled": "Precisa da aplicação móvel {{app}} para participar na reunião com o seu telefone.",
|
||||
"description": "Não acontece nada? Estamos a tentar iniciar a sua reunião na aplicação desktop {{app}}. Tente novamente ou inicie na aplicação web {{app}}.",
|
||||
"descriptionWithoutWeb": "Não aconteceu nada? Tentamos iniciar a sua reunião na aplicação desktop {{app}}.",
|
||||
"downloadApp": "Transfira a Aplicação",
|
||||
"downloadApp": "Transfira a aplicação",
|
||||
"ifDoNotHaveApp": "Se ainda não tem a aplicação:",
|
||||
"ifHaveApp": "Se já tem a aplicação:",
|
||||
"joinInApp": "Participe nesta reunião utilizando a aplicação",
|
||||
"launchWebButton": "Iniciar na web",
|
||||
"openApp": "Continue na aplicação",
|
||||
"title": "A iniciar a sua reunião na {{app}}...",
|
||||
"tryAgainButton": "Tente novamente no desktop"
|
||||
},
|
||||
@@ -377,19 +388,19 @@
|
||||
"keyboardShortcuts": {
|
||||
"focusLocal": "Focar no seu vídeo",
|
||||
"focusRemote": "Focar no vídeo de outro participante",
|
||||
"fullScreen": "Entrar ou sair da tela cheia",
|
||||
"fullScreen": "Entrar ou sair do ecrã completo",
|
||||
"keyboardShortcuts": "Atalhos de teclado",
|
||||
"localRecording": "Mostrar ou ocultar controlos de gravação local",
|
||||
"mute": "Deixar mudo ou não o microfone",
|
||||
"mute": "Ligar ou desligar o seu microfone",
|
||||
"pushToTalk": "Pressione para falar",
|
||||
"raiseHand": "Erga ou baixe sua mão",
|
||||
"showSpeakerStats": "Exibir estatísticas do alto falante",
|
||||
"toggleChat": "Abrir ou fechar o painel de bate-papo",
|
||||
"raiseHand": "Levantar ou baixar a sua mão",
|
||||
"showSpeakerStats": "Mostrar as estatísticas dos participantes",
|
||||
"toggleChat": "Abrir ou fechar o painel de chat",
|
||||
"toggleFilmstrip": "Mostrar ou ocultar miniaturas de vídeo",
|
||||
"toggleScreensharing": "Trocar entre câmera e compartilhamento de tela",
|
||||
"toggleParticipantsPane": "Mostrar ou ocultar o painel de participantes",
|
||||
"toggleScreensharing": "Alternar entre a partilha de câmara e de ecrã",
|
||||
"toggleShortcuts": "Mostrar ou ocultar atalhos de teclado",
|
||||
"videoMute": "Iniciar ou parar sua câmera",
|
||||
"videoQuality": "Gerenciar qualidade da chamada"
|
||||
"videoMute": "Iniciar ou parar a sua câmara"
|
||||
},
|
||||
"liveStreaming": {
|
||||
"busy": "Estamos trabalhando para liberar os recursos de transmissão. Tente novamente em alguns minutos.",
|
||||
@@ -488,8 +499,58 @@
|
||||
"newDeviceAudioTitle": "Novo dispositivo de áudio detectado",
|
||||
"newDeviceAction": "Usar"
|
||||
},
|
||||
"participantsPane": {
|
||||
"close": "Fechar",
|
||||
"header": "Participantes",
|
||||
"headings": {
|
||||
"lobby": "Sala de espera ({{count}})",
|
||||
"participantsList": "Participantes da reunião ({{count}})",
|
||||
"waitingLobby": "Aguardam na sala de espera ({{count}})"
|
||||
},
|
||||
"actions": {
|
||||
"allow": "Permitir aos participantes:",
|
||||
"blockEveryoneMicCamera": "Bloquear o microfone e a câmara de todos",
|
||||
"invite": "Convidar alguém",
|
||||
"askUnmute": "Pedir para ligar o microfone",
|
||||
"mute": "Silenciar",
|
||||
"muteAll": "Silenciar todos",
|
||||
"muteEveryoneElse": "Silenciar todos os outros",
|
||||
"startModeration": "Ligar o microfone ou a câmara.",
|
||||
"stopEveryonesVideo": "Desligar a câmara de todos",
|
||||
"stopVideo": "Desligar a câmara",
|
||||
"unblockEveryoneMicCamera": "Desbloquear o microfone e a câmara de todos"
|
||||
}
|
||||
},
|
||||
"passwordSetRemotely": "Definido por outro participante",
|
||||
"passwordDigitsOnly": "Até {{number}} dígitos",
|
||||
"polls": {
|
||||
"create": {
|
||||
"addOption": "Adicionar opção",
|
||||
"answerPlaceholder": "Opção {{index}}",
|
||||
"create": "Criar uma sondagem",
|
||||
"cancel": "Cancelar",
|
||||
"pollOption" : "Opção de sondagem {{index}}",
|
||||
"pollQuestion" : "Pergunta de Sondagem",
|
||||
"questionPlaceholder": "Faça uma pergunta",
|
||||
"removeOption": "Remover opção",
|
||||
"send": "Enviar"
|
||||
},
|
||||
"answer": {
|
||||
"skip": "Ignorar",
|
||||
"submit": "Submeter"
|
||||
},
|
||||
"results": {
|
||||
"vote": "Voto",
|
||||
"changeVote": "Mudar o voto",
|
||||
"empty": "Ainda não há sondagens na reunião. Comece aqui uma sondagem!",
|
||||
"hideDetailedResults": "Ocultar detalhes",
|
||||
"showDetailedResults": "Mostrar detalhes"
|
||||
},
|
||||
"notification": {
|
||||
"title": "Uma nova sondagem foi adicionada a esta reunião",
|
||||
"description": "Abrir o separador das sondagens para votar"
|
||||
}
|
||||
},
|
||||
"poweredby": "distribuído por",
|
||||
"prejoin": {
|
||||
"audioAndVideoError": "Erro no áudio e vídeo:",
|
||||
@@ -600,6 +661,12 @@
|
||||
"sectionList": {
|
||||
"pullToRefresh": "Puxe para atualizar"
|
||||
},
|
||||
"security": {
|
||||
"about": "Pode adicionar uma $t(lockRoomPassword) à sua reunião. Os participantes terão de fornecer a $t(lockRoomPassword) antes de serem autorizados a participar na reunião.",
|
||||
"aboutReadOnly": "Os participantes moderadores podem acrescentar uma $t(lockRoomPassword) à reunião. Os participantes terão de fornecer a $t(lockRoomPassword) antes de serem autorizados a participar na reunião.",
|
||||
"insecureRoomNameWarning": "O nome da sala é inseguro. Participantes indesejados podem juntar-se à sua conferência. Considere proteger a sua reunião utilizando o botão de segurança.",
|
||||
"securityOptions": "Opções de segurança"
|
||||
},
|
||||
"settings": {
|
||||
"calendar": {
|
||||
"about": "A integração do calendário {{appName}} é utilizada para aceder com segurança ao seu calendário para que este possa ler os próximos eventos.",
|
||||
@@ -633,7 +700,7 @@
|
||||
"speakers": "Participantes",
|
||||
"startAudioMuted": "Todos começam com microfone desligado",
|
||||
"startVideoMuted": "Todos começam com câmara desligada",
|
||||
"talkWhileMuted": "se fala e está com microfone desligado",
|
||||
"talkWhileMuted": "Se fala e está com microfone desligado",
|
||||
"title": "Definições"
|
||||
},
|
||||
"settingsView": {
|
||||
@@ -659,18 +726,19 @@
|
||||
"dialInfoText": "\n\n=====\n\nDeseja apenas discar no seu telefone?\n\n{{defaultDialInNumber}}Clique neste link para ver os números de telefone para esta reunião\n{{dialInfoPageUrl}}",
|
||||
"mainText": "Clique no seguinte link para entrar na reunião:{{roomUrl}}\n"
|
||||
},
|
||||
"speaker": "Alto-falantes",
|
||||
"speaker": "Participante",
|
||||
"speakerStats": {
|
||||
"hours": "{{count}}h",
|
||||
"minutes": "{{count}}m",
|
||||
"name": "Nome",
|
||||
"seconds": "{{count}}s",
|
||||
"speakerStats": "Estatísticas do Apresentador",
|
||||
"speakerTime": "Tempo do Apresentador"
|
||||
"speakerStats": "Estatísticas dos Participantes",
|
||||
"speakerTime": "Tempo do Participante"
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
"title": "O {{app}} precisa usar seu microfone e câmera."
|
||||
"genericTitle": "A reunião precisa de usar o seu microfone e câmara.",
|
||||
"title": "{{app}} precisa de usar o seu microfone e câmara."
|
||||
},
|
||||
"suspendedoverlay": {
|
||||
"rejoinKeyTitle": "Reconectar",
|
||||
@@ -689,7 +757,7 @@
|
||||
"embedMeeting": "Reunião incorporada",
|
||||
"feedback": "Deixar comentários",
|
||||
"fullScreen": "Mudar para ecrã completo",
|
||||
"grantModerator": "Atribuir Moderador",
|
||||
"grantModerator": "Converter em moderador",
|
||||
"hangup": "Sair da reunião",
|
||||
"help": "Ajuda",
|
||||
"invite": "Convidar pessoas",
|
||||
@@ -700,7 +768,7 @@
|
||||
"moreActions": "Mais ações",
|
||||
"moreActionsMenu": "Menu de mais ações",
|
||||
"moreOptions": "Mostrar mais opções",
|
||||
"mute": "Mudo / Não mudo",
|
||||
"mute": "Ativar / Desativar microfone",
|
||||
"muteEveryone": "Silenciar a todos",
|
||||
"muteEveryoneElse": "Silenciar todos os outros",
|
||||
"muteEveryonesVideo": "Desativar a câmara de todos",
|
||||
@@ -711,7 +779,7 @@
|
||||
"profile": "Editar o seu perfil",
|
||||
"raiseHand": "Levantar / Baixar a mão",
|
||||
"recording": "Mudar gravação",
|
||||
"remoteMute": "Participante mudo",
|
||||
"remoteMute": "Participante sem som",
|
||||
"remoteVideoMute": "Desativar a câmara do participante",
|
||||
"security": "Opções de segurança",
|
||||
"Settings": "Mudar configurações",
|
||||
@@ -761,7 +829,7 @@
|
||||
"lowerYourHand": "Baixar a mão",
|
||||
"moreActions": "Mais ações",
|
||||
"moreOptions": "Mais opções",
|
||||
"mute": "Mudo / Não mudo",
|
||||
"mute": "Ativar / Desativar microfone",
|
||||
"muteEveryone": "Silenciar todos",
|
||||
"muteEveryonesVideo": "Desativar a câmara de todos",
|
||||
"noAudioSignalTitle": "Não há nenhuma entrada vinda do seu microfone!",
|
||||
@@ -790,7 +858,7 @@
|
||||
"stopScreenSharing": "Parar partilha de ecrã",
|
||||
"stopSubtitles": "Parar legendas",
|
||||
"stopSharedVideo": "Parar vídeo do YouTube",
|
||||
"talkWhileMutedPopup": "Está a tentar falar? Está mudo.",
|
||||
"talkWhileMutedPopup": "Está a tentar falar? Está com o som desligado.",
|
||||
"tileViewToggle": "Mudar para vista em quadrícula",
|
||||
"toggleCamera": "Mudar a câmara",
|
||||
"videomute": "Iniciar / Parar câmara",
|
||||
@@ -849,15 +917,20 @@
|
||||
"standardDefinition": "Definição padrão"
|
||||
},
|
||||
"videothumbnail": {
|
||||
"domute": "Mudo",
|
||||
"connectionInfo": "Informações sobre a ligação",
|
||||
"domute": "Sem som",
|
||||
"domuteVideo": "Desativar a câmara",
|
||||
"domuteOthers": "Silenciar todos os outros",
|
||||
"domuteVideoOfOthers": "Desactivar a câmara de todos os outros",
|
||||
"flip": "Inverter",
|
||||
"grantModerator": "Converter em moderador",
|
||||
"kick": "Expulsar",
|
||||
"moderator": "Moderador",
|
||||
"mute": "Participante está mudo",
|
||||
"muted": "Mudo",
|
||||
"remoteControl": "Controle remoto",
|
||||
"show": "Mostrar no palco",
|
||||
"videomute": "O participante parou a câmera"
|
||||
"mute": "Participante está sem som",
|
||||
"muted": "Sem som",
|
||||
"videomute": "O participante parou a câmara",
|
||||
"remoteControl": "Iniciar / Parar o controlo remoto",
|
||||
"show": "Mostrar no palco"
|
||||
},
|
||||
"welcomepage": {
|
||||
"accessibilityLabel": {
|
||||
@@ -888,5 +961,41 @@
|
||||
"sendFeedback": "Enviar comentários",
|
||||
"terms": "Termos",
|
||||
"title": "Videoconferências mais seguras, flexíveis e totalmente gratuitas"
|
||||
},
|
||||
"lobby": {
|
||||
"admit": "Aceitar",
|
||||
"admitAll": "Aceitar todos",
|
||||
"knockingParticipantList": "Lista de participantes a expulsar",
|
||||
"allow": "Permitir",
|
||||
"backToKnockModeButton": "Sem senha, peça para aderir em vez disso",
|
||||
"dialogTitle": "Modo sala de espera",
|
||||
"disableDialogContent": "O modo sala de espera está actualmente activada. Esta característica assegura que os participantes indesejados não possam juntar-se à sua reunião. Quer desativá-la?",
|
||||
"disableDialogSubmit": "Desativar",
|
||||
"emailField": "Introduza o seu endereço de e-mail",
|
||||
"enableDialogPasswordField": "Definir senha (opcional)",
|
||||
"enableDialogSubmit": "Habilitar",
|
||||
"enableDialogText": "O modo sala de espera permite-lhe proteger a sua reunião apenas permitindo a entrada de pessoas após uma aprovação formal por um moderador.",
|
||||
"enterPasswordButton": "Introduza a senha da reunião",
|
||||
"enterPasswordTitle": "Introduzir a senha para participar na reunião",
|
||||
"invalidPassword": "Senha inválida",
|
||||
"joiningMessage": "Participará na reunião assim que alguém aceitar o seu pedido",
|
||||
"joinWithPasswordMessage": "Tentando aderir com senha, por favor aguarde...",
|
||||
"joinRejectedMessage": "O seu pedido de adesão foi rejeitado por um moderador.",
|
||||
"joinTitle": "Junte-se à reunião",
|
||||
"joiningTitle": "Pedir para participar na reunião...",
|
||||
"joiningWithPasswordTitle": "Participando com senha...",
|
||||
"knockButton": "Pedir para participar",
|
||||
"knockTitle": "Alguém quer juntar-se à reunião",
|
||||
"nameField": "Introduza o seu nome",
|
||||
"notificationLobbyAccessDenied": "{{targetParticipantName}} foi recusada a adesão por {{originParticipantName}}",
|
||||
"notificationLobbyAccessGranted": "{{targetParticipantName}} foi autorizado a aderir por {{originParticipantName}}",
|
||||
"notificationLobbyDisabled": "A sala de espera foi desactivada por {{originParticipantName}}",
|
||||
"notificationLobbyEnabled": "A sala de espera foi activada por {{originParticipantName}}",
|
||||
"notificationTitle": "Sala de espera",
|
||||
"passwordField": "Introduza a senha da reunião",
|
||||
"passwordJoinButton": "Solicitar",
|
||||
"reject": "Rejeitar",
|
||||
"rejectAll": "Rejeitar todos",
|
||||
"toggleLabel": "Ativar sala de espera"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"copyInvite": "Copiar convite da reunião",
|
||||
"copyLink": "Copiar link da reunião",
|
||||
"copyStream": "Copiar link da transmissão ao vivo",
|
||||
"contacts": "contatos",
|
||||
"countryNotSupported": "Ainda não suportamos este destino.",
|
||||
"countryReminder": "Ligando de fora dos EUA? Por favor, certifique-se de começar com o código do país!",
|
||||
"defaultEmail": "Seu email padrão",
|
||||
@@ -16,18 +17,14 @@
|
||||
"inviteMoreMailSubject": "Entre na reunião {{appName}}",
|
||||
"inviteMorePrompt": "Convide mais pessoas",
|
||||
"linkCopied": "Link copiado para a área de transferência",
|
||||
"loading": "Procurando por pessoas e números de telefone",
|
||||
"loadingNumber": "Validando o número de telefone",
|
||||
"loadingPeople": "Procurando por pessoas para convidar",
|
||||
"noResults": "Nenhum resultado de busca correspondente",
|
||||
"noValidNumbers": "Por favor, digite um número de telefone",
|
||||
"outlookEmail": "E-mail Outlook ",
|
||||
"searchNumbers": "Adicionar números de telefone",
|
||||
"searchPeople": "Pesquisar pessoas",
|
||||
"searchPeopleAndNumbers": "Pesquisar por pessoas ou adicionar seus números de telefone",
|
||||
"phoneNumbers": "números de telefone",
|
||||
"searching": "Procurando...",
|
||||
"shareInvite": "Compartilhar convite da reunião",
|
||||
"shareLink": "Compartilhando o link da reunião",
|
||||
"shareStream": "Compartilhar o link da transmissão ao vivo",
|
||||
"sipAddresses": "endereços SIP",
|
||||
"telephone": "Telefone: {{number}}",
|
||||
"title": "Convide pessoas para sua reunião",
|
||||
"yahooEmail": "E-mail Yahoo"
|
||||
@@ -61,6 +58,7 @@
|
||||
"today": "Hoje"
|
||||
},
|
||||
"chat": {
|
||||
"enter": "Entrar no bate-papo",
|
||||
"error": "Erro: sua mensagem não foi enviada. Motivo: {{error}}",
|
||||
"fieldPlaceHolder": "Digite sua mensagem aqui",
|
||||
"messagebox": "Digite uma mensagem",
|
||||
@@ -72,12 +70,17 @@
|
||||
},
|
||||
"privateNotice": "Mensagem privada para {{recipient}}",
|
||||
"title": "Bate-papo",
|
||||
"you": "você"
|
||||
"you": "você",
|
||||
"message": "Mensagem",
|
||||
"messageAccessibleTitle": "{{user}} disse:",
|
||||
"messageAccessibleTitleMe": "Você disse:",
|
||||
"smileysPanel": "Painel de Emojis"
|
||||
},
|
||||
"chromeExtensionBanner": {
|
||||
"installExtensionText": "Instale a extensão para integrar com Google Calendar e Office 365",
|
||||
"buttonText": "Instalar extensão do Chrome",
|
||||
"dontShowAgain": "Não me mostre isso de novo"
|
||||
"dontShowAgain": "Não me mostre isso de novo",
|
||||
"close": "Fechar"
|
||||
},
|
||||
"connectingOverlay": {
|
||||
"joiningRoom": "Conectando você à reunião…"
|
||||
@@ -174,12 +177,14 @@
|
||||
"alreadySharedVideoMsg": "Outro participante já está compartilhando um vídeo. Esta conferência permite apenas um vídeo compartilhado por vez.",
|
||||
"alreadySharedVideoTitle": "Somente um vídeo compartilhado é permitido por vez",
|
||||
"applicationWindow": "Janela de aplicativo",
|
||||
"authenticationRequired": "Autenticação requerida",
|
||||
"Back": "Voltar",
|
||||
"cameraConstraintFailedError": "Sua câmera não satisfaz algumas condições necessárias.",
|
||||
"cameraNotFoundError": "A câmera não foi encontrada.",
|
||||
"cameraNotSendingData": "Estamos incapazes de acessar sua câmera. Verifique se outra aplicação está usando este dispositivo, selecione outro dispositivo do menu de configurações ou recarregue a aplicação.",
|
||||
"cameraNotSendingDataTitle": "Incapaz de acessar a câmera",
|
||||
"cameraPermissionDeniedError": "Não foi permitido acessar a sua câmera. Você ainda pode entrar na conferência, mas sem exibir o seu vídeo. Clique no botão da câmera para tentar reparar.",
|
||||
"cameraTimeoutError": "Não foi possível iniciar fonte de vídeo. Tempo esgotado!",
|
||||
"cameraUnknownError": "Não pode usar a câmera por uma razão desconhecida.",
|
||||
"cameraUnsupportedResolutionError": "Sua câmera não suporta a resolução de vídeo requerida.",
|
||||
"Cancel": "Cancelar",
|
||||
@@ -194,7 +199,7 @@
|
||||
"connectError": "Oops! Alguma coisa está errada e nós não pudemos conectar à conferência.",
|
||||
"connectErrorWithMsg": "Oops! Alguma coisa está errada e não podemos conectar à conferência: {{msg}}",
|
||||
"connecting": "Conectando",
|
||||
"contactSupport": "Contate o suporte",
|
||||
"contactSupport": "Entrar em contato com o suporte",
|
||||
"copied": "Copiado",
|
||||
"copy": "Copiar",
|
||||
"dismiss": "Dispensar",
|
||||
@@ -204,28 +209,31 @@
|
||||
"e2eeLabel": "Enable End-to-End Encryption",
|
||||
"e2eeWarning": "WARNING: Not all participants in this meeting seem to have support for End-to-End encryption. If you enable it they won't be able to see nor hear you.",
|
||||
"enterDisplayName": "Digite seu nome aqui",
|
||||
"enterDisplayNameToJoin": "Digite seu nome para participar",
|
||||
"embedMeeting": "Reunião em formato compacto",
|
||||
"error": "Erro",
|
||||
"gracefulShutdown": "Nosso serviço está em manutenção. Tente novamente mais tarde.",
|
||||
"grantModeratorDialog": "Tem certeza que quer participar como moderador da reunião?",
|
||||
"grantModeratorTitle": "Permitir moderador",
|
||||
"hideShareAudioHelper": "Não mostre este diálogo novamente",
|
||||
"IamHost": "Eu sou o anfitrião",
|
||||
"incorrectRoomLockPassword": "Senha incorreta",
|
||||
"incorrectPassword": "Usuário ou senha incorretos",
|
||||
"internalError": "Oops! Alguma coisa está errada. O seguinte erro ocorreu: {{error}}",
|
||||
"internalErrorTitle": "Erro interno",
|
||||
"kickMessage": "Você pode contatar com {{participantDisplayName}} para obter mais detalhes.",
|
||||
"kickMessage": "Você pode entrar em contato com {{participantDisplayName}} para obter mais detalhes.",
|
||||
"kickParticipantButton": "Remover",
|
||||
"kickParticipantDialog": "Tem certeza de que deseja remover este participante?",
|
||||
"kickParticipantTitle": "Chutar este participante?",
|
||||
"kickTitle": "Ai! {{participantDisplayName}} expulsou você da reunião",
|
||||
"kickParticipantTitle": "Remover este participante?",
|
||||
"kickTitle": "{{participantDisplayName}} removeu você da reunião",
|
||||
"liveStreaming": "Transmissão ao Vivo",
|
||||
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Não é possível transmitir enquanto a gravação está ativa",
|
||||
"liveStreamingDisabledForGuestTooltip": "Visitantes não podem iniciar transmissão ao vivo.",
|
||||
"liveStreamingDisabledTooltip": "Iniciar transmissão ao vivo desativada.",
|
||||
"lockMessage": "Falha ao travar a conferência.",
|
||||
"lockMessage": "Falha ao bloquear a conferência.",
|
||||
"lockRoom": "Adicionar reunião $t(lockRoomPasswordUppercase)",
|
||||
"lockTitle": "Bloqueio falhou",
|
||||
"logoutQuestion": "Deseja encerrar a sessão e finalizar a conferência?",
|
||||
"login": "Entrar",
|
||||
"logoutTitle": "Encerrar sessão",
|
||||
"maxUsersLimitReached": "O limite para o número máximo de participantes foi atingido. A conferência está cheia. Entre em contato com o proprietário da reunião ou tente novamente mais tarde!",
|
||||
"maxUsersLimitReachedTitle": "Limite máximo de participantes atingido",
|
||||
@@ -234,28 +242,42 @@
|
||||
"micNotSendingData": "Vá para as configurações do seu computador para ativar o som do microfone e ajustar seu nível",
|
||||
"micNotSendingDataTitle": "Seu microfone está mudo pelas configurações do sistema",
|
||||
"micPermissionDeniedError": "Não foi permitido acessar o seu microfone. Você ainda pode entrar na conferência, mas sem enviar áudio. Clique no botão do microfone para tentar reparar.",
|
||||
"micTimeoutError": "Não foi possível iniciar fonte de áudio. Tempo esgotado!",
|
||||
"micUnknownError": "Não pode usar o microfone por uma razão desconhecida.",
|
||||
"muteEveryoneElseDialog": "Uma vez silenciados, você não poderá reativar o som deles, mas eles poderão reativar o som a qualquer momento.",
|
||||
"muteEveryoneElseTitle": "Silenciar todo mundo exceto {{whom}}?",
|
||||
"muteEveryoneDialog": "Tem certeza que deseja silenciar todos? Você não poderá ativar o som deles, mas eles podem ativar o som eles mesmo a qualquer momento.",
|
||||
"muteEveryoneTitle": "Silenciar todos?",
|
||||
"muteEveryoneElsesVideoDialog": "Você não poderá reativar posteriormente, mas cada participante pode ativar sua própria câmera a qualquer momento.",
|
||||
"muteEveryoneElsesVideoTitle": "Desativar a câmera de todos exceto {{whom}}?",
|
||||
"muteEveryonesVideoDialog": "Tem certeza que deseja desativar a câmera de todos? Você não poderá reativar posteriormente, mas cada participante pode ativar sua própria câmera a qualquer momento.",
|
||||
"muteEveryonesVideoDialogOk": "Desabilitar",
|
||||
"muteEveryonesVideoTitle": "Desativar a câmera de todos?",
|
||||
"muteEveryoneSelf": "a si próprio",
|
||||
"muteEveryoneStartMuted": "Todos iniciam silenciados daqui para frente",
|
||||
"muteParticipantBody": "Você não está habilitado para tirar o mudo deles, mas eles podem tirar o mudo deles mesmos a qualquer tempo.",
|
||||
"muteParticipantButton": "Mudo",
|
||||
"muteParticipantDialog": "Tem certeza de que deseja silenciar este participante? Você não poderá desfazer isso, mas o participante pode reabilitar o áudio a qualquer momento.",
|
||||
"muteParticipantsVideoDialog": "Tem certeza de que deseja desativar a câmera deste participante? Você não poderá reativar posteriormente, mas o participante pode ativar sua própria câmera a qualquer momento.",
|
||||
"muteParticipantTitle": "Deixar mudo este participante?",
|
||||
"Ok": "Ok",
|
||||
"passwordLabel": "A reunião foi travada por um participante. Por favor, insira a $t(lockRoomPassword) para entrar.",
|
||||
"muteParticipantsVideoButton": "Desativar a câmera",
|
||||
"muteParticipantsVideoTitle": "Desativar a câmera deste participante?",
|
||||
"muteParticipantsVideoBody": "Você não poderá reativar posteriormente, mas o participante pode ativar sua própria câmera a qualquer momento.",
|
||||
"noDropboxToken": "Nenhum token do Dropbox válido",
|
||||
"Ok": "OK",
|
||||
"password": "Senha",
|
||||
"passwordLabel": "A reunião foi bloqueada por um participante. Por favor, insira a $t(lockRoomPassword) para entrar.",
|
||||
"passwordNotSupported": "A configuração de uma reunião $t(lockRoomPassword) não é suportada.",
|
||||
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) não suportado",
|
||||
"passwordRequired": "$t(lockRoomPasswordUppercase) requerido",
|
||||
"permissionErrorTitle": "Permissão necessária",
|
||||
"permissionCameraRequiredError": "É necessário permitir acesso à câmera para participar de reuniões com vídeo. Ative a permissão nas configurações",
|
||||
"permissionMicRequiredError": "É necessário permitir acesso ao microfone para participar de reuniões com áudio. Ative a permissão nas configurações",
|
||||
"popupError": "Seu navegador está bloqueando janelas popup deste site. Habilite os popups nas configurações de segurança no seu navegador e tente novamente.",
|
||||
"popupErrorTitle": "Popup bloqueado",
|
||||
"readMore": "mais...",
|
||||
"recording": "Gravando",
|
||||
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Não é possível transmitir enquanto a gravação está ativa",
|
||||
"recordingDisabledForGuestTooltip": "Visitantes não podem iniciar gravações.",
|
||||
"recordingDisabledTooltip": "Iniciar gravação desativada.",
|
||||
"rejoinNow": "Reconectar agora",
|
||||
"remoteControlAllowedMessage": "{{user}} aceitou sua requisição de controle remoto!",
|
||||
@@ -272,7 +294,7 @@
|
||||
"reservationError": "Erro de sistema de reserva",
|
||||
"reservationErrorMsg": "Código do erro: {{code}}, mensagem: {{msg}}",
|
||||
"retry": "Tentar novamente",
|
||||
"screenSharingAudio": "Compartilha áudio",
|
||||
"screenSharingAudio": "Compartilhar áudio",
|
||||
"screenSharingFailed": "Oops! Alguma coisa de errado aconteceu, não é possível habilitar o compartilhamento de tela!",
|
||||
"screenSharingFailedTitle": "Falha ao compartilhar a tela!",
|
||||
"screenSharingPermissionDeniedError": "Oops! Alguma coisa está errada com suas permissões de compartilhamento de tela. Recarregue e tente de novo.",
|
||||
@@ -282,15 +304,26 @@
|
||||
"sendPrivateMessageTitle": "Enviar em privado?",
|
||||
"serviceUnavailable": "Serviço indisponível",
|
||||
"sessTerminated": "Chamada terminada",
|
||||
"sessionRestarted": "Chamada reiniciada pelo bridge",
|
||||
"Share": "Compartilhar",
|
||||
"shareAudio": "Continuar",
|
||||
"shareAudioTitle": "Como compartilhar áudio",
|
||||
"shareAudioWarningTitle": "Você precisa parar o compartilhamento de tela antes de compartilhar seu áudio",
|
||||
"shareAudioWarningH1": "Se você quiser compartilhar apenas seu áudio:",
|
||||
"shareAudioWarningD1": "você precisa parar o compartilhamento de tela antes de compartilhar seu áudio.",
|
||||
"shareAudioWarningD2": "você precisa reiniciar o compartilhamento de tela e selecionar a opção \"compartilhar áudio\".",
|
||||
"shareMediaWarningGenericH2": "Se você quiser compartilhar sua tela e seu áudio",
|
||||
"shareVideoLinkError": "Por favor, forneça um link do youtube correto.",
|
||||
"shareVideoTitle": "Compartilhar um vídeo",
|
||||
"shareYourScreen": "Compartilhar sua tela",
|
||||
"shareYourScreenDisabled": "Compartilhamento de tela desativada.",
|
||||
"shareYourScreenDisabledForGuest": "Visitantes não podem compartilhar tela.",
|
||||
"startLiveStreaming": "Iniciar transmissão ao vivo",
|
||||
"startRecording": "Iniciar gravação",
|
||||
"startRemoteControlErrorMessage": "Um erro ocorreu enquanto tentava iniciar uma sessão de controle remoto!",
|
||||
"shareScreenWarningTitle": "Você precisa parar o compartilhamento de áudio antes de compartilhar sua tela",
|
||||
"shareScreenWarningH1": "Se você quiser compartilhar apenas sua tela:",
|
||||
"shareScreenWarningD1": "você precisa parar o compartilhamento de áudio antes de compartilhar sua tela.",
|
||||
"shareScreenWarningD2": "você precisa parar o compartilhamento de áudio, iniciar o compartilhamento de tela e selecionar a opção \"compartilhar áudio\".",
|
||||
"stopLiveStreaming": "Parar transmissão ao vivo",
|
||||
"stopRecording": "Parar a gravação",
|
||||
"stopRecordingWarning": "Tem certeza que deseja parar a gravação?",
|
||||
@@ -303,13 +336,20 @@
|
||||
"tokenAuthFailedTitle": "Falha de autenticação",
|
||||
"transcribing": "Transcrevendo",
|
||||
"unlockRoom": "Remove a reunião $t(lockRoomPassword)",
|
||||
"user": "Usuário",
|
||||
"userIdentifier": "identificação do usuário",
|
||||
"userPassword": "senha do usuário",
|
||||
"videoLink": "Link do vídeo",
|
||||
"viewUpgradeOptions": "Ver opções de atualização",
|
||||
"viewUpgradeOptionsContent": "Para obter acesso ilimitado a recursos premium tais como gravação, transcrição, streaming RTMP e muito mais, você precisa atualizar seu plano.",
|
||||
"viewUpgradeOptionsTitle": "Você descobriu um recurso premium!",
|
||||
"WaitForHostMsg": "A conferência <b>{{room}}</b> ainda não começou. Se você é o anfitrião, faça a autenticação. Do contrário, aguarde a chegada do anfitrião.",
|
||||
"WaitForHostMsgWOk": "A conferência <b>{{room}}</b> ainda não começou. Se você é o anfitrião, pressione Ok para autenticar. Do contrário, aguarde a chegada do anfitrião.",
|
||||
"WaitingForHost": "Esperando o anfitrião...",
|
||||
"WaitForHostMsgWOk": "A conferência <b>{{room}}</b> ainda não começou. Se você é o anfitrião, pressione OK para autenticar. Do contrário, aguarde a chegada do anfitrião.",
|
||||
"WaitingForHostTitle": "Esperando o anfitrião...",
|
||||
"Yes": "Sim",
|
||||
"yourEntireScreen": "Toda sua tela"
|
||||
"yourEntireScreen": "Toda sua tela",
|
||||
"remoteUserControls": "Controles de usuários remotos de {{username}}",
|
||||
"localUserControls": "Controles de usuários locais"
|
||||
},
|
||||
"dialOut": {
|
||||
"statusMessage": "está agora {{status}}"
|
||||
@@ -323,27 +363,27 @@
|
||||
"embedMeeting": {
|
||||
"title": "Reunião em formato compacto"
|
||||
},
|
||||
"virtualBackground": {
|
||||
"apply": "Aplicar",
|
||||
"title": "Fundos virtuais",
|
||||
"blur": "Desfoque",
|
||||
"slightBlur": "Desfoque suave",
|
||||
"removeBackground": "Remover fundo",
|
||||
"addBackground": "Adicionar novo fundo",
|
||||
"pleaseWait": "Aguarde...",
|
||||
"none": "Nenhum",
|
||||
"uploadedImage": "Imagem enviada {{index}}",
|
||||
"deleteImage": "Excluir imagem",
|
||||
"image1" : "Praia",
|
||||
"image2" : "Parede branca neutra",
|
||||
"image3" : "Quarto branco vazio",
|
||||
"image4" : "Luminária preta",
|
||||
"image5" : "Montanha",
|
||||
"image6" : "Floresta",
|
||||
"image7" : "Alvorada",
|
||||
"desktopShareError": "Não foi possível compartilhar o desktop",
|
||||
"desktopShare":"Compartilhar desktop",
|
||||
"webAssemblyWarning": "Não há suporte para WebAssembly"
|
||||
"virtualBackground": {
|
||||
"apply": "Aplicar",
|
||||
"title": "Fundos virtuais",
|
||||
"blur": "Desfoque",
|
||||
"slightBlur": "Desfoque suave",
|
||||
"removeBackground": "Remover fundo",
|
||||
"addBackground": "Adicionar novo fundo",
|
||||
"pleaseWait": "Aguarde...",
|
||||
"none": "Nenhum",
|
||||
"uploadedImage": "Imagem enviada {{index}}",
|
||||
"deleteImage": "Excluir imagem",
|
||||
"image1": "Praia",
|
||||
"image2": "Parede branca neutra",
|
||||
"image3": "Quarto branco vazio",
|
||||
"image4": "Luminária preta",
|
||||
"image5": "Montanha",
|
||||
"image6": "Floresta",
|
||||
"image7": "Alvorada",
|
||||
"desktopShareError": "Não foi possível compartilhar o desktop",
|
||||
"desktopShare": "Compartilhar desktop",
|
||||
"webAssemblyWarning": "Não há suporte para WebAssembly"
|
||||
},
|
||||
"feedback": {
|
||||
"average": "Média",
|
||||
@@ -352,7 +392,8 @@
|
||||
"good": "Boa",
|
||||
"rateExperience": "Avalie sua experiência na reunião",
|
||||
"veryBad": "Muito ruim",
|
||||
"veryGood": "Muito boa"
|
||||
"veryGood": "Muito boa",
|
||||
"star": "Estrela"
|
||||
},
|
||||
"incomingCall": {
|
||||
"answer": "Responder",
|
||||
@@ -369,6 +410,7 @@
|
||||
"country": "País",
|
||||
"dialANumber": "Para entrar na reunião, disque um desses números e depois insira o PIN.",
|
||||
"dialInConferenceID": "PIN:",
|
||||
"copyNumber": "Copiar número",
|
||||
"dialInNotSupported": "Desculpe, a discagem não é atualmente suportada.",
|
||||
"dialInNumber": "Discar:",
|
||||
"dialInSummaryError": "Ocorreu um erro ao buscar a informação de discagem. Tente novamente mais tarde.",
|
||||
@@ -377,6 +419,11 @@
|
||||
"inviteLiveStream": "Para ver a transmissão ao vivo da reunião, clique no link: {{url}}",
|
||||
"invitePhone": "Para participar por telefone, toque aqui: {{number}},,{{conferenceID}}#\n",
|
||||
"invitePhoneAlternatives": "Procurando um número de discagem diferente?\nVeja os números de discagem da reunião: {{url}} \n\n\nSe você também estiver discando através de um telefone da sala, participe sem conectar-se ao áudio: {{silentUrl}}",
|
||||
"inviteSipEndpoint": "Para participar usando o endereço SIP, insira isto: {{sipUri}}",
|
||||
"inviteTextiOSPersonal": "{{name}} está convidando você para uma reunião.",
|
||||
"inviteTextiOSJoinSilent": "Se você estiver discando através de um telefone da sala, use este link para participar sem conectar-se ao áudio: {{silentUrl}}.",
|
||||
"inviteTextiOSInviteUrl": "Clique no seguinte link para entrar: {{inviteUrl}}.",
|
||||
"inviteTextiOSPhone": "Para participar por telefone, use este número: {{number}},,{{conferenceID}}#. Se você estiver procurando um número diferente, esta é a lista completa: {{didUrl}}.",
|
||||
"inviteURLFirstPartGeneral": "Você foi convidado para uma reunião.",
|
||||
"inviteURLFirstPartPersonal": "{{name}} está convidando você para uma reunião.\n",
|
||||
"inviteURLSecondPart": "\nEntre na reunião:\n{{url}}\n",
|
||||
@@ -387,6 +434,7 @@
|
||||
"noRoom": "Nenhuma sala foi especificada para entrar.",
|
||||
"numbers": "Números de discagem",
|
||||
"password": "$t(lockRoomPasswordUppercase):",
|
||||
"sip": "endereço SIP",
|
||||
"title": "Compartilhar",
|
||||
"tooltip": "Compartilhar link e discagem para esta reunião",
|
||||
"label": "Informações da reunião"
|
||||
@@ -405,6 +453,7 @@
|
||||
"support": "Suporte",
|
||||
"supportMsg": "Se isso continuar acontecendo, chegar a"
|
||||
},
|
||||
"jitsiHome": "Logo de {{logo}} (direciona para página inicial)",
|
||||
"keyboardShortcuts": {
|
||||
"focusLocal": "Focar no seu vídeo",
|
||||
"focusRemote": "Focar no vídeo de outro participante",
|
||||
@@ -417,10 +466,10 @@
|
||||
"showSpeakerStats": "Exibir estatísticas do alto falante",
|
||||
"toggleChat": "Abrir ou fechar o painel de bate-papo",
|
||||
"toggleFilmstrip": "Mostrar ou ocultar miniaturas de vídeo",
|
||||
"toggleParticipantsPane": "Exibir ou ocultar o quadro de participantes",
|
||||
"toggleScreensharing": "Trocar entre câmera e compartilhamento de tela",
|
||||
"toggleShortcuts": "Mostrar ou ocultar atalhos de teclado",
|
||||
"videoMute": "Iniciar ou parar sua câmera",
|
||||
"videoQuality": "Gerenciar qualidade da chamada"
|
||||
"videoMute": "Iniciar ou parar sua câmera"
|
||||
},
|
||||
"liveStreaming": {
|
||||
"limitNotificationDescriptionWeb": "Devido a alta demanda sua transmissão será limitada a {{limit}} minutos. Para transmissão ilimitada tente <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
|
||||
@@ -496,19 +545,24 @@
|
||||
"focus": "Foco da conferência",
|
||||
"focusFail": "{{component}} não disponível - tente em {{ms}} seg",
|
||||
"grantedTo": "Direitos de moderador concedido para {{to}}!",
|
||||
"hostAskedUnmute": "O anfitrião deseja que você ative o som",
|
||||
"invitedOneMember": "{{name}} foi convidado(a)",
|
||||
"invitedThreePlusMembers": "{{name}} e {{count}} outros foram convidados",
|
||||
"invitedTwoMembers": "{{first}} e {{second}} foram convidados",
|
||||
"kickParticipant": "{{kicked}} foi chutado por {{kicker}}",
|
||||
"kickParticipant": "{{kicked}} foi removido por {{kicker}}",
|
||||
"me": "Eu",
|
||||
"moderator": "Direitos de moderador concedidos!",
|
||||
"muted": "Você iniciou uma conversa em mudo.",
|
||||
"mutedTitle": "Você está mudo!",
|
||||
"mutedRemotelyTitle": "Você foi silenciado por {{participantDisplayName}}!",
|
||||
"mutedRemotelyDescription": "Você sempre pode ativar o som quando estiver pronto para falar. Retire o som quando terminar para manter o ruído longe da reunião.",
|
||||
"videoMutedRemotelyTitle": "Sua câmera foi desativada por {{participantDisplayName}}!",
|
||||
"videoMutedRemotelyDescription": "Você pode ativar sua câmera a qualquer momento.",
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removido por outro participante",
|
||||
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) definido por outro participante",
|
||||
"raisedHand": "{{name}} gostaria de falar.",
|
||||
"screenShareNoAudio": "O compartilhamento de áudio não foi selecionado na tela de escolha de janela.",
|
||||
"screenShareNoAudioTitle": "Compartilhamento de áudio não selecionado",
|
||||
"somebody": "Alguém",
|
||||
"startSilentTitle": "Você entrou sem saída de áudio!",
|
||||
"startSilentDescription": "Volte à reunião para habilitar o áudio",
|
||||
@@ -521,18 +575,41 @@
|
||||
"OldElectronAPPTitle": "Vulnerabilidade de segurança!",
|
||||
"oldElectronClientDescription1": "Você está usando um versão antiga do cliente Jitsi Meet que possui uma conhecida vulnerabilidade de segurança. Por favor tenha certeza de atulizar para a nossa ",
|
||||
"oldElectronClientDescription2": "última versão",
|
||||
"oldElectronClientDescription3": " agora!"
|
||||
"oldElectronClientDescription3": " agora!",
|
||||
"moderationInEffectDescription": "Levante a mão se quiser falar",
|
||||
"moderationInEffectCSDescription": "Levante a mão se quiser compartilhar seu vídeo",
|
||||
"moderationInEffectVideoDescription": "Levante a mão se quiser que seu vídeo fique visível",
|
||||
"moderationInEffectTitle": "O microfone foi silenciado pelo moderador",
|
||||
"moderationInEffectCSTitle": "O compartilhamento de conteúdo foi desativado pelo moderador",
|
||||
"moderationInEffectVideoTitle": "O vídeo foi desativado pelo moderador",
|
||||
"moderationRequestFromModerator": "O anfitrião deseja que você ative o som",
|
||||
"moderationRequestFromParticipant": "Quer falar",
|
||||
"moderationStartedTitle": "Moderação iniciada",
|
||||
"moderationStoppedTitle": "Moderação encerrada",
|
||||
"moderationToggleDescription": "por {{participantDisplayName}}",
|
||||
"raiseHandAction": "Levantar a mão",
|
||||
"groupTitle": "Notificações"
|
||||
},
|
||||
"participantsPane": {
|
||||
"close": "Fechar",
|
||||
"headings": {
|
||||
"lobby": "Sala de espera ({{count}})",
|
||||
"participantsList": "Participantes da reunião ({{count}})"
|
||||
},
|
||||
"actions": {
|
||||
"invite": "Convidar alguém",
|
||||
"muteAll": "Silenciar todos",
|
||||
"stopVideo": "Parar vídeo"
|
||||
"participantsPane": {
|
||||
"close": "Fechar",
|
||||
"header": "Participantes",
|
||||
"headings": {
|
||||
"lobby": "Sala de espera ({{count}})",
|
||||
"participantsList": "Participantes da reunião ({{count}})",
|
||||
"waitingLobby": "Aguardando na sala de espera ({{count}})"
|
||||
},
|
||||
"actions": {
|
||||
"allow": "Permitir aos participantes:",
|
||||
"blockEveryoneMicCamera": "Bloquear microfone e câmera de todos",
|
||||
"invite": "Convidar alguém",
|
||||
"askUnmute": "Pedir para ativar som",
|
||||
"mute": "Silenciar",
|
||||
"muteAll": "Silenciar todos",
|
||||
"muteEveryoneElse": "Silenciar todos os demais",
|
||||
"startModeration": "Ativar som eles mesmos ou iniciar vídeo",
|
||||
"stopEveryonesVideo": "Parar vídeo de todos",
|
||||
"stopVideo": "Parar vídeo",
|
||||
"unblockEveryoneMicCamera": "Desbloquear microfone e câmera de todos"
|
||||
}
|
||||
},
|
||||
"passwordSetRemotely": "Definido por outro participante",
|
||||
@@ -588,7 +665,8 @@
|
||||
"lookGood": "Seu microfone está funcionando corretamente",
|
||||
"or": "ou",
|
||||
"premeeting": "Pré-reunião",
|
||||
"showScreen": "Habiltar tela pré-reunião",
|
||||
"showScreen": "Habilitar tela pré-reunião",
|
||||
"keyboardShortcuts": "Habilitar atalhos de teclado",
|
||||
"startWithPhone": "Iniciar com o áudio da ligação",
|
||||
"screenSharingError": "Erro de compartilhamento de tela:",
|
||||
"videoOnlyError": "Erro de vídeo:",
|
||||
@@ -610,6 +688,7 @@
|
||||
"ringing": "Tocando..."
|
||||
},
|
||||
"profile": {
|
||||
"avatar": "avatar",
|
||||
"setDisplayNameLabel": "Definir seu nome de exibição",
|
||||
"setEmailInput": "Digite e-mail",
|
||||
"setEmailLabel": "Definir seu e-mail de Gravatar",
|
||||
@@ -624,12 +703,15 @@
|
||||
"beta": "BETA",
|
||||
"busy": "Estamos trabalhando para liberar recursos de gravação. Tente novamente em alguns minutos.",
|
||||
"busyTitle": "Todas as gravações estão atualmente ocupadas",
|
||||
"copyLink": "Copiar Link",
|
||||
"error": "A gravação falhou. Tente novamente.",
|
||||
"errorFetchingLink": "Erro ao buscar link da gravação.",
|
||||
"expandedOff": "Gravação finalizada",
|
||||
"expandedOn": "A reunião está sendo gravada.",
|
||||
"expandedPending": "Iniciando gravação...",
|
||||
"failedToStart": "Falha ao iniciar a gravação",
|
||||
"fileSharingdescription": "Compartilhar gravação com participantes da reunião",
|
||||
"linkGenerated": "Geramos um link para sua gravação.",
|
||||
"live": "AOVIVO",
|
||||
"loggedIn": "Conectado como {{userName}}",
|
||||
"off": "Gravação parada",
|
||||
@@ -639,11 +721,13 @@
|
||||
"pending": "Preparando para gravar a reunião...",
|
||||
"rec": "REC",
|
||||
"serviceDescription": "Sua gravação será salva pelo serviço de gravação",
|
||||
"serviceDescriptionCloud": "Gravação na nuvem",
|
||||
"serviceName": "Serviço de gravação",
|
||||
"signIn": "Entrar",
|
||||
"signOut": "Sair",
|
||||
"unavailable": "Oops! O {{serviceName}} está indisponível. Estamos trabalhando para resolver o problema. Por favor, tente mais tarde.",
|
||||
"unavailableTitle": "Gravação indisponível"
|
||||
"unavailableTitle": "Gravação indisponível",
|
||||
"uploadToCloud": "Enviar para a nuvem"
|
||||
},
|
||||
"sectionList": {
|
||||
"pullToRefresh": "Puxe para atualizar"
|
||||
@@ -662,8 +746,13 @@
|
||||
"signedIn": "Atualmente acessando eventos do calendário para {{email}}. Clique no botão Desconectar abaixo para parar de acessar os eventos da agenda.",
|
||||
"title": "Calendário"
|
||||
},
|
||||
"desktopShareFramerate": "Taxa de quadros do compartilhamento de desktop",
|
||||
"desktopShareWarning": "Você precisa reiniciar o compartilhamento de tela para que as novas configurações tenham efeito.",
|
||||
"desktopShareHighFpsWarning": "Uma taxa de quadros mais alta para compartilhamento de desktop pode afetar sua largura de banda. Você precisa reiniciar o compartilhamento de tela para que as novas configurações tenham efeito.",
|
||||
"devices": "Dispositivos",
|
||||
"followMe": "Todos me seguem",
|
||||
"framesPerSecond": "quadros por segundo",
|
||||
"incomingMessage": "Mensagem recebida",
|
||||
"language": "Idioma",
|
||||
"loggedIn": "Conectado como {{name}}",
|
||||
"microphones": "Microfones",
|
||||
@@ -671,12 +760,18 @@
|
||||
"more": "Mais",
|
||||
"name": "Nome",
|
||||
"noDevice": "Nenhum",
|
||||
"participantJoined": "Participante Entrou",
|
||||
"participantLeft": "Participante Saiu",
|
||||
"playSounds": "Tocar sons",
|
||||
"sameAsSystem": "Igual ao sistema ({{label}})",
|
||||
"selectAudioOutput": "Saída de áudio",
|
||||
"selectCamera": "Câmera",
|
||||
"selectMic": "Microfone",
|
||||
"sounds": "Sons",
|
||||
"speakers": "Alto-faltantes",
|
||||
"startAudioMuted": "Todos iniciam mudos",
|
||||
"startVideoMuted": "Todos iniciam ocultos",
|
||||
"talkWhileMuted": "Falar mesmo silenciado",
|
||||
"title": "Configurações"
|
||||
},
|
||||
"settingsView": {
|
||||
@@ -716,6 +811,7 @@
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
"genericTitle": "A reunião precisa usar seu microfone e câmera.",
|
||||
"title": "O {{app}} precisa usar seu microfone e câmera."
|
||||
},
|
||||
"suspendedoverlay": {
|
||||
@@ -727,12 +823,14 @@
|
||||
"accessibilityLabel": {
|
||||
"audioOnly": "Alternar para apenas áudio",
|
||||
"audioRoute": "Selecionar o dispositivo de som",
|
||||
"boo": "Vaia",
|
||||
"callQuality": "Gerenciar qualidade do vídeo",
|
||||
"cc": "Alternar legendas",
|
||||
"chat": "Alternar para janela de chat",
|
||||
"clap": "Aplauso",
|
||||
"document": "Alternar para documento compartilhado",
|
||||
"download": "Baixe nossos aplicativos",
|
||||
"embedMeeting": "Reunião em modo compacto",
|
||||
"embedMeeting": "Reunião em formato compacto",
|
||||
"feedback": "Deixar feedback",
|
||||
"fullScreen": "Alternar para tela cheia",
|
||||
"grantModerator": "Atribuir Moderador",
|
||||
@@ -740,6 +838,8 @@
|
||||
"help": "Ajuda",
|
||||
"invite": "Convidar pessoas",
|
||||
"kick": "Remover participante",
|
||||
"laugh": "Risada",
|
||||
"like": "Gostei",
|
||||
"lobbyButton": "Habilitar/desabilitar sala de espera",
|
||||
"localRecording": "Alternar controles de gravação local",
|
||||
"lockRoom": "Ativar/desativar senha de reunião",
|
||||
@@ -748,36 +848,51 @@
|
||||
"moreOptions": "Mostrar mais opções",
|
||||
"mute": "Alternar mudo do áudio",
|
||||
"muteEveryone": "Silenciar todos",
|
||||
"muteEveryoneElse": "Silenciar todos os demais",
|
||||
"muteEveryonesVideo": "Desativar a câmera de todos",
|
||||
"muteEveryoneElsesVideo": "Desativar a câmera de todos os demais",
|
||||
"participants": "Participantes",
|
||||
"party": "Festa",
|
||||
"pip": "Alternar modo Picture-in-Picture",
|
||||
"privateMessage": "Enviar mensagem privada",
|
||||
"profile": "Editar seu perfil",
|
||||
"raiseHand": "Alternar levantar a mão",
|
||||
"reactionsMenu": "Abrir / fechar menu de reações",
|
||||
"recording": "Alternar gravação",
|
||||
"remoteMute": "Silenciar participante",
|
||||
"remoteVideoMute": "Desativar a câmera do participante",
|
||||
"security": "Opções de segurança",
|
||||
"Settings": "Alternar configurações",
|
||||
"shareaudio": "Compartilhar áudio",
|
||||
"sharedvideo": "Alternar compartilhamento de vídeo do YouTube",
|
||||
"shareRoom": "Convidar alguém",
|
||||
"shareYourScreen": "Alternar compartilhamento de tela",
|
||||
"shortcuts": "Alternar atalhos",
|
||||
"show": "Mostrar no palco",
|
||||
"speakerStats": "Alternar estatísticas do apresentador",
|
||||
"surprised": "Surpresa",
|
||||
"tileView": "Alternar visualização em blocos",
|
||||
"toggleCamera": "Alternar câmera",
|
||||
"toggleFilmstrip": "Alterar tira de filme",
|
||||
"videomute": "Alternar mudo do vídeo",
|
||||
"videoblur": "Alternar desfoque de vídeo"
|
||||
"videoblur": "Alternar desfoque de vídeo",
|
||||
"selectBackground": "Selecionar Fundo",
|
||||
"expand": "Expandir",
|
||||
"collapse": "Recolher"
|
||||
},
|
||||
"addPeople": "Adicionar pessoas à sua chamada",
|
||||
"audioSettings": "Configurações de áudio",
|
||||
"videoSettings": "Configurações de vídeo",
|
||||
"audioOnlyOff": "Desabilitar modo de largura de banda baixa",
|
||||
"audioOnlyOn": "Habilitar modo de largura de banda baixa",
|
||||
"audioRoute": "Selecionar o dispositivo de som",
|
||||
"authenticate": "Autenticar",
|
||||
"boo": "Vaia",
|
||||
"callQuality": "Gerenciar qualidade do vídeo",
|
||||
"chat": "Abrir ou fechar o bate-papo",
|
||||
"clap": "Aplauso",
|
||||
"closeChat": "Fechar chat",
|
||||
"closeReactionsMenu": "Fechar menu de reações",
|
||||
"documentClose": "Fechar documento compartilhado",
|
||||
"documentOpen": "Abrir documento compartilhado",
|
||||
"download": "Baixe nossos aplicativos",
|
||||
@@ -791,6 +906,8 @@
|
||||
"hangup": "Sair",
|
||||
"help": "Ajuda",
|
||||
"invite": "Convidar pessoas",
|
||||
"laugh": "Risada",
|
||||
"like": "Gostei",
|
||||
"lobbyButtonDisable": "Desabilitar sala de espera",
|
||||
"lobbyButtonEnable": "Habilitar sala de espera",
|
||||
"login": "Iniciar sessão",
|
||||
@@ -800,6 +917,7 @@
|
||||
"moreOptions": "Mais opções",
|
||||
"mute": "Mudo / Não mudo",
|
||||
"muteEveryone": "Silenciar todos",
|
||||
"muteEveryonesVideo": "Desativar a câmera de todos",
|
||||
"noAudioSignalTitle": "Não há entrada de áudio vindo do seu microfone!",
|
||||
"noAudioSignalDesc": "Se você não o desativou propositalmente das configurações do sistema ou do hardware, considere trocar o dispositivo.",
|
||||
"noAudioSignalDescSuggestion": "Se você não o desativou propositalmente das configurações do sistema ou do hardware, considere trocar para o dispositivo sugerido.",
|
||||
@@ -808,28 +926,38 @@
|
||||
"noisyAudioInputTitle": "O seu microfone parece estar barulhento!",
|
||||
"noisyAudioInputDesc": "Parece que o microfone está fazendo barulho, considere silenciar ou alterar o dispositivo.",
|
||||
"openChat": "Abrir chat",
|
||||
"openReactionsMenu": "Abrir menu de reações",
|
||||
"participants": "Participantes",
|
||||
"party": "Festa",
|
||||
"pip": "Entrar em modo Quadro-a-Quadro",
|
||||
"privateMessage": "Enviar mensagem privada",
|
||||
"profile": "Editar seu perfil",
|
||||
"raiseHand": "Erguer / Baixar sua mão",
|
||||
"raiseYourHand": "Levantar a mão",
|
||||
"reactionBoo": "Enviar reação de vaia",
|
||||
"reactionClap": "Enviar reação de aplauso",
|
||||
"reactionLaugh": "Enviar reação de risada",
|
||||
"reactionLike": "Enviar reação de gostei",
|
||||
"reactionParty": "Enviar reação de festa",
|
||||
"reactionSurprised": "Enviar reação de surpresa",
|
||||
"security": "Opções de segurança",
|
||||
"Settings": "Configurações",
|
||||
"shareaudio": "Compartilhar áudio",
|
||||
"sharedvideo": "Compartilhar um vídeo do YouTube",
|
||||
"shareRoom": "Convidar alguém",
|
||||
"shortcuts": "Ver atalhos",
|
||||
"speakerStats": "Estatísticas do Apresentador",
|
||||
"startScreenSharing": "Iniciar compart. de tela",
|
||||
"startSubtitles": "Iniciar legendas",
|
||||
"stopAudioSharing": "Parar compart. de áudio",
|
||||
"stopScreenSharing": "Parar compart. de tela",
|
||||
"stopSubtitles": "Parar legendas",
|
||||
"stopSharedVideo": "Parar vídeo do YouTube",
|
||||
"surprised": "Surpresa",
|
||||
"talkWhileMutedPopup": "Tentando falar? Você está em mudo.",
|
||||
"tileViewToggle": "Alternar visualização em blocos",
|
||||
"toggleCamera": "Alternar câmera",
|
||||
"videomute": "Iniciar ou parar a câmera",
|
||||
"startvideoblur": "Desfocar meu plano de fundo",
|
||||
"stopvideoblur": "Desativar desfoque de fundo",
|
||||
"selectBackground": "Selecionar fundo"
|
||||
},
|
||||
"transcribing": {
|
||||
@@ -856,6 +984,7 @@
|
||||
"react-nativeGrantPermissions": "Selecione <b><i>Permitir</i></b> quando seu navegador perguntar pelas permissões.",
|
||||
"safariGrantPermissions": "Selecione <b><i>OK</i></b> quando seu navegador perguntar pelas permissões."
|
||||
},
|
||||
"volumeSlider": "Controle de volume",
|
||||
"videoSIPGW": {
|
||||
"busy": "Estamos trabalhando para liberar recursos. Por favor, tente novamente em alguns minutos.",
|
||||
"busyTitle": "O serviço da sala está ocupado",
|
||||
@@ -878,26 +1007,29 @@
|
||||
"ld": "LD",
|
||||
"ldTooltip": "Ver vídeo em baixa definição",
|
||||
"lowDefinition": "Baixa definição (LD)",
|
||||
"onlyAudioAvailable": "Somente áudio disponível",
|
||||
"onlyAudioSupported": "Suportamos somente áudio neste navegador.",
|
||||
"sd": "SD",
|
||||
"sdTooltip": "Ver vídeo em definição padrão",
|
||||
"standardDefinition": "Definição padrão"
|
||||
},
|
||||
"videothumbnail": {
|
||||
"connectionInfo": "Informações da Conexão",
|
||||
"domute": "Mudo",
|
||||
"domuteVideo": "Desativar a câmera",
|
||||
"domuteOthers": "Silenciar todos os demais",
|
||||
"domuteVideoOfOthers": "Desativar a câmera de todos os demais",
|
||||
"flip": "Inverter",
|
||||
"grantModerator": "Atribuir Moderador",
|
||||
"kick": "Expulsar",
|
||||
"kick": "Remover",
|
||||
"moderator": "Moderador",
|
||||
"mute": "Participante está mudo",
|
||||
"muted": "Mudo",
|
||||
"videoMuted": "Câmera desativada",
|
||||
"remoteControl": "Controle remoto",
|
||||
"show": "Mostrar no palco",
|
||||
"videomute": "O participante parou a câmera"
|
||||
},
|
||||
"welcomepage": {
|
||||
"addMeetingName": "Adicionar nome da reunião",
|
||||
"accessibilityLabel": {
|
||||
"join": "Toque para entrar",
|
||||
"roomname": "Digite o nome da sala"
|
||||
@@ -914,8 +1046,14 @@
|
||||
"getHelp": "Obter ajuda",
|
||||
"go": "IR",
|
||||
"goSmall": "IR",
|
||||
"join": "CRIAR / ENTRAR",
|
||||
"headerTitle": "Jitsi Meet",
|
||||
"headerSubtitle": "Reuniões com segurança e alta qualidade",
|
||||
"info": "Informações",
|
||||
"join": "CRIAR / ENTRAR",
|
||||
"jitsiOnMobile": "Jitsi em dispositivos móveis – baixe nossos aplicativos e inicie uma reunião em qualquer lugar",
|
||||
"mobileDownLoadLinkIos": "Baixar aplicativo móvel para iOS",
|
||||
"mobileDownLoadLinkAndroid": "Baixar aplicativo móvel para Android",
|
||||
"mobileDownLoadLinkFDroid": "Baixar aplicativo móvel para F-Droid",
|
||||
"moderatedMessage": "Ou <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">agende uma URL de reunião</a> antes, onde você é o único moderador.",
|
||||
"privacy": "Política de Privacidade",
|
||||
"recentList": "Recente",
|
||||
@@ -926,27 +1064,38 @@
|
||||
"roomname": "Digite o nome da sala",
|
||||
"roomnameHint": "Digite o nome ou a URL da sala que você deseja entrar. Você pode digitar um nome, e apenas deixe para as pessoas que você quer se reunir digitem o mesmo nome.",
|
||||
"sendFeedback": "Enviar comentários",
|
||||
"startMeeting": "Iniciar reunião",
|
||||
"terms": "Termos",
|
||||
"title": "Videoconferências mais seguras, flexíveis e totalmente gratuitas"
|
||||
"title": "Videoconferências mais seguras, flexíveis e totalmente gratuitas",
|
||||
"logo": {
|
||||
"calendar": "Logo do Calendário",
|
||||
"microsoftLogo": "Logo da Microsoft",
|
||||
"logoDeepLinking": "Logo do Jitsi Meet",
|
||||
"desktopPreviewThumbnail": "Miniatura de Visualização do Desktop",
|
||||
"googleLogo": "Logo do Google",
|
||||
"policyLogo": "Logo da Política de Privacidade"
|
||||
}
|
||||
},
|
||||
"lonelyMeetingExperience": {
|
||||
"youAreAlone": "Você é o único na reunião",
|
||||
"button": "Convidar outros"
|
||||
"button": "Convidar outros",
|
||||
"youAreAlone": "Você é o único na reunião"
|
||||
},
|
||||
"helpView": {
|
||||
"header": "Centro de ajuda"
|
||||
},
|
||||
"lobby": {
|
||||
"admit": "Aceitar",
|
||||
"admitAll": "Aceitar todos",
|
||||
"knockingParticipantList": "Remover lista de participantes",
|
||||
"allow": "Permitir",
|
||||
"backToKnockModeButton": "Sem senha, peça para se juntar",
|
||||
"dialogTitle": "modo Lobby",
|
||||
"disableDialogContent": "O modo Lobby está habilitado. Este recurso evita que particpantes não convidados juntem-se à sua conferência. Deseja desabilitar?",
|
||||
"dialogTitle": "Modo sala de espera",
|
||||
"disableDialogContent": "O modo sala de espera está habilitado. Este recurso evita que particpantes não convidados juntem-se à sua conferência. Deseja desabilitar?",
|
||||
"disableDialogSubmit": "Desabilitar",
|
||||
"emailField": "Informe seu email",
|
||||
"enableDialogPasswordField": "Definir senha (opcional)",
|
||||
"enableDialogSubmit": "Habilitar",
|
||||
"enableDialogText": "O modo Lobby protege a sua conferência, permitindo a entrada de participantes apenas após a aprovação formal do moderador.",
|
||||
"enableDialogText": "O modo sala de espera protege a sua conferência, permitindo a entrada de participantes apenas após a aprovação formal do moderador.",
|
||||
"enterPasswordButton": "Informe a senha da conferência",
|
||||
"enterPasswordTitle": "Informe a senha para se juntar à conferência",
|
||||
"invalidPassword": "Senha inválida",
|
||||
@@ -961,12 +1110,13 @@
|
||||
"nameField": "Informe seu nome",
|
||||
"notificationLobbyAccessDenied": "{{targetParticipantName}} foi rejeitado por {{originParticipantName}}",
|
||||
"notificationLobbyAccessGranted": "{{targetParticipantName}} foi aceito por {{originParticipantName}}",
|
||||
"notificationLobbyDisabled": "Sala de Espera foi desabilitada por {{originParticipantName}}",
|
||||
"notificationLobbyEnabled": "Sala de Espera foi habilitada por {{originParticipantName}}",
|
||||
"notificationLobbyDisabled": "Sala de espera foi desabilitada por {{originParticipantName}}",
|
||||
"notificationLobbyEnabled": "Sala de espera foi habilitada por {{originParticipantName}}",
|
||||
"notificationTitle": "Sala de espera",
|
||||
"passwordField": "Informe a senha da conferência",
|
||||
"passwordJoinButton": "Solicitar",
|
||||
"reject": "Rejeitar",
|
||||
"toggleLabel": "Habilitar lobby"
|
||||
"rejectAll": "Rejeitar todos",
|
||||
"toggleLabel": "Habilitar sala de espera"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"today": "Today"
|
||||
},
|
||||
"chat": {
|
||||
"enter": "Enter chat room",
|
||||
"enter": "Enter room",
|
||||
"error": "Error: your message was not sent. Reason: {{error}}",
|
||||
"fieldPlaceHolder": "Type your message here",
|
||||
"messagebox": "Type a message",
|
||||
@@ -66,15 +66,19 @@
|
||||
"noMessagesMessage": "There are no messages in the meeting yet. Start a conversation here!",
|
||||
"nickname": {
|
||||
"popover": "Choose a nickname",
|
||||
"title": "Enter a nickname to use chat"
|
||||
"title": "Enter a nickname to use chat and polls"
|
||||
},
|
||||
"privateNotice": "Private message to {{recipient}}",
|
||||
"title": "Chat",
|
||||
"you": "you",
|
||||
"message": "Message",
|
||||
"messageAccessibleTitle": "{{user}} says:",
|
||||
"messageAccessibleTitleMe": "me says:",
|
||||
"smileysPanel": "Emoji panel"
|
||||
"smileysPanel": "Emoji panel",
|
||||
"tabs": {
|
||||
"chat": "Chat",
|
||||
"polls": "Polls"
|
||||
},
|
||||
"title": "Chat and Polls",
|
||||
"you": "you"
|
||||
},
|
||||
"chromeExtensionBanner": {
|
||||
"installExtensionText": "Install the extension for Google Calendar and Office 365 integration",
|
||||
@@ -205,7 +209,7 @@
|
||||
"dismiss": "Dismiss",
|
||||
"displayNameRequired": "Hi! What’s your name?",
|
||||
"done": "Done",
|
||||
"e2eeDescription": "End-to-End Encryption is currently EXPERIMENTAL. Please keep in mind that turning on end-to-end encryption will effectively disable server-side provided services such as: recording, live streaming and phone participation. Also keep in mind that the meeting will only work for people joining from browsers with support for insertable streams.",
|
||||
"e2eeDescription": "End-to-End Encryption is currently EXPERIMENTAL. Please keep in mind that turning on end-to-end encryption will effectively disable server-side provided services such as: phone participation. Also keep in mind that the meeting will only work for people joining from browsers with support for insertable streams.",
|
||||
"e2eeLabel": "Enable End-to-End Encryption",
|
||||
"e2eeWarning": "WARNING: Not all participants in this meeting seem to have support for End-to-End encryption. If you enable it they won't be able to see nor hear you.",
|
||||
"enterDisplayName": "Please enter your name here",
|
||||
@@ -614,6 +618,34 @@
|
||||
},
|
||||
"passwordSetRemotely": "Set by another participant",
|
||||
"passwordDigitsOnly": "Up to {{number}} digits",
|
||||
"polls": {
|
||||
"create": {
|
||||
"addOption": "Add option",
|
||||
"answerPlaceholder": "Option {{index}}",
|
||||
"create": "Create a poll",
|
||||
"cancel": "Cancel",
|
||||
"pollOption" : "Poll option {{index}}",
|
||||
"pollQuestion" : "Poll Question",
|
||||
"questionPlaceholder": "Ask a question",
|
||||
"removeOption": "Remove option",
|
||||
"send": "Send"
|
||||
},
|
||||
"answer": {
|
||||
"skip": "Skip",
|
||||
"submit": "Submit"
|
||||
},
|
||||
"results": {
|
||||
"vote": "Vote",
|
||||
"changeVote": "Change vote",
|
||||
"empty": "There are no polls in the meeting yet. Start a poll here!",
|
||||
"hideDetailedResults": "Hide details",
|
||||
"showDetailedResults": "Show details"
|
||||
},
|
||||
"notification": {
|
||||
"title": "A new poll was added to this meeting",
|
||||
"description": "Open polls tab to vote"
|
||||
}
|
||||
},
|
||||
"poweredby": "powered by",
|
||||
"prejoin": {
|
||||
"audioAndVideoError": "Audio and video error:",
|
||||
@@ -802,6 +834,7 @@
|
||||
},
|
||||
"speaker": "Speaker",
|
||||
"speakerStats": {
|
||||
"search": "Search",
|
||||
"hours": "{{count}}h",
|
||||
"minutes": "{{count}}m",
|
||||
"name": "Name",
|
||||
|
||||
@@ -12,9 +12,6 @@ var loggingConfig = {
|
||||
// {@link #defaultLogLevel}:
|
||||
'modules/RTC/TraceablePeerConnection.js': 'info',
|
||||
'modules/statistics/CallStats.js': 'info',
|
||||
'modules/sdp/SDPUtil.js': 'info',
|
||||
'modules/xmpp/JingleSessionPC.js': 'info',
|
||||
'modules/xmpp/strophe.jingle.js': 'info',
|
||||
'modules/xmpp/strophe.util.js': 'log'
|
||||
};
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -11087,8 +11087,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#6a3df11ffa7a2204b579326e23cdaa85a79521d1",
|
||||
"from": "github:jitsi/lib-jitsi-meet#6a3df11ffa7a2204b579326e23cdaa85a79521d1",
|
||||
"version": "github:jitsi/lib-jitsi-meet#c23abfa2bcd2b04710e4180f9b878bacba33ba16",
|
||||
"from": "github:jitsi/lib-jitsi-meet#c23abfa2bcd2b04710e4180f9b878bacba33ba16",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "1.0.2",
|
||||
"@jitsi/sdp-interop": "github:jitsi/sdp-interop#5fc4af6dcf8a6e6af9fedbcd654412fd47b1b4ae",
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#6a3df11ffa7a2204b579326e23cdaa85a79521d1",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#c23abfa2bcd2b04710e4180f9b878bacba33ba16",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.1",
|
||||
|
||||
@@ -11,6 +11,39 @@ index bd48f44..d243ed0 100644
|
||||
withDispatchGroup:(dispatch_group_t)dispatchGroup
|
||||
lazilyDiscovered:(BOOL)lazilyDiscovered
|
||||
{
|
||||
diff --git a/node_modules/react-native/React/Modules/RCTTiming.m b/node_modules/react-native/React/Modules/RCTTiming.m
|
||||
index 8a09022..265d7b6 100644
|
||||
--- a/node_modules/react-native/React/Modules/RCTTiming.m
|
||||
+++ b/node_modules/react-native/React/Modules/RCTTiming.m
|
||||
@@ -130,6 +130,11 @@ - (void)setBridge:(RCTBridge *)bridge
|
||||
object:nil];
|
||||
}
|
||||
|
||||
+ [[NSNotificationCenter defaultCenter] addObserver:self
|
||||
+ selector:@selector(proximityChanged)
|
||||
+ name:UIDeviceProximityStateDidChangeNotification
|
||||
+ object:nil];
|
||||
+
|
||||
_bridge = bridge;
|
||||
}
|
||||
|
||||
@@ -276,6 +281,16 @@ - (void)didUpdateFrame:(RCTFrameUpdate *)update
|
||||
}
|
||||
}
|
||||
|
||||
+-(void)proximityChanged
|
||||
+{
|
||||
+ BOOL near = [UIDevice currentDevice].proximityState;
|
||||
+ if (near) {
|
||||
+ [self appDidMoveToBackground];
|
||||
+ } else {
|
||||
+ [self appDidMoveToForeground];
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
- (void)scheduleSleepTimer:(NSDate *)sleepTarget
|
||||
{
|
||||
@synchronized (self) {
|
||||
diff --git a/node_modules/react-native/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.mm b/node_modules/react-native/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.mm
|
||||
index 3cb73b5..e4a14b4 100644
|
||||
--- a/node_modules/react-native/ReactCommon/turbomodule/core/platform/ios/RCTTurboModuleManager.mm
|
||||
|
||||
@@ -34,6 +34,8 @@ import '../large-video/middleware';
|
||||
import '../lobby/middleware';
|
||||
import '../notifications/middleware';
|
||||
import '../overlay/middleware';
|
||||
import '../polls/middleware';
|
||||
import '../polls/subscriber';
|
||||
import '../reactions/middleware';
|
||||
import '../recent-list/middleware';
|
||||
import '../recording/middleware';
|
||||
|
||||
@@ -41,6 +41,7 @@ import '../lobby/reducer';
|
||||
import '../notifications/reducer';
|
||||
import '../overlay/reducer';
|
||||
import '../participants-pane/reducer';
|
||||
import '../polls/reducer';
|
||||
import '../reactions/reducer';
|
||||
import '../recent-list/reducer';
|
||||
import '../recording/reducer';
|
||||
|
||||
@@ -53,20 +53,20 @@ export const disableModeration = (mediaType: MediaType, actor: Object) => {
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function dismissPendingAudioParticipant(participant: Object) {
|
||||
return dismissPendingParticipant(participant, MEDIA_TYPE.AUDIO);
|
||||
return dismissPendingParticipant(participant.id, MEDIA_TYPE.AUDIO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the notification with the participant that asked to unmute.
|
||||
*
|
||||
* @param {Object} participant - The participant for which the notification to be hidden.
|
||||
* @param {string} id - The participant id for which the notification to be hidden.
|
||||
* @param {MediaType} mediaType - The media type.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function dismissPendingParticipant(participant: Object, mediaType: MediaType) {
|
||||
export function dismissPendingParticipant(id: string, mediaType: MediaType) {
|
||||
return {
|
||||
type: DISMISS_PENDING_PARTICIPANT,
|
||||
participant,
|
||||
id,
|
||||
mediaType
|
||||
};
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action
|
||||
}
|
||||
|
||||
if (videoModerationEnabled) {
|
||||
hasStateChanged = _updatePendingParticipant(MEDIA_TYPE.VIDEO, participant, state);
|
||||
hasStateChanged = hasStateChanged || _updatePendingParticipant(MEDIA_TYPE.VIDEO, participant, state);
|
||||
}
|
||||
|
||||
// If the state has changed we need to return a new object reference in order to trigger subscriber updates.
|
||||
@@ -183,19 +183,19 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action
|
||||
}
|
||||
|
||||
case DISMISS_PENDING_PARTICIPANT: {
|
||||
const { participant, mediaType } = action;
|
||||
const { id, mediaType } = action;
|
||||
|
||||
if (mediaType === MEDIA_TYPE.AUDIO) {
|
||||
return {
|
||||
...state,
|
||||
pendingAudio: state.pendingAudio.filter(pending => pending.id !== participant.id)
|
||||
pendingAudio: state.pendingAudio.filter(pending => pending.id !== id)
|
||||
};
|
||||
}
|
||||
|
||||
if (mediaType === MEDIA_TYPE.VIDEO) {
|
||||
return {
|
||||
...state,
|
||||
pendingVideo: state.pendingVideo.filter(pending => pending.id !== participant.id)
|
||||
pendingVideo: state.pendingVideo.filter(pending => pending.id !== id)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ function _addConferenceListeners(conference, dispatch, state) {
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
|
||||
id => dispatch(dominantSpeakerChanged(id, conference)));
|
||||
(dominant, previous) => dispatch(dominantSpeakerChanged(dominant, previous, conference)));
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import { getName } from '../../app/functions';
|
||||
import { determineTranscriptionLanguage } from '../../transcribing/functions';
|
||||
import { JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
@@ -231,12 +232,13 @@ export function getConferenceOptions(stateful: Function | Object) {
|
||||
}
|
||||
|
||||
options.applicationName = getName();
|
||||
options.transcriptionLanguage = determineTranscriptionLanguage(options);
|
||||
|
||||
// Disable analytics, if requessted.
|
||||
// Disable analytics, if requested.
|
||||
if (options.disableThirdPartyRequests) {
|
||||
delete config.analytics.scriptURLs;
|
||||
delete config.analytics.amplitudeAPPKey;
|
||||
delete config.analytics.googleAnalyticsTrackingId;
|
||||
delete config.analytics?.scriptURLs;
|
||||
delete config.analytics?.amplitudeAPPKey;
|
||||
delete config.analytics?.googleAnalyticsTrackingId;
|
||||
delete options.callStatsID;
|
||||
delete options.callStatsSecret;
|
||||
} else {
|
||||
|
||||
@@ -50,6 +50,7 @@ export default [
|
||||
*/
|
||||
'callHandle',
|
||||
'callStatsConfIDNamespace',
|
||||
'callStatsConfigParams',
|
||||
'callStatsID',
|
||||
'callStatsSecret',
|
||||
|
||||
@@ -91,6 +92,7 @@ export default [
|
||||
'disableJoinLeaveSounds',
|
||||
'disableLocalVideoFlip',
|
||||
'disableNS',
|
||||
'disablePolls',
|
||||
'disableProfile',
|
||||
'disableRemoteControl',
|
||||
'disableRemoteMute',
|
||||
@@ -98,6 +100,7 @@ export default [
|
||||
'disableRtx',
|
||||
'disableShortcuts',
|
||||
'disableShowMoreStats',
|
||||
'disableSpeakerStatsSearch',
|
||||
'disableSimulcast',
|
||||
'disableThirdPartyRequests',
|
||||
'disableTileView',
|
||||
|
||||
@@ -33,7 +33,6 @@ export function createFakeConfig(baseURL: string) {
|
||||
muc: `conference.${url.hostname}`
|
||||
},
|
||||
bosh: `${baseURL}http-bind`,
|
||||
clientNode: 'https://jitsi.org/jitsi-meet',
|
||||
p2p: {
|
||||
enabled: true
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ export * from './functions.any';
|
||||
export function _cleanupConfig(config: Object) {
|
||||
config.analytics.scriptURLs = [];
|
||||
if (NativeModules.AppInfo.LIBRE_BUILD) {
|
||||
delete config.analytics.amplitudeAPPKey;
|
||||
delete config.analytics.googleAnalyticsTrackingId;
|
||||
delete config.analytics?.amplitudeAPPKey;
|
||||
delete config.analytics?.googleAnalyticsTrackingId;
|
||||
delete config.callStatsID;
|
||||
delete config.callStatsSecret;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class BaseDialog<P: Props, S: State> extends AbstractDialog<P, S> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _dialogStyles, style } = this.props;
|
||||
const { _dialogStyles, style, t, titleKey } = this.props;
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback>
|
||||
@@ -65,13 +65,18 @@ class BaseDialog<P: Props, S: State> extends AbstractDialog<P, S> {
|
||||
_dialogStyles.dialog,
|
||||
style
|
||||
] }>
|
||||
<TouchableOpacity
|
||||
onPress = { this._onCancel }
|
||||
style = { styles.closeWrapper }>
|
||||
<Icon
|
||||
src = { IconClose }
|
||||
style = { _dialogStyles.closeStyle } />
|
||||
</TouchableOpacity>
|
||||
<View style = { styles.headerWrapper }>
|
||||
<Text style = { styles.dialogTitle }>
|
||||
{ titleKey ? t(titleKey) : ' ' }
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
onPress = { this._onCancel }
|
||||
style = { styles.closeWrapper }>
|
||||
<Icon
|
||||
src = { IconClose }
|
||||
style = { _dialogStyles.closeStyle } />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{ this._renderContent() }
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
@@ -28,6 +28,12 @@ type Props = BaseProps & {
|
||||
*/
|
||||
contentKey: string | { key: string, params: Object},
|
||||
|
||||
/**
|
||||
* The handler for the event when clicking the 'confirmNo' button.
|
||||
* Defaults to onCancel if absent.
|
||||
*/
|
||||
onDecline?: Function,
|
||||
|
||||
t: Function
|
||||
}
|
||||
|
||||
@@ -55,11 +61,11 @@ class ConfirmDialog extends BaseSubmitDialog<Props, *> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderAdditionalButtons() {
|
||||
const { _dialogStyles, cancelKey, t } = this.props;
|
||||
const { _dialogStyles, cancelKey, onDecline, t } = this.props;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress = { this._onCancel }
|
||||
onPress = { onDecline || this._onCancel }
|
||||
style = { [
|
||||
_dialogStyles.button,
|
||||
brandedDialog.buttonFarLeft,
|
||||
|
||||
@@ -81,10 +81,20 @@ export const brandedDialog = {
|
||||
},
|
||||
|
||||
closeWrapper: {
|
||||
alignSelf: 'flex-end',
|
||||
padding: BoxModel.padding
|
||||
},
|
||||
|
||||
dialogTitle: {
|
||||
fontWeight: 'bold',
|
||||
paddingLeft: BoxModel.padding * 2
|
||||
},
|
||||
|
||||
headerWrapper: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
|
||||
mainWrapper: {
|
||||
alignSelf: 'stretch',
|
||||
padding: BoxModel.padding * 2,
|
||||
|
||||
@@ -71,6 +71,12 @@ type Props = {
|
||||
*/
|
||||
isModal: boolean,
|
||||
|
||||
/**
|
||||
* The handler for the event when clicking the 'confirmNo' button.
|
||||
* Defaults to onCancel if absent.
|
||||
*/
|
||||
onDecline?: Function,
|
||||
|
||||
/**
|
||||
* Disables rendering of the submit button.
|
||||
*/
|
||||
@@ -268,7 +274,8 @@ class StatelessDialog extends Component<Props> {
|
||||
}
|
||||
|
||||
const {
|
||||
t /* The following fixes a flow error: */ = _.identity
|
||||
t /* The following fixes a flow error: */ = _.identity,
|
||||
onDecline
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -276,7 +283,7 @@ class StatelessDialog extends Component<Props> {
|
||||
appearance = 'subtle'
|
||||
id = { CANCEL_BUTTON_ID }
|
||||
key = 'cancel'
|
||||
onClick = { this._onCancel }
|
||||
onClick = { onDecline || this._onCancel }
|
||||
type = 'button'>
|
||||
{ t(this.props.cancelKey || 'dialog.Cancel') }
|
||||
</Button>
|
||||
|
||||
@@ -92,6 +92,12 @@ export const IOS_RECORDING_ENABLED = 'ios.recording.enabled';
|
||||
*/
|
||||
export const IOS_SCREENSHARING_ENABLED = 'ios.screensharing.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if screen sharing should be enabled in android.
|
||||
* Default: enabled (true).
|
||||
*/
|
||||
export const ANDROID_SCREENSHARING_ENABLED = 'android.screensharing.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if kickout is enabled.
|
||||
* Default: enabled (true).
|
||||
|
||||
@@ -153,6 +153,8 @@ export default function Icon(props: Props) {
|
||||
}
|
||||
}, [ onClick, onKeyPress ]);
|
||||
|
||||
const jitsiIconClassName = calculatedColor ? 'jitsi-icon' : 'jitsi-icon jitsi-icon-default';
|
||||
|
||||
return (
|
||||
<Container
|
||||
{ ...rest }
|
||||
@@ -163,7 +165,7 @@ export default function Icon(props: Props) {
|
||||
aria-haspopup = { ariaHasPopup }
|
||||
aria-label = { ariaLabel }
|
||||
aria-pressed = { ariaPressed }
|
||||
className = { `jitsi-icon ${className || ''}` }
|
||||
className = { `${jitsiIconClassName} ${className || ''}` }
|
||||
id = { containerId }
|
||||
onClick = { onClick }
|
||||
onKeyDown = { onKeyDown }
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
* {
|
||||
* type: DOMINANT_SPEAKER_CHANGED,
|
||||
* participant: {
|
||||
* id: string
|
||||
* conference: JitsiConference,
|
||||
* id: string,
|
||||
* previousSpeakers: Array<string>
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
@@ -31,7 +31,8 @@ import logger from './logger';
|
||||
/**
|
||||
* Create an action for when dominant speaker changes.
|
||||
*
|
||||
* @param {string} id - Participant's ID.
|
||||
* @param {string} dominantSpeaker - Participant ID of the dominant speaker.
|
||||
* @param {Array<string>} previousSpeakers - Participant IDs of the previous speakers.
|
||||
* @param {JitsiConference} conference - The {@code JitsiConference} associated
|
||||
* with the participant identified by the specified {@code id}. Only the local
|
||||
* participant is allowed to not specify an associated {@code JitsiConference}
|
||||
@@ -40,16 +41,18 @@ import logger from './logger';
|
||||
* type: DOMINANT_SPEAKER_CHANGED,
|
||||
* participant: {
|
||||
* conference: JitsiConference,
|
||||
* id: string
|
||||
* id: string,
|
||||
* previousSpeakers: Array<string>
|
||||
* }
|
||||
* }}
|
||||
*/
|
||||
export function dominantSpeakerChanged(id, conference) {
|
||||
export function dominantSpeakerChanged(dominantSpeaker, previousSpeakers, conference) {
|
||||
return {
|
||||
type: DOMINANT_SPEAKER_CHANGED,
|
||||
participant: {
|
||||
conference,
|
||||
id
|
||||
id: dominantSpeaker,
|
||||
previousSpeakers
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -443,3 +443,52 @@ async function _getFirstLoadableAvatarUrl(participant, store) {
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Selector for retrieving sorted participants by display name.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state
|
||||
* features/base/participants.
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
export function getSortedParticipants(stateful: Object | Function) {
|
||||
const localParticipant = getLocalParticipant(stateful);
|
||||
const remoteParticipants = getRemoteParticipants(stateful);
|
||||
|
||||
const items = [];
|
||||
const dominantSpeaker = getDominantSpeakerParticipant(stateful);
|
||||
|
||||
remoteParticipants.forEach(p => {
|
||||
if (p !== dominantSpeaker) {
|
||||
items.push(p);
|
||||
}
|
||||
});
|
||||
|
||||
items.sort((a, b) =>
|
||||
getParticipantDisplayName(stateful, a.id).localeCompare(getParticipantDisplayName(stateful, b.id))
|
||||
);
|
||||
|
||||
items.unshift(localParticipant);
|
||||
|
||||
if (dominantSpeaker && dominantSpeaker !== localParticipant) {
|
||||
items.unshift(dominantSpeaker);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for retrieving ids of alphabetically sorted participants by name.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state
|
||||
* features/base/participants.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
export function getSortedParticipantIds(stateful: Object | Function): Array<string> {
|
||||
const participantIds = getSortedParticipants(stateful).map((p): Object => p.id);
|
||||
|
||||
return participantIds;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
|
||||
import { isParticipantModerator } from './functions';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* Participant object.
|
||||
* @typedef {Object} Participant
|
||||
@@ -30,8 +32,6 @@ import { isParticipantModerator } from './functions';
|
||||
* @property {string} email - Participant email.
|
||||
*/
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* The participant properties which cannot be updated through
|
||||
* {@link PARTICIPANT_UPDATED}. They either identify the participant or can only
|
||||
@@ -53,13 +53,15 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
|
||||
];
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
haveParticipantWithScreenSharingFeature: false,
|
||||
dominantSpeaker: undefined,
|
||||
everyoneIsModerator: false,
|
||||
pinnedParticipant: undefined,
|
||||
fakeParticipants: new Map(),
|
||||
haveParticipantWithScreenSharingFeature: false,
|
||||
local: undefined,
|
||||
pinnedParticipant: undefined,
|
||||
remote: new Map(),
|
||||
fakeParticipants: new Map()
|
||||
sortedRemoteParticipants: new Map(),
|
||||
speakersList: []
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -93,8 +95,13 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
|
||||
}
|
||||
case DOMINANT_SPEAKER_CHANGED: {
|
||||
const { participant } = action;
|
||||
const { id } = participant;
|
||||
const { dominantSpeaker } = state;
|
||||
const { id, previousSpeakers = [] } = participant;
|
||||
const { dominantSpeaker, local } = state;
|
||||
const speakersList = [];
|
||||
|
||||
// Update the speakers list.
|
||||
id !== local?.id && speakersList.push(id);
|
||||
speakersList.push(...previousSpeakers.filter(p => p !== local?.id));
|
||||
|
||||
// Only one dominant speaker is allowed.
|
||||
if (dominantSpeaker) {
|
||||
@@ -104,7 +111,8 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
|
||||
if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) {
|
||||
return {
|
||||
...state,
|
||||
dominantSpeaker: id
|
||||
dominantSpeaker: id,
|
||||
speakersList
|
||||
};
|
||||
}
|
||||
|
||||
@@ -182,21 +190,22 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
|
||||
}
|
||||
case PARTICIPANT_JOINED: {
|
||||
const participant = _participantJoined(action);
|
||||
const { id, isFakeParticipant, name, pinned } = participant;
|
||||
const { pinnedParticipant, dominantSpeaker } = state;
|
||||
|
||||
if (participant.pinned) {
|
||||
if (pinned) {
|
||||
if (pinnedParticipant) {
|
||||
_updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
|
||||
}
|
||||
|
||||
state.pinnedParticipant = participant.id;
|
||||
state.pinnedParticipant = id;
|
||||
}
|
||||
|
||||
if (participant.dominantSpeaker) {
|
||||
if (dominantSpeaker) {
|
||||
_updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
|
||||
}
|
||||
state.dominantSpeaker = participant.id;
|
||||
state.dominantSpeaker = id;
|
||||
}
|
||||
|
||||
const isModerator = isParticipantModerator(participant);
|
||||
@@ -215,10 +224,21 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
|
||||
};
|
||||
}
|
||||
|
||||
state.remote.set(participant.id, participant);
|
||||
state.remote.set(id, participant);
|
||||
|
||||
if (participant.isFakeParticipant) {
|
||||
state.fakeParticipants.set(participant.id, participant);
|
||||
// Insert the new participant.
|
||||
const displayName = name
|
||||
?? (typeof interfaceConfig === 'object' ? interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME : 'Fellow Jitser');
|
||||
const sortedRemoteParticipants = Array.from(state.sortedRemoteParticipants);
|
||||
|
||||
sortedRemoteParticipants.push([ id, displayName ]);
|
||||
sortedRemoteParticipants.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
|
||||
// The sort order of participants is preserved since Map remembers the original insertion order of the keys.
|
||||
state.sortedRemoteParticipants = new Map(sortedRemoteParticipants);
|
||||
|
||||
if (isFakeParticipant) {
|
||||
state.fakeParticipants.set(id, participant);
|
||||
}
|
||||
|
||||
return { ...state };
|
||||
@@ -244,6 +264,8 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
|
||||
return state;
|
||||
}
|
||||
|
||||
state.sortedRemoteParticipants.delete(id);
|
||||
|
||||
if (!state.everyoneIsModerator && !isParticipantModerator(oldParticipant)) {
|
||||
state.everyoneIsModerator = _isEveryoneModerator(state);
|
||||
}
|
||||
@@ -274,6 +296,9 @@ ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, a
|
||||
state.dominantSpeaker = undefined;
|
||||
}
|
||||
|
||||
// Remove the participant from the list of speakers.
|
||||
state.speakersList = state.speakersList.filter(speaker => speaker !== id);
|
||||
|
||||
if (pinnedParticipant === id) {
|
||||
state.pinnedParticipant = undefined;
|
||||
}
|
||||
|
||||
@@ -64,3 +64,13 @@ export const SEND_MESSAGE = 'SEND_MESSAGE';
|
||||
* }
|
||||
*/
|
||||
export const SET_PRIVATE_MESSAGE_RECIPIENT = 'SET_PRIVATE_MESSAGE_RECIPIENT';
|
||||
|
||||
/**
|
||||
* The type of action which signals the update a _isPollsTabFocused.
|
||||
*
|
||||
* {
|
||||
* isPollsTabFocused: boolean,
|
||||
* type: SET_PRIVATE_MESSAGE_RECIPIENT
|
||||
* }
|
||||
*/
|
||||
export const SET_IS_POLL_TAB_FOCUSED = 'SET_IS_POLL_TAB_FOCUSED';
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
CLEAR_MESSAGES,
|
||||
CLOSE_CHAT,
|
||||
SEND_MESSAGE,
|
||||
SET_PRIVATE_MESSAGE_RECIPIENT
|
||||
SET_PRIVATE_MESSAGE_RECIPIENT,
|
||||
SET_IS_POLL_TAB_FOCUSED
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
@@ -97,3 +98,16 @@ export function setPrivateMessageRecipient(participant: Object) {
|
||||
type: SET_PRIVATE_MESSAGE_RECIPIENT
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of _isPollsTabFocused.
|
||||
*
|
||||
* @param {boolean} isPollsTabFocused - The new value for _isPollsTabFocused.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setIsPollsTabFocused(isPollsTabFocused: boolean) {
|
||||
return {
|
||||
isPollsTabFocused,
|
||||
type: SET_IS_POLL_TAB_FOCUSED
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Component } from 'react';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { getLocalParticipant } from '../../base/participants';
|
||||
import { sendMessage } from '../actions';
|
||||
import { sendMessage, setIsPollsTabFocused } from '../actions';
|
||||
import { SMALL_WIDTH_THRESHOLD } from '../constants';
|
||||
|
||||
/**
|
||||
@@ -22,11 +22,31 @@ export type Props = {
|
||||
*/
|
||||
_isOpen: boolean,
|
||||
|
||||
/**
|
||||
* True if the polls feature is enabled.
|
||||
*/
|
||||
_isPollsEnabled: boolean,
|
||||
|
||||
/**
|
||||
* Whether the poll tab is focused or not.
|
||||
*/
|
||||
_isPollsTabFocused: boolean,
|
||||
|
||||
/**
|
||||
* All the chat messages in the conference.
|
||||
*/
|
||||
_messages: Array<Object>,
|
||||
|
||||
/**
|
||||
* Number of unread chat messages.
|
||||
*/
|
||||
_nbUnreadMessages: number,
|
||||
|
||||
/**
|
||||
* Number of unread poll messages.
|
||||
*/
|
||||
_nbUnreadPolls: number,
|
||||
|
||||
/**
|
||||
* Function to send a text message.
|
||||
*
|
||||
@@ -34,6 +54,20 @@ export type Props = {
|
||||
*/
|
||||
_onSendMessage: Function,
|
||||
|
||||
/**
|
||||
* Function to display the chat tab.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
_onToggleChatTab: Function,
|
||||
|
||||
/**
|
||||
* Function to display the polls tab.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
_onTogglePollsTab: Function,
|
||||
|
||||
/**
|
||||
* Function to toggle the chat window.
|
||||
*/
|
||||
@@ -52,7 +86,7 @@ export type Props = {
|
||||
/**
|
||||
* Function to be used to translate i18n labels.
|
||||
*/
|
||||
t: Function
|
||||
t: Function,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -71,6 +105,8 @@ export default class AbstractChat<P: Props> extends Component<P> {
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onSendMessage = this._onSendMessage.bind(this);
|
||||
this._onToggleChatTab = this._onToggleChatTab.bind(this);
|
||||
this._onTogglePollsTab = this._onTogglePollsTab.bind(this);
|
||||
}
|
||||
|
||||
_onSendMessage: (string) => void;
|
||||
@@ -86,6 +122,30 @@ export default class AbstractChat<P: Props> extends Component<P> {
|
||||
_onSendMessage(text: string) {
|
||||
this.props.dispatch(sendMessage(text));
|
||||
}
|
||||
|
||||
_onToggleChatTab: () => void;
|
||||
|
||||
/**
|
||||
* Display the Chat tab.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToggleChatTab() {
|
||||
this.props.dispatch(setIsPollsTabFocused(false));
|
||||
}
|
||||
|
||||
_onTogglePollsTab: () => void;
|
||||
|
||||
/**
|
||||
* Display the Polls tab.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onTogglePollsTab() {
|
||||
this.props.dispatch(setIsPollsTabFocused(true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,13 +161,19 @@ export default class AbstractChat<P: Props> extends Component<P> {
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object) {
|
||||
const { isOpen, messages } = state['features/chat'];
|
||||
const { isOpen, isPollsTabFocused, messages, nbUnreadMessages } = state['features/chat'];
|
||||
const { nbUnreadPolls } = state['features/polls'];
|
||||
const _localParticipant = getLocalParticipant(state);
|
||||
const { disablePolls } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
_isModal: window.innerWidth <= SMALL_WIDTH_THRESHOLD,
|
||||
_isOpen: isOpen,
|
||||
_isPollsEnabled: !disablePolls,
|
||||
_isPollsTabFocused: isPollsTabFocused,
|
||||
_messages: messages,
|
||||
_nbUnreadMessages: nbUnreadMessages,
|
||||
_nbUnreadPolls: nbUnreadPolls,
|
||||
_showNamePrompt: !_localParticipant?.name
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { JitsiModal } from '../../../base/modal';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { PollsPane } from '../../../polls/components';
|
||||
import { closeChat } from '../../actions.any';
|
||||
import { CHAT_VIEW_MODAL_ID } from '../../constants';
|
||||
import { BUTTON_MODES, CHAT_VIEW_MODAL_ID } from '../../constants';
|
||||
import AbstractChat, {
|
||||
_mapStateToProps,
|
||||
type Props
|
||||
@@ -15,6 +18,7 @@ import AbstractChat, {
|
||||
import ChatInputBar from './ChatInputBar';
|
||||
import MessageContainer from './MessageContainer';
|
||||
import MessageRecipient from './MessageRecipient';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* Implements a React native component that renders the chat window (modal) of
|
||||
@@ -45,10 +49,49 @@ class Chat extends AbstractChat<Props> {
|
||||
}}
|
||||
modalId = { CHAT_VIEW_MODAL_ID }
|
||||
onClose = { this._onClose }>
|
||||
|
||||
<MessageContainer messages = { this.props._messages } />
|
||||
<MessageRecipient />
|
||||
<ChatInputBar onSend = { this._onSendMessage } />
|
||||
{this.props._isPollsEnabled && <View style = { styles.tabContainer }>
|
||||
<Button
|
||||
color = '#17a0db'
|
||||
mode = {
|
||||
this.props._isPollsTabFocused
|
||||
? BUTTON_MODES.CONTAINED
|
||||
: BUTTON_MODES.TEXT
|
||||
}
|
||||
onPress = { this._onToggleChatTab }
|
||||
style = { styles.tabLeftButton }
|
||||
uppercase = { false }>
|
||||
{`${this.props.t('chat.tabs.chat')}${this.props._isPollsTabFocused
|
||||
&& this.props._nbUnreadMessages > 0
|
||||
? `(${this.props._nbUnreadMessages})`
|
||||
: ''
|
||||
}`}
|
||||
</Button>
|
||||
<Button
|
||||
color = '#17a0db'
|
||||
mode = {
|
||||
this.props._isPollsTabFocused
|
||||
? BUTTON_MODES.TEXT
|
||||
: BUTTON_MODES.CONTAINED
|
||||
}
|
||||
onPress = { this._onTogglePollsTab }
|
||||
style = { styles.tabRightButton }
|
||||
uppercase = { false }>
|
||||
{`${this.props.t('chat.tabs.polls')}${!this.props._isPollsTabFocused
|
||||
&& this.props._nbUnreadPolls > 0
|
||||
? `(${this.props._nbUnreadPolls})`
|
||||
: ''
|
||||
}`}
|
||||
</Button>
|
||||
</View>}
|
||||
{this.props._isPollsTabFocused
|
||||
? <PollsPane />
|
||||
: (
|
||||
<>
|
||||
<MessageContainer messages = { this.props._messages } />
|
||||
<MessageRecipient />
|
||||
<ChatInputBar onSend = { this._onSendMessage } />
|
||||
</>
|
||||
)}
|
||||
</JitsiModal>
|
||||
);
|
||||
}
|
||||
@@ -57,6 +100,9 @@ class Chat extends AbstractChat<Props> {
|
||||
|
||||
_onClose: () => boolean
|
||||
|
||||
_onTogglePollsTab: () => void;
|
||||
_onToggleChatTab: () => void;
|
||||
|
||||
/**
|
||||
* Closes the modal.
|
||||
*
|
||||
|
||||
@@ -124,6 +124,25 @@ export default {
|
||||
timeText: {
|
||||
color: 'rgb(164, 184, 209)',
|
||||
fontSize: 13
|
||||
},
|
||||
|
||||
tabContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
tabLeftButton: {
|
||||
flex: 1,
|
||||
borderTopLeftRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomLeftRadius: 0
|
||||
},
|
||||
|
||||
tabRightButton: {
|
||||
flex: 1,
|
||||
borderTopLeftRadius: 0,
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { PollsPane } from '../../../polls/components';
|
||||
import { toggleChat } from '../../actions.web';
|
||||
import AbstractChat, {
|
||||
_mapStateToProps,
|
||||
@@ -128,8 +129,20 @@ class Chat extends AbstractChat<Props> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderChat() {
|
||||
|
||||
if (this.props._isPollsTabFocused) {
|
||||
return (
|
||||
<>
|
||||
{ this.props._isPollsEnabled && this._renderTabs()}
|
||||
<PollsPane />
|
||||
<KeyboardAvoider />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.props._isPollsEnabled && this._renderTabs()}
|
||||
<TouchmoveHack isModal = { this.props._isModal }>
|
||||
<MessageContainer
|
||||
messages = { this.props._messages }
|
||||
@@ -144,6 +157,50 @@ class Chat extends AbstractChat<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a React Element showing the Chat and Polls tab.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderTabs() {
|
||||
|
||||
return (
|
||||
<div className = { 'chat-tabs-container' }>
|
||||
<div
|
||||
className = { `chat-tab ${
|
||||
this.props._isPollsTabFocused ? '' : 'chat-tab-focus'
|
||||
}` }
|
||||
onClick = { this._onToggleChatTab }>
|
||||
<span className = { 'chat-tab-title' }>
|
||||
{this.props.t('chat.tabs.chat')}
|
||||
</span>
|
||||
{this.props._isPollsTabFocused
|
||||
&& this.props._nbUnreadMessages > 0 && (
|
||||
<span className = { 'chat-tab-badge' }>
|
||||
{this.props._nbUnreadMessages}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className = { `chat-tab ${
|
||||
this.props._isPollsTabFocused ? 'chat-tab-focus' : ''
|
||||
}` }
|
||||
onClick = { this._onTogglePollsTab }>
|
||||
<span className = { 'chat-tab-title' }>
|
||||
{this.props.t('chat.tabs.polls')}
|
||||
</span>
|
||||
{!this.props._isPollsTabFocused
|
||||
&& this.props._nbUnreadPolls > 0 && (
|
||||
<span className = { 'chat-tab-badge' }>
|
||||
{this.props._nbUnreadPolls}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a React Element to display at the top of {@code Chat} to
|
||||
* close {@code Chat}.
|
||||
@@ -233,6 +290,8 @@ class Chat extends AbstractChat<Props> {
|
||||
_onToggleChat() {
|
||||
this.props.dispatch(toggleChat());
|
||||
}
|
||||
_onTogglePollsTab: () => void;
|
||||
_onToggleChatTab: () => void;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { connect } from '../../../base/redux';
|
||||
import { getUnreadPollCount } from '../../../polls/functions';
|
||||
import { getUnreadCount } from '../../functions';
|
||||
|
||||
/**
|
||||
@@ -64,8 +65,10 @@ function _mapStateToProps(state) {
|
||||
const { isOpen } = state['features/chat'];
|
||||
|
||||
return {
|
||||
_count: getUnreadCount(state),
|
||||
|
||||
_count: getUnreadCount(state) + getUnreadPollCount(state),
|
||||
_isOpen: isOpen
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -31,3 +31,11 @@ export const MESSAGE_TYPE_LOCAL = 'local';
|
||||
export const MESSAGE_TYPE_REMOTE = 'remote';
|
||||
|
||||
export const SMALL_WIDTH_THRESHOLD = 580;
|
||||
|
||||
/**
|
||||
* The modes of the buttons of the chat and polls tabs.
|
||||
*/
|
||||
export const BUTTON_MODES = {
|
||||
CONTAINED: 'contained',
|
||||
TEXT: 'text'
|
||||
};
|
||||
|
||||
@@ -78,3 +78,15 @@ export function getUnreadCount(state: Object) {
|
||||
|
||||
return messagesCount - (lastReadIndex + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for calculating the number of unread chat messages.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {number} The number of unread messages.
|
||||
*/
|
||||
export function getUnreadMessagesCount(state: Object) {
|
||||
const { nbUnreadMessages } = state['features/chat'];
|
||||
|
||||
return nbUnreadMessages;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
||||
import { openDisplayNamePrompt } from '../display-name';
|
||||
import { resetNbUnreadPollsMessages } from '../polls/actions';
|
||||
import { ADD_REACTION_MESSAGE } from '../reactions/actionTypes';
|
||||
import { pushReactions } from '../reactions/actions.any';
|
||||
import { getReactionMessageFromBuffer } from '../reactions/functions.any';
|
||||
@@ -33,7 +34,7 @@ import {
|
||||
setToolboxVisible
|
||||
} from '../toolbox/actions.web';
|
||||
|
||||
import { ADD_MESSAGE, SEND_MESSAGE, OPEN_CHAT, CLOSE_CHAT } from './actionTypes';
|
||||
import { ADD_MESSAGE, SEND_MESSAGE, OPEN_CHAT, CLOSE_CHAT, SET_IS_POLL_TAB_FOCUSED } from './actionTypes';
|
||||
import { addMessage, clearMessages } from './actions';
|
||||
import { closeChat } from './actions.any';
|
||||
import { ChatPrivacyDialog } from './components';
|
||||
@@ -112,15 +113,27 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
}
|
||||
break;
|
||||
|
||||
case CLOSE_CHAT:
|
||||
case CLOSE_CHAT: {
|
||||
const isPollTabOpen = getState()['features/chat'].isPollsTabFocused;
|
||||
|
||||
unreadCount = 0;
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyChatUpdated(unreadCount, false);
|
||||
}
|
||||
|
||||
if (isPollTabOpen) {
|
||||
dispatch(resetNbUnreadPollsMessages());
|
||||
}
|
||||
|
||||
dispatch(setActiveModalId());
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_IS_POLL_TAB_FOCUSED: {
|
||||
dispatch(resetNbUnreadPollsMessages());
|
||||
break;
|
||||
}
|
||||
|
||||
case SEND_MESSAGE: {
|
||||
const state = store.getState();
|
||||
|
||||
@@ -7,13 +7,17 @@ import {
|
||||
CLEAR_MESSAGES,
|
||||
CLOSE_CHAT,
|
||||
OPEN_CHAT,
|
||||
SET_PRIVATE_MESSAGE_RECIPIENT
|
||||
SET_PRIVATE_MESSAGE_RECIPIENT,
|
||||
SET_IS_POLL_TAB_FOCUSED
|
||||
} from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
isOpen: false,
|
||||
isPollsTabFocused: false,
|
||||
lastReadMessage: undefined,
|
||||
lastReadPoll: undefined,
|
||||
messages: [],
|
||||
nbUnreadMessages: 0,
|
||||
privateMessageRecipient: undefined
|
||||
};
|
||||
|
||||
@@ -46,6 +50,7 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
||||
...state,
|
||||
lastReadMessage:
|
||||
action.hasRead ? newMessage : state.lastReadMessage,
|
||||
nbUnreadMessages: state.isPollsTabFocused ? state.nbUnreadMessages + 1 : state.nbUnreadMessages,
|
||||
messages
|
||||
};
|
||||
}
|
||||
@@ -78,6 +83,13 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
||||
navigator.product === 'ReactNative' ? 0 : state.messages.length - 1],
|
||||
privateMessageRecipient: action.participant
|
||||
};
|
||||
|
||||
case SET_IS_POLL_TAB_FOCUSED: {
|
||||
return {
|
||||
...state,
|
||||
isPollsTabFocused: action.isPollsTabFocused,
|
||||
nbUnreadMessages: 0
|
||||
}; }
|
||||
}
|
||||
|
||||
return state;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { translate } from '../../base/i18n';
|
||||
import { Switch } from '../../base/react';
|
||||
import { connect } from '../../base/redux';
|
||||
import { toggleE2EE } from '../actions';
|
||||
|
||||
import { doesEveryoneSupportE2EE } from '../functions';
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -38,12 +38,7 @@ type State = {
|
||||
/**
|
||||
* True if the switch is toggled on.
|
||||
*/
|
||||
enabled: boolean,
|
||||
|
||||
/**
|
||||
* True if the section description should be expanded, false otherwise.
|
||||
*/
|
||||
expand: boolean
|
||||
enabled: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -78,13 +73,10 @@ class E2EESection extends Component<Props, State> {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
enabled: false,
|
||||
expand: false
|
||||
enabled: false
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onExpand = this._onExpand.bind(this);
|
||||
this._onExpandKeyPress = this._onExpandKeyPress.bind(this);
|
||||
this._onToggle = this._onToggle.bind(this);
|
||||
}
|
||||
|
||||
@@ -96,7 +88,7 @@ class E2EESection extends Component<Props, State> {
|
||||
*/
|
||||
render() {
|
||||
const { _everyoneSupportE2EE, t } = this.props;
|
||||
const { enabled, expand } = this.state;
|
||||
const { enabled } = this.state;
|
||||
const description = t('dialog.e2eeDescription');
|
||||
|
||||
return (
|
||||
@@ -105,25 +97,10 @@ class E2EESection extends Component<Props, State> {
|
||||
aria-live = 'polite'
|
||||
className = 'description'
|
||||
id = 'e2ee-section-description'>
|
||||
{ expand && description }
|
||||
{ !expand && description.substring(0, 100) }
|
||||
{ !expand && <span
|
||||
aria-controls = 'e2ee-section-description'
|
||||
aria-expanded = { expand }
|
||||
className = 'read-more'
|
||||
onClick = { this._onExpand }
|
||||
onKeyPress = { this._onExpandKeyPress }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
... { t('dialog.readMore') }
|
||||
</span> }
|
||||
{ description }
|
||||
{ !_everyoneSupportE2EE && <br /> }
|
||||
{ !_everyoneSupportE2EE && t('dialog.e2eeWarning') }
|
||||
</p>
|
||||
{
|
||||
!_everyoneSupportE2EE
|
||||
&& <span className = 'warning'>
|
||||
{ t('dialog.e2eeWarning') }
|
||||
</span>
|
||||
}
|
||||
<div className = 'control-row'>
|
||||
<label htmlFor = 'e2ee-section-switch'>
|
||||
{ t('dialog.e2eeLabel') }
|
||||
@@ -137,35 +114,6 @@ class E2EESection extends Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
_onExpand: () => void;
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the description is expanded.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onExpand() {
|
||||
this.setState({
|
||||
expand: true
|
||||
});
|
||||
}
|
||||
|
||||
_onExpandKeyPress: (Object) => void;
|
||||
|
||||
/**
|
||||
* KeyPress handler for accessibility.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onExpandKeyPress(e) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this._onExpand();
|
||||
}
|
||||
}
|
||||
|
||||
_onToggle: () => void;
|
||||
|
||||
/**
|
||||
@@ -194,11 +142,11 @@ class E2EESection extends Component<Props, State> {
|
||||
* @returns {Props}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
const { enabled, everyoneSupportE2EE } = state['features/e2ee'];
|
||||
const { enabled } = state['features/e2ee'];
|
||||
|
||||
return {
|
||||
_enabled: enabled,
|
||||
_everyoneSupportE2EE: everyoneSupportE2EE
|
||||
_everyoneSupportE2EE: doesEveryoneSupportE2EE(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
29
react/features/e2ee/functions.js
Normal file
29
react/features/e2ee/functions.js
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
import { getParticipantCount } from '../base/participants/functions';
|
||||
import { toState } from '../base/redux';
|
||||
|
||||
/**
|
||||
* Gets the value of a specific React {@code Component} prop of the currently
|
||||
* mounted {@link App}.
|
||||
*
|
||||
* @param {Function|Object} stateful - The redux store or {@code getState}
|
||||
* function.
|
||||
* @param {string} propName - The name of the React {@code Component} prop of
|
||||
* the currently mounted {@code App} to get.
|
||||
* @returns {*} The value of the specified React {@code Component} prop of the
|
||||
* currently mounted {@code App}.
|
||||
*/
|
||||
export function doesEveryoneSupportE2EE(stateful) {
|
||||
const state = toState(stateful);
|
||||
const { everyoneSupportE2EE } = state['features/e2ee'];
|
||||
const { e2eeSupported } = state['features/base/conference'];
|
||||
const participantCount = getParticipantCount(state);
|
||||
|
||||
if (typeof everyoneSupportE2EE === 'undefined' && participantCount === 1) {
|
||||
// This will happen if we are alone.
|
||||
|
||||
return e2eeSupported;
|
||||
}
|
||||
|
||||
return everyoneSupportE2EE;
|
||||
}
|
||||
@@ -50,6 +50,15 @@ export const SET_TILE_VIEW_DIMENSIONS = 'SET_TILE_VIEW_DIMENSIONS';
|
||||
*/
|
||||
export const SET_HORIZONTAL_VIEW_DIMENSIONS = 'SET_HORIZONTAL_VIEW_DIMENSIONS';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the reordered list of the remote participants in the filmstrip.
|
||||
* {
|
||||
* type: SET_REMOTE_PARTICIPANTS,
|
||||
* participants: Array<string>
|
||||
* }
|
||||
*/
|
||||
export const SET_REMOTE_PARTICIPANTS = 'SET_REMOTE_PARTICIPANTS';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the dimensions of the thumbnails in vertical view.
|
||||
*
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getLocalParticipant, getRemoteParticipants, pinParticipant } from '../b
|
||||
|
||||
import {
|
||||
SET_HORIZONTAL_VIEW_DIMENSIONS,
|
||||
SET_REMOTE_PARTICIPANTS,
|
||||
SET_TILE_VIEW_DIMENSIONS,
|
||||
SET_VERTICAL_VIEW_DIMENSIONS,
|
||||
SET_VISIBLE_REMOTE_PARTICIPANTS,
|
||||
@@ -25,6 +26,23 @@ import {
|
||||
calculateThumbnailSizeForVerticalView
|
||||
} from './functions';
|
||||
|
||||
/**
|
||||
* Sets the list of the reordered remote participants based on which the visible participants in the filmstrip will be
|
||||
* determined.
|
||||
*
|
||||
* @param {Array<string>} participants - The list of the remote participant endpoint IDs.
|
||||
* @returns {{
|
||||
type: SET_REMOTE_PARTICIPANTS,
|
||||
participants: Array<string>
|
||||
}}
|
||||
*/
|
||||
export function setRemoteParticipants(participants: Array<string>) {
|
||||
return {
|
||||
type: SET_REMOTE_PARTICIPANTS,
|
||||
participants
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the dimensions of the tile view grid.
|
||||
*
|
||||
|
||||
@@ -269,11 +269,11 @@ class Filmstrip extends PureComponent <Props> {
|
||||
return `empty-${index}`;
|
||||
}
|
||||
|
||||
if (index === _remoteParticipantsLength) {
|
||||
if (index === 0) {
|
||||
return 'local';
|
||||
}
|
||||
|
||||
return _remoteParticipants[index];
|
||||
return _remoteParticipants[index - 1];
|
||||
}
|
||||
|
||||
_onListItemsRendered: Object => void;
|
||||
@@ -287,7 +287,7 @@ class Filmstrip extends PureComponent <Props> {
|
||||
_onListItemsRendered({ visibleStartIndex, visibleStopIndex }) {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(setVisibleRemoteParticipants(visibleStartIndex, visibleStopIndex));
|
||||
dispatch(setVisibleRemoteParticipants(visibleStartIndex, visibleStopIndex + 1));
|
||||
}
|
||||
|
||||
_onGridItemsRendered: Object => void;
|
||||
@@ -305,9 +305,12 @@ class Filmstrip extends PureComponent <Props> {
|
||||
visibleRowStopIndex
|
||||
}) {
|
||||
const { _columns, dispatch } = this.props;
|
||||
const startIndex = (visibleRowStartIndex * _columns) + visibleColumnStartIndex;
|
||||
let startIndex = (visibleRowStartIndex * _columns) + visibleColumnStartIndex;
|
||||
const endIndex = (visibleRowStopIndex * _columns) + visibleColumnStopIndex;
|
||||
|
||||
// In tile view, the start index needs to be offset by 1 because the first participant is the local
|
||||
// participant.
|
||||
startIndex = startIndex > 0 ? startIndex - 1 : 0;
|
||||
dispatch(setVisibleRemoteParticipants(startIndex, endIndex));
|
||||
}
|
||||
|
||||
|
||||
@@ -671,6 +671,10 @@ class Thumbnail extends Component<Props, State> {
|
||||
statsPopoverPosition = 'left-start';
|
||||
tooltipPosition = 'left';
|
||||
break;
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
|
||||
statsPopoverPosition = 'top';
|
||||
tooltipPosition = 'top';
|
||||
break;
|
||||
default:
|
||||
statsPopoverPosition = 'auto';
|
||||
tooltipPosition = 'top';
|
||||
|
||||
@@ -126,7 +126,8 @@ function _mapStateToProps(state, ownProps) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (index === remoteParticipantsLength) {
|
||||
// Make the local participant as the first thumbnail (top left corner) in tile view.
|
||||
if (index === 0) {
|
||||
return {
|
||||
_participantID: 'local',
|
||||
_horizontalOffset: horizontalOffset
|
||||
@@ -134,7 +135,7 @@ function _mapStateToProps(state, ownProps) {
|
||||
}
|
||||
|
||||
return {
|
||||
_participantID: remoteParticipants[index],
|
||||
_participantID: remoteParticipants[index - 1],
|
||||
_horizontalOffset: horizontalOffset
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
isRemoteTrackMuted
|
||||
} from '../base/tracks/functions';
|
||||
|
||||
import { setRemoteParticipants } from './actions.web';
|
||||
import {
|
||||
ASPECT_RATIO_BREAKPOINT,
|
||||
DISPLAY_AVATAR,
|
||||
@@ -265,3 +266,36 @@ export function computeDisplayMode(input: Object) {
|
||||
// check hovering and change state to avatar with name
|
||||
return isHovered ? DISPLAY_AVATAR_WITH_NAME : DISPLAY_AVATAR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the reorderd list of the remote participants.
|
||||
*
|
||||
* @param {*} store - The redux store.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
export function updateRemoteParticipants(store: Object) {
|
||||
const state = store.getState();
|
||||
const { fakeParticipants, sortedRemoteParticipants, speakersList } = state['features/base/participants'];
|
||||
const { remoteScreenShares } = state['features/video-layout'];
|
||||
const screenShares = (remoteScreenShares || []).slice();
|
||||
let speakers = (speakersList || []).slice();
|
||||
const remoteParticipants = new Map(sortedRemoteParticipants);
|
||||
const sharedVideos = fakeParticipants ? Array.from(fakeParticipants.keys()) : [];
|
||||
|
||||
for (const screenshare of screenShares) {
|
||||
remoteParticipants.delete(screenshare);
|
||||
speakers = speakers.filter(speaker => speaker !== screenshare);
|
||||
}
|
||||
for (const sharedVideo of sharedVideos) {
|
||||
remoteParticipants.delete(sharedVideo);
|
||||
speakers = speakers.filter(speaker => speaker !== sharedVideo);
|
||||
}
|
||||
for (const speaker of speakers) {
|
||||
remoteParticipants.delete(speaker);
|
||||
}
|
||||
const reorderedParticipants
|
||||
= [ ...screenShares.reverse(), ...sharedVideos, ...speakers, ...Array.from(remoteParticipants.keys()) ];
|
||||
|
||||
store.dispatch(setRemoteParticipants(reorderedParticipants));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { CLIENT_RESIZED } from '../base/responsive-ui';
|
||||
import { SETTINGS_UPDATED } from '../base/settings';
|
||||
@@ -9,8 +10,13 @@ import {
|
||||
LAYOUTS
|
||||
} from '../video-layout';
|
||||
|
||||
import { setHorizontalViewDimensions, setTileViewDimensions, setVerticalViewDimensions } from './actions.web';
|
||||
|
||||
import {
|
||||
setHorizontalViewDimensions,
|
||||
setRemoteParticipants,
|
||||
setTileViewDimensions,
|
||||
setVerticalViewDimensions
|
||||
} from './actions.web';
|
||||
import { updateRemoteParticipants } from './functions.web';
|
||||
import './subscriber.web';
|
||||
|
||||
/**
|
||||
@@ -41,6 +47,14 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PARTICIPANT_JOINED: {
|
||||
updateRemoteParticipants(store);
|
||||
break;
|
||||
}
|
||||
case PARTICIPANT_LEFT: {
|
||||
_updateRemoteParticipantsOnLeave(store, action.participant?.id);
|
||||
break;
|
||||
}
|
||||
case SETTINGS_UPDATED: {
|
||||
if (typeof action.settings?.localFlipX === 'boolean') {
|
||||
// TODO: This needs to be removed once the large video is Reactified.
|
||||
@@ -53,3 +67,22 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Private helper to calculate the reordered list of remote participants when a participant leaves.
|
||||
*
|
||||
* @param {*} store - The redux store.
|
||||
* @param {string} participantId - The endpoint id of the participant leaving the call.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function _updateRemoteParticipantsOnLeave(store, participantId = null) {
|
||||
if (!participantId) {
|
||||
return;
|
||||
}
|
||||
const state = store.getState();
|
||||
const { remoteParticipants } = state['features/filmstrip'];
|
||||
const reorderedParticipants = new Set(remoteParticipants);
|
||||
|
||||
reorderedParticipants.delete(participantId)
|
||||
&& store.dispatch(setRemoteParticipants(Array.from(reorderedParticipants)));
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants';
|
||||
import { PARTICIPANT_LEFT } from '../base/participants';
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
SET_FILMSTRIP_ENABLED,
|
||||
SET_FILMSTRIP_VISIBLE,
|
||||
SET_HORIZONTAL_VIEW_DIMENSIONS,
|
||||
SET_REMOTE_PARTICIPANTS,
|
||||
SET_TILE_VIEW_DIMENSIONS,
|
||||
SET_VERTICAL_VIEW_DIMENSIONS,
|
||||
SET_VISIBLE_REMOTE_PARTICIPANTS,
|
||||
@@ -40,8 +41,8 @@ const DEFAULT_STATE = {
|
||||
/**
|
||||
* The ordered IDs of the remote participants displayed in the filmstrip.
|
||||
*
|
||||
* NOTE: Currently the order will match the one from the base/participants array. But this is good initial step for
|
||||
* reordering the remote participants.
|
||||
* @public
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
remoteParticipants: [],
|
||||
|
||||
@@ -77,22 +78,21 @@ const DEFAULT_STATE = {
|
||||
*/
|
||||
visibleParticipantsEndIndex: 0,
|
||||
|
||||
/**
|
||||
* The visible participants in the filmstrip.
|
||||
*
|
||||
* @public
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
visibleParticipants: [],
|
||||
|
||||
|
||||
/**
|
||||
* The start index in the remote participants array that is visible in the filmstrip.
|
||||
*
|
||||
* @public
|
||||
* @type {number}
|
||||
*/
|
||||
visibleParticipantsStartIndex: 0
|
||||
visibleParticipantsStartIndex: 0,
|
||||
|
||||
/**
|
||||
* The visible remote participants in the filmstrip.
|
||||
*
|
||||
* @public
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
visibleRemoteParticipants: new Set()
|
||||
};
|
||||
|
||||
ReducerRegistry.register(
|
||||
@@ -116,6 +116,14 @@ ReducerRegistry.register(
|
||||
...state,
|
||||
horizontalViewDimensions: action.dimensions
|
||||
};
|
||||
case SET_REMOTE_PARTICIPANTS: {
|
||||
const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state;
|
||||
|
||||
state.remoteParticipants = action.participants;
|
||||
state.visibleRemoteParticipants = new Set(state.remoteParticipants.slice(startIndex, endIndex));
|
||||
|
||||
return { ...state };
|
||||
}
|
||||
case SET_TILE_VIEW_DIMENSIONS:
|
||||
return {
|
||||
...state,
|
||||
@@ -138,27 +146,13 @@ ReducerRegistry.register(
|
||||
[action.participantId]: action.volume
|
||||
}
|
||||
};
|
||||
case SET_VISIBLE_REMOTE_PARTICIPANTS:
|
||||
case SET_VISIBLE_REMOTE_PARTICIPANTS: {
|
||||
return {
|
||||
...state,
|
||||
visibleParticipantsStartIndex: action.startIndex,
|
||||
visibleParticipantsEndIndex: action.endIndex,
|
||||
visibleParticipants: state.remoteParticipants.slice(action.startIndex, action.endIndex + 1)
|
||||
visibleRemoteParticipants: new Set(state.remoteParticipants.slice(action.startIndex, action.endIndex))
|
||||
};
|
||||
case PARTICIPANT_JOINED: {
|
||||
const { id, local } = action.participant;
|
||||
|
||||
if (!local) {
|
||||
state.remoteParticipants = [ ...state.remoteParticipants, id ];
|
||||
|
||||
const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state;
|
||||
|
||||
if (state.remoteParticipants.length - 1 <= endIndex) {
|
||||
state.visibleParticipants = state.remoteParticipants.slice(startIndex, endIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
case PARTICIPANT_LEFT: {
|
||||
const { id, local } = action.participant;
|
||||
@@ -166,25 +160,6 @@ ReducerRegistry.register(
|
||||
if (local) {
|
||||
return state;
|
||||
}
|
||||
|
||||
let removedParticipantIndex = 0;
|
||||
|
||||
state.remoteParticipants = state.remoteParticipants.filter((participantId, index) => {
|
||||
if (participantId === id) {
|
||||
removedParticipantIndex = index;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const { visibleParticipantsStartIndex: startIndex, visibleParticipantsEndIndex: endIndex } = state;
|
||||
|
||||
if (removedParticipantIndex >= startIndex && removedParticipantIndex <= endIndex) {
|
||||
state.visibleParticipants = state.remoteParticipants.slice(startIndex, endIndex + 1);
|
||||
}
|
||||
|
||||
delete state.participantsVolume[id];
|
||||
|
||||
return state;
|
||||
|
||||
@@ -8,13 +8,18 @@ import { getParticipantsPaneOpen } from '../participants-pane/functions';
|
||||
import { setOverflowDrawer } from '../toolbox/actions.web';
|
||||
import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
|
||||
|
||||
import { setHorizontalViewDimensions, setTileViewDimensions, setVerticalViewDimensions } from './actions.web';
|
||||
import {
|
||||
setHorizontalViewDimensions,
|
||||
setTileViewDimensions,
|
||||
setVerticalViewDimensions
|
||||
} from './actions.web';
|
||||
import {
|
||||
ASPECT_RATIO_BREAKPOINT,
|
||||
DISPLAY_DRAWER_THRESHOLD,
|
||||
SINGLE_COLUMN_BREAKPOINT,
|
||||
TWO_COLUMN_BREAKPOINT
|
||||
} from './constants';
|
||||
import { updateRemoteParticipants } from './functions.web';
|
||||
|
||||
/**
|
||||
* Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
|
||||
@@ -153,3 +158,36 @@ StateListenerRegistry.register(
|
||||
store.dispatch(setTileViewDimensions(gridDimensions));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Listens for changes to the screensharing status of the remote participants to recompute the reordered list of the
|
||||
* remote endpoints.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/video-layout'].remoteScreenShares,
|
||||
/* listener */ (remoteScreenShares, store) => updateRemoteParticipants(store));
|
||||
|
||||
/**
|
||||
* Listens for changes to the dominant speaker to recompute the reordered list of the remote endpoints.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/participants'].dominantSpeaker,
|
||||
/* listener */ (dominantSpeaker, store) => _reorderDominantSpeakers(store));
|
||||
|
||||
/**
|
||||
* Private helper function that reorders the remote participants based on dominant speaker changes.
|
||||
*
|
||||
* @param {*} store - The redux store.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
function _reorderDominantSpeakers(store) {
|
||||
const state = store.getState();
|
||||
const { dominantSpeaker, local } = state['features/base/participants'];
|
||||
const { visibleRemoteParticipants } = state['features/filmstrip'];
|
||||
|
||||
// Reorder the participants if the new dominant speaker is currently not visible.
|
||||
if (dominantSpeaker !== local?.id && !visibleRemoteParticipants.has(dominantSpeaker)) {
|
||||
updateRemoteParticipants(store);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PureComponent } from 'react';
|
||||
|
||||
import { isLocalParticipantModerator } from '../../base/participants';
|
||||
import { setKnockingParticipantApproval } from '../actions';
|
||||
import { getLobbyState } from '../functions';
|
||||
import { getKnockingParticipants, getLobbyEnabled } from '../functions';
|
||||
|
||||
export type Props = {
|
||||
|
||||
@@ -67,7 +67,8 @@ export default class AbstractKnockingParticipantList<P: Props = Props> extends P
|
||||
* @returns {Props}
|
||||
*/
|
||||
export function mapStateToProps(state: Object): $Shape<Props> {
|
||||
const { knockingParticipants, lobbyEnabled } = getLobbyState(state);
|
||||
const lobbyEnabled = getLobbyEnabled(state);
|
||||
const knockingParticipants = getKnockingParticipants(state);
|
||||
|
||||
return {
|
||||
_participants: knockingParticipants,
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* Selector to return lobby state.
|
||||
*
|
||||
* @param {any} state - State object.
|
||||
* @returns {any}
|
||||
*/
|
||||
export function getLobbyState(state: any) {
|
||||
return state['features/lobby'];
|
||||
* Selector to return lobby enable state.
|
||||
*
|
||||
* @param {any} state - State object.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getLobbyEnabled(state: any) {
|
||||
return state['features/lobby'].lobbyEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector to return a list of knocking participants.
|
||||
*
|
||||
* @param {any} state - State object.
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
export function getKnockingParticipants(state: any) {
|
||||
return state['features/lobby'].knockingParticipants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector to return array with knocking participant ids.
|
||||
@@ -18,7 +27,5 @@ export function getLobbyState(state: any) {
|
||||
* @returns {Array}
|
||||
*/
|
||||
export function getKnockingParticipantsById(state: any) {
|
||||
const { knockingParticipants } = state['features/lobby'];
|
||||
|
||||
return knockingParticipants.map(participant => participant.id);
|
||||
return getKnockingParticipants(state).map(participant => participant.id);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ export default createStyleSheet({
|
||||
deviceRow: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
height: MD_ITEM_HEIGHT
|
||||
height: MD_ITEM_HEIGHT,
|
||||
marginLeft: 16
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,14 +21,19 @@ type Props = {
|
||||
className: string,
|
||||
|
||||
/**
|
||||
* The `data-testid` used for the button.
|
||||
* CSS id of the button.
|
||||
*/
|
||||
testId: string,
|
||||
id?: string,
|
||||
|
||||
/**
|
||||
* The participant.
|
||||
*/
|
||||
participant: Object
|
||||
participant: Object,
|
||||
|
||||
/**
|
||||
* The `data-testid` used for the button.
|
||||
*/
|
||||
testId: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,7 +41,7 @@ type Props = {
|
||||
*
|
||||
* @returns {React$Element<'button'>}
|
||||
*/
|
||||
export default function({ action, children, className, testId, participant }: Props) {
|
||||
export default function({ action, children, className, participant, id, testId }: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const onClick = useCallback(() => dispatch(action(participant.id)), [ dispatch, participant ]);
|
||||
|
||||
@@ -44,6 +49,7 @@ export default function({ action, children, className, testId, participant }: Pr
|
||||
<button
|
||||
className = { className }
|
||||
data-testid = { testId }
|
||||
id = { id }
|
||||
onClick = { onClick }
|
||||
type = 'button'>
|
||||
{ children }
|
||||
|
||||
@@ -80,6 +80,7 @@ export default function({
|
||||
{ <NotificationButton
|
||||
action = { onApprove }
|
||||
className = 'primary'
|
||||
id = 'unmute-button'
|
||||
participant = { p }
|
||||
testId = { `${testIdPrefix}.allow` }>
|
||||
{ approveButtonText }
|
||||
@@ -87,6 +88,7 @@ export default function({
|
||||
{ <NotificationButton
|
||||
action = { onReject }
|
||||
className = 'borderLess'
|
||||
id = 'dismiss-button'
|
||||
participant = { p }
|
||||
testId = { `${testIdPrefix}.reject` }>
|
||||
{ rejectButtonText }
|
||||
|
||||
@@ -15,9 +15,9 @@ type Props = {
|
||||
askUnmuteText: string,
|
||||
|
||||
/**
|
||||
* Participant id.
|
||||
* Participant participantID.
|
||||
*/
|
||||
id: string
|
||||
participantID: string,
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,14 +26,15 @@ type Props = {
|
||||
* @param {Object} participant - Participant reference.
|
||||
* @returns {React$Element<'button'>}
|
||||
*/
|
||||
export default function AskToUnmuteButton({ id, askUnmuteText }: Props) {
|
||||
export default function AskToUnmuteButton({ askUnmuteText, participantID }: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const askToUnmute = useCallback(() => {
|
||||
dispatch(approveParticipant(id));
|
||||
}, [ dispatch, id ]);
|
||||
dispatch(approveParticipant(participantID));
|
||||
}, [ dispatch, participantID ]);
|
||||
|
||||
return (
|
||||
<QuickActionButton
|
||||
aria-label = { `unmute-${participantID}` }
|
||||
onClick = { askToUnmute }
|
||||
primary = { true }
|
||||
theme = {{
|
||||
|
||||
@@ -21,7 +21,8 @@ import { MuteEveryonesVideoDialog } from '../../video-menu/components';
|
||||
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuItem
|
||||
ContextMenuItem,
|
||||
ContextMenuItemGroup
|
||||
} from './web/styled';
|
||||
|
||||
const useStyles = makeStyles(() => {
|
||||
@@ -33,11 +34,11 @@ const useStyles = makeStyles(() => {
|
||||
right: 0,
|
||||
top: '-8px',
|
||||
transform: 'translateY(-100%)',
|
||||
width: '238px'
|
||||
width: '283px'
|
||||
},
|
||||
text: {
|
||||
marginLeft: '52px',
|
||||
lineHeight: '40px'
|
||||
color: '#C2C2C2',
|
||||
padding: '10px 16px 10px 52px'
|
||||
},
|
||||
paddedAction: {
|
||||
marginLeft: '36px;'
|
||||
@@ -74,23 +75,24 @@ export const FooterContextMenu = ({ onMouseLeave }: Props) => {
|
||||
<ContextMenu
|
||||
className = { classes.contextMenu }
|
||||
onMouseLeave = { onMouseLeave }>
|
||||
<ContextMenuItem
|
||||
id = 'participants-pane-context-menu-stop-video'
|
||||
onClick = { muteAllVideo }>
|
||||
<Icon
|
||||
size = { 20 }
|
||||
src = { IconVideoOff } />
|
||||
<span>{ t('participantsPane.actions.stopEveryonesVideo') }</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItemGroup>
|
||||
<ContextMenuItem
|
||||
id = 'participants-pane-context-menu-stop-video'
|
||||
onClick = { muteAllVideo }>
|
||||
<Icon
|
||||
size = { 20 }
|
||||
src = { IconVideoOff } />
|
||||
<span>{ t('participantsPane.actions.stopEveryonesVideo') }</span>
|
||||
</ContextMenuItem>
|
||||
</ContextMenuItemGroup>
|
||||
{ isModerationSupported && !allModerators ? (
|
||||
<>
|
||||
<ContextMenuItemGroup>
|
||||
<div className = { classes.text }>
|
||||
{t('participantsPane.actions.allow')}
|
||||
</div>
|
||||
{ isModerationEnabled ? (
|
||||
<ContextMenuItem
|
||||
id = 'participants-pane-context-menu-start-moderation'
|
||||
id = 'participants-pane-context-menu-stop-moderation'
|
||||
onClick = { disable }>
|
||||
<span className = { classes.paddedAction }>
|
||||
{ t('participantsPane.actions.startModeration') }
|
||||
@@ -98,7 +100,7 @@ export const FooterContextMenu = ({ onMouseLeave }: Props) => {
|
||||
</ContextMenuItem>
|
||||
) : (
|
||||
<ContextMenuItem
|
||||
id = 'participants-pane-context-menu-stop-moderation'
|
||||
id = 'participants-pane-context-menu-start-moderation'
|
||||
onClick = { enable }>
|
||||
<Icon
|
||||
size = { 20 }
|
||||
@@ -106,7 +108,7 @@ export const FooterContextMenu = ({ onMouseLeave }: Props) => {
|
||||
<span>{ t('participantsPane.actions.startModeration') }</span>
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
</>
|
||||
</ContextMenuItemGroup>
|
||||
) : undefined
|
||||
}
|
||||
</ContextMenu>
|
||||
|
||||
@@ -9,6 +9,11 @@ import { QuickActionButton } from './web/styled';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The translated ask unmute aria label.
|
||||
*/
|
||||
ariaLabel?: boolean,
|
||||
|
||||
/**
|
||||
* The translated "ask unmute" text.
|
||||
*/
|
||||
@@ -24,6 +29,9 @@ type Props = {
|
||||
*/
|
||||
muteAudio: Function,
|
||||
|
||||
/**
|
||||
* Label for mute participant button.
|
||||
*/
|
||||
muteParticipantButtonText: string,
|
||||
|
||||
/**
|
||||
@@ -59,7 +67,7 @@ export default function ParticipantQuickAction({
|
||||
return (
|
||||
<AskToUnmuteButton
|
||||
askUnmuteText = { askUnmuteText }
|
||||
id = { participantID } />
|
||||
participantID = { participantID } />
|
||||
);
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -10,6 +10,11 @@ import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/com
|
||||
*/
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
* Whether or not the participants pane is open.
|
||||
*/
|
||||
_isOpen: boolean,
|
||||
|
||||
/**
|
||||
* External handler for click action.
|
||||
*/
|
||||
@@ -34,6 +39,31 @@ class ParticipantsPaneButton extends AbstractButton<Props, *> {
|
||||
_handleClick() {
|
||||
this.props.handleClick();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this button is in toggled state or not.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isToggled() {
|
||||
return this.props._isOpen;
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(ParticipantsPaneButton));
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {Props}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
const { isOpen } = state['features/participants-pane'];
|
||||
|
||||
return {
|
||||
_isOpen: isOpen
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(ParticipantsPaneButton));
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Button, withTheme } from 'react-native-paper';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { admitMultiple } from '../../../lobby/actions.native';
|
||||
import { getLobbyState } from '../../../lobby/functions';
|
||||
import { getKnockingParticipants, getLobbyEnabled } from '../../../lobby/functions';
|
||||
|
||||
import { LobbyParticipantItem } from './LobbyParticipantItem';
|
||||
import styles from './styles';
|
||||
@@ -21,10 +21,9 @@ type Props = {
|
||||
};
|
||||
|
||||
const LobbyParticipantList = ({ theme }: Props) => {
|
||||
const {
|
||||
lobbyEnabled,
|
||||
knockingParticipants: participants
|
||||
} = useSelector(getLobbyState);
|
||||
const lobbyEnabled = useSelector(getLobbyEnabled);
|
||||
const participants = useSelector(getKnockingParticipants);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const admitAll = useCallback(() =>
|
||||
dispatch(admitMultiple(participants)),
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Icon, IconInviteMore } from '../../../base/icons';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantCountWithFake,
|
||||
getRemoteParticipants
|
||||
getSortedParticipants
|
||||
} from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { doInvitePeople } from '../../../invite/actions.native';
|
||||
@@ -35,11 +35,9 @@ type Props = {
|
||||
|
||||
const MeetingParticipantList = ({ _localVideoOwner }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const items = [];
|
||||
const localParticipant = useSelector(getLocalParticipant);
|
||||
const onInvite = useCallback(() => dispatch(doInvitePeople()), [ dispatch ]);
|
||||
const participants = useSelector(getRemoteParticipants);
|
||||
const participantsCount = useSelector(getParticipantCountWithFake);
|
||||
const sortedParticipants = useSelector(getSortedParticipants);
|
||||
const showInviteButton = useSelector(shouldRenderInviteButton);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -73,12 +71,6 @@ const MeetingParticipantList = ({ _localVideoOwner }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
items.push(renderParticipant(localParticipant));
|
||||
|
||||
participants.forEach(p => {
|
||||
items.push(renderParticipant(p));
|
||||
});
|
||||
|
||||
return (
|
||||
<View style = { styles.meetingList }>
|
||||
<Text style = { styles.meetingListDescription }>
|
||||
@@ -100,7 +92,7 @@ const MeetingParticipantList = ({ _localVideoOwner }: Props) => {
|
||||
onPress = { onInvite }
|
||||
style = { styles.inviteButton } />
|
||||
}
|
||||
{ items }
|
||||
{ sortedParticipants.map(renderParticipant) }
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import { admitMultiple } from '../../../lobby/actions.web';
|
||||
import { getLobbyState } from '../../../lobby/functions';
|
||||
import { getKnockingParticipants, getLobbyEnabled } from '../../../lobby/functions';
|
||||
|
||||
import { LobbyParticipantItem } from './LobbyParticipantItem';
|
||||
|
||||
@@ -32,10 +32,9 @@ const useStyles = makeStyles(theme => {
|
||||
|
||||
|
||||
export const LobbyParticipantList = () => {
|
||||
const {
|
||||
lobbyEnabled,
|
||||
knockingParticipants: participants
|
||||
} = useSelector(getLobbyState);
|
||||
const lobbyEnabled = useSelector(getLobbyEnabled);
|
||||
const participants = useSelector(getKnockingParticipants);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const classes = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -6,9 +6,8 @@ import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import { openDialog } from '../../../base/dialog';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantCountWithFake,
|
||||
getRemoteParticipants
|
||||
getSortedParticipantIds
|
||||
} from '../../../base/participants';
|
||||
import MuteRemoteParticipantDialog from '../../../video-menu/components/web/MuteRemoteParticipantDialog';
|
||||
import { findStyledAncestor, shouldRenderInviteButton } from '../../functions';
|
||||
@@ -46,8 +45,7 @@ const initialState = Object.freeze(Object.create(null));
|
||||
export function MeetingParticipantList() {
|
||||
const dispatch = useDispatch();
|
||||
const isMouseOverMenu = useRef(false);
|
||||
const participants = useSelector(getRemoteParticipants);
|
||||
const localParticipant = useSelector(getLocalParticipant);
|
||||
const sortedParticipantIds = useSelector(getSortedParticipantIds);
|
||||
|
||||
// This is very important as getRemoteParticipants is not changing its reference object
|
||||
// and we will not re-render on change, but if count changes we will do
|
||||
@@ -130,19 +128,12 @@ export function MeetingParticipantList() {
|
||||
youText = { youText } />
|
||||
);
|
||||
|
||||
const items = [];
|
||||
|
||||
localParticipant && items.push(renderParticipant(localParticipant?.id));
|
||||
participants.forEach(p => {
|
||||
items.push(renderParticipant(p?.id));
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading>{t('participantsPane.headings.participantsList', { count: participantsCount })}</Heading>
|
||||
{showInviteButton && <InviteButton />}
|
||||
<div>
|
||||
{ items }
|
||||
{sortedParticipantIds.map(renderParticipant)}
|
||||
</div>
|
||||
<MeetingParticipantContextMenu
|
||||
muteAudio = { muteAudio }
|
||||
|
||||
@@ -4,17 +4,16 @@ import React, { type Node } from 'react';
|
||||
|
||||
import { Avatar } from '../../../base/avatar';
|
||||
import {
|
||||
Icon,
|
||||
IconCameraEmpty,
|
||||
IconCameraEmptyDisabled,
|
||||
IconMicrophoneEmpty,
|
||||
IconMicrophoneEmptySlash
|
||||
} from '../../../base/icons';
|
||||
import { ACTION_TRIGGER, MEDIA_STATE, type ActionTrigger, type MediaState } from '../../constants';
|
||||
ACTION_TRIGGER,
|
||||
AudioStateIcons,
|
||||
MEDIA_STATE,
|
||||
type ActionTrigger,
|
||||
type MediaState,
|
||||
VideoStateIcons
|
||||
} from '../../constants';
|
||||
|
||||
import { RaisedHandIndicator } from './RaisedHandIndicator';
|
||||
import {
|
||||
ColoredIcon,
|
||||
ParticipantActionsHover,
|
||||
ParticipantActionsPermanent,
|
||||
ParticipantContainer,
|
||||
@@ -32,54 +31,6 @@ const Actions = {
|
||||
[ACTION_TRIGGER.PERMANENT]: ParticipantActionsPermanent
|
||||
};
|
||||
|
||||
/**
|
||||
* Icon mapping for possible participant audio states.
|
||||
*/
|
||||
const AudioStateIcons: {[MediaState]: React$Element<any> | null} = {
|
||||
[MEDIA_STATE.FORCE_MUTED]: (
|
||||
<ColoredIcon color = '#E04757'>
|
||||
<Icon
|
||||
size = { 16 }
|
||||
src = { IconMicrophoneEmptySlash } />
|
||||
</ColoredIcon>
|
||||
),
|
||||
[MEDIA_STATE.MUTED]: (
|
||||
<Icon
|
||||
size = { 16 }
|
||||
src = { IconMicrophoneEmptySlash } />
|
||||
),
|
||||
[MEDIA_STATE.UNMUTED]: (
|
||||
<ColoredIcon color = '#1EC26A'>
|
||||
<Icon
|
||||
size = { 16 }
|
||||
src = { IconMicrophoneEmpty } />
|
||||
</ColoredIcon>
|
||||
),
|
||||
[MEDIA_STATE.NONE]: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Icon mapping for possible participant video states.
|
||||
*/
|
||||
const VideoStateIcons = {
|
||||
[MEDIA_STATE.FORCE_MUTED]: (
|
||||
<Icon
|
||||
size = { 16 }
|
||||
src = { IconCameraEmptyDisabled } />
|
||||
),
|
||||
[MEDIA_STATE.MUTED]: (
|
||||
<Icon
|
||||
size = { 16 }
|
||||
src = { IconCameraEmptyDisabled } />
|
||||
),
|
||||
[MEDIA_STATE.UNMUTED]: (
|
||||
<Icon
|
||||
size = { 16 }
|
||||
src = { IconCameraEmpty } />
|
||||
),
|
||||
[MEDIA_STATE.NONE]: null
|
||||
};
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
@@ -161,7 +112,9 @@ export default function ParticipantItem({
|
||||
|
||||
return (
|
||||
<ParticipantContainer
|
||||
id = { `participant-item-${participantID}` }
|
||||
isHighlighted = { isHighlighted }
|
||||
local = { local }
|
||||
onMouseLeave = { onLeave }
|
||||
trigger = { actionsTrigger }>
|
||||
<Avatar
|
||||
|
||||
@@ -99,7 +99,7 @@ export const ContextMenuItem = styled.div`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
min-height: 40px;
|
||||
padding: 8px 16px;
|
||||
padding: 10px 16px;
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: 16px;
|
||||
@@ -190,12 +190,6 @@ export const Heading = styled.div`
|
||||
margin: 8px 0 ${props => props.theme.panePadding}px;
|
||||
`;
|
||||
|
||||
export const ColoredIcon = styled.div`
|
||||
& > div > svg {
|
||||
fill: ${props => props.color || '#fff'};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ParticipantActionButton = styled(Button)`
|
||||
height: ${props => props.theme.participantActionButtonHeight}px;
|
||||
padding: 6px 10px;
|
||||
@@ -252,6 +246,25 @@ export const ParticipantContent = styled.div`
|
||||
padding-right: ${props => props.theme.panePadding}px;
|
||||
`;
|
||||
|
||||
export const ParticipantStates = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
& > * {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.jitsi-icon {
|
||||
padding: 3px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ParticipantContainer = styled.div`
|
||||
align-items: center;
|
||||
color: white;
|
||||
@@ -262,6 +275,12 @@ export const ParticipantContainer = styled.div`
|
||||
padding-left: ${props => props.theme.panePadding}px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
${ParticipantStates} {
|
||||
${props => !props.local && 'display: none'};
|
||||
}
|
||||
}
|
||||
|
||||
${props => !props.isHighlighted && '&:hover {'}
|
||||
background-color: #292929;
|
||||
|
||||
@@ -302,21 +321,6 @@ export const ParticipantNameContainer = styled.div`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const ParticipantStates = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
& > * {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const RaisedHandIndicatorBackground = styled.div`
|
||||
background-color: #ed9e1b;
|
||||
border-radius: 3px;
|
||||
|
||||
@@ -25,17 +25,19 @@ export const ACTION_TRIGGER: {HOVER: ActionTrigger, PERMANENT: ActionTrigger} =
|
||||
PERMANENT: 'Permanent'
|
||||
};
|
||||
|
||||
export type MediaState = 'Muted' | 'ForceMuted' | 'Unmuted' | 'None';
|
||||
export type MediaState = 'DominantSpeaker' | 'Muted' | 'ForceMuted' | 'Unmuted' | 'None';
|
||||
|
||||
/**
|
||||
* Enum of possible participant media states.
|
||||
*/
|
||||
export const MEDIA_STATE: {
|
||||
DOMINANT_SPEAKER: MediaState,
|
||||
MUTED: MediaState,
|
||||
FORCE_MUTED: MediaState,
|
||||
UNMUTED: MediaState,
|
||||
NONE: MediaState,
|
||||
} = {
|
||||
DOMINANT_SPEAKER: 'DominantSpeaker',
|
||||
MUTED: 'Muted',
|
||||
FORCE_MUTED: 'ForceMuted',
|
||||
UNMUTED: 'Unmuted',
|
||||
@@ -61,6 +63,12 @@ export const QUICK_ACTION_BUTTON: {
|
||||
* Icon mapping for possible participant audio states.
|
||||
*/
|
||||
export const AudioStateIcons: {[MediaState]: React$Element<any> | null} = {
|
||||
[MEDIA_STATE.DOMINANT_SPEAKER]: (
|
||||
<Icon
|
||||
className = 'jitsi-icon-dominant-speaker'
|
||||
size = { 16 }
|
||||
src = { IconMicrophoneEmpty } />
|
||||
),
|
||||
[MEDIA_STATE.FORCE_MUTED]: (
|
||||
<Icon
|
||||
color = '#E04757'
|
||||
@@ -74,7 +82,6 @@ export const AudioStateIcons: {[MediaState]: React$Element<any> | null} = {
|
||||
),
|
||||
[MEDIA_STATE.UNMUTED]: (
|
||||
<Icon
|
||||
color = '#1EC26A'
|
||||
size = { 16 }
|
||||
src = { IconMicrophoneEmpty } />
|
||||
),
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { getFeatureFlag, INVITE_ENABLED } from '../base/flags';
|
||||
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
|
||||
import {
|
||||
getDominantSpeakerParticipant,
|
||||
getParticipantCount,
|
||||
isLocalParticipantModerator,
|
||||
isParticipantModerator
|
||||
@@ -74,6 +75,12 @@ export function isForceMuted(participant: Object, mediaType: MediaType, state: O
|
||||
* @returns {MediaState}
|
||||
*/
|
||||
export function getParticipantAudioMediaState(participant: Object, muted: Boolean, state: Object) {
|
||||
const dominantSpeaker = getDominantSpeakerParticipant(state);
|
||||
|
||||
if (participant === dominantSpeaker) {
|
||||
return MEDIA_STATE.DOMINANT_SPEAKER;
|
||||
}
|
||||
|
||||
if (muted) {
|
||||
if (isForceMuted(participant, MEDIA_TYPE.AUDIO, state)) {
|
||||
return MEDIA_STATE.FORCE_MUTED;
|
||||
|
||||
55
react/features/polls/actionTypes.js
Normal file
55
react/features/polls/actionTypes.js
Normal file
@@ -0,0 +1,55 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* The type of the action which signals that a new Poll was received.
|
||||
*
|
||||
* {
|
||||
* type: RECEIVE_POLL,
|
||||
* poll: Poll,
|
||||
* pollId: string,
|
||||
* notify: boolean
|
||||
* }
|
||||
*
|
||||
*/
|
||||
export const RECEIVE_POLL = 'RECEIVE_POLL';
|
||||
|
||||
/**
|
||||
* The type of the action which signals that a new Answer was received.
|
||||
*
|
||||
* {
|
||||
* type: RECEIVE_ANSWER,
|
||||
* answer: Answer,
|
||||
* pollId: string,
|
||||
* }
|
||||
*/
|
||||
export const RECEIVE_ANSWER = 'RECEIVE_ANSWER';
|
||||
|
||||
/**
|
||||
* The type of the action which registers a vote.
|
||||
*
|
||||
* {
|
||||
* type: REGISTER_VOTE,
|
||||
* answers: Array<boolean> | null,
|
||||
* pollId: string
|
||||
* }
|
||||
*/
|
||||
export const REGISTER_VOTE = 'REGISTER_VOTE';
|
||||
|
||||
/**
|
||||
* The type of the action which retracts a vote.
|
||||
*
|
||||
* {
|
||||
* type: RETRACT_VOTE,
|
||||
* pollId: string,
|
||||
* }
|
||||
*/
|
||||
export const RETRACT_VOTE = 'RETRACT_VOTE';
|
||||
|
||||
/**
|
||||
* The type of the action triggered when the poll tab in chat pane is closed
|
||||
*
|
||||
* {
|
||||
* type: RESET_NB_UNREAD_POLLS,
|
||||
* }
|
||||
*/
|
||||
export const RESET_NB_UNREAD_POLLS = 'RESET_NB_UNREAD_POLLS';
|
||||
99
react/features/polls/actions.js
Normal file
99
react/features/polls/actions.js
Normal file
@@ -0,0 +1,99 @@
|
||||
// @flow
|
||||
|
||||
import {
|
||||
RESET_NB_UNREAD_POLLS,
|
||||
RECEIVE_ANSWER,
|
||||
RECEIVE_POLL,
|
||||
REGISTER_VOTE,
|
||||
RETRACT_VOTE
|
||||
} from './actionTypes';
|
||||
import type { Answer, Poll } from './types';
|
||||
|
||||
/**
|
||||
* Action to signal that a new poll was received.
|
||||
*
|
||||
* @param {string} pollId - The id of the incoming poll.
|
||||
* @param {Poll} poll - The incoming Poll object.
|
||||
* @param {boolean} notify - Whether to send or not a notification.
|
||||
* @returns {{
|
||||
* type: RECEIVE_POLL,
|
||||
* poll: Poll,
|
||||
* pollId: string,
|
||||
* notify: boolean
|
||||
* }}
|
||||
*/
|
||||
export const receivePoll = (pollId: string, poll: Poll, notify: boolean) => {
|
||||
return {
|
||||
type: RECEIVE_POLL,
|
||||
poll,
|
||||
pollId,
|
||||
notify
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Action to signal that a new answer was received.
|
||||
*
|
||||
* @param {string} pollId - The id of the incoming poll.
|
||||
* @param {Answer} answer - The incoming Answer object.
|
||||
* @returns {{
|
||||
* type: RECEIVE_ANSWER,
|
||||
* answer: Answer,
|
||||
* pollId: string
|
||||
* }}
|
||||
*/
|
||||
export const receiveAnswer = (pollId: string, answer: Answer) => {
|
||||
return {
|
||||
type: RECEIVE_ANSWER,
|
||||
answer,
|
||||
pollId
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Action to register a vote on a poll.
|
||||
*
|
||||
* @param {string} pollId - The id of the poll.
|
||||
* @param {?Array<boolean>} answers - The new answers.
|
||||
* @returns {{
|
||||
* type: REGISTER_VOTE,
|
||||
* answers: ?Array<boolean>,
|
||||
* pollId: string
|
||||
* }}
|
||||
*/
|
||||
export const registerVote = (pollId: string, answers: Array<boolean> | null) => {
|
||||
return {
|
||||
type: REGISTER_VOTE,
|
||||
answers,
|
||||
pollId
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Action to retract a vote on a poll.
|
||||
*
|
||||
* @param {string} pollId - The id of the poll.
|
||||
* @returns {{
|
||||
* type: RETRACT_VOTE,
|
||||
* pollId: string
|
||||
* }}
|
||||
*/
|
||||
export const retractVote = (pollId: string) => {
|
||||
return {
|
||||
type: RETRACT_VOTE,
|
||||
pollId
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Action to signal the closing of the polls tab.
|
||||
*
|
||||
* @returns {{
|
||||
* type: POLL_TAB_CLOSED
|
||||
* }}
|
||||
*/
|
||||
export function resetNbUnreadPollsMessages() {
|
||||
return {
|
||||
type: RESET_NB_UNREAD_POLLS
|
||||
};
|
||||
}
|
||||
101
react/features/polls/components/AbstractPollAnswer.js
Normal file
101
react/features/polls/components/AbstractPollAnswer.js
Normal file
@@ -0,0 +1,101 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import type { AbstractComponent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { getLocalParticipant, getParticipantById } from '../../base/participants';
|
||||
import { registerVote } from '../actions';
|
||||
import { COMMAND_ANSWER_POLL } from '../constants';
|
||||
import type { Poll } from '../types';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of inheriting component.
|
||||
*/
|
||||
type InputProps = {
|
||||
pollId: string,
|
||||
};
|
||||
|
||||
/*
|
||||
* Props that will be passed by the AbstractPollAnswer to its
|
||||
* concrete implementations (web/native).
|
||||
**/
|
||||
export type AbstractProps = {
|
||||
checkBoxStates: Function,
|
||||
poll: Poll,
|
||||
setCheckbox: Function,
|
||||
skipAnswer: Function,
|
||||
submitAnswer: Function,
|
||||
t: Function,
|
||||
};
|
||||
|
||||
/**
|
||||
* Higher Order Component taking in a concrete PollAnswer component and
|
||||
* augmenting it with state/behavior common to both web and native implementations.
|
||||
*
|
||||
* @param {React.AbstractComponent} Component - The concrete component.
|
||||
* @returns {React.AbstractComponent}
|
||||
*/
|
||||
const AbstractPollAnswer = (Component: AbstractComponent<AbstractProps>) => (props: InputProps) => {
|
||||
|
||||
const { pollId } = props;
|
||||
|
||||
const conference: Object = useSelector(state => state['features/base/conference'].conference);
|
||||
|
||||
const poll: Poll = useSelector(state => state['features/polls'].polls[pollId]);
|
||||
|
||||
const { id: localId } = useSelector(getLocalParticipant);
|
||||
|
||||
const [ checkBoxStates, setCheckBoxState ] = useState(() => {
|
||||
if (poll.lastVote !== null) {
|
||||
return [ ...poll.lastVote ];
|
||||
}
|
||||
|
||||
return new Array(poll.answers.length).fill(false);
|
||||
});
|
||||
|
||||
const setCheckbox = useCallback((index, state) => {
|
||||
const newCheckBoxStates = [ ...checkBoxStates ];
|
||||
|
||||
newCheckBoxStates[index] = state;
|
||||
setCheckBoxState(newCheckBoxStates);
|
||||
}, [ checkBoxStates ]);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const localParticipant = useSelector(state => getParticipantById(state, localId));
|
||||
const localName: string = localParticipant.name ? localParticipant.name : 'Fellow Jitster';
|
||||
|
||||
const submitAnswer = useCallback(() => {
|
||||
conference.sendMessage({
|
||||
type: COMMAND_ANSWER_POLL,
|
||||
pollId,
|
||||
voterId: localId,
|
||||
voterName: localName,
|
||||
answers: checkBoxStates
|
||||
});
|
||||
|
||||
dispatch(registerVote(pollId, checkBoxStates));
|
||||
|
||||
return false;
|
||||
}, [ pollId, localId, localName, checkBoxStates, conference ]);
|
||||
|
||||
const skipAnswer = useCallback(() => {
|
||||
dispatch(registerVote(pollId, null));
|
||||
|
||||
}, [ pollId ]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (<Component
|
||||
checkBoxStates = { checkBoxStates }
|
||||
poll = { poll }
|
||||
setCheckbox = { setCheckbox }
|
||||
skipAnswer = { skipAnswer }
|
||||
submitAnswer = { submitAnswer }
|
||||
t = { t } />);
|
||||
|
||||
};
|
||||
|
||||
export default AbstractPollAnswer;
|
||||
135
react/features/polls/components/AbstractPollCreate.js
Normal file
135
react/features/polls/components/AbstractPollCreate.js
Normal file
@@ -0,0 +1,135 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import type { AbstractComponent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getParticipantDisplayName } from '../../base/participants';
|
||||
import { COMMAND_NEW_POLL } from '../constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of inheriting component.
|
||||
*/
|
||||
type InputProps = {
|
||||
setCreateMode: boolean => void,
|
||||
};
|
||||
|
||||
/*
|
||||
* Props that will be passed by the AbstractPollCreate to its
|
||||
* concrete implementations (web/native).
|
||||
**/
|
||||
export type AbstractProps = InputProps & {
|
||||
answers: Array<string>,
|
||||
question: string,
|
||||
setQuestion: string => void,
|
||||
setAnswer: (number, string) => void,
|
||||
addAnswer: ?number => void,
|
||||
moveAnswer: (number, number) => void,
|
||||
removeAnswer: number => void,
|
||||
onSubmit: Function,
|
||||
isSubmitDisabled: boolean,
|
||||
t: Function,
|
||||
};
|
||||
|
||||
/**
|
||||
* Higher Order Component taking in a concrete PollCreate component and
|
||||
* augmenting it with state/behavior common to both web and native implementations.
|
||||
*
|
||||
* @param {React.AbstractComponent} Component - The concrete component.
|
||||
* @returns {React.AbstractComponent}
|
||||
*/
|
||||
const AbstractPollCreate = (Component: AbstractComponent<AbstractProps>) => (props: InputProps) => {
|
||||
|
||||
const { setCreateMode } = props;
|
||||
|
||||
const [ question, setQuestion ] = useState('');
|
||||
|
||||
const [ answers, setAnswers ] = useState([ '', '' ]);
|
||||
|
||||
const setAnswer = useCallback((i, answer) => {
|
||||
const newAnswers = [ ...answers ];
|
||||
|
||||
newAnswers[i] = answer;
|
||||
setAnswers(newAnswers);
|
||||
});
|
||||
|
||||
const addAnswer = useCallback((i: ?number) => {
|
||||
|
||||
const newAnswers = [ ...answers ];
|
||||
|
||||
newAnswers.splice(typeof i === 'number' ? i : answers.length, 0, '');
|
||||
setAnswers(newAnswers);
|
||||
});
|
||||
|
||||
const moveAnswer = useCallback((i, j) => {
|
||||
const newAnswers = [ ...answers ];
|
||||
|
||||
const answer = answers[i];
|
||||
|
||||
newAnswers.splice(i, 1);
|
||||
newAnswers.splice(j, 0, answer);
|
||||
setAnswers(newAnswers);
|
||||
});
|
||||
|
||||
const removeAnswer = useCallback(i => {
|
||||
if (answers.length <= 2) {
|
||||
return;
|
||||
}
|
||||
const newAnswers = [ ...answers ];
|
||||
|
||||
newAnswers.splice(i, 1);
|
||||
setAnswers(newAnswers);
|
||||
});
|
||||
|
||||
const conference = useSelector(state => state['features/base/conference'].conference);
|
||||
const myId = conference.myUserId();
|
||||
const myName = useSelector(state => getParticipantDisplayName(state, myId));
|
||||
|
||||
const onSubmit = useCallback(ev => {
|
||||
if (ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
const filteredAnswers = answers.filter(answer => answer.trim().length > 0);
|
||||
|
||||
if (filteredAnswers.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
conference.sendMessage({
|
||||
type: COMMAND_NEW_POLL,
|
||||
pollId: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36),
|
||||
senderId: myId,
|
||||
senderName: myName,
|
||||
question,
|
||||
answers: filteredAnswers
|
||||
});
|
||||
|
||||
setCreateMode(false);
|
||||
|
||||
}, [ conference, question, answers ]);
|
||||
|
||||
// Check if the poll create form can be submitted i.e. if the send button should be disabled.
|
||||
const isSubmitDisabled
|
||||
= question.trim().length <= 0 // If no question is provided
|
||||
|| answers.filter(answer => answer.trim().length > 0).length < 2; // If not enough options are provided
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (<Component
|
||||
addAnswer = { addAnswer }
|
||||
answers = { answers }
|
||||
isSubmitDisabled = { isSubmitDisabled }
|
||||
moveAnswer = { moveAnswer }
|
||||
onSubmit = { onSubmit }
|
||||
question = { question }
|
||||
removeAnswer = { removeAnswer }
|
||||
setAnswer = { setAnswer }
|
||||
setCreateMode = { setCreateMode }
|
||||
setQuestion = { setQuestion }
|
||||
t = { t } />);
|
||||
|
||||
};
|
||||
|
||||
export default AbstractPollCreate;
|
||||
124
react/features/polls/components/AbstractPollResults.js
Normal file
124
react/features/polls/components/AbstractPollResults.js
Normal file
@@ -0,0 +1,124 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type { AbstractComponent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { getLocalParticipant, getParticipantById } from '../../base/participants/functions';
|
||||
import { retractVote } from '../actions';
|
||||
import { COMMAND_ANSWER_POLL } from '../constants';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of inheriting component.
|
||||
*/
|
||||
type InputProps = {
|
||||
|
||||
/**
|
||||
* ID of the poll to display
|
||||
*/
|
||||
pollId: string,
|
||||
};
|
||||
|
||||
export type AnswerInfo = {
|
||||
name: string,
|
||||
percentage: number,
|
||||
voters?: Array<{ id: number, name: string }>,
|
||||
voterCount: number
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AbstractPollResults}.
|
||||
*/
|
||||
export type AbstractProps = {
|
||||
answers: Array<AnswerInfo>,
|
||||
changeVote: Function,
|
||||
showDetails: boolean,
|
||||
question: string,
|
||||
t: Function,
|
||||
toggleIsDetailed: Function,
|
||||
haveVoted: boolean,
|
||||
};
|
||||
|
||||
/**
|
||||
* Higher Order Component taking in a concrete PollResult component and
|
||||
* augmenting it with state/behavior common to both web and native implementations.
|
||||
*
|
||||
* @param {React.AbstractComponent} Component - The concrete component.
|
||||
* @returns {React.AbstractComponent}
|
||||
*/
|
||||
const AbstractPollResults = (Component: AbstractComponent<AbstractProps>) => (props: InputProps) => {
|
||||
const { pollId } = props;
|
||||
|
||||
const pollDetails = useSelector(state => state['features/polls'].polls[pollId]);
|
||||
|
||||
const [ showDetails, setShowDetails ] = useState(false);
|
||||
const toggleIsDetailed = useCallback(() => {
|
||||
setShowDetails(!showDetails);
|
||||
});
|
||||
|
||||
const answers: Array<AnswerInfo> = useMemo(() => {
|
||||
const voterSet = new Set();
|
||||
|
||||
// Getting every voters ID that participates to the poll
|
||||
for (const answer of pollDetails.answers) {
|
||||
for (const [ voterId ] of answer.voters) {
|
||||
voterSet.add(voterId);
|
||||
}
|
||||
}
|
||||
|
||||
const totalVoters = voterSet.size;
|
||||
|
||||
return pollDetails.answers.map(answer => {
|
||||
const percentage = totalVoters === 0 ? 0 : Math.round(answer.voters.size / totalVoters * 100);
|
||||
|
||||
let voters = null;
|
||||
|
||||
if (showDetails) {
|
||||
voters = [ ...answer.voters ].map(([ id, name ]) => {
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name: answer.name,
|
||||
percentage,
|
||||
voters,
|
||||
voterCount: answer.voters.size
|
||||
};
|
||||
});
|
||||
}, [ pollDetails.answers, showDetails ]);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const conference: Object = useSelector(state => state['features/base/conference'].conference);
|
||||
const localId = useSelector(state => getLocalParticipant(state).id);
|
||||
const localParticipant = useSelector(state => getParticipantById(state, localId));
|
||||
const localName: string = localParticipant ? localParticipant.name : 'Fellow Jitster';
|
||||
const changeVote = useCallback(() => {
|
||||
conference.sendMessage({
|
||||
type: COMMAND_ANSWER_POLL,
|
||||
pollId,
|
||||
voterId: localId,
|
||||
voterName: localName,
|
||||
answers: new Array(pollDetails.answers.length).fill(false)
|
||||
});
|
||||
dispatch(retractVote(pollId));
|
||||
}, [ pollId, localId, localName, pollDetails ]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (<Component
|
||||
answers = { answers }
|
||||
changeVote = { changeVote }
|
||||
haveVoted = { pollDetails.lastVote !== null }
|
||||
question = { pollDetails.question }
|
||||
showDetails = { showDetails }
|
||||
t = { t }
|
||||
toggleIsDetailed = { toggleIsDetailed } />);
|
||||
};
|
||||
|
||||
export default AbstractPollResults;
|
||||
44
react/features/polls/components/AbstractPollsPane.js
Normal file
44
react/features/polls/components/AbstractPollsPane.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// @flow
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import type { AbstractComponent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/*
|
||||
* Props that will be passed by the AbstractPollsPane to its
|
||||
* concrete implementations (web/native).
|
||||
**/
|
||||
export type AbstractProps = {
|
||||
createMode: boolean,
|
||||
onCreate: void => void,
|
||||
setCreateMode: boolean => void,
|
||||
t: Function,
|
||||
};
|
||||
|
||||
/**
|
||||
* Higher Order Component taking in a concrete PollsPane component and
|
||||
* augmenting it with state/behavior common to both web and native implementations.
|
||||
*
|
||||
* @param {React.AbstractComponent} Component - The concrete component.
|
||||
* @returns {React.AbstractComponent}
|
||||
*/
|
||||
const AbstractPollsPane = (Component: AbstractComponent<AbstractProps>) => () => {
|
||||
|
||||
const [ createMode, setCreateMode ] = useState(false);
|
||||
|
||||
const onCreate = () => {
|
||||
setCreateMode(true);
|
||||
};
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (<Component
|
||||
createMode = { createMode }
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
onCreate = { onCreate }
|
||||
setCreateMode = { setCreateMode }
|
||||
t = { t } />);
|
||||
|
||||
};
|
||||
|
||||
export default AbstractPollsPane;
|
||||
3
react/features/polls/components/_.native.js
Normal file
3
react/features/polls/components/_.native.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from './native';
|
||||
3
react/features/polls/components/_.web.js
Normal file
3
react/features/polls/components/_.web.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from './web';
|
||||
3
react/features/polls/components/index.js
Normal file
3
react/features/polls/components/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export * from './_';
|
||||
69
react/features/polls/components/native/PollAnswer.js
Normal file
69
react/features/polls/components/native/PollAnswer.js
Normal file
@@ -0,0 +1,69 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { Switch, Text, View } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
|
||||
import { BUTTON_MODES } from '../../../chat/constants';
|
||||
import AbstractPollAnswer from '../AbstractPollAnswer';
|
||||
import type { AbstractProps } from '../AbstractPollAnswer';
|
||||
|
||||
import { chatStyles, dialogStyles } from './styles';
|
||||
|
||||
|
||||
const PollAnswer = (props: AbstractProps) => {
|
||||
|
||||
const {
|
||||
checkBoxStates,
|
||||
poll,
|
||||
setCheckbox,
|
||||
skipAnswer,
|
||||
submitAnswer,
|
||||
t
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View>
|
||||
<Text style = { dialogStyles.question } >{ poll.question }</Text>
|
||||
</View>
|
||||
<View style = { chatStyles.answerContent }>
|
||||
{poll.answers.map((answer, index) => (
|
||||
<View
|
||||
key = { index }
|
||||
style = { chatStyles.switchRow } >
|
||||
<Switch
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
onValueChange = { state => setCheckbox(index, state) }
|
||||
value = { checkBoxStates[index] } />
|
||||
<Text>{answer.name}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<View style = { chatStyles.buttonRow }>
|
||||
<Button
|
||||
color = '#3D3D3D'
|
||||
mode = { BUTTON_MODES.CONTAINED }
|
||||
onPress = { skipAnswer }
|
||||
style = { chatStyles.pollCreateButton } >
|
||||
{t('polls.answer.skip')}
|
||||
</Button>
|
||||
<Button
|
||||
color = '#17a0db'
|
||||
mode = { BUTTON_MODES.CONTAINED }
|
||||
onPress = { submitAnswer }
|
||||
style = { chatStyles.pollCreateButton } >
|
||||
{t('polls.answer.submit')}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* We apply AbstractPollAnswer to fill in the AbstractProps common
|
||||
* to both the web and native implementations.
|
||||
*/
|
||||
// eslint-disable-next-line new-cap
|
||||
export default AbstractPollAnswer(PollAnswer);
|
||||
185
react/features/polls/components/native/PollCreate.js
Normal file
185
react/features/polls/components/native/PollCreate.js
Normal file
@@ -0,0 +1,185 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { View, TextInput, FlatList, TouchableOpacity } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
|
||||
import { Icon, IconClose } from '../../../base/icons';
|
||||
import { BUTTON_MODES } from '../../../chat/constants';
|
||||
import AbstractPollCreate from '../AbstractPollCreate';
|
||||
import type { AbstractProps } from '../AbstractPollCreate';
|
||||
|
||||
import { chatStyles, dialogStyles } from './styles';
|
||||
|
||||
const PollCreate = (props: AbstractProps) => {
|
||||
|
||||
|
||||
const {
|
||||
addAnswer,
|
||||
answers,
|
||||
isSubmitDisabled,
|
||||
onSubmit,
|
||||
question,
|
||||
removeAnswer,
|
||||
setAnswer,
|
||||
setCreateMode,
|
||||
setQuestion,
|
||||
t
|
||||
} = props;
|
||||
|
||||
const answerListRef = useRef(null);
|
||||
|
||||
/*
|
||||
* This ref stores the Array of answer input fields, allowing us to focus on them.
|
||||
* This array is maintained by registerfieldRef and the useEffect below.
|
||||
*/
|
||||
const answerInputs = useRef([]);
|
||||
const registerFieldRef = useCallback((i, input) => {
|
||||
if (input === null) {
|
||||
return;
|
||||
}
|
||||
answerInputs.current[i] = input;
|
||||
},
|
||||
[ answerInputs ]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
answerInputs.current = answerInputs.current.slice(0, answers.length);
|
||||
}, [ answers ]);
|
||||
|
||||
/*
|
||||
* This state allows us to requestFocus asynchronously, without having to worry
|
||||
* about whether a newly created input field has been rendered yet or not.
|
||||
*/
|
||||
const [ lastFocus, requestFocus ] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (lastFocus === null) {
|
||||
return;
|
||||
}
|
||||
const input = answerInputs.current[lastFocus];
|
||||
|
||||
if (input === undefined) {
|
||||
return;
|
||||
}
|
||||
input.focus();
|
||||
|
||||
}, [ answerInputs, lastFocus ]);
|
||||
|
||||
|
||||
const onQuestionKeyDown = useCallback(() => {
|
||||
answerInputs.current[0].focus();
|
||||
});
|
||||
|
||||
// Called on keypress in answer fields
|
||||
const onAnswerKeyDown = useCallback((index: number, ev) => {
|
||||
const { key } = ev.nativeEvent;
|
||||
const currentText = answers[index];
|
||||
|
||||
if (key === 'Backspace' && currentText === '' && answers.length > 1) {
|
||||
removeAnswer(index);
|
||||
requestFocus(index > 0 ? index - 1 : 0);
|
||||
}
|
||||
}, [ answers, addAnswer, removeAnswer, requestFocus ]);
|
||||
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
const createIconButton = (icon, onPress, style) => (
|
||||
<TouchableOpacity
|
||||
activeOpacity = { 0.8 }
|
||||
onPress = { onPress }
|
||||
style = { [ dialogStyles.buttonContainer, style ] }>
|
||||
<Icon
|
||||
size = { 24 }
|
||||
src = { icon }
|
||||
style = { dialogStyles.icon } />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
const renderListItem = ({ index }: { index: number }) =>
|
||||
|
||||
// padding to take into account the two default options
|
||||
(
|
||||
<View
|
||||
style = { dialogStyles.optionContainer }>
|
||||
<TextInput
|
||||
blurOnSubmit = { false }
|
||||
multiline = { true }
|
||||
onChangeText = { text => setAnswer(index, text) }
|
||||
onKeyPress = { ev => onAnswerKeyDown(index, ev) }
|
||||
placeholder = { t('polls.create.answerPlaceholder', { index: index + 1 }) }
|
||||
ref = { input => registerFieldRef(index, input) }
|
||||
style = { dialogStyles.field }
|
||||
value = { answers[index] } />
|
||||
|
||||
{answers.length > 2
|
||||
&& createIconButton(IconClose, () => removeAnswer(index))
|
||||
}
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style = { chatStyles.pollCreateContainer }>
|
||||
<View style = { chatStyles.pollCreateSubContainer }>
|
||||
<TextInput
|
||||
autoFocus = { true }
|
||||
blurOnSubmit = { false }
|
||||
multiline = { true }
|
||||
onChangeText = { setQuestion }
|
||||
onSubmitEditing = { onQuestionKeyDown }
|
||||
placeholder = { t('polls.create.questionPlaceholder') }
|
||||
style = { dialogStyles.question }
|
||||
value = { question } />
|
||||
<FlatList
|
||||
blurOnSubmit = { true }
|
||||
data = { answers }
|
||||
extraData = { answers }
|
||||
keyExtractor = { (item, index) => index.toString() }
|
||||
ref = { answerListRef }
|
||||
renderItem = { renderListItem } />
|
||||
|
||||
<Button
|
||||
color = '#3D3D3D'
|
||||
mode = { BUTTON_MODES.CONTAINED }
|
||||
onPress = { () => {
|
||||
// adding and answer
|
||||
addAnswer();
|
||||
requestFocus(answers.length);
|
||||
} }
|
||||
style = { chatStyles.pollCreateAddButton }>
|
||||
{t('polls.create.addOption')}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style = { chatStyles.buttonRow }>
|
||||
|
||||
<Button
|
||||
color = '#3D3D3D'
|
||||
mode = { BUTTON_MODES.CONTAINED }
|
||||
onPress = { () => setCreateMode(false) }
|
||||
style = { chatStyles.pollCreateButton } >
|
||||
{t('polls.create.cancel')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color = '#17a0db'
|
||||
disabled = { isSubmitDisabled }
|
||||
mode = { BUTTON_MODES.CONTAINED }
|
||||
onPress = { onSubmit }
|
||||
style = { chatStyles.pollCreateButton } >
|
||||
{t('polls.create.send')}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
/*
|
||||
* We apply AbstractPollCreate to fill in the AbstractProps common
|
||||
* to both the web and native implementations.
|
||||
*/
|
||||
// eslint-disable-next-line new-cap
|
||||
export default AbstractPollCreate(PollCreate);
|
||||
40
react/features/polls/components/native/PollItem.js
Normal file
40
react/features/polls/components/native/PollItem.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { shouldShowResults } from '../../functions';
|
||||
|
||||
import { chatStyles } from './styles';
|
||||
|
||||
import { PollAnswer, PollResults } from '.';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Id of the poll
|
||||
*/
|
||||
pollId: string,
|
||||
|
||||
}
|
||||
|
||||
const PollItem = ({ pollId }: Props) => {
|
||||
const showResults = useSelector(state => shouldShowResults(state, pollId));
|
||||
|
||||
return (
|
||||
<View
|
||||
style = { chatStyles.pollItemContainer }>
|
||||
{ showResults
|
||||
? <PollResults
|
||||
key = { pollId }
|
||||
pollId = { pollId } />
|
||||
: <PollAnswer
|
||||
pollId = { pollId } />
|
||||
}
|
||||
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default PollItem;
|
||||
120
react/features/polls/components/native/PollResults.js
Normal file
120
react/features/polls/components/native/PollResults.js
Normal file
@@ -0,0 +1,120 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { View, Text, FlatList, TouchableOpacity } from 'react-native';
|
||||
|
||||
import AbstractPollResults from '../AbstractPollResults';
|
||||
import type { AbstractProps, AnswerInfo } from '../AbstractPollResults';
|
||||
|
||||
import { chatStyles, dialogStyles, resultsStyles } from './styles';
|
||||
|
||||
|
||||
/**
|
||||
* Component that renders the poll results.
|
||||
*
|
||||
* @param {Props} props - The passed props.
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
const PollResults = (props: AbstractProps) => {
|
||||
const {
|
||||
answers,
|
||||
changeVote,
|
||||
haveVoted,
|
||||
showDetails,
|
||||
question,
|
||||
t,
|
||||
toggleIsDetailed
|
||||
} = props;
|
||||
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
/**
|
||||
* Render a header summing up answer information.
|
||||
*
|
||||
* @param {string} answer - The name of the answer.
|
||||
* @param {number} percentage - The percentage of voters.
|
||||
* @param {number} nbVotes - The number of collected votes.
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
const renderHeader = (answer: string, percentage: number, nbVotes: number) => (
|
||||
<View style = { resultsStyles.answerHeader }>
|
||||
<Text style = { resultsStyles.answer }>{ answer }</Text>
|
||||
<Text style = { resultsStyles.answer }>({nbVotes}) {percentage}%</Text>
|
||||
|
||||
{/* <Text style = { resultsStyles.answer }>{ answer } - { percentage }%</Text>
|
||||
<Text style = { resultsStyles.answerVoteCount }>
|
||||
{ t('polls.answer.vote', { count: nbVotes }) }
|
||||
</Text> */}
|
||||
</View>
|
||||
);
|
||||
|
||||
/**
|
||||
* Render voters of and answer
|
||||
* @param {AnswerInfo} answer - the answer info
|
||||
* @returns {React.Node}
|
||||
*/
|
||||
const renderRow = useCallback((answer: AnswerInfo) => {
|
||||
const { name, percentage, voters, voterCount } = answer;
|
||||
|
||||
if (showDetails) {
|
||||
return (
|
||||
<View style = { resultsStyles.answerContainer }>
|
||||
{ renderHeader(name, percentage, voterCount) }
|
||||
{ voters && voterCount > 0
|
||||
&& <View style = { resultsStyles.voters }>
|
||||
{voters.map(({ id, name: voterName }) =>
|
||||
<Text key = { id }>{ voterName }</Text>
|
||||
)}
|
||||
</View>}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// else, we display a simple list
|
||||
// We add a progress bar by creating an empty view of width equal to percentage.
|
||||
return (
|
||||
<View style = { resultsStyles.answerContainer }>
|
||||
{ renderHeader(answer.name, percentage, voterCount) }
|
||||
<View style = { resultsStyles.barContainer }>
|
||||
<View style = { [ resultsStyles.bar, { width: `${percentage}%` } ] } />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
}, [ showDetails ]);
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
return (
|
||||
<View>
|
||||
<View>
|
||||
<Text style = { dialogStyles.question } >{ question }</Text>
|
||||
</View>
|
||||
<FlatList
|
||||
data = { answers }
|
||||
keyExtractor = { (item, index) => index.toString() }
|
||||
renderItem = { answer => renderRow(answer.item) } />
|
||||
<View style = { chatStyles.bottomLinks }>
|
||||
<TouchableOpacity onPress = { toggleIsDetailed }>
|
||||
<Text
|
||||
style = { chatStyles.toggleText }>
|
||||
{showDetails ? t('polls.results.hideDetailedResults') : t('polls.results.showDetailedResults')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress = { changeVote }>
|
||||
<Text
|
||||
style = { chatStyles.toggleText }>
|
||||
{haveVoted ? t('polls.results.changeVote') : t('polls.results.vote')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* We apply AbstractPollResults to fill in the AbstractProps common
|
||||
* to both the web and native implementations.
|
||||
*/
|
||||
// eslint-disable-next-line new-cap
|
||||
export default AbstractPollResults(PollResults);
|
||||
51
react/features/polls/components/native/PollsList.js
Normal file
51
react/features/polls/components/native/PollsList.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FlatList } from 'react-native';
|
||||
import { Text } from 'react-native-paper';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { chatStyles } from './styles';
|
||||
|
||||
import { PollItem } from '.';
|
||||
|
||||
|
||||
const PollsList = () => {
|
||||
|
||||
const polls = useSelector(state => state['features/polls'].polls);
|
||||
const { t } = useTranslation();
|
||||
const listPolls = Object.keys(polls);
|
||||
|
||||
const renderItem = useCallback(({ item }) => (
|
||||
<PollItem
|
||||
key = { item }
|
||||
pollId = { item } />)
|
||||
, []);
|
||||
|
||||
const flatlistRef = useRef();
|
||||
|
||||
const scrollToBottom = () => {
|
||||
flatlistRef.current.scrollToEnd({ animating: true });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [ polls ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{listPolls.length === 0
|
||||
&& <Text style = { chatStyles.noPollText } >
|
||||
{t('polls.results.empty')}
|
||||
</Text>}
|
||||
<FlatList
|
||||
data = { listPolls }
|
||||
extraData = { listPolls }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
keyExtractor = { (item, index) => index.toString() }
|
||||
ref = { flatlistRef }
|
||||
renderItem = { renderItem } />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PollsList;
|
||||
45
react/features/polls/components/native/PollsPane.js
Normal file
45
react/features/polls/components/native/PollsPane.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/* eslint-disable react-native/no-color-literals */
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
|
||||
import { BUTTON_MODES } from '../../../chat/constants';
|
||||
import AbstractPollsPane from '../AbstractPollsPane';
|
||||
import type { AbstractProps } from '../AbstractPollsPane';
|
||||
|
||||
import PollCreate from './PollCreate';
|
||||
import PollsList from './PollsList';
|
||||
import { chatStyles } from './styles';
|
||||
|
||||
const PollsPane = (props: AbstractProps) => {
|
||||
|
||||
const { createMode, onCreate, setCreateMode, t } = props;
|
||||
|
||||
return (
|
||||
<View style = { chatStyles.PollPane }>
|
||||
{ createMode
|
||||
? <PollCreate setCreateMode = { setCreateMode } />
|
||||
: <View style = { chatStyles.PollPaneContent }>
|
||||
{/* <View /> */}
|
||||
<PollsList />
|
||||
<Button
|
||||
color = '#17a0db'
|
||||
mode = { BUTTON_MODES.CONTAINED }
|
||||
onPress = { onCreate }
|
||||
style = { chatStyles.createPollButton } >
|
||||
{t('polls.create.create')}
|
||||
</Button>
|
||||
</View>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* We apply AbstractPollsPane to fill in the AbstractProps common
|
||||
* to both the web and native implementations.
|
||||
*/
|
||||
// eslint-disable-next-line new-cap
|
||||
export default AbstractPollsPane(PollsPane);
|
||||
7
react/features/polls/components/native/index.js
Normal file
7
react/features/polls/components/native/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// @flow
|
||||
|
||||
export { default as PollResults } from './PollResults';
|
||||
export { default as PollsPane } from './PollsPane';
|
||||
export { default as PollItem } from './PollItem';
|
||||
export { default as PollAnswer } from './PollAnswer';
|
||||
export { default as PollCreate } from './PollCreate';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user