Compare commits

...

47 Commits

Author SHA1 Message Date
Avram Tudor
1ad9046a38 Improve premeeting screens ux (#9726)
* feat(prejoin) move invite to toolbar section

* feat(premeeting) redesign prejoin and lobby screens

* code review changes

* fix prejoin flicker and avatar id

* fix password error message and native lobby dialog close position
2021-08-20 11:53:11 +03:00
Avram Tudor
49a73ac446 fix(jaas) do not redirect to plan limit page on auth errors (#9746) 2021-08-20 11:36:09 +03:00
Avram Tudor
8e4a22bdbf fix(moderation) fix ui styles for advanced moderation context menu (#9758) 2021-08-20 11:23:37 +03:00
Saúl Ibarra Corretgé
ddbf334930 fix(e2ee) fix showing not supported warning when alone 2021-08-19 17:19:00 +02:00
Saúl Ibarra Corretgé
cd5f2b483f fix(e2ee) show entire content at all times 2021-08-19 17:19:00 +02:00
Saúl Ibarra Corretgé
cf34b0a783 fix(e2ee) update E2EE warning message 2021-08-19 17:19:00 +02:00
Saúl Ibarra Corretgé
10cc3b2b31 fix(ios) fix conference failing when proximity sensor is near
React Native links timers to the display, so they cannot run when the display is
not running. Builtin timers already take being in the background into account,
but not the proximity sensor.

Credits: https://github.com/react-native-webrtc/react-native-callkeep/issues/143

Fixes: https://github.com/jitsi/jitsi-meet/issues/9619
2021-08-19 13:55:35 +02:00
Saúl Ibarra Corretgé
27e4e862fd feat(xmpp) remove clientNode config option 2021-08-19 13:28:58 +02:00
Avram Tudor
4b3d92dcbd fix(moderation) highlight dominant speaker (#9750) 2021-08-19 14:08:30 +03:00
Saúl Ibarra Corretgé
5c1e2b4bd2 fix(AudioRoutePickerDialog) add proper margin 2021-08-19 10:47:53 +02:00
Saúl Ibarra Corretgé
a4d516ca86 fix(ios) fix deadlock when selecting audio device
In WebRTC M92 the RTCAudioSession lock changed from a recursive one to a regular
mutex one, so make sure we don't attempt to lock it  while already holding the
lock.
2021-08-19 10:47:53 +02:00
Avram Tudor
d41c597446 fix(moderation) hide status icons on hover (#9747) 2021-08-19 11:32:36 +03:00
José Luís Andrade
4788720edd fix(lang) update portuguese translation 2021-08-19 09:56:20 +02:00
Jaya Allamsetty
40099e97ec feat(Filmstrip): Reorder the visible participants in the filmstrip. (#9707)
* feat(Filmstrip): Reorder the visible participants in the filmstrip.
The participants are ordered alphabetically and the endpoints with screenshares, shared-videos and dominant speakers (in that order) are bumped to the top of the list. The local participant is also moved to the top left corner as opposed to the bottom right corner.

* squash: Implement review comments.

* squash: store alphabetically sorted list in redux and move shared videos to top.

* squash: Use the DEFAULT_REMOTE_DISPLAY_NAME from interfaceConfig for users without a display name.
2021-08-18 18:34:01 -04:00
Дамян Минков
a7a44902ec fix: Fixes dot shown left of logo.
Empty list with border is showing 2px dot.
2021-08-18 17:18:39 -05:00
Scott Boone
7af23f35ba added comment for future proofing 2021-08-18 14:01:11 -05:00
Calin Chitu
cf49af1a9f fix(participants-pane) fixed participants pane button toggled state 2021-08-18 12:39:27 -05:00
Saúl Ibarra Corretgé
186125607f fix(prosody) fix username not being set in mod_auth_jitsi-anonymous
If the session was not resumed the plugin must generate a random username, just
like the regular anonymous authentication plugin does.
2021-08-18 15:16:53 +02:00
Saúl Ibarra Corretgé
508decc3f3 fix(prosody) fix typo in filename 2021-08-18 15:16:53 +02:00
Avram Tudor
6d3a4b920b fix(moderation) display green mic icon only for active speaker (#9744) 2021-08-18 15:10:16 +03:00
Avram Tudor
595df524b0 fix(participants) sort participants alphabetically (#9741) 2021-08-18 14:29:41 +03:00
Avram Tudor
9914a5d14a fix(share-video) fix links not being trimmed (#9740) 2021-08-18 12:09:04 +03:00
Дамян Минков
fbf9d489f0 fix: Fixes an error with configuration without analytics.
Fixes an error `Cannot convert undefined or null to object` on load.
2021-08-17 12:54:29 -05:00
AJ-عجائب البرمجة
8106fb06e4 fix(rn,sidebar) fix not appearing on RTL languages 2021-08-17 13:15:22 +02:00
Calin Chitu
036ef0f387 fix(video-menu) fixed position for horizontal filmstrip view 2021-08-17 13:54:54 +03:00
Calinteodor
307699a34c feat(toolbox) added android screen share flag
Fixes issue #9435
2021-08-17 13:42:29 +03:00
tmoldovan8x8
c23375793e fix(rn): import React from react package 2021-08-17 13:14:20 +03:00
hmuresan
4dc642fb4d feat(callstats) Add callStatsConfigParams config 2021-08-16 13:15:44 +03:00
Andrei Gavrilescu
d47e67c28c fix(screen-share): never mute audio screen share track #9725 2021-08-16 12:24:01 +03:00
hmuresan
db0b861353 chore(deps) lib-jitsi-meet@latest
6eaffc4b11...c23abfa2bc
2021-08-16 12:12:03 +03:00
Calinteodor
e40d4a48b8 feat(av-moderation) id and aria-label updates for av-moderation test (#9592)
* feat(av-moderation) raised hand ask to unmute aria-label

* feat(av-moderation) fixed test

* feat(av-moderation) added id for notification buttons

* feat(av-moderation) fixed lint error

* feat(av-moderation) added id for non raised hand participant

* feat(av-moderation) added extra id naming for ask to unmute button and participant items

* feat(av-moderation) fixed lint errors

* feat(av-moderation) changed id to participantID

* feat(av-moderation) removed semicolon

* squash: Drop unused id for participantItem.

* squash: Drop unused fields for raisedHand.

Co-authored-by: Дамян Минков <damencho@jitsi.org>
2021-08-15 00:27:18 -05:00
Jade Guiton
8c82c0f56e feat(polls) Ability to create polls inside Jitsi (#9166)
* feat(polls) Added boilerplate code for polls feature

* feat(polls) Implemented simple poll creation and answer modals in web app

feat(polls) Added button to create a poll in toolbar
feat(polls) Added Modal to answer an incoming poll
feat(polls) Implemented basic client-side sending and reception of polls
feat(polls): linked Poll creation to poll answering
fix(polls) Linted code
feat(polls.create) Added fields for question and answers (#3)
* feat(polls.create) Added fields for question and answers + keyboard navigation
* feat(polls.create) Minor changes, added some comments
feat(PollAnswer Component): Component to display modal to answer poll #1 (#2)
* fix(polls) removing necessity of current_poll_id variable
* fix(polls) linting, polls are now updated when an answer is sent
* feat(polls answer) added translation
* fix(polls answer) remove extra comments, fixed typo
* improvement (polls answer) use useSelector instead of mapStateToProps. cleaner code
* fix (polls create) renamed sender to senderId
* fix (polls answer) turned arrow function into useCallBack
feat(PollResults Component): Component to display poll results (#1)
* feat(PollResults Component): fist version of the component
* feat(detailed votes): Display the detailed results of a poll
* feat(Poll results): Use display name instead of ids in detailed results mode
* fix(Poll): change title to question
* fix(Poll type): import Poll type from types.js
* fix(Poll): change title to question
* fix(Poll): get participants out of the map
* fix(Poll): replace filter with find
feat(polls.create) Added "+" and "x" buttons in poll creation form + improved keyboard navigation a bit
feat (polls) Answer modal now display results in real time after validation or skip
feat(polls.create) Minor improvements to poll creation form
feat(poll result) Added default message when trying to display no answer
fix (polls) result windows is now small by default
fix (polls) sanitizes imports to allow startup on react native

* feat(polls.native) Implemented native toolbar button & poll create modal

feat( poll native) added poll creation button in native toolbar
improvement(polls) only one file used for PollCreateButton
feat (polls native) added an example dialog
feat (polls native) added possibility to create and delete options in poll creation
improvement (polls) better styling for PollCreateDialog

* feat(polls) Added ability to drag&drop answers in web poll creation form

* feat(polls) Added native poll answer modal + chat integration, refactored components

Merge branch 'polls-native' of https://github.com/jade-guiton/jitsi-meet into polls-native
improvement (poll) Better styling for poll answer, now uses icons
feat(poll.PollResults): Add native version of PollResults
feat(poll.PollResults): Post results in chat in Native
fix(poll.PollResults): Fix linter error in ChatMessage
feat(polls.native) Improved styling for native poll answer dialog (required some internal changes)

* fix(polls) Heavily refactored and added bars to poll results, other minor changes

fix(poll.create): Move title to Dialog title
feat(poll.create) Minor changes to poll creation / answer dialogs
fix(poll.create) Refactored and improved translations
feat(poll) Improved CSS for modals in web version
fix(poll.pollcreate): Fix button size in native
fix(polls) Refactored poll results component and other minor changes
fix (polls) remove double import
refactor(poll) Heavily refactored poll results (native + web)
feat(polls.results) Added percentage bars and vote counts in web poll results, minor changes to mobile poll results

* fix(polls) Fixes and linting

fix(polls) Reformatted and fixed some linter and Flow errors
fix(polls.results) Fixed voter list border appearing with 0 voters

* feat(polls): Add modal with detailed votes that can be open from the result summary in the chat

* fix(polls) Fixes, refactorings, and minor design changes

feat(polls.results): Refactored poll chat message and improved design in web app
feat(polls.results) Same as last commit, but for mobile version
refactor(polls.results) Refactored PollResultsMessage and removed unnecessary prop in PollResults
fix(polls.results) Fixed all remaining linter and Flow errors
improvement(polls) removed console logs, added comments
fix (polls) linting
fix(polls.results) Fixed bug with poll chat message displaying the wrong name
feat(polls.results) Minor improvement on poll results display (web)
fix(poll.results): Use getParticipantDisplayName to get participant name and avoid empty string as name

* Feat(poll.results): Remember voters names to display after they left the conference (#10)

* feat(poll.results): Add the sender name in Poll object to remember names if participants leave the conference. Names are also updated if changed
* refactor(poll.results): Refactor the memorization of the names of voters to use the same logic as in  the chat
* refactor(poll.results): use Map instead of Array.From(
* refactor(poll.answer): change the way names are stored in poll answers to persist if participant left the call
* Update react/features/polls/components/AbstractPollAnswerDialog.js
* Update react/features/polls/components/AbstractPollCreateDialog.js
* refactor(poll.answer): use voterName instead of senderName to avoid confusion with senderId the id of the sender of the poll
* improvement(polls) Simplified poll answer voter name logic

Co-authored-by: Fabien Zucchet <fabien.zucchet@student-cs.fr>
Co-authored-by: Jade Guiton <guiton.jade@gmail.com>

* fix(poll.native): Fix UI overflow when asking long questions & long options in the mobile app (#11)

Co-authored-by: Fabien Zucchet <fabien.zucchet@student-cs.fr>

* fix(polls) Fixed close button behavior in answer and results dialog (#12)

* fix(polls) Fixed close button behavior in answer and results dialog
* fix(polls) Fixed linter error

* fix(polls) Added a poll queue to avoid overwriting open modals (#13)

* fix(polls) Added a poll queue to avoid overwriting open modals
* fix(polls) Updated documentation for action RECEIVE_POLL

* Refactor(poll.chatresults): Add message in chat with hidden results until the participant has answered (#14)

* refactor(poll.chat): Display poll results in chat when the poll is created instead of when the participant has ansered
* refactor(poll.chat): Hide results until the participant has answered, skipped or canceled a responde to the poll
* Use getParticipantDisplayName instead of only getStore()
* Hide results also in native
* fix(polls) Fixed previous merge

Co-authored-by: Fabien Zucchet <fabien.zucchet@student-cs.fr>
Co-authored-by: Jade Guiton <jade.guiton@centralesupelec.fr>

* minor improvements (polls)

refactor (polls) uniformized string for command names
refactor (polls) changed pollId type to number everywhere

* feat(polls) Added persistence to polls using sendMessage instead of sendCommandOnce (#16)

* feat(polls) Using sendMessage instead of sendCommandOnce, switched poll IDs to string, and ability to receive old polls from backend
* improvement(polls) Linted everything, fixed Flow errors, and added Prosody plugin for polls
* improvement(polls) Historic polls are now displayed in chronological order

* (polls) Minor improvements (#17)

* renaming (polls) Renaming senderId -> voterID for voters
* improvement (polls) sender's name is now provided with poll
* comments (polls) updated comments for senderName types
* fix(polls) Finished merging with json-messages feature
* fix(polls) Fixed incorrect json-message sent with 0 polls

Co-authored-by: Jade Guiton <guiton.jade@gmail.com>

* Move polls to tab (#23)

* Draft(polls): Move polls to polls-pane ; first version for web
* Draft(polls): Move polls to polls-pane ; clean styled.js and remove Participant objects
* fix missing newline at the end of file
* Change behaviour to allow answer poll later
* Fix(polls): change pollId type from number to string for consistency
* feat(polls-pane): Ability to answer to a poll in polls-pane
* feat(polls-pane): Ability to create to a poll in polls-pane
* feat (polls.pane) display a notification when a new poll arrives
* refactor(polls-pane): Update CSS to have a design closer to the mockups
* fix(poll.vote count): Fix votes counting when computing percentage
* fix(poll.vote count): Fix votes counting when computing percentage
* refresh fork with jitsi/jitsi-meet
* design (polls) Better look for poll creation
* refactor(polls pane): Move polls-pane as a chat tab
* Remove the first version of the polls-pane and the button to open it
* Fix notifications and typo
* Translate new polls tab in chat
* Change polls_pane to polls-pane
* Remove unless functions
* Remove usage of styled.js
* Improve responsiveness
* Separate web and native logic
* Remove Create a Poll button in web toolbox
* improvement (polls) added auto scrolling to bottom when a new poll arrives
* Add tabs to swicth between polls and chat in native
* Add AbstractPollsPane
* Add AbstractPollCreate
* Add AbstractPollAnswer
* Add PollAnswer, PollItem and PollList for native
* Add PollCreate for native
* Remove dialogs in web and native
* Remove dialog queue
* Remove useless files
* Move _polls.scss outside dialog folder
* Add possibility to skip answer
* Add (useless for now) see details link
* Add possibility to show detailed results for a poll
* Resize progress bar to make details display
* refactor, design (polls) better style to native design chat
* fix (polls) Removed unecessary files
* translate (polls) added french translation to empty polls
* design fix (polls.native) 'show details' now correctly switch between progress bar and voters mode
* Change See detailed results for Show details and add cursor: pointer
* Fix progress bars not aligned with text
* fix (polls.native) added autoselection of newly created option
* Remove poll answer
* improvement(polls.create) Improved web poll creation form marginally
* improvement(polls.change) Simplified answer removal by reusing poll-answer command
* fix linter
* Fix(translation): update translation

Co-authored-by: Fabien Zucchet <fabien.zucchet@student-cs.fr>
Co-authored-by: spineki <marras.antoine@gmail.com>
Co-authored-by: Fabien Zucchet <fabien.zucchet@viarezo.fr>

* Merge pull request #22 from jade-guiton/polls-with-notification

feat (polls) chat notification badge now display the sum of unread  messages and unread polls
fix(translation): Fix missing translation
Fix flow error

* Cleaned up, fixed, and uniformized translations

* Small improvements to PollAnswer and PollResult + Much refactoring

Specifically:
- "Change vote" button now says "Vote" if voting was skipped
- Clicking on "Change vote" resets the voting form to the last submitted answers instead of a blank slate

- The "answered" field of Polls was replaced by "showResults" and "lastVote"
- The "setAnsweredStatus" action was replaced by "registerVote" and "retractVote"
- Some newly unreachable/useless code was removed
- "showDetails" state is now handled by AbstractPollResults instead of PollItem

* fix(polls tab): change tab underline color to #525252

* fix(poll create): Enforce at least two options to create a poll

* fix(poll create): change 'remove option' color to #E04757

* fix(poll create): Update Poll create CSS to adapt to design

* fix(poll answer): Adapt CSS to make poll answer closer to mockup

* fix(poll result): Udpdate poll result CSS to match mockups

* fix(poll result): Udpdate poll result CSS to match mockups

* fix(poll create): Display 'remove option' only when there is at least 3 options

* fix(polls button): Add hover, active, focus and disabled state to polls buttons

* Last improvements for web

* Native design fixes

* Fix rebase issue in land/main.json

* Fix french translation after rebase

* Fixmobile behaviour

* Fixed keyboard navigation in web poll creation form

* Fixed Flow error related to "no polls" icon in PollsList

* fix(polls): Enabled polls Prosody module in Debian config files

* doc(polls) Added comments to the Prosody module code

* fix(polls): Switched from using an internal LJM event to ones from the public API

* Capitalize I of setIsPollsTabFocused

* extract the 2 button modes into a const

* remove extra new lines

* Rename CLOSE_POLL_TAB for POLL_TAB_CLOSED for clarity

* Rename answers2 for answersParsed for clarity

* use switch instead of if/else chain

* improve syntax for localId fetching

* Refactor: Use BUTTON_MODE.CONTAINED variable instead of 'contained'

* Disable send poll button if not enough data is provided in the form (#30)

* Feat: Add notification badge on chat and poll tabs (#31)

* Feat: Add notification badge on chat and poll tabs

* Add badge equivalent for native

* Update displayNameForm text to mention polls (#34)

* Disable polls UI with a config in config.js (#33)

* Change remove option text color from red to grey (#32)

Co-authored-by: spineki <marras.antoine@gmail.com>
Co-authored-by: Fabien Zucchet <fabien.zucchet@student-cs.fr>
Co-authored-by: Fabien Zucchet <80532941+fabienzucchet@users.noreply.github.com>
Co-authored-by: Fabien Zucchet <fabien.zucchet@viarezo.fr>
2021-08-14 08:29:42 -05:00
dimitardelchev93
c123ff9e15 feat: Add search to speaker stats (#9510)
* Additional setting to add search to speaker stats

* Add translation for speaker stats search placeholder

* Unset speaker stats search input autocomplete

* Fix lint errors for speaker stats search

* Change setting to disableSpeakerStatsSearch

* Better Object.prototype.hasOwnProperty.call alternative

* Make SpeakerStatsSearch a functional component

* Align header with input and use material-ui styles instead of scss and remove SpeakerStats header and fix dialog close

* Resolve code style remark in SpeakerStats constructor

* Resolve component empty return value remark in SpeakerStatsSearch

* Resolve get config property in outside function remark in SpeakerStatsSearch

* Resolve unnecessary anonymous function remark in SpeakerStatsSearch
2021-08-13 11:10:05 -05:00
tmoldovan8x8
ae33755913 feat(rn,sdk) add setConfigOverride to JitsiMeetConferenceOptions
Allows for overriding any (overridable, of course) config option.
2021-08-13 12:03:41 +02:00
BlackXixo
bcc870daa7 fix(lang) update Spanish translation 2021-08-13 11:31:37 +02:00
dependabot[bot]
a6a95ad13b chore(deps): bump path-parse from 1.0.6 to 1.0.7 in /resources/load-test
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-13 08:23:15 +02:00
Saúl Ibarra Corretgé
195508ea60 chore(deps) lib-jitsi-meet@latest
* fix(ProxyConnection) add new required stubs
* fix(tpc) fix extracting ssrc map when using single stream
* fix(transcribing): send transcripton_language only when necessary (#1677)

97ff597425...6eaffc4b11
2021-08-12 16:26:45 -05:00
José Luís Andrade
5ce4b82c2c update portuguese translation (#9703)
* update portuguese translation

"participantsPane" section

* Update main-pt.json

add "security" translation

* Update main-pt.json

add "lobby" translation
2021-08-12 14:42:23 -05:00
Mauro José da Silva
ab1c016108 fix(lang) update Brazilian Portuguese translations 2021-08-11 09:53:35 +02:00
Дамян Минков
29dd0cf726 fix: Adds a null check in mod_limits_exceptions.
It is failing on prosody 0.11.4 with mod_limits_exception.lua:29: attempt to call method 'set_stanza_size_limit' (a nil value). That prosody misses set_stanza_size_limit.
2021-08-10 14:34:20 -05:00
durduman
1e0c25d816 fix(ios) remove exitPictureInPicture() call from resetBounds(bounds:) 2021-08-10 15:49:31 +02:00
José Luís Andrade
dfd28c501b fix(lang) update portuguese translation 2021-08-10 07:51:04 +02:00
Дамян Минков
ed98eca326 feat: Rises the stanza limit for unlimited jids (jicofo).
c5c4449fbc/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example (L24)
2021-08-09 11:33:40 -05:00
Andrei Gavrilescu
c5c4449fbc chore(deps) lib-jitsi-meet@latest (#9686)
* feat(transcribing): configurable transcription language

40fd6bdeaa...97ff597425
2021-08-09 13:46:38 +03:00
Andrei Gavrilescu
e45cab9a80 feat(transcribing): configurable transcription language (#9684)
* configurable transcription language

* merge conflict

* set default config value

* fix lint
2021-08-09 12:29:50 +03:00
Дамян Минков
6f44368647 fix(av-moderation): Fixes approving and dismissing the notification.
When participants panel is open and we approve a participant to unmute, the notification was not hidden as we were not correctly updating the state. We were expecting a participant object, but an id of the participant was used.
2021-08-06 15:04:26 -05:00
Jaya Allamsetty
a31a10ba38 fix(logging): Set the log level to debug again.
Plan is to make the Strophe logs more restrictive.
Revert "fix(logging) reduce overly vebose logging"

This reverts commit 09af88088d.
2021-08-06 12:46:29 -04:00
186 changed files with 5464 additions and 1595 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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'

View File

@@ -17,6 +17,7 @@ import {
JitsiConnectionErrors,
JitsiConnectionEvents
} from './react/features/base/lib-jitsi-meet';
import { getCustomerDetails } from './react/features/jaas/actions.any';
import { isVpaasMeeting, getJaasJWT } from './react/features/jaas/functions';
import { setPrejoinDisplayNameRequired } from './react/features/prejoin/actions';
const logger = Logger.getLogger(__filename);
@@ -90,9 +91,13 @@ export async function connect(id, password, roomName) {
let { jwt } = state['features/base/jwt'];
const { iAmRecorder, iAmSipGateway } = state['features/base/config'];
if (!iAmRecorder && !iAmSipGateway && !jwt && isVpaasMeeting(state)) {
jwt = await getJaasJWT(state);
APP.store.dispatch(setJWT(jwt));
if (!iAmRecorder && !iAmSipGateway && isVpaasMeeting(state)) {
await APP.store.dispatch(getCustomerDetails());
if (!jwt) {
jwt = await getJaasJWT(state);
APP.store.dispatch(setJWT(jwt));
}
}
// Use Websocket URL for the web app if configured. Note that there is no 'isWeb' check, because there's assumption

View File

@@ -51,8 +51,10 @@ body {
}
}
.jitsi-icon svg {
fill: white;
.jitsi-icon {
&-default svg {
fill: white;
}
}
.disabled .jitsi-icon svg {

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -206,13 +206,3 @@
bottom: 0;
width: 35%;
}
/**
* Resizes elements width to fill the whole screen width with some margin
*/
@mixin adjust-for-max-width($width, $margin) {
@media (max-width: $width) {
margin: 0 $margin;
width: $width - 2 * $margin;
}
}

View File

@@ -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
View 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;
}
}

View File

@@ -1,153 +0,0 @@
.prejoin {
&-input-area {
margin: 0 auto;
text-align: center;
&-label {
display: block;
margin-bottom: 5px;
color: #ffffff;
font-weight: 300;
font-size: 15px;
line-height: 24px;
}
}
&-title {
color: #fff;
font-size: 24px;
line-height: 32px;
margin-bottom: 16px;
}
&-text-btns {
display: flex;
justify-content: space-between;
}
&-input-label {
color: #A4B8D1;
font-size: 13px;
line-height: 20px;
margin-top: 32px 0 8px 0;
text-align: center;
width: 100%;
}
&-checkbox {
border: 0;
height: 16px;
margin-right: 8px;
padding: 0;
width: 16px;
}
&-checkbox-container {
margin-bottom: 14px;
width: 100%;
}
&-error {
color: white;
background-color: rgba(225, 45, 45, 0.6);
border-radius: 3px;
width: 100%;
padding: 2px;
box-sizing: border-box;
margin-top: 4px;
font-size: 13px;
text-align: center;
}
}
@mixin name-placeholder {
color: #fff;
font-weight: 300;
opacity: 0.6;
}
.prejoin-preview {
&-status {
align-items: center;
align-self: stretch;
bottom: 0;
color: #fff;
display: flex;
font-size: 13px;
min-height: 24px;
justify-content: center;
position: absolute;
text-align: center;
width: 100%;
z-index: 1;
&--warning {
background: rgba(241, 173, 51, 1);
}
&--ok {
background: rgba(49, 183, 106, 1);
}
}
&-icon {
background-position: center;
background-repeat: no-repeat;
display: inline-block;
height: 16px;
margin-right: 8px;
width: 16px;
}
&-error-desc {
margin-right: 4px;
color: #fff;
font-weight: bold;
}
.settings-button-container {
width: 49px;
margin: 0 8px;
}
&-dropdown-btns {
width: 320px;
padding: 8px 0;
@include adjust-for-max-width(320px, 8px);
}
&-dropdown-btn {
align-items: center;
color: #1C2025;
cursor: pointer;
display: flex;
height: 40px;
font-size: 15px;
line-height: 24px;
padding: 0 16px;
&:hover {
background-color: #DAEBFA;
}
}
&-dropdown-icon {
display: inline-block;
margin-right: 16px;
& > svg {
fill: #1C2025;
}
}
&-dropdown-container {
margin-top: 16px;
& > div:nth-child(2) {
background: #fff;
padding: 0;
}
}
}

View File

@@ -334,7 +334,7 @@
border-radius: 0;
display: flex;
justify-content: space-evenly;
padding: 6px 0;
padding: 8px 0;
width: 100%;
}

View File

@@ -264,3 +264,9 @@ $chromeExtensionBannerRightInMeeeting: 10px;
*/
$smallScreen: 700px;
$verySmallScreen: 500px;
/**
* Prejoin / premeeting screen
*/
$prejoinDefaultContentWidth: 336px;

View File

@@ -79,7 +79,6 @@ $flagsImagePath: "../images/";
@import 'filmstrip/vertical_filmstrip';
@import 'filmstrip/vertical_filmstrip_overrides';
@import 'labels';
@import 'lobby';
@import 'unsupported-browser/main';
@import 'modals/invite/add-people';
@import 'deep-linking/main';
@@ -95,18 +94,16 @@ $flagsImagePath: "../images/";
@import 'meter';
@import 'audio-preview';
@import 'video-preview';
@import 'prejoin';
@import 'prejoin-dialog';
@import 'premeeting/main';
@import 'country-picker';
@import 'modals/invite/invite_more';
@import 'modals/security/security';
@import 'premeeting-screens';
@import 'e2ee';
@import 'responsive';
@import 'connection-status';
@import 'drawer';
@import 'participants-pane';
@import 'reactions-menu';
@import 'plan-limit';
@import 'polls';
/* Modules END */

View File

@@ -1,32 +1,24 @@
.con-status {
border-radius: 6px;
color: #fff;
font-size: 12px;
letter-spacing: 0.16px;
line-height: 16px;
position: absolute;
top: 24px;
width: 100%;
z-index: $toolbarZ + 3;
&-container {
border-radius: 3px;
color: #fff;
font-size: 13px;
line-height: 13px;
margin: 0 auto;
width: 320px;
@include adjust-for-max-width(320px, 8px);
}
&-header {
background: rgba(28, 32, 37, .5);
background-color: rgba(0, 0, 0, 0.7);
align-items: center;
display: flex;
justify-content: space-between;
padding: 8px 12px;
}
&-circle {
border-radius: 50%;
display: inline-block;
padding: 4px;
margin: 8px;
margin-right: 16px;
}
&--good {
@@ -42,14 +34,7 @@
}
&-arrow {
height: 36px;
width: 36px;
border-radius: 3px;
margin-left: 8px;
margin-right: 2px;
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
transition: background-color 0.16s ease-out;
&--up {
@@ -70,7 +55,7 @@
}
&-details {
background: rgba(28, 32, 37, .5);
background-color: rgba(0, 0, 0, 0.7);
border-top: 1px solid #5E6D7A;
padding: 16px;
transition: opacity 0.16s ease-out;

View File

@@ -0,0 +1,35 @@
.device {
&-status {
align-items: center;
align-self: stretch;
color: #fff;
display: flex;
font-size: 14px;
font-weight: 400;
justify-content: center;
line-height: 20px;
margin-top: 8px;
padding: 6px;
text-align: center;
}
&-icon {
background-position: center;
background-repeat: no-repeat;
display: inline-block;
height: 16px;
margin-right: 10px;
width: 16px;
&--warning {
svg path {
fill: rgba(241, 173, 51, 1);
}
}
&--ok {
svg path {
fill: #189b55;
}
}
}
}

View File

@@ -1,18 +1,21 @@
#lobby-screen {
.content {
.lobby-screen {
font-size: 16px;
font-weight: 400;
line-height: 26px;
.container {
align-items: center;
display: flex;
flex-direction: column;
&-content {
align-items: center;
display: flex;
flex-direction: column;
.spinner {
margin: 30px;
}
.spinner {
margin: 8px;
}
.joining-message {
margin: 10px;
}
.joining-message {
color: white;
margin: 24px auto;
text-align: center;
}
}
}
@@ -51,6 +54,10 @@
top: 30px;
z-index: $toolbarZ + 1;
&:empty {
border: none;
}
&.toolbox-visible {
// Same as toolbox subject position
top: 120px;
@@ -64,7 +71,7 @@
button {
align-self: stretch;
margin: 8px 0;
margin-bottom: 8px 0;
padding: 12px;
transition: .2s transform ease;

View File

@@ -0,0 +1,7 @@
@import 'connection-status';
@import 'device-status';
@import 'lobby';
@import 'premeeting-screens';
@import 'prejoin';
@import 'prejoin-dialog';
@import 'prejoin-third-party';

View File

@@ -0,0 +1,39 @@
$sidePanelWidth: 300px;
.prejoin-third-party {
flex-direction: column-reverse;
.content {
height: auto;
margin: 0 auto;
.new-toolbox {
width: auto;
}
}
#preview {
background-color: transparent;
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
.avatar {
display: none;
}
}
&.splash {
.content {
margin-left: calc((100% - #{$prejoinDefaultContentWidth} + #{$sidePanelWidth}) / 2)
}
}
&.guest {
.content {
margin-bottom: auto;
}
}
}

View File

@@ -0,0 +1,73 @@
.prejoin {
&-input-area {
width: 100%;
}
&-checkbox-container {
margin-bottom: 16px;
width: 100%;
text-align: center;
}
&-error {
color: white;
background-color: #E04757;
border-radius: 6px;
padding: 4px;
box-sizing: border-box;
margin-bottom: 16px;
margin-top: -8px;
font-size: 12px;
text-align: center;
width: 100%;
}
}
.prejoin-preview {
&-dropdown-btns {
padding: 8px 0;
width: calc(100% - 48px);
}
&-dropdown-btn {
align-items: center;
color: #1C2025;
cursor: pointer;
display: flex;
height: 40px;
font-size: 15px;
line-height: 24px;
padding: 0 16px;
&:hover {
background-color: #DAEBFA;
}
}
&-dropdown-icon {
display: inline-block;
margin-right: 16px;
& > svg {
fill: #1C2025;
}
}
&-dropdown-container {
position: relative;
width: 100%;
/**
* Override default InlineDialog behaviour, since it does not play nicely with relative widths
*/
& > div:nth-child(2) {
background: #fff;
padding: 0;
position: absolute !important;
top: 48px !important;
transform: none !important;
width: 100%;
}
}
}

View File

@@ -1,47 +1,27 @@
/**
* Shared style for full screen local track based dialogs/modals.
*/
.premeeting-screen {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.premeeting-screen {
align-items: stretch;
background: radial-gradient(50% 50% at 50% 50%, #2A3A4B 20.83%, #1E2A36 100%);
background: #292929;
bottom: 0;
display: flex;
flex-direction: column;
font-size: 1.3em;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: $toolbarZ + 1;
&-avatar {
background-color: #A4B8D1;
margin-bottom: 24px;
text {
fill: black;
font-size: 26px;
font-weight: 400;
}
}
.action-btn {
border-radius: 3px;
border-radius: 6px;
box-sizing: border-box;
color: #fff;
cursor: pointer;
display: inline-block;
font-size: 15px;
font-size: 14px;
line-height: 24px;
margin-bottom: 16px;
padding: 7px 16px;
position: relative;
text-align: center;
width: 320px;
@include adjust-for-max-width(320px, 8px);
width: 100%;
&.primary {
background: #0376DA;
@@ -49,8 +29,8 @@
}
&.secondary {
background: transparent;
border: 1px solid #5E6D7A;
background: #3D3D3D;
border: 1px solid transparent;
}
&.text {
@@ -96,130 +76,150 @@
.content {
align-items: center;
box-sizing: border-box;
display: flex;
flex: 1;
flex-direction: column;
justify-content: flex-end;
padding-bottom: 24px;
flex-shrink: 0;
height: 100%;
margin: 0 110px;
padding: 24px 0 16px;
position: relative;
width: $prejoinDefaultContentWidth;
z-index: $toolbarZ + 2;
.title {
color: #fff;
font-size: 24px;
line-height: 32px;
margin-bottom: 16px;
}
.copy-meeting {
&-controls {
align-items: center;
cursor: pointer;
color: #fff;
display: flex;
flex-direction: column;
font-size: 15px;
font-weight: 300;
justify-content: center;
line-height: 24px;
margin-bottom: 16px;
margin: auto;
width: 100%;
.url {
background: rgba(28, 32, 37, 0.5);
border-radius: 4px;
display: flex;
padding: 8px 10px;
transition: background 0.16s ease-out;
&:hover {
background: #1C2025;
.title {
color: #fff;
font-size: 28px;
font-weight: 600;
letter-spacing: -0.015;
line-height: 36px;
margin-bottom: 32px;
text-align: center;
}
input.field {
background-color: white;
border: none;
outline: none;
border-radius: 6px;
font-size: 14px;
line-height: 20px;
margin-bottom: 16px;
color: #1C2025;
padding: 10px 16px;
text-align: center;
width: 100%;
&.error {
border: 1px solid #E04757;
}
&.done {
background: #31B76A;
}
.jitsi-icon {
margin-left: 10px;
&.focused {
box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px white;
}
}
.copy-button{
width: 298px;
}
.copy-meeting-text {
width: 266px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#new-toolbox {
bottom: 0;
margin-bottom: 16px;
position: relative;
transition: none;
&:hover {
align-self: stretch;
}
textarea {
border-width: 0;
height: 0;
opacity: 0;
padding: 0;
width: 0;
}
}
input.field {
background-color: white;
border: none;
outline: none;
border-radius: 3px;
font-size: 15px;
line-height: 24px;
color: #1C2025;
padding: 8px 0;
text-align: center;
width: 320px;
@include adjust-for-max-width(320px, 8px);
&.error {
box-shadow: 0px 0px 4px 3px rgba(225, 45, 45, 0.4);
}
&.focused {
box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px white;
.toolbox-content,
.toolbox-content-wrapper,
.toolbox-content-items {
box-sizing: border-box;
width: 100%;
}
}
}
}
.media-btn-container {
display: flex;
justify-content: center;
margin: 24px 0 16px 0;
width: 100%;
&> div {
margin: 0 12px;
@media (max-width: 1000px) {
flex-direction: column-reverse;
.content {
height: auto;
margin: 0 auto;
}
.con-status {
margin: 24px auto;
position: fixed;
top: 0;
width: $prejoinDefaultContentWidth;
}
}
@media (max-width: 400px) {
.content {
padding: 16px;
width: 100%;
.title {
font-size: 20px;
line-height: 28px;
letter-spacing: -0.012;
margin-bottom: 24px;
}
}
.con-status {
margin: 16px;
width: calc(100% - 32px);
}
input.field {
font-size: 16px;
padding: 14px 16px;
}
.action-btn {
font-size: 16px;
padding: 11px 16px;
}
.toolbox-content-items {
border-radius: 0;
display: flex;
justify-content: space-evenly;
padding: 8px 0;
}
}
input::placeholder {
color: #040404;
}
}
#preview {
background: #040404;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
position: absolute;
width: 100%;
&.no-video {
background: radial-gradient(50% 50% at 50% 50%, #5B6F80 0%, #365067 100%), #FFFFFF;
text-align: center;
}
.avatar {
background: #A4B8D1;
margin: 0 auto;
background: #0045B3;
text {
fill: white;
font-size: 26px;
font-weight: 400;
}
}
video {
height: 100%;
object-fit: cover;
position: absolute;
width: 100%;
}
}
@@ -241,16 +241,14 @@
}
.toggle-button {
border-radius: 3px;
border-radius: 6px;
cursor: pointer;
color: #fff;
font-size: 13px;
height: 40px;
margin: 0 auto;
transition: background 0.16s ease-out;
width: 320px;
@include adjust-for-max-width(320px, 8px);
@include flex-centered();
svg {

View File

@@ -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" }

View File

@@ -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.

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -586,6 +586,7 @@
},
"speaker": "Говорещ",
"speakerStats": {
"search": "Търсене",
"hours": "{{count}}ч",
"minutes": "{{count}}мин",
"name": "Име",

View File

@@ -209,7 +209,6 @@
"e2eeLabel": "Ende-zu-Ende-Verschlüsselung aktivieren",
"e2eeWarning": "WARNUNG: Nicht alle Personen dieser Konferenz scheinen Ende-zu-Ende-Verschlüsselung zu unterstützen. Wenn Sie diese aktivieren, können die entsprechenden Personen nichts mehr sehen oder hören.",
"enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
"enterDisplayNameToJoin" : "Benutzername für Konferenz eingeben" ,
"embedMeeting": "Besprechung einbetten",
"error": "Fehler",
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
@@ -802,6 +801,7 @@
},
"speaker": "Sprecher/-in",
"speakerStats": {
"search": "Suche",
"hours": "{{count}} Std. ",
"minutes": "{{count}} Min. ",
"name": "Name",

View File

@@ -179,7 +179,7 @@
"e2eeLabel": "Ŝlosilo",
"e2eeTitle": "Tutvoja ĉifrado",
"e2eeWarning": "<br /><p><strong>ATENTIGO:</strong> Ne ĉiuj partoprenantoj en ĉi tiu kunveno ŝajnas havi subtenon de tutvoja ĉifrado. Se vi ŝaltos ĝin, ili ne povos vidi aŭ aŭdi vin.</p>",
"enterDisplayName": "Please enter your name here",
"enterDisplayName": "Enter your name here",
"error": "Eraro",
"externalInstallationMsg": "Vi devas instali nian ekranvidadan kromprogramon.",
"externalInstallationTitle": "Kromprogramo bezonata",

View File

@@ -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"
}
}
}

View File

@@ -203,7 +203,6 @@
"e2eeLabel": "Aktibatu puntutik punturako zifratzea",
"e2eeWarning": "OHARRA: bileraren partaide guztiek ezin dute puntutik punturako zifratzea erabili. Aukera hau aktibatzen baduzu, batzuk ezingo zaituzte ikusi eta entzun.",
"enterDisplayName": "Sartu zure izena hemen",
"enterDisplayNameToJoin": "Mesedez idatzi zure izena bileran sartzeko",
"embedMeeting": "Kapsulatu bilera",
"error": "Errorea",
"gracefulShutdown": "Zerbitzua ez dago erabilgarri mantentze-lanak direla eta. Saiatu berriro beranduago.",

View File

@@ -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",
@@ -209,7 +213,6 @@
"e2eeLabel": "Activer le chiffrement de Bout-en-Bout",
"e2eeWarning": "ATTENTION : Tous les participants de cette réunion ne semblent pas prendre en charge le chiffrement de Bout-en-Bout. Si vous activez le chiffrement, ils ne pourront ni vous voir, ni vous entendre.",
"enterDisplayName": "Merci de saisir votre nom ici",
"enterDisplayNameToJoin": "Merci de saisir votre nom pour rejoindre",
"embedMeeting": "Intégrer la réunion",
"error": "Erreur",
"gracefulShutdown": "Notre service est actuellement en maintenance. Veuillez réessayer plus tard.",
@@ -568,6 +571,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:",

View File

@@ -175,7 +175,7 @@
"dismiss": "Dismiss",
"displayNameRequired": "Hi! Whats your name?",
"done": "Done",
"enterDisplayName": "Please enter your name here",
"enterDisplayName": "Enter your name here",
"error": "Error",
"externalInstallationMsg": "You need to install our desktop sharing extension.",
"externalInstallationTitle": "Extension required",

View File

@@ -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"
},
@@ -186,7 +197,6 @@
"e2eeLabel": "Habilitar encriptação de ponta a ponta",
"e2eeWarning": "AVISO: Nem todos os participantes neste encontro parecem ter apoio para a encriptação de ponta a ponta. Se o permitir, eles não o poderão ver nem ouvir.",
"enterDisplayName": "Digite o seu nome aqui",
"enterDisplayNameToJoin": "Por favor, digite o seu nome para participar",
"embedMeeting": "Embutir reunião",
"error": "Erro",
"gracefulShutdown": "O nosso serviço está atualmente em manutenção. Por favor, tente novamente mais tarde.",
@@ -377,19 +387,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 +498,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 +660,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 +699,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 +725,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 +756,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 +767,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 +778,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 +828,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 +857,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 +916,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 +960,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"
}
}

View File

@@ -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,30 @@
"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",
"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 +241,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 +293,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 +303,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 +335,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 +362,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 +391,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 +409,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 +418,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 +433,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 +452,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 +465,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 +544,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 +574,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 +664,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 +687,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 +702,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 +720,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 +745,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 +759,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 +810,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 +822,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 +837,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 +847,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 +905,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 +916,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 +925,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 +983,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 +1006,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 +1045,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 +1063,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 +1109,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"
}
}

View File

@@ -209,7 +209,6 @@
"e2eeLabel": "Aktivizo Fshehtëzim Skaj-më-Skaj",
"e2eeWarning": "KUJDES: Jo të gjithë pjesëmarrësit në këtë takim duket të kenë mbulim për fshehtëzim Skaj-më-Skaj. Në e aktivizofshi, ata sdo të jenë në gjendje tju shohin apo dëgjojnë.",
"enterDisplayName": "Ju lutemi, jepni këtu emrin tuaj",
"enterDisplayNameToJoin": "Që të merrni pjesë, ju lutemi, jepni emrin tuaj",
"embedMeeting": "Trupëzoni takim",
"error": "Gabim",
"gracefulShutdown": "Shërbimi ynë është aktualisht i ndërprerë, për punë mirëmbajtjeje. Ju lutemi, riprovoni më vonë.",

View File

@@ -209,7 +209,6 @@
"e2eeLabel": "啟用端對端加密",
"e2eeWarning": "警告:看來不是每位此會議的參與者都有啟用端對端加密,如果您啟用了,他們可能無法看/聽到您。",
"enterDisplayName": "請在此輸入您自己的名字",
"enterDisplayNameToJoin": "請輸入您的名字以加入",
"embedMeeting": "嵌入會議",
"error": "錯誤",
"gracefulShutdown": "我們的服務目前關閉維護中,請稍後再試。",

View File

@@ -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,11 +209,10 @@
"dismiss": "Dismiss",
"displayNameRequired": "Hi! Whats 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",
"enterDisplayNameToJoin": "Please enter your name to join",
"enterDisplayName": "Enter your name here",
"embedMeeting": "Embed meeting",
"error": "Error",
"gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
@@ -614,6 +617,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:",
@@ -662,7 +693,7 @@
"joinWithoutAudio": "Join without audio",
"initiated": "Call initiated",
"linkCopied": "Link copied to clipboard",
"lookGood": "It sounds like your microphone is working properly",
"lookGood": "Your microphone is working properly",
"or": "or",
"premeeting": "Pre meeting",
"showScreen": "Enable pre meeting screen",
@@ -802,6 +833,7 @@
},
"speaker": "Speaker",
"speakerStats": {
"search": "Search",
"hours": "{{count}}h",
"minutes": "{{count}}m",
"name": "Name",
@@ -1088,7 +1120,7 @@
"admitAll": "Admit all",
"knockingParticipantList": "Knocking participant list",
"allow": "Allow",
"backToKnockModeButton": "No password, ask to join instead",
"backToKnockModeButton": "Ask to join",
"dialogTitle": "Lobby mode",
"disableDialogContent": "Lobby mode is currently enabled. This feature ensures that unwanted participants can't join your meeting. Do you want to disable it?",
"disableDialogSubmit": "Disable",
@@ -1098,6 +1130,7 @@
"enableDialogText": "Lobby mode lets you protect your meeting by only allowing people to enter after a formal approval by a moderator.",
"enterPasswordButton": "Enter meeting password",
"enterPasswordTitle": "Enter password to join meeting",
"errorMissingPassword": "Please enter the meeting password",
"invalidPassword": "Invalid password",
"joiningMessage": "You'll join the meeting as soon as someone accepts your request",
"joinWithPasswordMessage": "Trying to join with password, please wait...",

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -43,9 +43,11 @@ export class App extends AbstractApp {
*/
_renderDialogContainer() {
return (
<AtlasKitThemeProvider mode = 'dark'>
<DialogContainer />
</AtlasKitThemeProvider>
<JitsiThemeProvider>
<AtlasKitThemeProvider mode = 'dark'>
<DialogContainer />
</AtlasKitThemeProvider>
</JitsiThemeProvider>
);
}
}

View File

@@ -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';

View File

@@ -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';

View File

@@ -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
};
}

View File

@@ -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)
};
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -1,21 +1,20 @@
// @flow
import { setPrejoinPageVisibility, setSkipPrejoinOnReload } from '../../prejoin';
import { PREJOIN_SCREEN_STATES } from '../../prejoin/constants';
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
import { MiddlewareRegistry } from '../redux';
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from './actionTypes';
import './middleware.any';
declare var APP: Object;
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const { enableForcedReload } = getState()['features/base/config'];
switch (action.type) {
case CONFERENCE_JOINED: {
if (enableForcedReload) {
dispatch(setPrejoinPageVisibility(false));
dispatch(setPrejoinPageVisibility(PREJOIN_SCREEN_STATES.HIDDEN));
dispatch(setSkipPrejoinOnReload(false));
}

View File

@@ -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',

View File

@@ -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
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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,

View File

@@ -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,

View File

@@ -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>

View File

@@ -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).

View File

@@ -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 }

View File

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.666687 9.00002C0.666687 13.6024 4.39765 17.3334 9.00002 17.3334C13.6024 17.3334 17.3334 13.6024 17.3334 9.00002C17.3334 4.39765 13.6024 0.666687 9.00002 0.666687C4.39765 0.666687 0.666687 4.39765 0.666687 9.00002ZM13.7119 5.86983C13.3639 5.56869 12.8376 5.60672 12.5365 5.95477L7.55616 11.711L5.42261 9.57743C5.09717 9.25199 4.56954 9.25199 4.2441 9.57743C3.91866 9.90287 3.91866 10.4305 4.2441 10.7559L7.01102 13.5229C7.35319 13.865 7.91386 13.8448 8.23047 13.4789L13.7969 7.04527C14.098 6.69722 14.06 6.17096 13.7119 5.86983Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 699 B

View File

@@ -26,6 +26,7 @@ export { default as IconChat } from './chat.svg';
export { default as IconChatSend } from './send.svg';
export { default as IconChatUnread } from './chat-unread.svg';
export { default as IconCheck } from './check.svg';
export { default as IconCheckSolid } from './check-solid.svg';
export { default as IconClose } from './close.svg';
export { default as IconCloseCircle } from './close-circle.svg';
export { default as IconCloseX } from './close-x.svg';

View File

@@ -6,7 +6,9 @@
* {
* type: DOMINANT_SPEAKER_CHANGED,
* participant: {
* id: string
* conference: JitsiConference,
* id: string,
* previousSpeakers: Array<string>
* }
* }
*/

View File

@@ -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
}
};
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -79,37 +79,35 @@ function ConnectionStatus({ connectionDetails, t, connectionType }: Props) {
return (
<div className = 'con-status'>
<div className = 'con-status-container'>
<div
aria-level = { 1 }
className = 'con-status-header'
role = 'heading'>
<div className = { `con-status-circle ${connectionClass}` }>
<Icon
size = { 16 }
src = { icon } />
</div>
<span
aria-hidden = { !showDetails }
className = 'con-status-text'
id = 'connection-status-description'>{t(connectionText)}</span>
<div
aria-level = { 1 }
className = 'con-status-header'
role = 'heading'>
<div className = { `con-status-circle ${connectionClass}` }>
<Icon
ariaDescribedBy = 'connection-status-description'
ariaPressed = { showDetails }
className = { arrowClassName }
onClick = { onToggleDetails }
onKeyPress = { onKeyPressToggleDetails }
role = 'button'
size = { 24 }
src = { IconArrowDownSmall }
tabIndex = { 0 } />
size = { 16 }
src = { icon } />
</div>
<div
aria-level = '2'
className = { `con-status-details ${detailsClassName}` }
role = 'heading'>
{detailsText}</div>
<span
aria-hidden = { !showDetails }
className = 'con-status-text'
id = 'connection-status-description'>{t(connectionText)}</span>
<Icon
ariaDescribedBy = 'connection-status-description'
ariaPressed = { showDetails }
className = { arrowClassName }
onClick = { onToggleDetails }
onKeyPress = { onKeyPressToggleDetails }
role = 'button'
size = { 24 }
src = { IconArrowDownSmall }
tabIndex = { 0 } />
</div>
<div
aria-level = '2'
className = { `con-status-details ${detailsClassName}` }
role = 'heading'>
{detailsText}</div>
</div>
);
}

View File

@@ -1,67 +0,0 @@
// @flow
import React, { Component } from 'react';
import CopyMeetingLinkSection
from '../../../../invite/components/add-people-dialog/web/CopyMeetingLinkSection';
import { getCurrentConferenceUrl } from '../../../connection';
import { translate } from '../../../i18n';
import { connect } from '../../../redux';
type Props = {
/**
* The meeting url.
*/
url: string,
/**
* Used for translation.
*/
t: Function,
/**
* Used to determine if invitation link should be automatically copied
* after creating a meeting.
*/
_enableAutomaticUrlCopy: boolean,
};
/**
* Component used to copy meeting url on prejoin page.
*/
class CopyMeetingUrl extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<div className = 'copy-meeting'>
<CopyMeetingLinkSection url = { this.props.url } />
</div>
);
}
}
/**
* Maps (parts of) the redux state to the React {@code Component} props.
*
* @param {Object} state - The redux state.
* @returns {Object}
*/
function mapStateToProps(state) {
const { enableAutomaticUrlCopy } = state['features/base/config'];
const { customizationReady } = state['features/dynamic-branding'];
return {
url: customizationReady ? getCurrentConferenceUrl(state) : '',
_enableAutomaticUrlCopy: enableAutomaticUrlCopy || false
};
}
export default connect(mapStateToProps)(translate(CopyMeetingUrl));

View File

@@ -2,14 +2,10 @@
import React, { PureComponent } from 'react';
import { AudioSettingsButton, VideoSettingsButton } from '../../../../toolbox/components/web';
import { VideoBackgroundButton } from '../../../../virtual-background';
import { checkBlurSupport } from '../../../../virtual-background/functions';
import { Avatar } from '../../../avatar';
import { allowUrlSharing } from '../../functions';
import DeviceStatus from '../../../../prejoin/components/preview/DeviceStatus';
import { Toolbox } from '../../../../toolbox/components/web';
import ConnectionStatus from './ConnectionStatus';
import CopyMeetingUrl from './CopyMeetingUrl';
import Preview from './Preview';
type Props = {
@@ -17,12 +13,12 @@ type Props = {
/**
* Children component(s) to be rendered on the screen.
*/
children: React$Node,
children?: React$Node,
/**
* Footer to be rendered for the page (if any).
* Additional CSS class names to set on the icon container.
*/
footer?: React$Node,
className?: string,
/**
* The name of the participant.
@@ -35,25 +31,25 @@ type Props = {
showCopyUrlButton: boolean,
/**
* Indicates whether the avatar should be shown when video is off
* Indicates whether the device status should be shown
*/
showAvatar: boolean,
/**
* Indicates whether the label and copy url action should be shown
*/
showConferenceInfo: boolean,
/**
* Title of the screen.
*/
title: string,
showDeviceStatus: boolean,
/**
* The 'Skip prejoin' button to be rendered (if any).
*/
skipPrejoinButton?: React$Node,
/**
* Title of the screen.
*/
title?: string,
/**
* Override for default toolbar buttons
*/
toolbarButtons?: Array<string>,
/**
* True if the preview overlay should be muted, false otherwise.
*/
@@ -62,14 +58,11 @@ type Props = {
/**
* The video track to render as preview (if omitted, the default local track will be rendered).
*/
videoTrack?: Object,
/**
* Array with the buttons which this Toolbox should display.
*/
visibleButtons?: Array<string>
videoTrack?: Object
}
const buttons = [ 'microphone', 'camera', 'select-background', 'invite', 'settings' ];
/**
* Implements a pre-meeting screen that can be used at various pre-meeting phases, for example
* on the prejoin screen (pre-connection) or lobby (post-connection).
@@ -81,9 +74,8 @@ export default class PreMeetingScreen extends PureComponent<Props> {
* @static
*/
static defaultProps = {
showAvatar: true,
showCopyUrlButton: true,
showConferenceInfo: true
showToolbox: true
};
/**
@@ -93,57 +85,37 @@ export default class PreMeetingScreen extends PureComponent<Props> {
*/
render() {
const {
name,
showAvatar,
showConferenceInfo,
showCopyUrlButton,
children,
className,
showDeviceStatus,
skipPrejoinButton,
title,
toolbarButtons,
videoMuted,
videoTrack,
visibleButtons
videoTrack
} = this.props;
const showSharingButton = allowUrlSharing() && showCopyUrlButton;
const containerClassName = `premeeting-screen ${className ? className : ''}`;
return (
<div
className = 'premeeting-screen'
id = 'lobby-screen'>
<ConnectionStatus />
<div className = { containerClassName }>
<div className = 'content'>
<ConnectionStatus />
<div className = 'content-controls'>
<h1 className = 'title'>
{ title }
</h1>
{ children }
<Toolbox toolbarButtons = { toolbarButtons || buttons } />
{ skipPrejoinButton }
{ showDeviceStatus && <DeviceStatus /> }
</div>
</div>
<Preview
videoMuted = { videoMuted }
videoTrack = { videoTrack } />
<div className = 'content'>
{showAvatar && videoMuted && (
<Avatar
className = 'premeeting-screen-avatar'
displayName = { name }
dynamicColor = { false }
participantId = 'local'
size = { 80 } />
)}
{showConferenceInfo && (
<>
<h1 className = 'title'>
{ title }
</h1>
{showSharingButton ? <CopyMeetingUrl /> : null}
</>
)}
{ this.props.children }
<div className = 'media-btn-container'>
<div className = 'toolbox-content'>
<div className = 'toolbox-content-items'>
<AudioSettingsButton visible = { true } />
<VideoSettingsButton visible = { true } />
{ ((visibleButtons && visibleButtons.includes('select-background'))
|| (visibleButtons && visibleButtons.includes('videobackgroundblur')))
&& <VideoBackgroundButton visible = { checkBlurSupport() } /> }
</div>
</div>
</div>
{ this.props.skipPrejoinButton }
{ this.props.footer }
</div>
</div>
);
}

View File

@@ -2,17 +2,30 @@
import React from 'react';
import { getDisplayName } from '../../../../base/settings';
import { Avatar } from '../../../avatar';
import { Video } from '../../../media';
import { getLocalParticipant } from '../../../participants';
import { connect } from '../../../redux';
import { getLocalVideoTrack } from '../../../tracks';
export type Props = {
/**
* Local participant id
*/
_participantId: string,
/**
* Flag controlling whether the video should be flipped or not.
*/
flipVideo: boolean,
/**
* The name of the user that is about to join.
*/
name: string,
/**
* Flag signaling the visibility of camera preview.
*/
@@ -31,20 +44,27 @@ export type Props = {
* @returns {ReactElement}
*/
function Preview(props: Props) {
const { videoMuted, videoTrack, flipVideo } = props;
const { _participantId, flipVideo, name, videoMuted, videoTrack } = props;
const className = flipVideo ? 'flipVideoX' : '';
if (!videoMuted && videoTrack) {
return (
<div id = 'preview'>
<Video
className = { className }
videoTrack = {{ jitsiTrack: videoTrack }} />
</div>
);
}
return null;
return (
<div id = 'preview'>
{!videoMuted && videoTrack
? (
<Video
className = { className }
videoTrack = {{ jitsiTrack: videoTrack }} />
)
: (
<Avatar
className = 'premeeting-screen-avatar'
displayName = { name }
dynamicColor = { false }
participantId = { _participantId }
size = { 180 } />
)}
</div>
);
}
/**
@@ -55,8 +75,13 @@ function Preview(props: Props) {
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const name = getDisplayName(state);
const { id: _participantId } = getLocalParticipant(state);
return {
_participantId,
flipVideo: state['features/base/settings'].localFlipX,
name,
videoMuted: ownProps.videoTrack ? ownProps.videoMuted : state['features/base/media'].video.muted,
videoTrack: ownProps.videoTrack || (getLocalVideoTrack(state['features/base/tracks']) || {}).jitsiTrack
};

View File

@@ -213,14 +213,3 @@ export function getConnectionData(state: Object) {
connectionDetails: []
};
}
/**
* Returns if url sharing is enabled in interface configuration.
*
* @returns {boolean}
*/
export function allowUrlSharing() {
return typeof interfaceConfig === 'undefined'
|| typeof interfaceConfig.SHARING_FEATURES === 'undefined'
|| (interfaceConfig.SHARING_FEATURES.length && interfaceConfig.SHARING_FEATURES.indexOf('url') > -1);
}

View File

@@ -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';

View File

@@ -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
};
}

View File

@@ -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
};
}

View File

@@ -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.
*

View File

@@ -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
}
};

View File

@@ -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;
}

View File

@@ -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
};
}

View File

@@ -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'
};

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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;

View File

@@ -22,6 +22,8 @@ import {
import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { KnockingParticipantList } from '../../../lobby';
import { LobbyScreen } from '../../../lobby/components/native';
import { getIsLobbyVisible } from '../../../lobby/functions';
import { BackButtonRegistry } from '../../../mobile/back-button';
import { ParticipantsPane } from '../../../participants-pane/components/native';
import { Captions } from '../../../subtitles';
@@ -98,6 +100,11 @@ type Props = AbstractProps & {
*/
_toolboxVisible: boolean,
/**
* Indicates whether the lobby screen should be visible.
*/
_showLobby: boolean,
/**
* The redux {@code dispatch} function.
*/
@@ -154,7 +161,11 @@ class Conference extends AbstractConference<Props, *> {
* @returns {ReactElement}
*/
render() {
const { _fullscreenEnabled } = this.props;
const { _fullscreenEnabled, _showLobby } = this.props;
if (_showLobby) {
return <LobbyScreen />;
}
return (
<Container style = { styles.conference }>
@@ -427,6 +438,7 @@ function _mapStateToProps(state) {
_largeVideoParticipantId: state['features/large-video'].participantId,
_pictureInPictureEnabled: getFeatureFlag(state, PIP_ENABLED),
_reducedUI: reducedUI,
_showLobby: getIsLobbyVisible(state),
_toolboxVisible: isToolboxVisible(state)
};
}

View File

@@ -15,9 +15,10 @@ import { Filmstrip } from '../../../filmstrip';
import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { KnockingParticipantList, LobbyScreen } from '../../../lobby';
import { getIsLobbyVisible } from '../../../lobby/functions';
import { ParticipantsPane } from '../../../participants-pane/components/web';
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
import { Prejoin, isPrejoinPageVisible, isPrejoinPageLoading } from '../../../prejoin';
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
import { Toolbox } from '../../../toolbox/components/web';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
@@ -70,11 +71,6 @@ type Props = AbstractProps & {
*/
_backgroundAlpha: number,
/**
* Returns true if the 'lobby screen' is visible.
*/
_isLobbyScreenVisible: boolean,
/**
* If participants pane is visible or not.
*/
@@ -96,6 +92,11 @@ type Props = AbstractProps & {
*/
_roomName: string,
/**
* If lobby page is visible or not.
*/
_showLobby: boolean,
/**
* If prejoin page is visible or not.
*/
@@ -207,9 +208,9 @@ class Conference extends AbstractConference<Props, *> {
*/
render() {
const {
_isLobbyScreenVisible,
_isParticipantsPaneVisible,
_layoutClassName,
_showLobby,
_showPrejoin
} = this.props;
@@ -237,7 +238,7 @@ class Conference extends AbstractConference<Props, *> {
<Filmstrip />
</div>
{ _showPrejoin || _isLobbyScreenVisible || <Toolbox /> }
{ _showPrejoin || _showLobby || <Toolbox /> }
<Chat />
{ this.renderNotificationsContainer() }
@@ -245,7 +246,7 @@ class Conference extends AbstractConference<Props, *> {
<CalleeInfoContainer />
{ _showPrejoin && <Prejoin />}
{ _showLobby && <LobbyScreen />}
</div>
<ParticipantsPane />
</div>
@@ -373,12 +374,12 @@ function _mapStateToProps(state) {
return {
...abstractMapStateToProps(state),
_backgroundAlpha: backgroundAlpha,
_isLobbyScreenVisible: state['features/base/dialog']?.component === LobbyScreen,
_isParticipantsPaneVisible: getParticipantsPaneOpen(state),
_layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state)],
_mouseMoveCallbackInterval: mouseMoveCallbackInterval,
_roomName: getConferenceNameForTitle(state),
_showPrejoin: isPrejoinPageVisible(state)
_showLobby: getIsLobbyVisible(state),
_showPrejoin: isPrejoinPageVisible(state) || isPrejoinPageLoading(state)
};
}

View File

@@ -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)
};
}

View 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;
}

View File

@@ -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.
*

View File

@@ -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.
*

View File

@@ -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));
}

View File

@@ -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';

View File

@@ -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
};
}

View File

@@ -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));
}

View File

@@ -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)));
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -0,0 +1,44 @@
// @flow
import { createToolbarEvent, sendAnalytics } from '../../../../analytics';
import { translate } from '../../../../base/i18n';
import { IconAddPeople } from '../../../../base/icons';
import { connect } from '../../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../../base/toolbox/components';
import { beginAddPeople } from '../../../actions.any';
/**
* The type of the React {@code Component} props of {@link EmbedMeetingButton}.
*/
type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
};
/**
* Implementation of a button for opening invite people dialog.
*/
class InviteButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.invite';
icon = IconAddPeople;
label = 'toolbar.invite';
tooltip = 'toolbar.invite';
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @protected
* @returns {void}
*/
_handleClick() {
const { dispatch } = this.props;
sendAnalytics(createToolbarEvent('invite'));
dispatch(beginAddPeople());
}
}
export default translate(connect()(InviteButton));

View File

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

View File

@@ -0,0 +1,46 @@
// @flow
import { SET_DETAILS } from './actionTypes';
import { getVpaasTenant, sendGetDetailsRequest } from './functions';
import logger from './logger';
/**
* Action used to set the jaas customer details in store.
*
* @param {Object} details - The customer details object.
* @returns {Object}
*/
function setCustomerDetails(details) {
return {
type: SET_DETAILS,
payload: details
};
}
/**
* Sends a request for retrieving jaas customer details.
*
* @returns {Function}
*/
export function getCustomerDetails() {
return async function(dispatch: Function, getState: Function) {
const state = getState();
const baseUrl = state['features/base/config'].jaasActuatorUrl || 'https://api-vo-pilot.jitsi.net/jaas-actuator';
const appId = getVpaasTenant(state);
const shouldSendRequest = Boolean(baseUrl && appId);
if (shouldSendRequest) {
try {
const details = await sendGetDetailsRequest({
appId,
baseUrl
});
dispatch(setCustomerDetails(details));
} catch (err) {
logger.error('Could not send request', err);
}
}
};
}

View File

@@ -2,55 +2,8 @@
import { openDialog } from '../base/dialog';
import { SET_DETAILS } from './actionTypes';
import { PremiumFeatureDialog } from './components';
import { VPAAS_TENANT_PREFIX } from './constants';
import { getVpaasTenant, isFeatureDisabled, sendGetDetailsRequest } from './functions';
import logger from './logger';
/**
* Action used to set the jaas customer details in store.
*
* @param {Object} details - The customer details object.
* @returns {Object}
*/
function setCustomerDetails(details) {
return {
type: SET_DETAILS,
payload: details
};
}
/**
* Sends a request for retrieving jaas customer details.
*
* @returns {Function}
*/
export function getCustomerDetails() {
return async function(dispatch: Function, getState: Function) {
const state = getState();
const baseUrl = state['features/base/config'].jaasActuatorUrl;
const jwt = state['features/base/jwt'].jwt;
const appId = getVpaasTenant(state).replace(VPAAS_TENANT_PREFIX, '');
const shouldSendRequest = Boolean(baseUrl && jwt && appId);
if (shouldSendRequest) {
try {
const details = await sendGetDetailsRequest({
baseUrl,
jwt,
appId
});
dispatch(setCustomerDetails(details));
} catch (err) {
logger.error('Could not send request', err);
}
}
};
}
import { isFeatureDisabled } from './functions';
/**
* Shows a dialog prompting users to upgrade, if requested feature is disabled.

View File

@@ -54,24 +54,16 @@ export function isVpaasMeeting(state: Object) {
* @param {Object} reqData - The request info.
* @param {string} reqData.appId - The client appId.
* @param {string} reqData.baseUrl - The base url for the request.
* @param {string} reqData.jwt - The JWT token.
* @returns {void}
*/
export async function sendGetDetailsRequest({ appId, baseUrl, jwt }: {
export async function sendGetDetailsRequest({ appId, baseUrl }: {
appId: string,
baseUrl: string,
jwt: string,
}) {
const fullUrl = `${baseUrl}/v1/customers/${encodeURIComponent(appId)}`;
const headers = {
'Authorization': `Bearer ${jwt}`
};
const fullUrl = `${baseUrl}/v1/public/tenants/${encodeURIComponent(appId)}`;
try {
const res = await fetch(fullUrl, {
method: 'GET',
headers
});
const res = await fetch(fullUrl);
if (res.ok) {
return res.json();

Some files were not shown because too many files have changed in this diff Show More