mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-02-08 15:00:18 +00:00
Compare commits
31 Commits
update-js-
...
3973
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
953f838a2a | ||
|
|
bf5f1f0168 | ||
|
|
cdf977ff3f | ||
|
|
ee1dc9dd8e | ||
|
|
7bec68e492 | ||
|
|
97fff02c15 | ||
|
|
b372b2ccf2 | ||
|
|
d00ead932b | ||
|
|
bb2b1b58ec | ||
|
|
ceeea7314c | ||
|
|
9d6a93119b | ||
|
|
08be68cda4 | ||
|
|
3a2081ffed | ||
|
|
57d14d9517 | ||
|
|
c5e693f14a | ||
|
|
a9da047d3a | ||
|
|
171b065db1 | ||
|
|
9da0b7fee3 | ||
|
|
716c9eb46f | ||
|
|
a85c72d859 | ||
|
|
0ba567856e | ||
|
|
7f1eb83dbd | ||
|
|
6e4c1f64d8 | ||
|
|
82aa51770a | ||
|
|
990c77bd3d | ||
|
|
5bb23b2d17 | ||
|
|
9cc05ef838 | ||
|
|
a21e3a1946 | ||
|
|
c05ca1d9fc | ||
|
|
1b05d7269c | ||
|
|
86ebfe8dad |
@@ -390,6 +390,9 @@ var config = {
|
||||
// userRegion: "asia"
|
||||
},
|
||||
|
||||
// Decides whether the start/stop recording audio notifications should play on record.
|
||||
// disableRecordAudioNotification: false,
|
||||
|
||||
// Information for the chrome extension banner
|
||||
// chromeExtensionBanner: {
|
||||
// // The chrome extension to be installed address
|
||||
|
||||
130
css/_audio-preview.css
Normal file
130
css/_audio-preview.css
Normal file
@@ -0,0 +1,130 @@
|
||||
.audio-preview {
|
||||
&-content {
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
max-height: 456px;
|
||||
overflow: auto;
|
||||
width: 328px;
|
||||
}
|
||||
|
||||
&-header {
|
||||
color: #fff;
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
&-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-weight: bold;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&-entry {
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: 12px 0;
|
||||
margin-left: 48px;
|
||||
|
||||
&--selected {
|
||||
background: rgba(28,32,37,0.5);
|
||||
cursor: initial;
|
||||
margin-left: 0;
|
||||
padding-left: 21px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
display: inline-block;
|
||||
line-height: 24px;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 213px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&-speaker {
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.audio-preview-entry {
|
||||
background: rgba(255,255,255, 0.2);
|
||||
margin-left: 0;
|
||||
padding-left: 48px;
|
||||
|
||||
&--selected {
|
||||
padding-left: 21px;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-preview-test-button {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-preview-entry-text {
|
||||
max-width: 256px;
|
||||
}
|
||||
}
|
||||
|
||||
&-microphone {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
&-icon {
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
|
||||
& svg {
|
||||
fill: #1C2025;
|
||||
}
|
||||
|
||||
&--check {
|
||||
background: #31B76A;
|
||||
margin-right: 13px;
|
||||
}
|
||||
|
||||
&--exclamation {
|
||||
margin-left: 6px;
|
||||
& svg {
|
||||
fill: #E54B4B;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-test-button {
|
||||
display: none;
|
||||
background: #FFF;
|
||||
border: 1px solid #D1DBE8;
|
||||
border-radius: 3px;
|
||||
color: #1C2025;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
padding: 4px 16px;
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
&-meter-mic {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
// Override @atlaskit/InlineDialog container which is made with styled components
|
||||
& > div > div:nth-child(2) > div > div {
|
||||
outline: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
30
css/_meter.css
Normal file
30
css/_meter.css
Normal file
@@ -0,0 +1,30 @@
|
||||
.jitsi-icon {
|
||||
&.metr {
|
||||
display: inline-block;
|
||||
|
||||
& > svg {
|
||||
fill: #4E5E6C;
|
||||
width: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
&.metr--disabled {
|
||||
& > svg {
|
||||
fill: #4E5E6C;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metr-l-0 {
|
||||
rect:first-child {
|
||||
fill: #31B76A;
|
||||
}
|
||||
}
|
||||
|
||||
@for $i from 1 through 7 {
|
||||
.metr-l-#{$i} {
|
||||
rect:nth-child(-n+#{$i+1}) {
|
||||
fill: #31B76A;
|
||||
}
|
||||
}
|
||||
}
|
||||
79
css/_settings-button.scss
Normal file
79
css/_settings-button.scss
Normal file
@@ -0,0 +1,79 @@
|
||||
.settings-button {
|
||||
&-container {
|
||||
position: relative;
|
||||
|
||||
.toolbox-icon {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #d1dbe8;
|
||||
justify-content: center;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
|
||||
&:hover {
|
||||
background-color: #daebfa;
|
||||
border: 1px solid #daebfa;
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
background: #2a3a4b;
|
||||
border: 1px solid #5e6d7a;
|
||||
|
||||
svg {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #5e6d7a;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled, .disabled & {
|
||||
cursor: initial;
|
||||
color: #fff;
|
||||
background-color: #a4b8d1;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: #5e6d7a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-small-icon {
|
||||
background: #FFF;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 50%;
|
||||
bottom: 0;
|
||||
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.25);
|
||||
cursor: pointer;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
right: 4px;
|
||||
width: 16px;
|
||||
|
||||
&> svg {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
background-color: #a4b8d1;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&--hovered {
|
||||
bottom: -1px;
|
||||
height: 20px;
|
||||
right: 2px;
|
||||
width: 20px;
|
||||
|
||||
&> svg {
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
65
css/_video-preview.css
Normal file
65
css/_video-preview.css
Normal file
@@ -0,0 +1,65 @@
|
||||
.video-preview {
|
||||
max-height: 290px;
|
||||
overflow: auto;
|
||||
|
||||
&-entry {
|
||||
cursor: pointer;
|
||||
height: 135px;
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
width: 240px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
border: 3px solid #31B76A;
|
||||
cursor: default;
|
||||
height: 129px;
|
||||
width: 234px;
|
||||
}
|
||||
}
|
||||
|
||||
&-video {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-overlay {
|
||||
background: rgba(42, 58, 75, 0.6);
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&-error {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-label {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
overflow: hidden;
|
||||
padding: 8px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
width: 220px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
// Override @atlaskit/InlineDialog container which is made with styled components
|
||||
& > div > div:nth-child(2) > div > div {
|
||||
outline: none;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
@@ -86,5 +86,9 @@ $flagsImagePath: "../images/";
|
||||
@import 'avatar';
|
||||
@import 'promotional-footer';
|
||||
@import 'chrome-extension-banner';
|
||||
@import 'settings-button';
|
||||
@import 'meter';
|
||||
@import 'audio-preview';
|
||||
@import 'video-preview';
|
||||
|
||||
/* Modules END */
|
||||
|
||||
1
debian/jitsi-meet-turnserver.links
vendored
1
debian/jitsi-meet-turnserver.links
vendored
@@ -1 +0,0 @@
|
||||
/usr/share/jitsi-meet-turnserver/jitsi-meet.conf /etc/nginx/modules-enabled/60-jitsi-meet.conf
|
||||
117
debian/jitsi-meet-turnserver.postinst
vendored
117
debian/jitsi-meet-turnserver.postinst
vendored
@@ -36,20 +36,60 @@ case "$1" in
|
||||
NGINX_CONFIG="/etc/nginx/sites-available/$JVB_HOSTNAME.conf"
|
||||
JITSI_MEET_CONFIG="/etc/jitsi/meet/$JVB_HOSTNAME-config.js"
|
||||
|
||||
NGINX_SITES_ENABLED="/etc/nginx/sites-enabled/"
|
||||
NGINX_CONFIG_ENABLED="${NGINX_SITES_ENABLED}${JVB_HOSTNAME}.conf"
|
||||
for site in ${NGINX_SITES_ENABLED}*; do
|
||||
# if it is not a file continue
|
||||
[ -f "${site}" ] || continue
|
||||
# if it is our config skip
|
||||
[ "${site}" != "${NGINX_CONFIG_ENABLED}" ] || continue
|
||||
# check whether other enabled hosts has listen 443
|
||||
if cat ${site} | grep -v "^[[:space:]]*#" | grep listen | grep -q "^.*[[:space:]:]443[;[:space:]].*" ; then
|
||||
# nothing to do
|
||||
echo "------------------------------------------------"
|
||||
echo ""
|
||||
echo "turnserver not configured as other nginx sites use port 443"
|
||||
echo ""
|
||||
echo "------------------------------------------------"
|
||||
db_stop
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
# if there was a turn config backup it so we can configure
|
||||
# we cannot recognize at the moment is this a user config or default config when installing coturn
|
||||
if [[ -f $TURN_CONFIG ]] && ! grep -q "jitsi-meet coturn config" "$TURN_CONFIG" ; then
|
||||
mv $TURN_CONFIG $TURN_CONFIG.bak
|
||||
fi
|
||||
|
||||
# detect dpkg-reconfigure, just delete old links
|
||||
db_get jitsi-meet-turnserver/jvb-hostname
|
||||
JVB_HOSTNAME_OLD=$RET
|
||||
if [ -n "$RET" ] && [ ! "$JVB_HOSTNAME_OLD" = "$JVB_HOSTNAME" ] ; then
|
||||
rm -f $TURN_CONFIG
|
||||
if [[ -f $TURN_CONFIG ]] && grep -q "jitsi-meet coturn config" "$TURN_CONFIG" ; then
|
||||
rm -f $TURN_CONFIG
|
||||
fi
|
||||
fi
|
||||
|
||||
# this detect only old installations with no nginx
|
||||
db_get jitsi-meet/jvb-serve || true
|
||||
if [ ! -f $NGINX_CONFIG -o "$RET" = "true" ] ; then
|
||||
# nothing to do
|
||||
echo "------------------------------------------------"
|
||||
echo ""
|
||||
echo "turnserver not configured as no nginx found to multiplex traffic"
|
||||
echo ""
|
||||
echo "------------------------------------------------"
|
||||
db_stop
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -f $TURN_CONFIG ]] ; then
|
||||
echo "------------------------------------------------"
|
||||
echo ""
|
||||
echo "turnserver is already configured on this machine, skipping."
|
||||
echo ""
|
||||
echo "------------------------------------------------"
|
||||
db_stop
|
||||
exit 0
|
||||
fi
|
||||
@@ -65,46 +105,53 @@ case "$1" in
|
||||
fi
|
||||
TURN_SECRET="$RET"
|
||||
|
||||
if [[ -f $TURN_CONFIG ]] && ! grep -q "jitsi-meet coturn config" "$TURN_CONFIG" ; then
|
||||
PUBLIC_IP=$(dig +short myip.opendns.com @resolver1.opendns.com)
|
||||
cp /usr/share/jitsi-meet-turnserver/turnserver.conf $TURN_CONFIG
|
||||
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" $TURN_CONFIG
|
||||
sed -i "s/__turnSecret__/$TURN_SECRET/g" $TURN_CONFIG
|
||||
sed -i "s/__external_ip_address__/$JVB_HOSTNAME/g" $TURN_CONFIG
|
||||
# no turn config exists, lt's copy template and fill it in
|
||||
PUBLIC_IP=$(dig +short myip.opendns.com @resolver1.opendns.com)
|
||||
cp /usr/share/jitsi-meet-turnserver/turnserver.conf $TURN_CONFIG
|
||||
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" $TURN_CONFIG
|
||||
sed -i "s/__turnSecret__/$TURN_SECRET/g" $TURN_CONFIG
|
||||
sed -i "s/__external_ip_address__/$JVB_HOSTNAME/g" $TURN_CONFIG
|
||||
|
||||
# SSL for nginx
|
||||
db_get jitsi-meet/cert-choice
|
||||
CERT_CHOICE="$RET"
|
||||
# Hack Debian Buster coturn to be able to bind privileged port 443
|
||||
COTURN_UNIT_FILE="/lib/systemd/system/coturn.service"
|
||||
if [[ -f $COTURN_UNIT_FILE ]] && ! grep -q "CAP_NET_BIND_SERVICE" "$COTURN_UNIT_FILE" ; then
|
||||
sed -i "s/\[Service\]/\[Service\]\nAmbientCapabilities=CAP_NET_BIND_SERVICE/g" $COTURN_UNIT_FILE
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
|
||||
if [ "$CERT_CHOICE" = "I want to use my own certificate" ] ; then
|
||||
db_get jitsi-meet/cert-path-key
|
||||
CERT_KEY="$RET"
|
||||
db_get jitsi-meet/cert-path-crt
|
||||
CERT_CRT="$RET"
|
||||
# SSL for nginx
|
||||
db_get jitsi-meet/cert-choice
|
||||
CERT_CHOICE="$RET"
|
||||
|
||||
# replace self-signed certificate paths with user provided ones
|
||||
CERT_KEY_ESC=$(echo $CERT_KEY | sed 's/\./\\\./g')
|
||||
CERT_KEY_ESC=$(echo $CERT_KEY_ESC | sed 's/\//\\\//g')
|
||||
sed -i "s/pkey=\/etc\/jitsi\/meet\/.*key/pkey=$CERT_KEY_ESC/g" $TURN_CONFIG
|
||||
CERT_CRT_ESC=$(echo $CERT_CRT | sed 's/\./\\\./g')
|
||||
CERT_CRT_ESC=$(echo $CERT_CRT_ESC | sed 's/\//\\\//g')
|
||||
sed -i "s/cert=\/etc\/jitsi\/meet\/.*crt/cert=$CERT_CRT_ESC/g" $TURN_CONFIG
|
||||
fi
|
||||
if [ "$CERT_CHOICE" = "I want to use my own certificate" ] ; then
|
||||
db_get jitsi-meet/cert-path-key
|
||||
CERT_KEY="$RET"
|
||||
db_get jitsi-meet/cert-path-crt
|
||||
CERT_CRT="$RET"
|
||||
|
||||
sed -i "s/#TURNSERVER_ENABLED/TURNSERVER_ENABLED/g" /etc/default/coturn
|
||||
invoke-rc.d coturn restart || true
|
||||
# replace self-signed certificate paths with user provided ones
|
||||
CERT_KEY_ESC=$(echo $CERT_KEY | sed 's/\./\\\./g')
|
||||
CERT_KEY_ESC=$(echo $CERT_KEY_ESC | sed 's/\//\\\//g')
|
||||
sed -i "s/pkey=\/etc\/jitsi\/meet\/.*key/pkey=$CERT_KEY_ESC/g" $TURN_CONFIG
|
||||
CERT_CRT_ESC=$(echo $CERT_CRT | sed 's/\./\\\./g')
|
||||
CERT_CRT_ESC=$(echo $CERT_CRT_ESC | sed 's/\//\\\//g')
|
||||
sed -i "s/cert=\/etc\/jitsi\/meet\/.*crt/cert=$CERT_CRT_ESC/g" $TURN_CONFIG
|
||||
fi
|
||||
|
||||
NGINX_STREAM_CONFIG="/etc/nginx/modules-enabled/60-jitsi-meet.conf"
|
||||
if [ -f $NGINX_STREAM_CONFIG ] && [ -f $NGINX_CONFIG ] ; then
|
||||
sed -i "s/listen 443 ssl/listen 4444 ssl http2/g" $NGINX_CONFIG
|
||||
sed -i "s/listen \[\:\:\]\:443 ssl/listen \[\:\:\]\:4444 ssl http2/g" $NGINX_CONFIG
|
||||
invoke-rc.d nginx reload || true
|
||||
fi
|
||||
sed -i "s/#TURNSERVER_ENABLED/TURNSERVER_ENABLED/g" /etc/default/coturn
|
||||
invoke-rc.d coturn restart || true
|
||||
|
||||
# Enable turn server in config.js
|
||||
if [ -f $JITSI_MEET_CONFIG ] ; then
|
||||
sed -i "s/\/\/ useStunTurn: true/useStunTurn: true/g" $JITSI_MEET_CONFIG
|
||||
fi
|
||||
NGINX_STREAM_CONFIG="/etc/nginx/modules-enabled/60-jitsi-meet.conf"
|
||||
if [ ! -f $NGINX_STREAM_CONFIG ] && [ -f $NGINX_CONFIG ] ; then
|
||||
ln -s /usr/share/jitsi-meet-turnserver/jitsi-meet.conf $NGINX_STREAM_CONFIG
|
||||
sed -i "s/listen 443 ssl/listen 4444 ssl http2/g" $NGINX_CONFIG
|
||||
sed -i "s/listen \[\:\:\]\:443 ssl/listen \[\:\:\]\:4444 ssl http2/g" $NGINX_CONFIG
|
||||
invoke-rc.d nginx reload || true
|
||||
fi
|
||||
|
||||
# Enable turn server in config.js
|
||||
if [ -f $JITSI_MEET_CONFIG ] ; then
|
||||
sed -i "s/\/\/ useStunTurn: true/useStunTurn: true/g" $JITSI_MEET_CONFIG
|
||||
fi
|
||||
|
||||
# and we're done with debconf
|
||||
|
||||
3
debian/jitsi-meet-turnserver.postrm
vendored
3
debian/jitsi-meet-turnserver.postrm
vendored
@@ -24,6 +24,7 @@ set -e
|
||||
|
||||
case "$1" in
|
||||
remove)
|
||||
rm -rf /etc/nginx/modules-enabled/60-jitsi-meet.conf
|
||||
if [ -x "/etc/init.d/nginx" ]; then
|
||||
invoke-rc.d nginx reload || true
|
||||
fi
|
||||
@@ -32,6 +33,8 @@ case "$1" in
|
||||
fi
|
||||
;;
|
||||
purge)
|
||||
rm -rf /etc/nginx/modules-enabled/60-jitsi-meet.conf
|
||||
rm -rf /etc/turnserver.conf
|
||||
if [ -x "/etc/init.d/nginx" ]; then
|
||||
invoke-rc.d nginx reload || true
|
||||
fi
|
||||
|
||||
43
doc/README.md
Normal file
43
doc/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Documentation
|
||||
|
||||
This document is the entrypoint to different guides, divided in three groups:
|
||||
|
||||
* User guide: these documents are designed to help users of the service, to better
|
||||
understand all the available features and how to use them.
|
||||
|
||||
* Developer guide: these documents are designed to help developers who want to either
|
||||
integrate the Jitsi Meet API / SDK in their products or want to improve Jitsi Meet
|
||||
itself by developing new features or fixing bugs.
|
||||
|
||||
* DevOps guide: these documents are designed for DevOps folks, system administrators
|
||||
or anyone who wishes to deploy and operate their own Jitsi Meet instance.
|
||||
|
||||
## User guide
|
||||
|
||||
Work in progress.
|
||||
|
||||
## Developer guide
|
||||
|
||||
### Web
|
||||
|
||||
* [iframe API](https://github.com/jitsi/jitsi-meet/blob/master/doc/api.md)
|
||||
* [Jitsi Meet development](https://github.com/jitsi/jitsi-meet/blob/master/doc/development.md)
|
||||
|
||||
### Mobile
|
||||
|
||||
* [Building the mobile apps](https://github.com/jitsi/jitsi-meet/blob/master/doc/mobile.md)
|
||||
* [SDK usage examples](https://github.com/jitsi/jitsi-meet-sdk-samples)
|
||||
* [Enabling Dropbox support](https://github.com/jitsi/jitsi-meet/blob/master/doc/mobile-dropbox.md)
|
||||
* [Enabling Google authentication](https://github.com/jitsi/jitsi-meet/blob/master/doc/mobile-google-auth.md)
|
||||
|
||||
## DevOps guide
|
||||
|
||||
* [Quick install](https://github.com/jitsi/jitsi-meet/blob/master/doc/quick-install.md)
|
||||
* [Docker install](https://github.com/jitsi/docker-jitsi-meet/blob/master/README.md)
|
||||
* [Google Calendar, MS Calendar, Dropbox integrations](https://github.com/jitsi/jitsi-meet/blob/master/doc/integrations.md)
|
||||
* [Video tutorials on deployment and scalability](https://jitsi.org/tutorials/)
|
||||
* [Configuring a video SIP gateway](https://github.com/jitsi/jitsi-meet/blob/master/doc/sipgw-config.md)
|
||||
* [Enabling speaker stats](https://github.com/jitsi/jitsi-meet/blob/master/doc/speakerstats-prosody.md)
|
||||
* [Enabling TURN](https://github.com/jitsi/jitsi-meet/blob/master/doc/turn.md)
|
||||
* [Networking FAQ](https://github.com/jitsi/jitsi-meet/blob/master/doc/faq.md)
|
||||
* [Cloud APIs](https://github.com/jitsi/jitsi-meet/blob/master/doc/cloud-api.md)
|
||||
@@ -11,3 +11,5 @@ no-tcp
|
||||
listening-port=443
|
||||
tls-listening-port=4445
|
||||
external-ip=__external_ip_address__
|
||||
|
||||
syslog
|
||||
|
||||
@@ -23,7 +23,7 @@ Finally on the same machine test that you can ping the FQDN with: `ping "$(hostn
|
||||
### Add the Jitsi package repository
|
||||
```sh
|
||||
echo 'deb https://download.jitsi.org stable/' >> /etc/apt/sources.list.d/jitsi-stable.list
|
||||
wget -qO - https://download.jitsi.org/jitsi-key.gpg.key | apt-key add -
|
||||
wget -qO - https://download.jitsi.org/jitsi-key.gpg.key | sudo apt-key add -
|
||||
```
|
||||
|
||||
### Install Jitsi Meet
|
||||
|
||||
@@ -19,7 +19,7 @@ There are 2 ways to integrate the SDK into your project:
|
||||
|
||||
Follow the instructions [here](https://github.com/jitsi/jitsi-meet-ios-sdk-releases/blob/master/README.md).
|
||||
|
||||
### Builduing it yourself
|
||||
### Building it yourself
|
||||
|
||||
1. Install all required [dependencies](https://github.com/jitsi/jitsi-meet/blob/master/doc/mobile.md).
|
||||
|
||||
|
||||
@@ -264,13 +264,13 @@
|
||||
"startLiveStreaming": "Inicia la transmissió en directe",
|
||||
"startRecording": "Inicia l'enregistrament",
|
||||
"startRemoteControlErrorMessage": "S'ha produït un error en intentar iniciar la sessió de control remot!",
|
||||
"stopLiveStreaming": "Inicia la transmissió en directe",
|
||||
"stopRecording": "Inicia l'enregistrament",
|
||||
"stopRecordingWarning": "Esteu segur de voler iniciar l'enregistrament?",
|
||||
"stopLiveStreaming": "Atura la transmissió en directe",
|
||||
"stopRecording": "Atura l'enregistrament",
|
||||
"stopRecordingWarning": "Esteu segur de voler aturar l'enregistrament?",
|
||||
"stopStreamingWarning": "Esteu segur de voler aturar la transmissió en directe?",
|
||||
"streamKey": "Clau de transmissió en directe",
|
||||
"Submit": "Tramet",
|
||||
"thankYou": "Gràcies per usar {{appName}}!",
|
||||
"thankYou": "Gràcies per emprar {{appName}}!",
|
||||
"token": "identificador",
|
||||
"tokenAuthFailed": "No esteu autoritzat a unir-vos a aquesta trucada.",
|
||||
"tokenAuthFailedTitle": "L'autenticació ha fallat",
|
||||
|
||||
@@ -769,5 +769,9 @@
|
||||
"sendFeedback": "Feedback senden",
|
||||
"terms": "AGB",
|
||||
"title": "Sichere, mit umfassenden Funktionen ausgestattete und vollkommen kostenlose Videokonferenzen"
|
||||
},
|
||||
"lonelyMeetingExperience": {
|
||||
"button": "Weitere einladen",
|
||||
"youAreAlone": "Sie sind der einzige in diesem Meeting"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,6 +628,7 @@
|
||||
"logout": "Cerrar sesión",
|
||||
"lowerYourHand": "Bajar la mano",
|
||||
"moreActions": "Más acciones",
|
||||
"moreOptions": "Más opciones",
|
||||
"mute": "Activar / Desactivar Silencio",
|
||||
"noAudioSignalTitle": "¡No hay entrada proveniente de su micrófono!",
|
||||
"noAudioSignalDesc": "Si no lo silenció a propósito desde la configuración del sistema o el dispositivo, considere cambiar el dispositivo.",
|
||||
@@ -752,5 +753,9 @@
|
||||
"sendFeedback": "Enviar comentarios",
|
||||
"terms": "Términos",
|
||||
"title": "Seguro, lleno de funcionalidades y videoconferencias completamente gratuitas"
|
||||
},
|
||||
"lonelyMeetingExperience": {
|
||||
"button": "Invitar a otros",
|
||||
"youAreAlone": "Eres el único en la reunión"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,6 +628,7 @@
|
||||
"logout": "Déconnexion",
|
||||
"lowerYourHand": "Baisser la main",
|
||||
"moreActions": "Plus d'actions",
|
||||
"moreOptions": "Plus d'options",
|
||||
"mute": "Muet / Actif",
|
||||
"noAudioSignalTitle": "Il n'y a pas d'entrée provenant de votre micro !",
|
||||
"noAudioSignalDesc": "Si vous n'avez pas délibérément coupé le son des paramètres du système ou du matériel, envisagez de changer le périphérique.",
|
||||
@@ -752,5 +753,12 @@
|
||||
"sendFeedback": "Envoyer votre avis",
|
||||
"terms": "Termes",
|
||||
"title": "Vidéoconférence Sécurisée, entièrement en vedette et gratuite"
|
||||
},
|
||||
"lonelyMeetingExperience": {
|
||||
"button": "Inviter d'autres personnes",
|
||||
"youAreAlone": "Vous êtes le seul participant de la réunion"
|
||||
},
|
||||
"helpView": {
|
||||
"header": "Centre d'aide"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,6 +594,7 @@
|
||||
"logout": "Logout",
|
||||
"lowerYourHand": "Abbassa la mano",
|
||||
"moreActions": "Più azioni",
|
||||
"moreOptions": "Più opzioni",
|
||||
"mute": "Microfono Attiva / Disattiva",
|
||||
"openChat": "Apri una chat",
|
||||
"pip": "Abilita visualizzazione immagine nell’immagine",
|
||||
@@ -707,5 +708,9 @@
|
||||
"sendFeedback": "Invia feedback",
|
||||
"terms": "Termini di utilizzo",
|
||||
"title": "Il sistema di conferenza sicuro, funzionale e completamente gratuito."
|
||||
},
|
||||
"lonelyMeetingExperience": {
|
||||
"button": "Invita gli altri",
|
||||
"youAreAlone": "Sei l'unico in riunione"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,9 +569,9 @@
|
||||
"shareYourScreen": "Slå av eller på skärmdelning",
|
||||
"shortcuts": "Stäng eller öppna genvägar",
|
||||
"show": "",
|
||||
"speakerStats": "Stäng eller öppna högstalarstatistik",
|
||||
"speakerStats": "Stäng eller öppna talarstatistik",
|
||||
"tileView": "Öppna eller stäng panelvyn",
|
||||
"toggleCamera": "Byta kamera",
|
||||
"toggleCamera": "Växla kamera",
|
||||
"videomute": "Sätt på eller stäng av mikrofonen",
|
||||
"videoblur": ""
|
||||
},
|
||||
@@ -606,7 +606,7 @@
|
||||
"sharedvideo": "Dela en Youtube-video",
|
||||
"shareRoom": "Bjud in någon",
|
||||
"shortcuts": "Visa genvägar",
|
||||
"speakerStats": "Högtalarspecifikationer",
|
||||
"speakerStats": "Talarstatistik",
|
||||
"startScreenSharing": "Starta skärmdelning",
|
||||
"startSubtitles": "Starta undertextning",
|
||||
"stopScreenSharing": "Avsluta skämdelning",
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
"muteParticipantDialog": "Are you sure you want to mute this participant? You won't be able to unmute them, but they can unmute themselves at any time.",
|
||||
"muteParticipantTitle": "Mute this participant?",
|
||||
"Ok": "Ok",
|
||||
"passwordLabel": "$t(lockRoomPasswordUppercase)",
|
||||
"passwordLabel": "The meeting has been locked by a participant. Please enter the $t(lockRoomPassword) to join.",
|
||||
"passwordNotSupported": "Setting a meeting $t(lockRoomPassword) is not supported.",
|
||||
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) not supported",
|
||||
"passwordRequired": "$t(lockRoomPasswordUppercase) required",
|
||||
@@ -244,6 +244,7 @@
|
||||
"reservationError": "Reservation system error",
|
||||
"reservationErrorMsg": "Error code: {{code}}, message: {{msg}}",
|
||||
"retry": "Retry",
|
||||
"screenSharingAudio": "Share audio",
|
||||
"screenSharingFailedToInstall": "Oops! Your screen sharing extension failed to install.",
|
||||
"screenSharingFailedToInstallTitle": "Screen sharing extension failed to install",
|
||||
"screenSharingFirefoxPermissionDeniedError": "Something went wrong while we were trying to share your screen. Please make sure that you have given us permission to do so. ",
|
||||
@@ -525,6 +526,7 @@
|
||||
"followMe": "Everyone follows me",
|
||||
"language": "Language",
|
||||
"loggedIn": "Logged in as {{name}}",
|
||||
"microphones": "Microphones",
|
||||
"moderator": "Moderator",
|
||||
"more": "More",
|
||||
"name": "Name",
|
||||
@@ -532,6 +534,7 @@
|
||||
"selectAudioOutput": "Audio output",
|
||||
"selectCamera": "Camera",
|
||||
"selectMic": "Microphone",
|
||||
"speakers": "Speakers",
|
||||
"startAudioMuted": "Everyone starts muted",
|
||||
"startVideoMuted": "Everyone starts hidden",
|
||||
"title": "Settings"
|
||||
@@ -648,7 +651,7 @@
|
||||
"noAudioSignalDesc": "If you did not purposely mute it from system settings or hardware, consider switching the device.",
|
||||
"noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider switching to the suggested device.",
|
||||
"noAudioSignalDialInDesc": "You can also dial-in using:",
|
||||
"noAudioSignalDialInLinkDesc" : "Dial-in numbers",
|
||||
"noAudioSignalDialInLinkDesc": "Dial-in numbers",
|
||||
"noisyAudioInputTitle": "Your microphone appears to be noisy!",
|
||||
"noisyAudioInputDesc": "It sounds like your microphone is making noise, please consider muting or changing the device.",
|
||||
"openChat": "Open chat",
|
||||
@@ -755,6 +758,7 @@
|
||||
"connectCalendarButton": "Connect your calendar",
|
||||
"connectCalendarText": "Connect your calendar to view all your meetings in {{app}}. Plus, add {{provider}} meetings to your calendar and start them with one click.",
|
||||
"enterRoomTitle": "Start a new meeting",
|
||||
"getHelp": "Get help",
|
||||
"roomNameAllowedChars": "Meeting name should not contain any of these characters: ?, &, :, ', \", %, #.",
|
||||
"go": "GO",
|
||||
"goSmall": "GO",
|
||||
@@ -774,5 +778,8 @@
|
||||
"lonelyMeetingExperience": {
|
||||
"button": "Invite others",
|
||||
"youAreAlone": "You are the only one in the meeting"
|
||||
},
|
||||
"helpView": {
|
||||
"header": "Help centre"
|
||||
}
|
||||
}
|
||||
|
||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -10653,8 +10653,8 @@
|
||||
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
|
||||
},
|
||||
"js-utils": {
|
||||
"version": "github:jitsi/js-utils#91c5e53ca5fa42907c88d56bc78254e6e56e058d",
|
||||
"from": "github:jitsi/js-utils#91c5e53ca5fa42907c88d56bc78254e6e56e058d",
|
||||
"version": "github:jitsi/js-utils#0b2cef90613a74777fefd98d4ee3eda3879809ab",
|
||||
"from": "github:jitsi/js-utils#0b2cef90613a74777fefd98d4ee3eda3879809ab",
|
||||
"requires": {
|
||||
"bowser": "2.7.0",
|
||||
"js-md5": "0.7.3",
|
||||
@@ -10883,8 +10883,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#a7950f8ebb489225c2e8bf41fe65f330b3de0874",
|
||||
"from": "github:jitsi/lib-jitsi-meet#a7950f8ebb489225c2e8bf41fe65f330b3de0874",
|
||||
"version": "github:jitsi/lib-jitsi-meet#960eea3c5087ce07e9135fad70268c7d338e0de5",
|
||||
"from": "github:jitsi/lib-jitsi-meet#960eea3c5087ce07e9135fad70268c7d338e0de5",
|
||||
"requires": {
|
||||
"@jitsi/sdp-interop": "0.1.14",
|
||||
"@jitsi/sdp-simulcast": "0.2.2",
|
||||
@@ -10897,7 +10897,7 @@
|
||||
"strophe.js": "1.3.4",
|
||||
"strophejs-plugin-disco": "0.0.2",
|
||||
"strophejs-plugin-stream-management": "github:jitsi/strophejs-plugin-stream-management#cec7608601c1bc098543823fc658e3ddf758c009",
|
||||
"webrtc-adapter": "github:webrtc/adapter#1eec19782b4058d186341263e7d049cea3e3290a"
|
||||
"webrtc-adapter": "7.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"js-md5": {
|
||||
@@ -18999,11 +18999,12 @@
|
||||
}
|
||||
},
|
||||
"webrtc-adapter": {
|
||||
"version": "github:webrtc/adapter#1eec19782b4058d186341263e7d049cea3e3290a",
|
||||
"from": "github:webrtc/adapter#1eec19782b4058d186341263e7d049cea3e3290a",
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.5.0.tgz",
|
||||
"integrity": "sha512-cUqlw310uLLSYvO8FTNCVmGWSMlMt6vuSDkcYL1nW+RUvAILJ3jEIvAUgFQU5EFGnU+mf9/No14BFv3U+hoxBQ==",
|
||||
"requires": {
|
||||
"rtcpeerconnection-shim": "^1.1.13",
|
||||
"sdp": "^2.3.0"
|
||||
"rtcpeerconnection-shim": "^1.2.15",
|
||||
"sdp": "^2.12.0"
|
||||
}
|
||||
},
|
||||
"websocket-driver": {
|
||||
|
||||
@@ -53,10 +53,10 @@
|
||||
"jquery-contextmenu": "2.4.5",
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"js-utils": "github:jitsi/js-utils#91c5e53ca5fa42907c88d56bc78254e6e56e058d",
|
||||
"js-utils": "github:jitsi/js-utils#0b2cef90613a74777fefd98d4ee3eda3879809ab",
|
||||
"jsrsasign": "8.0.12",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#a7950f8ebb489225c2e8bf41fe65f330b3de0874",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#960eea3c5087ce07e9135fad70268c7d338e0de5",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.13",
|
||||
"moment": "2.19.4",
|
||||
|
||||
@@ -89,10 +89,22 @@ export class AbstractApp extends BaseApp<Props, *> {
|
||||
return (
|
||||
<Fragment>
|
||||
<OverlayContainer />
|
||||
{ this._createExtraPlatformSpecificElement() }
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders platform specific extra elements to be added alongside with the main element, if need be.
|
||||
*
|
||||
* NOTE: Overridden by child components.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_createExtraPlatformSpecificElement() {
|
||||
return null;
|
||||
}
|
||||
|
||||
_createMainElement: (React$Element<*>, Object) => ?React$Element<*>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Platform } from '../../base/react';
|
||||
import '../../base/responsive-ui';
|
||||
import { updateSettings } from '../../base/settings';
|
||||
import '../../google-api';
|
||||
import { HelpView } from '../../help';
|
||||
import '../../mobile/audio-mode';
|
||||
import '../../mobile/back-button';
|
||||
import '../../mobile/background';
|
||||
@@ -107,6 +108,17 @@ export class App extends AbstractApp {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders platform specific extra elements to be added alongside with the main element, if need be.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_createExtraPlatformSpecificElement() {
|
||||
return (
|
||||
<HelpView />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to disable the use of React Native
|
||||
* {@link ExceptionsManager#handleException} on platforms and in
|
||||
|
||||
@@ -39,6 +39,7 @@ export default {
|
||||
statusBarContent: ColorPalette.white,
|
||||
text: ColorPalette.white
|
||||
},
|
||||
'Modal': {},
|
||||
'LargeVideo': {
|
||||
background: 'rgb(42, 58, 75)'
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import extraConfigWhitelist from './extraConfigWhitelist';
|
||||
|
||||
/**
|
||||
* The config keys to whitelist, the keys that can be overridden.
|
||||
* Currently we can only whitelist the first part of the properties, like
|
||||
@@ -145,4 +147,4 @@ export default [
|
||||
'useStunTurn',
|
||||
'webrtcIceTcpDisable',
|
||||
'webrtcIceUdpDisable'
|
||||
];
|
||||
].concat(extraConfigWhitelist);
|
||||
|
||||
4
react/features/base/config/extraConfigWhitelist.js
Normal file
4
react/features/base/config/extraConfigWhitelist.js
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Deploy-specific configuration whitelists
|
||||
*/
|
||||
export default [];
|
||||
@@ -216,6 +216,18 @@ export function setAudioInputDevice(deviceId) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the output device id.
|
||||
*
|
||||
* @param {string} deviceId - The id of the new output device.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setAudioOutputDevice(deviceId) {
|
||||
return function(dispatch) {
|
||||
return setAudioOutputDeviceId(deviceId, dispatch);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to update the currently used video input device.
|
||||
*
|
||||
|
||||
@@ -174,6 +174,60 @@ export function formatDeviceLabel(label: string) {
|
||||
return formattedLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of objects containing all the microphone device ids and labels.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
export function getAudioInputDeviceData(state: Object) {
|
||||
return state['features/base/devices'].availableDevices.audioInput.map(
|
||||
({ deviceId, label }) => {
|
||||
return {
|
||||
deviceId,
|
||||
label
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of objectes containing all the output device ids and labels.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
export function getAudioOutputDeviceData(state: Object) {
|
||||
return state['features/base/devices'].availableDevices.audioOutput.map(
|
||||
({ deviceId, label }) => {
|
||||
return {
|
||||
deviceId,
|
||||
label
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all the camera device ids.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function getVideoDeviceIds(state: Object) {
|
||||
return state['features/base/devices'].availableDevices.videoInput.map(({ deviceId }) => deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there are devices of a specific type.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @param {string} type - The type of device: VideoOutput | audioOutput | audioInput.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function hasAvailableDevices(state: Object, type: string) {
|
||||
return state['features/base/devices'].availableDevices[type].length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set device id of the audio output device which is currently in use.
|
||||
* Empty string stands for default device.
|
||||
|
||||
3
react/features/base/icons/svg/arrow_down.svg
Normal file
3
react/features/base/icons/svg/arrow_down.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.07001 0.248238C8.3471 -0.0596449 8.82132 -0.0846038 9.1292 0.192491C9.43709 0.469585 9.46205 0.943802 9.18495 1.25168L5.65622 5.19348C5.35829 5.52451 4.83922 5.52451 4.54128 5.19348L1.06752 1.25168C0.79043 0.943802 0.81539 0.469585 1.12327 0.192491C1.43115 -0.0846038 1.90537 -0.0596449 2.18247 0.248238L5.09875 3.57062L8.07001 0.248238Z" fill="#5E6D7A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 509 B |
3
react/features/base/icons/svg/exclamation-solid.svg
Normal file
3
react/features/base/icons/svg/exclamation-solid.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 14.6667C4.3181 14.6667 1.33333 11.682 1.33333 8.00007C1.33333 4.31817 4.3181 1.3334 8 1.3334C11.6819 1.3334 14.6667 4.31817 14.6667 8.00007C14.6667 11.682 11.6819 14.6667 8 14.6667ZM7.33333 4.66676C7.33333 4.29857 7.6318 4.00009 8 4.00009C8.36819 4.00009 8.66666 4.29857 8.66666 4.66676V8.00009C8.66666 8.36828 8.36819 8.66676 8 8.66676C7.6318 8.66676 7.33333 8.36828 7.33333 8.00009V4.66676ZM8 10.0001C7.63181 10.0001 7.33333 10.2985 7.33333 10.6667C7.33333 11.0349 7.63181 11.3334 8 11.3334C8.36818 11.3334 8.66666 11.0349 8.66666 10.6667C8.66666 10.2985 8.36818 10.0001 8 10.0001Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 731 B |
3
react/features/base/icons/svg/exclamation.svg
Normal file
3
react/features/base/icons/svg/exclamation.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.33331 8.00004C1.33331 11.6819 4.31808 14.6667 7.99998 14.6667C11.6819 14.6667 14.6666 11.6819 14.6666 8.00004C14.6666 4.31814 11.6819 1.33337 7.99998 1.33337C4.31808 1.33337 1.33331 4.31814 1.33331 8.00004ZM13.3333 8.00005C13.3333 10.9456 10.9455 13.3334 7.99998 13.3334C5.05446 13.3334 2.66665 10.9456 2.66665 8.00005C2.66665 5.05453 5.05446 2.66672 7.99998 2.66672C10.9455 2.66672 13.3333 5.05453 13.3333 8.00005ZM7.33331 4.66673C7.33331 4.29854 7.63179 4.00006 7.99998 4.00006C8.36817 4.00006 8.66665 4.29854 8.66665 4.66673V8.00006C8.66665 8.36825 8.36817 8.66673 7.99998 8.66673C7.63179 8.66673 7.33331 8.36825 7.33331 8.00006V4.66673ZM7.99998 10C7.63179 10 7.33331 10.2985 7.33331 10.6667C7.33331 11.0349 7.63179 11.3334 7.99998 11.3334C8.36817 11.3334 8.66665 11.0349 8.66665 10.6667C8.66665 10.2985 8.36817 10 7.99998 10Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1001 B |
@@ -3,6 +3,7 @@
|
||||
export { default as IconAdd } from './add.svg';
|
||||
export { default as IconAddPeople } from './link.svg';
|
||||
export { default as IconArrowBack } from './arrow_back.svg';
|
||||
export { default as IconArrowDown } from './arrow_down.svg';
|
||||
export { default as IconAudioOnly } from './visibility.svg';
|
||||
export { default as IconAudioOnlyOff } from './visibility-off.svg';
|
||||
export { default as IconAudioRoute } from './volume.svg';
|
||||
@@ -27,6 +28,8 @@ export { default as IconDominantSpeaker } from './dominant-speaker.svg';
|
||||
export { default as IconDownload } from './download.svg';
|
||||
export { default as IconDragHandle } from './drag-handle.svg';
|
||||
export { default as IconEventNote } from './event_note.svg';
|
||||
export { default as IconExclamation } from './exclamation.svg';
|
||||
export { default as IconExclamationSolid } from './exclamation-solid.svg';
|
||||
export { default as IconExitFullScreen } from './exit-full-screen.svg';
|
||||
export { default as IconFeedback } from './feedback.svg';
|
||||
export { default as IconFullScreen } from './full-screen.svg';
|
||||
@@ -41,8 +44,10 @@ export { default as IconMenuDown } from './menu-down.svg';
|
||||
export { default as IconMenuThumb } from './thumb-menu.svg';
|
||||
export { default as IconMenuUp } from './menu-up.svg';
|
||||
export { default as IconMessage } from './message.svg';
|
||||
export { default as IconMeter } from './meter.svg';
|
||||
export { default as IconMicDisabled } from './mic-disabled.svg';
|
||||
export { default as IconMicrophone } from './microphone.svg';
|
||||
export { default as IconMicrophoneEmpty } from './microphone-empty.svg';
|
||||
export { default as IconModerator } from './star.svg';
|
||||
export { default as IconMuteEveryone } from './mute-everyone.svg';
|
||||
export { default as IconMuteEveryoneElse } from './mute-everyone-else.svg';
|
||||
@@ -76,3 +81,4 @@ export { default as IconVideoQualityHD } from './HD.svg';
|
||||
export { default as IconVideoQualityLD } from './LD.svg';
|
||||
export { default as IconVideoQualitySD } from './SD.svg';
|
||||
export { default as IconVolume } from './volume.svg';
|
||||
export { default as IconVolumeEmpty } from './volume-empty.svg';
|
||||
|
||||
10
react/features/base/icons/svg/meter.svg
Normal file
10
react/features/base/icons/svg/meter.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="38" height="12" viewBox="0 0 38 12" fill="#5E6D7A" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="3" height="12" rx="1"/>
|
||||
<rect x="5" width="3" height="12" rx="1" />
|
||||
<rect x="10" width="3" height="12" rx="1" />
|
||||
<rect x="15" width="3" height="12" rx="1" />
|
||||
<rect x="20" width="3" height="12" rx="1" />
|
||||
<rect x="25" width="3" height="12" rx="1" />
|
||||
<rect x="30" width="3" height="12" rx="1" />
|
||||
<rect x="35" width="3" height="12" rx="1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 457 B |
3
react/features/base/icons/svg/microphone-empty.svg
Normal file
3
react/features/base/icons/svg/microphone-empty.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 6C16 3.79086 14.2091 2 12 2C9.79086 2 8 3.79086 8 6V12C8 13.8666 9.27853 15.4346 11.0076 15.8759C11.0026 15.9166 11 15.958 11 16V17.917C8.16229 17.441 6 14.973 6 12C6 11.4477 5.55228 11 5 11C4.44772 11 4 11.4477 4 12C4 16.0796 7.05369 19.446 11 19.9381V21C11 21.5523 11.4477 22 12 22C12.5523 22 13 21.5523 13 21V19.9381C16.9463 19.446 20 16.0796 20 12C20 11.4477 19.5523 11 19 11C18.4477 11 18 11.4477 18 12C18 14.973 15.8377 17.441 13 17.917V16C13 15.958 12.9974 15.9166 12.9924 15.8759C14.7215 15.4346 16 13.8666 16 12V6ZM12 4C10.8954 4 10 4.89543 10 6V12C10 13.1046 10.8954 14 12 14C13.1046 14 14 13.1046 14 12V6C14 4.89543 13.1046 4 12 4Z" fill="#A4B8D1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 817 B |
3
react/features/base/icons/svg/volume-empty.svg
Normal file
3
react/features/base/icons/svg/volume-empty.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1799 3.68341L6 8H3C2.44772 8 2 8.44771 2 9V15C2 15.5523 2.44772 16 3 16H6L11.1799 20.3166C11.2698 20.3915 11.383 20.4325 11.5 20.4325C11.7761 20.4325 12 20.2086 12 19.9325V4.06752C12 3.95055 11.959 3.83728 11.8841 3.74743C11.7073 3.53529 11.392 3.50662 11.1799 3.68341ZM4 10H6.7241L10 7.27008V16.7299L6.7241 14H4V10ZM14 8C16.2091 8 18 9.79086 18 12C18 14.2091 16.2091 16 14 16V14C15.1046 14 16 13.1046 16 12C16 10.8954 15.1046 10 14 10V8ZM14 4C18.4183 4 22 7.58172 22 12C22 16.4183 18.4183 20 14 20V18C17.3137 18 20 15.3137 20 12C20 8.68629 17.3137 6 14 6V4Z" fill="#A4B8D1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 733 B |
6
react/features/base/modal/actionTypes.js
Normal file
6
react/features/base/modal/actionTypes.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* Action type to set the ID of the active modal (or undefined if needs to be hidden).
|
||||
*/
|
||||
export const SET_ACTIVE_MODAL_ID = 'SET_ACTIVE_MODAL_ID';
|
||||
19
react/features/base/modal/actions.js
Normal file
19
react/features/base/modal/actions.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// @flow
|
||||
|
||||
import { SET_ACTIVE_MODAL_ID } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Action to set the ID of the active modal (or undefined if needs to be hidden).
|
||||
*
|
||||
* @param {string} activeModalId - The new modal ID or undefined.
|
||||
* @returns {{
|
||||
* activeModalId: string,
|
||||
* type: SET_ACTIVE_MODAL_ID
|
||||
* }}
|
||||
*/
|
||||
export function setActiveModalId(activeModalId: ?string) {
|
||||
return {
|
||||
activeModalId,
|
||||
type: SET_ACTIVE_MODAL_ID
|
||||
};
|
||||
}
|
||||
151
react/features/base/modal/components/JitsiModal.js
Normal file
151
react/features/base/modal/components/JitsiModal.js
Normal file
@@ -0,0 +1,151 @@
|
||||
// @flow
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import { SafeAreaView, View } from 'react-native';
|
||||
|
||||
import { ColorSchemeRegistry } from '../../color-scheme';
|
||||
import { HeaderWithNavigation, SlidingView } from '../../react';
|
||||
import { connect } from '../../redux';
|
||||
import { StyleType } from '../../styles';
|
||||
|
||||
import { setActiveModalId } from '../actions';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The color schemed style of the common header component.
|
||||
*/
|
||||
_headerStyles: StyleType,
|
||||
|
||||
/**
|
||||
* True if the modal should be shown, false otherwise.
|
||||
*/
|
||||
_show: boolean,
|
||||
|
||||
/**
|
||||
* The color schemed style of the modal.
|
||||
*/
|
||||
_styles: StyleType,
|
||||
|
||||
/**
|
||||
* The children component(s) of the Modal, to be rendered.
|
||||
*/
|
||||
children: React$Node,
|
||||
|
||||
/**
|
||||
* The Redux Dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The i18n label key of the header title.
|
||||
*/
|
||||
headerLabelKey: string,
|
||||
|
||||
/**
|
||||
* The ID of the modal that is being rendered. This is used to show/hide the modal.
|
||||
*/
|
||||
modalId: string,
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the modal closes.
|
||||
*/
|
||||
onClose?: Function,
|
||||
|
||||
/**
|
||||
* The position from where the modal should be opened. This is derived from the
|
||||
* props of the {@code SlidingView} with the same name.
|
||||
*/
|
||||
position?: string
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a custom Jitsi Modal that doesn't use the built in native
|
||||
* Modal component of React Native.
|
||||
*/
|
||||
class JitsiModal extends PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
position: 'bottom'
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiates a new component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onRequestClose = this._onRequestClose.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code PureComponent#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { _headerStyles, _show, _styles, children, headerLabelKey, position } = this.props;
|
||||
|
||||
return (
|
||||
<SlidingView
|
||||
onHide = { this._onRequestClose }
|
||||
position = { position }
|
||||
show = { _show }>
|
||||
<View
|
||||
style = { [
|
||||
_headerStyles.page,
|
||||
_styles.page
|
||||
] }>
|
||||
<HeaderWithNavigation
|
||||
headerLabelKey = { headerLabelKey }
|
||||
onPressBack = { this._onRequestClose } />
|
||||
<SafeAreaView style = { styles.safeArea }>
|
||||
{ children }
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</SlidingView>
|
||||
);
|
||||
}
|
||||
|
||||
_onRequestClose: () => boolean;
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the SlidingView requests closing.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onRequestClose() {
|
||||
const { _show, dispatch, onClose } = this.props;
|
||||
|
||||
if (_show) {
|
||||
if (typeof onClose === 'function') {
|
||||
onClose();
|
||||
}
|
||||
dispatch(setActiveModalId());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redix state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Props} ownProps - The own props of the component.
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps): $Shape<Props> {
|
||||
return {
|
||||
_headerStyles: ColorSchemeRegistry.get(state, 'Header'),
|
||||
_show: state['features/base/modal'].activeModalId === ownProps.modalId,
|
||||
_styles: ColorSchemeRegistry.get(state, 'Modal')
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(JitsiModal);
|
||||
3
react/features/base/modal/components/index.native.js
Normal file
3
react/features/base/modal/components/index.native.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export { default as JitsiModal } from './JitsiModal';
|
||||
6
react/features/base/modal/components/index.web.js
Normal file
6
react/features/base/modal/components/index.web.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { Component } from 'react';
|
||||
|
||||
export const JitsiModal = Component;
|
||||
|
||||
15
react/features/base/modal/components/styles.js
Normal file
15
react/features/base/modal/components/styles.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
|
||||
import { ColorSchemeRegistry, schemeColor } from '../../color-scheme';
|
||||
|
||||
export default {
|
||||
safeArea: {
|
||||
flex: 1
|
||||
}
|
||||
};
|
||||
|
||||
ColorSchemeRegistry.register('Modal', {
|
||||
page: {
|
||||
backgroundColor: schemeColor('background')
|
||||
}
|
||||
});
|
||||
7
react/features/base/modal/index.js
Normal file
7
react/features/base/modal/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import './reducer';
|
||||
|
||||
export * from './actions';
|
||||
export * from './actionTypes';
|
||||
export * from './components';
|
||||
17
react/features/base/modal/reducer.js
Normal file
17
react/features/base/modal/reducer.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// @flow
|
||||
|
||||
import { ReducerRegistry } from '../redux';
|
||||
|
||||
import { SET_ACTIVE_MODAL_ID } from './actionTypes';
|
||||
|
||||
ReducerRegistry.register('features/base/modal', (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case SET_ACTIVE_MODAL_ID:
|
||||
return {
|
||||
...state,
|
||||
activeModalId: action.activeModalId
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
@@ -2,6 +2,36 @@
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
/**
|
||||
* Returns the deviceId for the currently used camera.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function getCurrentCameraDeviceId(state: Object) {
|
||||
return state['features/base/settings'].cameraDeviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deviceId for the currently used microphone.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function getCurrentMicDeviceId(state: Object) {
|
||||
return state['features/base/settings'].micDeviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deviceId for the currently used speaker.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function getCurrentOutputDeviceId(state: Object) {
|
||||
return state['features/base/settings'].audioOutputDeviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles changes to the `disableCallIntegration` setting.
|
||||
* Noop on web.
|
||||
|
||||
@@ -230,13 +230,14 @@ export default class AbstractButton<P: Props, S: *> extends Component<P, S> {
|
||||
|
||||
/**
|
||||
* Helper function to be implemented by subclasses, which must return a
|
||||
* {@code boolean} value indicating if this button is toggled or not.
|
||||
* {@code boolean} value indicating if this button is toggled or not or
|
||||
* undefined if the button is not toggleable.
|
||||
*
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
* @returns {?boolean}
|
||||
*/
|
||||
_isToggled() {
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
_onClick: (*) => void;
|
||||
|
||||
137
react/features/base/toolbox/components/ToolboxButtonWithIcon.js
Normal file
137
react/features/base/toolbox/components/ToolboxButtonWithIcon.js
Normal file
@@ -0,0 +1,137 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Icon } from '../../icons';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The decorated component (ToolboxButton).
|
||||
*/
|
||||
children: React$Node,
|
||||
|
||||
/**
|
||||
* Icon of the button.
|
||||
*/
|
||||
icon: Function,
|
||||
|
||||
/**
|
||||
* Flag used for disabling the small icon.
|
||||
*/
|
||||
iconDisabled: boolean,
|
||||
|
||||
/**
|
||||
* Click handler for the small icon.
|
||||
*/
|
||||
onIconClick: Function,
|
||||
|
||||
/**
|
||||
* Additional styles.
|
||||
*/
|
||||
styles?: Object,
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* Whether the button is hovered or not.
|
||||
*/
|
||||
isHovered: boolean,
|
||||
};
|
||||
|
||||
/**
|
||||
* Displayes the `ToolboxButtonWithIcon` component.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
export default class ToolboxButtonWithIcon extends Component<Props, State> {
|
||||
|
||||
/**
|
||||
* Initializes a new {@code ToolboxButtonWithIcon} instance.
|
||||
*
|
||||
* @param {Props} props - The props of the component.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isHovered: false
|
||||
};
|
||||
this._onMouseEnter = this._onMouseEnter.bind(this);
|
||||
this._onMouseLeave = this._onMouseLeave.bind(this);
|
||||
}
|
||||
|
||||
_onMouseEnter: () => void;
|
||||
|
||||
/**
|
||||
* Handler for when the small button has the mouse over.
|
||||
*
|
||||
* @returns {void}.
|
||||
*/
|
||||
_onMouseEnter() {
|
||||
this.setState({
|
||||
isHovered: true
|
||||
});
|
||||
}
|
||||
|
||||
_onMouseLeave: () => void;
|
||||
|
||||
/**
|
||||
* Handler for when the mouse leaves the small button.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMouseLeave() {
|
||||
this.setState({
|
||||
isHovered: false
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
icon,
|
||||
iconDisabled,
|
||||
onIconClick,
|
||||
styles
|
||||
} = this.props;
|
||||
|
||||
const iconProps = {};
|
||||
let size = 9;
|
||||
|
||||
if (iconDisabled) {
|
||||
iconProps.className
|
||||
= 'settings-button-small-icon settings-button-small-icon--disabled';
|
||||
} else {
|
||||
iconProps.className = 'settings-button-small-icon';
|
||||
iconProps.onClick = onIconClick;
|
||||
|
||||
if (this.state.isHovered) {
|
||||
iconProps.className = `${iconProps.className} settings-button-small-icon--hovered`;
|
||||
size = 11;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-button-container'
|
||||
styles = { styles }>
|
||||
{children}
|
||||
<div
|
||||
onMouseEnter = { this._onMouseEnter }
|
||||
onMouseLeave = { this._onMouseLeave }>
|
||||
<Icon
|
||||
{ ...iconProps }
|
||||
size = { size }
|
||||
src = { icon } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,69 @@ import type { Props } from './AbstractToolboxItem';
|
||||
* Web implementation of {@code AbstractToolboxItem}.
|
||||
*/
|
||||
export default class ToolboxItem extends AbstractToolboxItem<Props> {
|
||||
/**
|
||||
* Initializes a new {@code ToolboxItem} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onKeyDown = this._onKeyDown.bind(this);
|
||||
this._onKeyUp = this._onKeyUp.bind(this);
|
||||
}
|
||||
|
||||
_onKeyDown: (Object) => void;
|
||||
|
||||
/**
|
||||
* Handles 'Enter' key on the button to trigger onClick for accessibility.
|
||||
*
|
||||
* @param {Object} event - The key event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyDown(event) {
|
||||
// If the event coming to the dialog has been subject to preventDefault
|
||||
// we don't handle it here.
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.onClick();
|
||||
} else if (event.key === ' ') {
|
||||
// Space triggers button onKeyUp but we need to prevent PTT here
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyUp: (Object) => void;
|
||||
|
||||
/**
|
||||
* Handles ' ' (Space) key on the button to trigger onClick for
|
||||
* accessibility.
|
||||
*
|
||||
* @param {Object} event - The key event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyUp(event) {
|
||||
// If the event coming to the dialog has been subject to preventDefault
|
||||
// we don't handle it here.
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === ' ') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles rendering of the actual item. If the label is being shown, which
|
||||
* is controlled with the `showLabel` prop, the item is rendered for its
|
||||
@@ -27,14 +90,22 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
|
||||
elementAfter,
|
||||
onClick,
|
||||
showLabel,
|
||||
tooltipPosition
|
||||
tooltipPosition,
|
||||
toggled
|
||||
} = this.props;
|
||||
const className = showLabel ? 'overflow-menu-item' : 'toolbox-button';
|
||||
const props = {
|
||||
'aria-pressed': toggled,
|
||||
'aria-disabled': disabled,
|
||||
'aria-label': this.accessibilityLabel,
|
||||
className: className + (disabled ? ' disabled' : ''),
|
||||
onClick: disabled ? undefined : onClick
|
||||
onClick: disabled ? undefined : onClick,
|
||||
onKeyDown: this._onKeyDown,
|
||||
onKeyUp: this._onKeyUp,
|
||||
tabIndex: 0,
|
||||
role: 'button'
|
||||
};
|
||||
|
||||
const elementType = showLabel ? 'li' : 'div';
|
||||
const useTooltip = this.tooltip && this.tooltip.length > 0;
|
||||
let children = (
|
||||
|
||||
@@ -7,3 +7,4 @@ export { default as AbstractHangupButton } from './AbstractHangupButton';
|
||||
export { default as AbstractVideoMuteButton } from './AbstractVideoMuteButton';
|
||||
export { default as BetaTag } from './BetaTag';
|
||||
export { default as OverflowMenuItem } from './OverflowMenuItem';
|
||||
export { default as ToolboxButtonWithIcon } from './ToolboxButtonWithIcon';
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { KeyboardAvoidingView, SafeAreaView } from 'react-native';
|
||||
import { KeyboardAvoidingView } from 'react-native';
|
||||
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { HeaderWithNavigation, SlidingView } from '../../../base/react';
|
||||
import { JitsiModal } from '../../../base/modal';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { StyleType } from '../../../base/styles';
|
||||
|
||||
import { CHAT_VIEW_MODAL_ID } from '../../constants';
|
||||
|
||||
import AbstractChat, {
|
||||
_mapDispatchToProps,
|
||||
_mapStateToProps as _abstractMapStateToProps,
|
||||
type Props as AbstractProps
|
||||
_mapStateToProps,
|
||||
type Props
|
||||
} from '../AbstractChat';
|
||||
|
||||
import ChatInputBar from './ChatInputBar';
|
||||
@@ -20,88 +20,31 @@ import MessageContainer from './MessageContainer';
|
||||
import MessageRecipient from './MessageRecipient';
|
||||
import styles from './styles';
|
||||
|
||||
type Props = AbstractProps & {
|
||||
|
||||
/**
|
||||
* The color-schemed stylesheet of the feature.
|
||||
*/
|
||||
_styles: StyleType
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React native component that renders the chat window (modal) of
|
||||
* the mobile client.
|
||||
*/
|
||||
class Chat extends AbstractChat<Props> {
|
||||
/**
|
||||
* Instantiates a new instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onClose = this._onClose.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { _styles } = this.props;
|
||||
|
||||
return (
|
||||
<SlidingView
|
||||
onHide = { this._onClose }
|
||||
position = 'bottom'
|
||||
show = { this.props._isOpen } >
|
||||
<JitsiModal
|
||||
headerLabelKey = 'chat.title'
|
||||
modalId = { CHAT_VIEW_MODAL_ID }>
|
||||
<KeyboardAvoidingView
|
||||
behavior = 'padding'
|
||||
style = { styles.chatContainer }>
|
||||
<HeaderWithNavigation
|
||||
headerLabelKey = 'chat.title'
|
||||
onPressBack = { this._onClose } />
|
||||
<SafeAreaView style = { _styles.backdrop }>
|
||||
<MessageContainer messages = { this.props._messages } />
|
||||
<MessageRecipient />
|
||||
<ChatInputBar onSend = { this.props._onSendMessage } />
|
||||
</SafeAreaView>
|
||||
<MessageContainer messages = { this.props._messages } />
|
||||
<MessageRecipient />
|
||||
<ChatInputBar onSend = { this.props._onSendMessage } />
|
||||
</KeyboardAvoidingView>
|
||||
</SlidingView>
|
||||
</JitsiModal>
|
||||
);
|
||||
}
|
||||
|
||||
_onClose: () => boolean
|
||||
|
||||
/**
|
||||
* Closes the chat window.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onClose() {
|
||||
if (this.props._isOpen) {
|
||||
this.props._onToggleChat();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
..._abstractMapStateToProps(state),
|
||||
_styles: ColorSchemeRegistry.get(state, 'Chat')
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(Chat));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { IconChat, IconChatUnread } from '../../../base/icons';
|
||||
import { setActiveModalId } from '../../../base/modal';
|
||||
import { getLocalParticipant } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import {
|
||||
@@ -9,7 +10,7 @@ import {
|
||||
} from '../../../base/toolbox';
|
||||
import { openDisplayNamePrompt } from '../../../display-name';
|
||||
|
||||
import { toggleChat } from '../../actions';
|
||||
import { CHAT_VIEW_MODAL_ID } from '../../constants';
|
||||
import { getUnreadCount } from '../../functions';
|
||||
|
||||
type Props = AbstractButtonProps & {
|
||||
@@ -93,7 +94,7 @@ function _mapDispatchToProps(dispatch: Function) {
|
||||
* @returns {void}
|
||||
*/
|
||||
_displayChat() {
|
||||
dispatch(toggleChat());
|
||||
dispatch(setActiveModalId(CHAT_VIEW_MODAL_ID));
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,6 @@ import { TextInput, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Icon, IconChatSend } from '../../../base/icons';
|
||||
import { Platform } from '../../../base/react';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
@@ -136,7 +135,7 @@ class ChatInputBar extends Component<Props, State> {
|
||||
*/
|
||||
_onFocused(focused) {
|
||||
return () => {
|
||||
Platform.OS === 'android' && this.setState({
|
||||
this.setState({
|
||||
addPadding: focused
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// @flow
|
||||
|
||||
export const CHAT_VIEW_MODAL_ID = 'chatView';
|
||||
|
||||
/**
|
||||
* The audio ID of the audio element for which the {@link playAudio} action is
|
||||
* triggered when new chat message is received.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { SET_ACTIVE_MODAL_ID } from '../base/modal';
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import {
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
SET_PRIVATE_MESSAGE_RECIPIENT,
|
||||
TOGGLE_CHAT
|
||||
} from './actionTypes';
|
||||
import { CHAT_VIEW_MODAL_ID } from './constants';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
isOpen: false,
|
||||
@@ -56,6 +58,12 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
||||
messages: []
|
||||
};
|
||||
|
||||
case SET_ACTIVE_MODAL_ID:
|
||||
if (action.activeModalId === CHAT_VIEW_MODAL_ID) {
|
||||
return updateChatState(state);
|
||||
}
|
||||
|
||||
break;
|
||||
case SET_PRIVATE_MESSAGE_RECIPIENT:
|
||||
return {
|
||||
...state,
|
||||
@@ -64,14 +72,24 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
||||
};
|
||||
|
||||
case TOGGLE_CHAT:
|
||||
return {
|
||||
...state,
|
||||
isOpen: !state.isOpen,
|
||||
lastReadMessage: state.messages[
|
||||
navigator.product === 'ReactNative' ? 0 : state.messages.length - 1],
|
||||
privateMessageRecipient: state.isOpen ? undefined : state.privateMessageRecipient
|
||||
};
|
||||
return updateChatState(state);
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the chat status on opening the chat view.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function updateChatState(state) {
|
||||
return {
|
||||
...state,
|
||||
isOpen: !state.isOpen,
|
||||
lastReadMessage: state.messages[
|
||||
navigator.product === 'ReactNative' ? 0 : state.messages.length - 1],
|
||||
privateMessageRecipient: state.isOpen ? undefined : state.privateMessageRecipient
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout';
|
||||
import { connect, disconnect } from '../../../base/connection';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { connect as reactReduxConnect } from '../../../base/redux';
|
||||
import { getBackendSafeRoomName } from '../../../base/util';
|
||||
import { Chat } from '../../../chat';
|
||||
import { Filmstrip } from '../../../filmstrip';
|
||||
import { CalleeInfoContainer } from '../../../invite';
|
||||
@@ -78,6 +79,11 @@ type Props = AbstractProps & {
|
||||
*/
|
||||
_layoutClassName: string,
|
||||
|
||||
/**
|
||||
* Name for this conference room.
|
||||
*/
|
||||
_roomName: string,
|
||||
|
||||
dispatch: Function,
|
||||
t: Function
|
||||
}
|
||||
@@ -120,7 +126,7 @@ class Conference extends AbstractConference<Props, *> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
document.title = interfaceConfig.APP_NAME;
|
||||
document.title = `${this.props._roomName} | ${interfaceConfig.APP_NAME}`;
|
||||
this._start();
|
||||
}
|
||||
|
||||
@@ -259,11 +265,13 @@ class Conference extends AbstractConference<Props, *> {
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const currentLayout = getCurrentLayout(state);
|
||||
const roomName = getBackendSafeRoomName(state['features/base/conference'].room);
|
||||
|
||||
return {
|
||||
...abstractMapStateToProps(state),
|
||||
_iAmRecorder: state['features/base/config'].iAmRecorder,
|
||||
_layoutClassName: LAYOUT_CLASSNAMES[currentLayout]
|
||||
_layoutClassName: LAYOUT_CLASSNAMES[currentLayout],
|
||||
_roomName: roomName
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,11 @@ type Props = {
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The state of the audio screen share checkbox.
|
||||
*/
|
||||
screenShareAudio: boolean,
|
||||
|
||||
/**
|
||||
* The currently higlighted DesktopCapturerSource.
|
||||
*/
|
||||
@@ -128,6 +133,7 @@ class DesktopPicker extends PureComponent<Props, State> {
|
||||
_poller = null;
|
||||
|
||||
state = {
|
||||
screenShareAudio: false,
|
||||
selectedSource: {},
|
||||
selectedTab: 0,
|
||||
sources: {},
|
||||
@@ -153,6 +159,7 @@ class DesktopPicker extends PureComponent<Props, State> {
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onCloseModal = this._onCloseModal.bind(this);
|
||||
this._onPreviewClick = this._onPreviewClick.bind(this);
|
||||
this._onShareAudioChecked = this._onShareAudioChecked.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._onTabSelected = this._onTabSelected.bind(this);
|
||||
this._updateSources = this._updateSources.bind(this);
|
||||
@@ -241,7 +248,7 @@ class DesktopPicker extends PureComponent<Props, State> {
|
||||
return selectedSource;
|
||||
}
|
||||
|
||||
_onCloseModal: (?string, string) => void;
|
||||
_onCloseModal: (?string, string, ?boolean) => void;
|
||||
|
||||
/**
|
||||
* Dispatches an action to hide the DesktopPicker and invokes the passed in
|
||||
@@ -251,10 +258,12 @@ class DesktopPicker extends PureComponent<Props, State> {
|
||||
* the onSourceChoose callback.
|
||||
* @param {string} type - The type of the DesktopCapturerSource to pass into
|
||||
* the onSourceChoose callback.
|
||||
* @param {boolean} screenShareAudio - Whether or not to add system audio to
|
||||
* screen sharing session.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCloseModal(id = '', type) {
|
||||
this.props.onSourceChoose(id, type);
|
||||
_onCloseModal(id = '', type, screenShareAudio = false) {
|
||||
this.props.onSourceChoose(id, type, screenShareAudio);
|
||||
this.props.dispatch(hideDialog());
|
||||
}
|
||||
|
||||
@@ -285,9 +294,9 @@ class DesktopPicker extends PureComponent<Props, State> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmit() {
|
||||
const { id, type } = this.state.selectedSource;
|
||||
const { selectedSource: { id, type }, screenShareAudio } = this.state;
|
||||
|
||||
this._onCloseModal(id, type);
|
||||
this._onCloseModal(id, type, screenShareAudio);
|
||||
}
|
||||
|
||||
_onTabSelected: () => void;
|
||||
@@ -306,12 +315,29 @@ class DesktopPicker extends PureComponent<Props, State> {
|
||||
const { types, sources } = this.state;
|
||||
|
||||
this._selectedTabType = types[tabIndex];
|
||||
|
||||
// When we change tabs also reset the screenShareAudio state so we don't
|
||||
// use the option from one tab when sharing from another.
|
||||
this.setState({
|
||||
screenShareAudio: false,
|
||||
selectedSource: this._getSelectedSource(sources),
|
||||
selectedTab: tabIndex
|
||||
});
|
||||
}
|
||||
|
||||
_onShareAudioChecked: (boolean) => void;
|
||||
|
||||
/**
|
||||
* Set the screenSharingAudio state indicating whether or not to also share
|
||||
* system audio.
|
||||
*
|
||||
* @param {boolean} checked - Share audio or not.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onShareAudioChecked(checked) {
|
||||
this.setState({ screenShareAudio: checked });
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures and renders the tabs for display.
|
||||
*
|
||||
@@ -328,7 +354,8 @@ class DesktopPicker extends PureComponent<Props, State> {
|
||||
content: <DesktopPickerPane
|
||||
key = { type }
|
||||
onClick = { this._onPreviewClick }
|
||||
onDoubleClick = { this._onCloseModal }
|
||||
onDoubleClick = { this._onSubmit }
|
||||
onShareAudioChecked = { this._onShareAudioChecked }
|
||||
selectedSourceId = { selectedSource.id }
|
||||
sources = { sources[type] }
|
||||
type = { type } />,
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
/* @flow */
|
||||
|
||||
import { Checkbox } from '@atlaskit/checkbox';
|
||||
import Spinner from '@atlaskit/spinner';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Platform } from '../../base/react';
|
||||
import { translate } from '../../base/i18n';
|
||||
|
||||
import DesktopSourcePreview from './DesktopSourcePreview';
|
||||
|
||||
/**
|
||||
@@ -20,6 +24,11 @@ type Props = {
|
||||
*/
|
||||
onDoubleClick: Function,
|
||||
|
||||
/**
|
||||
* The handler to be invoked if the users checks the audio screen sharing checkbox.
|
||||
*/
|
||||
onShareAudioChecked: Function,
|
||||
|
||||
/**
|
||||
* The id of the DesktopCapturerSource that is currently selected.
|
||||
*/
|
||||
@@ -33,7 +42,12 @@ type Props = {
|
||||
/**
|
||||
* The source type of the DesktopCapturerSources to display.
|
||||
*/
|
||||
type: string
|
||||
type: string,
|
||||
|
||||
/**
|
||||
* Used to obtain translations.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -42,6 +56,31 @@ type Props = {
|
||||
* @extends Component
|
||||
*/
|
||||
class DesktopPickerPane extends Component<Props> {
|
||||
|
||||
/**
|
||||
* Initializes a new DesktopPickerPane instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onShareAudioCheck = this._onShareAudioCheck.bind(this);
|
||||
}
|
||||
|
||||
_onShareAudioCheck: (Object) => void;
|
||||
|
||||
/**
|
||||
* Function to be called when the Checkbox is used.
|
||||
*
|
||||
* @param {boolean} checked - Checkbox status (checked or not).
|
||||
* @returns {void}
|
||||
*/
|
||||
_onShareAudioCheck({ target: { checked } }) {
|
||||
this.props.onShareAudioChecked(checked);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
@@ -54,7 +93,8 @@ class DesktopPickerPane extends Component<Props> {
|
||||
onDoubleClick,
|
||||
selectedSourceId,
|
||||
sources,
|
||||
type
|
||||
type,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
const classNames
|
||||
@@ -77,12 +117,25 @@ class DesktopPickerPane extends Component<Props> {
|
||||
</div>
|
||||
);
|
||||
|
||||
let checkBox;
|
||||
|
||||
// Only display the share audio checkbox if we're on windows and on
|
||||
// desktop sharing tab.
|
||||
// App window and Mac OS screen sharing doesn't work with system audio.
|
||||
if (type === 'screen' && Platform.OS === 'windows') {
|
||||
checkBox = (<Checkbox
|
||||
label = { t('dialog.screenSharingAudio') }
|
||||
name = 'share-system-audio'
|
||||
onChange = { this._onShareAudioCheck } />);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { classNames }>
|
||||
{ previews }
|
||||
{ checkBox }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DesktopPickerPane;
|
||||
export default translate(DesktopPickerPane);
|
||||
|
||||
54
react/features/help/components/HelpView.js
Normal file
54
react/features/help/components/HelpView.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// @flow
|
||||
|
||||
import React, { PureComponent } from 'react';
|
||||
import WebView from 'react-native-webview';
|
||||
|
||||
import { JitsiModal } from '../../base/modal';
|
||||
import { connect } from '../../base/redux';
|
||||
|
||||
import { HELP_VIEW_MODAL_ID } from '../constants';
|
||||
|
||||
const DEFAULT_HELP_CENTRE_URL = 'https://web-cdn.jitsi.net/faq/meet-faq.html';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The URL to display in the Help Centre.
|
||||
*/
|
||||
_url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a page that renders the help content for the app.
|
||||
*/
|
||||
class HelpView extends PureComponent<Props> {
|
||||
/**
|
||||
* Implements {@code PureComponent#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<JitsiModal
|
||||
headerLabelKey = 'helpView.header'
|
||||
modalId = { HELP_VIEW_MODAL_ID }>
|
||||
<WebView source = {{ uri: this.props._url }} />
|
||||
</JitsiModal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_url: state['features/base/config'].helpCentreURL || DEFAULT_HELP_CENTRE_URL
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(HelpView);
|
||||
3
react/features/help/components/index.js
Normal file
3
react/features/help/components/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export { default as HelpView } from './HelpView';
|
||||
3
react/features/help/constants.js
Normal file
3
react/features/help/constants.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export const HELP_VIEW_MODAL_ID = 'helpView';
|
||||
4
react/features/help/index.js
Normal file
4
react/features/help/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// @flow
|
||||
|
||||
export * from './components';
|
||||
export * from './constants';
|
||||
@@ -67,6 +67,11 @@ type Props = AbstractProps & {
|
||||
|
||||
type State = AbstractState & {
|
||||
|
||||
/**
|
||||
* Boolean to show if an extra padding needs to be added to the bottom bar.
|
||||
*/
|
||||
bottomPadding: boolean,
|
||||
|
||||
/**
|
||||
* State variable to keep track of the search field value.
|
||||
*/
|
||||
@@ -94,6 +99,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
defaultState = {
|
||||
addToCallError: false,
|
||||
addToCallInProgress: false,
|
||||
bottomPadding: false,
|
||||
fieldValue: '',
|
||||
inviteItems: [],
|
||||
searchInprogress: false,
|
||||
@@ -194,16 +200,17 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
</View>
|
||||
<TextInput
|
||||
autoCorrect = { false }
|
||||
autoFocus = { true }
|
||||
clearButtonMode = 'always' // iOS only
|
||||
autoFocus = { false }
|
||||
onBlur = { this._onFocused(false) }
|
||||
onChangeText = { this._onTypeQuery }
|
||||
onFocus = { this._onFocused(true) }
|
||||
placeholder = {
|
||||
this.props.t(`inviteDialog.${placeholderKey}`)
|
||||
}
|
||||
ref = { this._setFieldRef }
|
||||
style = { styles.searchField }
|
||||
value = { this.state.fieldValue } />
|
||||
{ this._renderAndroidClearButton() }
|
||||
{ this._renderClearButton() }
|
||||
</View>
|
||||
{ Boolean(inviteItems.length) && <View style = { styles.invitedList }>
|
||||
<FlatList
|
||||
@@ -223,7 +230,11 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
renderItem = { this._renderItem } />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
<SafeAreaView style = { [ styles.bottomBar, _headerStyles.headerOverlay ] }>
|
||||
<SafeAreaView
|
||||
style = { [
|
||||
styles.bottomBar,
|
||||
_headerStyles.headerOverlay,
|
||||
this.state.bottomPadding ? styles.extraBarPadding : null ] }>
|
||||
{ this._renderShareMeetingButton() }
|
||||
</SafeAreaView>
|
||||
</KeyboardAvoidingView>
|
||||
@@ -317,6 +328,22 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
return false;
|
||||
}
|
||||
|
||||
_onFocused: boolean => Function;
|
||||
|
||||
/**
|
||||
* Constructs a callback to be used to update the padding of the field if necessary.
|
||||
*
|
||||
* @param {boolean} focused - True of the field is focused.
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onFocused(focused) {
|
||||
return () => {
|
||||
Platform.OS === 'android' && this.setState({
|
||||
bottomPadding: focused
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
_onInvite: () => void
|
||||
|
||||
/**
|
||||
@@ -433,14 +460,12 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
_query: (string) => Promise<Array<Object>>;
|
||||
|
||||
/**
|
||||
* Renders a button to clear the text field on Android.
|
||||
*
|
||||
* NOTE: For the best platform experience we use the native solution on iOS.
|
||||
* Renders a button to clear the text field.
|
||||
*
|
||||
* @returns {React#Element<*>}
|
||||
*/
|
||||
_renderAndroidClearButton() {
|
||||
if (Platform.OS !== 'android' || !this.state.fieldValue.length) {
|
||||
_renderClearButton() {
|
||||
if (!this.state.fieldValue.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,13 @@ export default {
|
||||
justifyContent: 'flex-start'
|
||||
},
|
||||
|
||||
/**
|
||||
* A special padding to avoid issues on some devices (such as Android devices with custom suggestions bar).
|
||||
*/
|
||||
extraBarPadding: {
|
||||
paddingBottom: 30
|
||||
},
|
||||
|
||||
invitedList: {
|
||||
padding: 3
|
||||
},
|
||||
|
||||
@@ -130,7 +130,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
// but we want to indicate those in case of sip gateway
|
||||
const {
|
||||
iAmRecorder,
|
||||
iAmSipGateway
|
||||
iAmSipGateway,
|
||||
disableRecordAudioNotification
|
||||
} = getState()['features/base/config'];
|
||||
|
||||
if (iAmRecorder && !iAmSipGateway) {
|
||||
@@ -153,6 +154,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
const initiatorName = initiator && getParticipantDisplayName(getState, initiator.getId());
|
||||
|
||||
initiatorName && dispatch(showStartedRecordingNotification(mode, initiatorName));
|
||||
sendAnalytics(createRecordingEvent('start', mode));
|
||||
|
||||
if (disableRecordAudioNotification) {
|
||||
break;
|
||||
}
|
||||
|
||||
let soundID;
|
||||
|
||||
@@ -163,7 +169,6 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
}
|
||||
|
||||
if (soundID) {
|
||||
sendAnalytics(createRecordingEvent('start', mode));
|
||||
dispatch(playSound(soundID));
|
||||
}
|
||||
} else if (updatedSessionData.status === OFF
|
||||
@@ -176,6 +181,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
duration
|
||||
= (Date.now() / 1000) - oldSessionData.timestamp;
|
||||
}
|
||||
sendAnalytics(createRecordingEvent('stop', mode, duration));
|
||||
|
||||
if (disableRecordAudioNotification) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (mode === JitsiRecordingConstants.mode.FILE) {
|
||||
soundOff = RECORDING_OFF_SOUND_ID;
|
||||
@@ -186,7 +196,6 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
}
|
||||
|
||||
if (soundOff && soundOn) {
|
||||
sendAnalytics(createRecordingEvent('stop', mode, duration));
|
||||
dispatch(stopSound(soundOn));
|
||||
dispatch(playSound(soundOff));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// The type of (redux) action which sets the visibility of the audio settings popup.
|
||||
export const SET_AUDIO_SETTINGS_VISIBILITY = 'SET_AUDIO_SETTINGS_VISIBILITY';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the visibility of the view/UI rendering
|
||||
* the app's settings.
|
||||
@@ -8,3 +11,6 @@
|
||||
* }
|
||||
*/
|
||||
export const SET_SETTINGS_VIEW_VISIBLE = 'SET_SETTINGS_VIEW_VISIBLE';
|
||||
|
||||
// The type of (redux) action which sets the visibility of the video settings popup.
|
||||
export const SET_VIDEO_SETTINGS_VISIBILITY = 'SET_VIDEO_SETTINGS_VISIBILITY';
|
||||
|
||||
@@ -4,7 +4,11 @@ import { setFollowMe, setStartMutedPolicy } from '../base/conference';
|
||||
import { openDialog } from '../base/dialog';
|
||||
import { i18next } from '../base/i18n';
|
||||
|
||||
import { SET_SETTINGS_VIEW_VISIBLE } from './actionTypes';
|
||||
import {
|
||||
SET_AUDIO_SETTINGS_VISIBILITY,
|
||||
SET_SETTINGS_VIEW_VISIBLE,
|
||||
SET_VIDEO_SETTINGS_VISIBILITY
|
||||
} from './actionTypes';
|
||||
import { SettingsDialog } from './components';
|
||||
import { getMoreTabProps, getProfileTabProps } from './functions';
|
||||
|
||||
@@ -38,6 +42,31 @@ export function openSettingsDialog(defaultTab: string) {
|
||||
return openDialog(SettingsDialog, { defaultTab });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visiblity of the audio settings.
|
||||
*
|
||||
* @param {boolean} value - The new value.
|
||||
* @returns {Function}
|
||||
*/
|
||||
function setAudioSettingsVisibility(value: boolean) {
|
||||
return {
|
||||
type: SET_AUDIO_SETTINGS_VISIBILITY,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the visiblity of the video settings.
|
||||
*
|
||||
* @param {boolean} value - The new value.
|
||||
* @returns {Function}
|
||||
*/
|
||||
function setVideoSettingsVisibility(value: boolean) {
|
||||
return {
|
||||
type: SET_VIDEO_SETTINGS_VISIBILITY,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the settings from the "More" tab of the settings dialog.
|
||||
@@ -84,3 +113,29 @@ export function submitProfileTab(newState: Object): Function {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visiblity of the audio settings.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function toggleAudioSettings() {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const value = getState()['features/settings'].audioSettingsVisible;
|
||||
|
||||
dispatch(setAudioSettingsVisibility(!value));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visiblity of the video settings.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function toggleVideoSettings() {
|
||||
return (dispatch: Function, getState: Function) => {
|
||||
const value = getState()['features/settings'].videoSettingsVisible;
|
||||
|
||||
dispatch(setVideoSettingsVisibility(!value));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import AudioSettingsHeader from './AudioSettingsHeader';
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { IconMicrophoneEmpty, IconVolumeEmpty } from '../../../../base/icons';
|
||||
import { createLocalAudioTrack } from '../../../functions';
|
||||
import MicrophoneEntry from './MicrophoneEntry';
|
||||
import SpeakerEntry from './SpeakerEntry';
|
||||
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The deviceId of the microphone in use.
|
||||
*/
|
||||
currentMicDeviceId: string,
|
||||
|
||||
/**
|
||||
* The deviceId of the output device in use.
|
||||
*/
|
||||
currentOutputDeviceId: string,
|
||||
|
||||
/**
|
||||
* Used to set a new microphone as the current one.
|
||||
*/
|
||||
setAudioInputDevice: Function,
|
||||
|
||||
/**
|
||||
* Used to set a new output device as the current one.
|
||||
*/
|
||||
setAudioOutputDevice: Function,
|
||||
|
||||
/**
|
||||
* A list of objects containing the labels and deviceIds
|
||||
* of all the output devices.
|
||||
*/
|
||||
outputDevices: Object[],
|
||||
|
||||
/**
|
||||
* A list with objects containing the labels and deviceIds
|
||||
* of all the input devices.
|
||||
*/
|
||||
microphoneDevices: Object[],
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* An object containing the jitsiTrack and the error (if the case)
|
||||
* for the microphone that is in use.
|
||||
*/
|
||||
currentMicData: Object
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displayes a list of all
|
||||
* the audio input & output devices to choose from.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class AudioSettingsContent extends Component<Props, State> {
|
||||
_componentWasUnmounted: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AudioSettingsContent} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onMicrophoneEntryClick = this._onMicrophoneEntryClick.bind(this);
|
||||
this._onSpeakerEntryClick = this._onSpeakerEntryClick.bind(this);
|
||||
|
||||
this.state = {
|
||||
currentMicData: {
|
||||
error: false,
|
||||
jitsiTrack: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_onMicrophoneEntryClick: (string) => void;
|
||||
|
||||
/**
|
||||
* Click handler for the microphone entries.
|
||||
*
|
||||
* @param {string} deviceId - The deviceId for the clicked microphone.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMicrophoneEntryClick(deviceId) {
|
||||
this.props.setAudioInputDevice(deviceId);
|
||||
}
|
||||
|
||||
_onSpeakerEntryClick: (string) => void;
|
||||
|
||||
/**
|
||||
* Click handler for the speaker entries.
|
||||
*
|
||||
* @param {string} deviceId - The deviceId for the clicked speaker.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSpeakerEntryClick(deviceId) {
|
||||
this.props.setAudioOutputDevice(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a single microphone entry.
|
||||
*
|
||||
* @param {Object} data - An object with the deviceId and label of the microphone.
|
||||
* @param {number} index - The index of the element, used for creating a key.
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderMicrophoneEntry(data, index) {
|
||||
const { deviceId, label } = data;
|
||||
const key = `me-${index}`;
|
||||
const isSelected = deviceId === this.props.currentMicDeviceId;
|
||||
let jitsiTrack = null;
|
||||
let hasError = false;
|
||||
|
||||
if (isSelected) {
|
||||
({ jitsiTrack, hasError } = this.state.currentMicData);
|
||||
}
|
||||
|
||||
return (
|
||||
<MicrophoneEntry
|
||||
deviceId = { deviceId }
|
||||
hasError = { hasError }
|
||||
isSelected = { isSelected }
|
||||
jitsiTrack = { jitsiTrack }
|
||||
key = { key }
|
||||
onClick = { this._onMicrophoneEntryClick }>
|
||||
{label}
|
||||
</MicrophoneEntry>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a single speaker entry.
|
||||
*
|
||||
* @param {Object} data - An object with the deviceId and label of the speaker.
|
||||
* @param {number} index - The index of the element, used for creating a key.
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderSpeakerEntry(data, index) {
|
||||
const { deviceId, label } = data;
|
||||
const key = `se-${index}`;
|
||||
|
||||
return (
|
||||
<SpeakerEntry
|
||||
deviceId = { deviceId }
|
||||
isSelected = { deviceId === this.props.currentOutputDeviceId }
|
||||
key = { key }
|
||||
onClick = { this._onSpeakerEntryClick }>
|
||||
{label}
|
||||
</SpeakerEntry>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the audio track for a given micData object.
|
||||
*
|
||||
* @param {Object} micData - The object holding the track.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
_disposeTrack(micData) {
|
||||
const { jitsiTrack } = micData;
|
||||
|
||||
return jitsiTrack ? jitsiTrack.dispose() : Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current microphone data.
|
||||
* Disposes previously created track and creates a new one.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
async _updateCurrentMicData() {
|
||||
await this._disposeTrack(this.state.currentMicData);
|
||||
|
||||
const currentMicData = await createLocalAudioTrack(
|
||||
this.props.currentMicDeviceId,
|
||||
);
|
||||
|
||||
// In case the component gets unmounted before the track is created
|
||||
// avoid a leak by not setting the state
|
||||
if (this._componentWasUnmounted) {
|
||||
this._disposeTrack(currentMicData);
|
||||
} else {
|
||||
this.setState({
|
||||
currentMicData
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidUpdate}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.currentMicDeviceId !== this.props.currentMicDeviceId) {
|
||||
this._updateCurrentMicData();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._updateCurrentMicData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillUnmount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._componentWasUnmounted = true;
|
||||
this._disposeTrack(this.state.currentMicData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { microphoneDevices, outputDevices, t } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className = 'audio-preview-content'>
|
||||
<AudioSettingsHeader
|
||||
IconComponent = { IconMicrophoneEmpty }
|
||||
text = { t('settings.microphones') } />
|
||||
{microphoneDevices.map((data, i) =>
|
||||
this._renderMicrophoneEntry(data, i),
|
||||
)}
|
||||
<AudioSettingsHeader
|
||||
IconComponent = { IconVolumeEmpty }
|
||||
text = { t('settings.speakers') } />
|
||||
{outputDevices.map((data, i) =>
|
||||
this._renderSpeakerEntry(data, i),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(AudioSettingsContent);
|
||||
@@ -0,0 +1,53 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Icon, IconCheck, IconExclamationSolid } from '../../../../base/icons';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AudioSettingsEntry}.
|
||||
*/
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The text for this component.
|
||||
*/
|
||||
children: React$Node,
|
||||
|
||||
/**
|
||||
* Flag indicating an error.
|
||||
*/
|
||||
hasError?: boolean,
|
||||
|
||||
/**
|
||||
* Flag indicating the selection state.
|
||||
*/
|
||||
isSelected: boolean,
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} representing an entry for the audio settings.
|
||||
*
|
||||
* @returns { ReactElement}
|
||||
*/
|
||||
export default function AudioSettingsEntry({ children, hasError, isSelected }: Props) {
|
||||
const className = `audio-preview-entry ${isSelected
|
||||
? 'audio-preview-entry--selected' : ''}`;
|
||||
|
||||
return (
|
||||
<div className = { className }>
|
||||
{isSelected && (
|
||||
<Icon
|
||||
className = 'audio-preview-icon audio-preview-icon--check'
|
||||
color = '#1C2025'
|
||||
size = { 14 }
|
||||
src = { IconCheck } />
|
||||
)}
|
||||
<span className = 'audio-preview-entry-text'>{children}</span>
|
||||
{hasError && <Icon
|
||||
className = 'audio-preview-icon audio-preview-icon--exclamation'
|
||||
size = { 16 }
|
||||
src = { IconExclamationSolid } />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { Icon } from '../../../../base/icons';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link AudioSettingsHeader}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The Icon used for the Header.
|
||||
*/
|
||||
IconComponent: Function,
|
||||
|
||||
/**
|
||||
* The text of the Header.
|
||||
*/
|
||||
text: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} representing the Header of an audio option group.
|
||||
*
|
||||
* @returns { ReactElement}
|
||||
*/
|
||||
export default function AudioSettingsHeader({ IconComponent, text }: Props) {
|
||||
return (
|
||||
<div className = 'audio-preview-header'>
|
||||
<div className = 'audio-preview-header-icon'>
|
||||
{ <Icon
|
||||
color = '#A4B8D1'
|
||||
size = { 24 }
|
||||
src = { IconComponent } />}
|
||||
</div>
|
||||
<div className = 'audio-preview-header-text'>{text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import InlineDialog from '@atlaskit/inline-dialog';
|
||||
|
||||
import AudioSettingsContent, { type Props as AudioSettingsContentProps } from './AudioSettingsContent';
|
||||
import { toggleAudioSettings } from '../../../actions';
|
||||
import {
|
||||
getAudioInputDeviceData,
|
||||
getAudioOutputDeviceData,
|
||||
setAudioInputDevice as setAudioInputDeviceAction,
|
||||
setAudioOutputDevice as setAudioOutputDeviceAction
|
||||
} from '../../../../base/devices';
|
||||
import { connect } from '../../../../base/redux';
|
||||
import { getAudioSettingsVisibility } from '../../../functions';
|
||||
import {
|
||||
getCurrentMicDeviceId,
|
||||
getCurrentOutputDeviceId
|
||||
} from '../../../../base/settings';
|
||||
|
||||
|
||||
type Props = AudioSettingsContentProps & {
|
||||
|
||||
/**
|
||||
* Component's children (the audio button).
|
||||
*/
|
||||
children: React$Node,
|
||||
|
||||
/**
|
||||
* Flag controlling the visibility of the popup.
|
||||
*/
|
||||
isOpen: boolean,
|
||||
|
||||
/**
|
||||
* Callback executed when the popup closes.
|
||||
*/
|
||||
onClose: Function,
|
||||
}
|
||||
|
||||
/**
|
||||
* Popup with audio settings.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function AudioSettingsPopup({
|
||||
children,
|
||||
currentMicDeviceId,
|
||||
currentOutputDeviceId,
|
||||
isOpen,
|
||||
microphoneDevices,
|
||||
setAudioInputDevice,
|
||||
setAudioOutputDevice,
|
||||
onClose,
|
||||
outputDevices
|
||||
}: Props) {
|
||||
return (
|
||||
<div className = 'audio-preview'>
|
||||
<InlineDialog
|
||||
content = { <AudioSettingsContent
|
||||
currentMicDeviceId = { currentMicDeviceId }
|
||||
currentOutputDeviceId = { currentOutputDeviceId }
|
||||
microphoneDevices = { microphoneDevices }
|
||||
outputDevices = { outputDevices }
|
||||
setAudioInputDevice = { setAudioInputDevice }
|
||||
setAudioOutputDevice = { setAudioOutputDevice } /> }
|
||||
isOpen = { isOpen }
|
||||
onClose = { onClose }
|
||||
position = 'top left'>
|
||||
{children}
|
||||
</InlineDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that maps parts of Redux state tree into component props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
currentMicDeviceId: getCurrentMicDeviceId(state),
|
||||
currentOutputDeviceId: getCurrentOutputDeviceId(state),
|
||||
isOpen: getAudioSettingsVisibility(state),
|
||||
microphoneDevices: getAudioInputDeviceData(state),
|
||||
outputDevices: getAudioOutputDeviceData(state)
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onClose: toggleAudioSettings,
|
||||
setAudioInputDevice: setAudioInputDeviceAction,
|
||||
setAudioOutputDevice: setAudioOutputDeviceAction
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AudioSettingsPopup);
|
||||
45
react/features/settings/components/web/audio/Meter.js
Normal file
45
react/features/settings/components/web/audio/Meter.js
Normal file
@@ -0,0 +1,45 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { Icon, IconMeter } from '../../../../base/icons';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Own class name for the component.
|
||||
*/
|
||||
className: string,
|
||||
|
||||
/**
|
||||
* Flag indicating whether the component is greyed out/disabled.
|
||||
*/
|
||||
isDisabled?: boolean,
|
||||
|
||||
/**
|
||||
* The level of the meter.
|
||||
* Should be between 0 and 7 as per the used SVG.
|
||||
*/
|
||||
level: number,
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} representing an audio level meter.
|
||||
*
|
||||
* @returns { ReactElement}
|
||||
*/
|
||||
export default function({ className, isDisabled, level }: Props) {
|
||||
let ownClassName;
|
||||
|
||||
if (level > -1) {
|
||||
ownClassName = `metr metr-l-${level}`;
|
||||
} else {
|
||||
ownClassName = `metr ${isDisabled ? 'metr--disabled' : ''}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Icon
|
||||
className = { `${ownClassName} ${className}` }
|
||||
size = { 12 }
|
||||
src = { IconMeter } />
|
||||
);
|
||||
}
|
||||
172
react/features/settings/components/web/audio/MicrophoneEntry.js
Normal file
172
react/features/settings/components/web/audio/MicrophoneEntry.js
Normal file
@@ -0,0 +1,172 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import AudioSettingsEntry, { type Props as AudioSettingsEntryProps } from './AudioSettingsEntry';
|
||||
import JitsiMeetJS from '../../../../base/lib-jitsi-meet/_';
|
||||
import Meter from './Meter';
|
||||
|
||||
const JitsiTrackEvents = JitsiMeetJS.events.track;
|
||||
|
||||
type Props = AudioSettingsEntryProps & {
|
||||
|
||||
/**
|
||||
* The deviceId of the microphone.
|
||||
*/
|
||||
deviceId: string,
|
||||
|
||||
/**
|
||||
* Flag indicating if there is a problem with the device.
|
||||
*/
|
||||
hasError?: boolean,
|
||||
|
||||
/**
|
||||
* The audio track for the current entry.
|
||||
*/
|
||||
jitsiTrack: Object,
|
||||
|
||||
/**
|
||||
* Click handler for component.
|
||||
*/
|
||||
onClick: Function,
|
||||
}
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* The audio level.
|
||||
*/
|
||||
level: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* React {@code Component} representing an entry for the microphone audio settings.
|
||||
*
|
||||
* @param {Props} props - The props of the component.
|
||||
* @returns { ReactElement}
|
||||
*/
|
||||
export default class MicrophoneEntry extends Component<Props, State> {
|
||||
/**
|
||||
* Initializes a new {@code MicrophoneEntry} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
level: -1
|
||||
};
|
||||
this._onClick = this._onClick.bind(this);
|
||||
this._updateLevel = this._updateLevel.bind(this);
|
||||
}
|
||||
|
||||
_onClick: () => void;
|
||||
|
||||
/**
|
||||
* Click handler for the entry.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
this.props.onClick(this.props.deviceId);
|
||||
}
|
||||
|
||||
_updateLevel: (number) => void;
|
||||
|
||||
/**
|
||||
* Updates the level of the meter.
|
||||
*
|
||||
* @param {number} num - The audio level provided by the jitsiTrack.
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateLevel(num) {
|
||||
this.setState({
|
||||
level: Math.floor(num / 0.125)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to audio level chanages comming from the jitsiTrack.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_startListening() {
|
||||
const { jitsiTrack } = this.props;
|
||||
|
||||
jitsiTrack && jitsiTrack.on(
|
||||
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
|
||||
this._updateLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from chanages comming from the jitsiTrack.
|
||||
*
|
||||
* @param {Object} jitsiTrack - The jitsiTrack to unsubscribe from.
|
||||
* @returns {void}
|
||||
*/
|
||||
_stopListening(jitsiTrack) {
|
||||
jitsiTrack && jitsiTrack.off(
|
||||
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
|
||||
this._updateLevel);
|
||||
this.setState({
|
||||
level: -1
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidUpdate}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.jitsiTrack !== this.props.jitsiTrack) {
|
||||
this._stopListening(prevProps.jitsiTrack);
|
||||
this._startListening();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._startListening();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillUnmount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
compmonentWillUnmount() {
|
||||
this._stopListening(this.props.jitsiTrack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { children, hasError, isSelected } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'audio-preview-microphone'
|
||||
onClick = { this._onClick }>
|
||||
<AudioSettingsEntry
|
||||
hasError = { hasError }
|
||||
isSelected = { isSelected }>
|
||||
{children}
|
||||
</AudioSettingsEntry>
|
||||
<Meter
|
||||
className = 'audio-preview-meter-mic'
|
||||
isDisabled = { hasError }
|
||||
level = { this.state.level } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
119
react/features/settings/components/web/audio/SpeakerEntry.js
Normal file
119
react/features/settings/components/web/audio/SpeakerEntry.js
Normal file
@@ -0,0 +1,119 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import AudioSettingsEntry from './AudioSettingsEntry';
|
||||
import logger from '../../../logger';
|
||||
import TestButton from './TestButton';
|
||||
|
||||
const TEST_SOUND_PATH = 'sounds/ring.wav';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link SpeakerEntry}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The text label for the entry.
|
||||
*/
|
||||
children: React$Node,
|
||||
|
||||
/**
|
||||
* Flag controlling the selection state of the entry.
|
||||
*/
|
||||
isSelected: boolean,
|
||||
|
||||
/**
|
||||
* The deviceId of the speaker.
|
||||
*/
|
||||
deviceId: string,
|
||||
|
||||
/**
|
||||
* Click handler for the component.
|
||||
*/
|
||||
onClick: Function,
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays an audio
|
||||
* output settings entry. The user can click and play a test sound.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
export default class SpeakerEntry extends Component<Props> {
|
||||
/**
|
||||
* A React ref to the HTML element containing the {@code audio} instance.
|
||||
*/
|
||||
audioRef: Object;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code SpeakerEntry} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.audioRef = React.createRef();
|
||||
this._onTestButtonClick = this._onTestButtonClick.bind(this);
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
_onClick: () => void;
|
||||
|
||||
/**
|
||||
* Click handler for the entry.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
this.props.onClick(this.props.deviceId);
|
||||
}
|
||||
|
||||
_onTestButtonClick: Object => void;
|
||||
|
||||
/**
|
||||
* Click handler for Test button.
|
||||
* Sets the current audio output id and plays a sound.
|
||||
*
|
||||
* @param {Object} e - The sythetic event.
|
||||
* @returns {void}
|
||||
*/
|
||||
async _onTestButtonClick(e) {
|
||||
e.stopPropagation();
|
||||
|
||||
try {
|
||||
await this.audioRef.current.setSinkId(this.props.deviceId);
|
||||
this.audioRef.current.play();
|
||||
} catch (err) {
|
||||
logger.log('Could not set sink id', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { children, isSelected, deviceId } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'audio-preview-speaker'
|
||||
onClick = { this._onClick }>
|
||||
<AudioSettingsEntry
|
||||
isSelected = { isSelected }
|
||||
key = { deviceId }>
|
||||
{children}
|
||||
</AudioSettingsEntry>
|
||||
<TestButton onClick = { this._onTestButtonClick } />
|
||||
<audio
|
||||
preload = 'auto'
|
||||
ref = { this.audioRef }
|
||||
src = { TEST_SOUND_PATH } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
26
react/features/settings/components/web/audio/TestButton.js
Normal file
26
react/features/settings/components/web/audio/TestButton.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Click handler for the button.
|
||||
*/
|
||||
onClick: Function,
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} representing an button used for testing output sound.
|
||||
*
|
||||
* @returns { ReactElement}
|
||||
*/
|
||||
export default function TestButton({ onClick }: Props) {
|
||||
return (
|
||||
<div
|
||||
className = 'audio-preview-test-button'
|
||||
onClick = { onClick }>
|
||||
Test
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
export { default as SettingsButton } from './SettingsButton';
|
||||
export { default as SettingsDialog } from './SettingsDialog';
|
||||
export { default as AudioSettingsPopup } from './audio/AudioSettingsPopup';
|
||||
export { default as VideoSettingsPopup } from './video/VideoSettingsPopup';
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { equals } from '../../../../base/redux';
|
||||
import Video from '../../../../base/media/components/Video';
|
||||
import { createLocalVideoTracks } from '../../../functions';
|
||||
|
||||
|
||||
const videoClassName = 'video-preview-video flipVideoX';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link VideoSettingsContent}.
|
||||
*/
|
||||
export type Props = {
|
||||
|
||||
/**
|
||||
* The deviceId of the camera device currently being used.
|
||||
*/
|
||||
currentCameraDeviceId: string,
|
||||
|
||||
/**
|
||||
* Callback invoked to change current camera.
|
||||
*/
|
||||
setVideoInputDevice: Function,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function,
|
||||
|
||||
/**
|
||||
* Callback invoked to toggle the settings popup visibility.
|
||||
*/
|
||||
toggleVideoSettings: Function,
|
||||
|
||||
/**
|
||||
* All the camera device ids currently connected.
|
||||
*/
|
||||
videoDeviceIds: string[],
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link VideoSettingsContent}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* An array of all the jitsiTracks and eventual errors.
|
||||
*/
|
||||
trackData: Object[],
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a list of video
|
||||
* previews to choose from.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class VideoSettingsContent extends Component<Props, State> {
|
||||
_componentWasUnmounted: boolean;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code VideoSettingsContent} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
trackData: new Array(props.videoDeviceIds.length).fill({
|
||||
jitsiTrack: null
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and updates the track data.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
async _setTracks() {
|
||||
this._disposeTracks(this.state.trackData);
|
||||
|
||||
const trackData = await createLocalVideoTracks(
|
||||
this.props.videoDeviceIds,
|
||||
);
|
||||
|
||||
// In case the component gets unmounted before the tracks are created
|
||||
// avoid a leak by not setting the state
|
||||
if (this._componentWasUnmounted) {
|
||||
this._disposeTracks(trackData);
|
||||
} else {
|
||||
this.setState({
|
||||
trackData
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys all the tracks from trackData object.
|
||||
*
|
||||
* @param {Object[]} trackData - An array of tracks that are to be disposed.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
_disposeTracks(trackData) {
|
||||
trackData.forEach(({ jitsiTrack }) => {
|
||||
jitsiTrack && jitsiTrack.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the click handler used when selecting the video preview.
|
||||
*
|
||||
* @param {string} deviceId - The id of the camera device.
|
||||
* @returns {Function}
|
||||
*/
|
||||
_onEntryClick(deviceId) {
|
||||
return () => {
|
||||
this.props.setVideoInputDevice(deviceId);
|
||||
this.props.toggleVideoSettings();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a preview entry.
|
||||
*
|
||||
* @param {Object} data - The track data.
|
||||
* @param {number} index - The index of the entry.
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderPreviewEntry(data, index) {
|
||||
const { error, jitsiTrack, deviceId } = data;
|
||||
const { currentCameraDeviceId, t } = this.props;
|
||||
const isSelected = deviceId === currentCameraDeviceId;
|
||||
const key = `vp-${index}`;
|
||||
const className = 'video-preview-entry';
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div
|
||||
className = { className }
|
||||
key = { key }>
|
||||
<div className = 'video-preview-error'>{t(error)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const props: Object = {
|
||||
className,
|
||||
key
|
||||
};
|
||||
const label = jitsiTrack && jitsiTrack.getTrackLabel();
|
||||
|
||||
if (isSelected) {
|
||||
props.className = `${className} video-preview-entry--selected`;
|
||||
} else {
|
||||
props.onClick = this._onEntryClick(deviceId);
|
||||
}
|
||||
|
||||
return (
|
||||
<div { ...props }>
|
||||
<div className = 'video-preview-label'>{label}</div>
|
||||
<div className = 'video-preview-overlay' />
|
||||
<Video
|
||||
className = { videoClassName }
|
||||
videoTrack = {{ jitsiTrack }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._setTracks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentWillUnmount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._componentWasUnmounted = true;
|
||||
this._disposeTracks(this.state.trackData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidUpdate}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!equals(this.props.videoDeviceIds, prevProps.videoDeviceIds)) {
|
||||
this._setTracks();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { trackData } = this.state;
|
||||
|
||||
return (
|
||||
<div className = 'video-preview'>
|
||||
{trackData.map((data, i) => this._renderPreviewEntry(data, i))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default translate(VideoSettingsContent);
|
||||
@@ -0,0 +1,85 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import InlineDialog from '@atlaskit/inline-dialog';
|
||||
|
||||
import { toggleVideoSettings } from '../../../actions';
|
||||
import {
|
||||
getVideoDeviceIds,
|
||||
setVideoInputDevice as setVideoInputDeviceAction
|
||||
} from '../../../../base/devices';
|
||||
import { getVideoSettingsVisibility } from '../../../functions';
|
||||
import { connect } from '../../../../base/redux';
|
||||
import { getCurrentCameraDeviceId } from '../../../../base/settings';
|
||||
import VideoSettingsContent, { type Props as VideoSettingsProps } from './VideoSettingsContent';
|
||||
|
||||
|
||||
type Props = VideoSettingsProps & {
|
||||
|
||||
/**
|
||||
* Component children (the Video button).
|
||||
*/
|
||||
children: React$Node,
|
||||
|
||||
/**
|
||||
* Flag controlling the visibility of the popup.
|
||||
*/
|
||||
isOpen: boolean,
|
||||
|
||||
/**
|
||||
* Callback executed when the popup closes.
|
||||
*/
|
||||
onClose: Function,
|
||||
}
|
||||
|
||||
/**
|
||||
* Popup with a preview of all the video devices.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function VideoSettingsPopup({
|
||||
currentCameraDeviceId,
|
||||
children,
|
||||
isOpen,
|
||||
onClose,
|
||||
setVideoInputDevice,
|
||||
videoDeviceIds
|
||||
}: Props) {
|
||||
return (
|
||||
<div className = 'video-preview'>
|
||||
<InlineDialog
|
||||
content = { <VideoSettingsContent
|
||||
currentCameraDeviceId = { currentCameraDeviceId }
|
||||
setVideoInputDevice = { setVideoInputDevice }
|
||||
toggleVideoSettings = { onClose }
|
||||
videoDeviceIds = { videoDeviceIds } /> }
|
||||
isOpen = { isOpen }
|
||||
onClose = { onClose }
|
||||
position = 'top right'>
|
||||
{ children }
|
||||
</InlineDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated {@code VideoSettingsPopup}'s
|
||||
* props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
currentCameraDeviceId: getCurrentCameraDeviceId(state),
|
||||
isOpen: getVideoSettingsVisibility(state),
|
||||
videoDeviceIds: getVideoDeviceIds(state)
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onClose: toggleVideoSettings,
|
||||
setVideoInputDevice: setVideoInputDeviceAction
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(VideoSettingsPopup);
|
||||
@@ -2,6 +2,7 @@
|
||||
import { toState } from '../base/redux';
|
||||
import { parseStandardURIString } from '../base/util';
|
||||
import { i18next, DEFAULT_LANGUAGE, LANGUAGES } from '../base/i18n';
|
||||
import { createLocalTrack } from '../base/lib-jitsi-meet/functions';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
isLocalParticipantModerator
|
||||
@@ -130,3 +131,73 @@ export function getProfileTabProps(stateful: Object | Function) {
|
||||
email: localParticipant.email
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise which resolves with a list of objects containing
|
||||
* all the video jitsiTracks and appropriate errors for the given device ids.
|
||||
*
|
||||
* @param {string[]} ids - The list of the camera ids for wich to create tracks.
|
||||
*
|
||||
* @returns {Promise<Object[]>}
|
||||
*/
|
||||
export function createLocalVideoTracks(ids: string[]) {
|
||||
return Promise.all(ids.map(deviceId => createLocalTrack('video', deviceId)
|
||||
.then(jitsiTrack => {
|
||||
return {
|
||||
jitsiTrack,
|
||||
deviceId
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
return {
|
||||
jitsiTrack: null,
|
||||
deviceId,
|
||||
error: 'deviceSelection.previewUnavailable'
|
||||
};
|
||||
})));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a promise which resolves with an object containing the corresponding
|
||||
* the audio jitsiTrack/error.
|
||||
*
|
||||
* @param {string} deviceId - The deviceId for the current microphone.
|
||||
*
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export function createLocalAudioTrack(deviceId: string) {
|
||||
return createLocalTrack('audio', deviceId)
|
||||
.then(jitsiTrack => {
|
||||
return {
|
||||
hasError: false,
|
||||
jitsiTrack
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
return {
|
||||
hasError: true,
|
||||
jitsiTrack: null
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the visibility state of the audio settings.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getAudioSettingsVisibility(state: Object) {
|
||||
return state['features/settings'].audioSettingsVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the visibility state of the video settings.
|
||||
*
|
||||
* @param {Object} state - The state of the application.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getVideoSettingsVisibility(state: Object) {
|
||||
return state['features/settings'].videoSettingsVisible;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import { SET_SETTINGS_VIEW_VISIBLE } from './actionTypes';
|
||||
import {
|
||||
SET_AUDIO_SETTINGS_VISIBILITY,
|
||||
SET_SETTINGS_VIEW_VISIBLE,
|
||||
SET_VIDEO_SETTINGS_VISIBILITY
|
||||
} from './actionTypes';
|
||||
|
||||
ReducerRegistry.register('features/settings', (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
@@ -11,6 +15,16 @@ ReducerRegistry.register('features/settings', (state = {}, action) => {
|
||||
...state,
|
||||
visible: action.visible
|
||||
};
|
||||
case SET_AUDIO_SETTINGS_VISIBILITY:
|
||||
return {
|
||||
...state,
|
||||
audioSettingsVisible: action.value
|
||||
};
|
||||
case SET_VIDEO_SETTINGS_VISIBILITY:
|
||||
return {
|
||||
...state,
|
||||
videoSettingsVisible: action.value
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
|
||||
127
react/features/toolbox/components/web/AudioSettingsButton.js
Normal file
127
react/features/toolbox/components/web/AudioSettingsButton.js
Normal file
@@ -0,0 +1,127 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import AudioMuteButton from '../AudioMuteButton';
|
||||
import { hasAvailableDevices } from '../../../base/devices';
|
||||
import { IconArrowDown } from '../../../base/icons';
|
||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
||||
import { ToolboxButtonWithIcon } from '../../../base/toolbox';
|
||||
import { connect } from '../../../base/redux';
|
||||
|
||||
import { AudioSettingsPopup, toggleAudioSettings } from '../../../settings';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Click handler for the small icon. Opens audio options.
|
||||
*/
|
||||
onAudioOptionsClick: Function,
|
||||
|
||||
/**
|
||||
* If the user has audio input or audio output devices.
|
||||
*/
|
||||
hasDevices: boolean,
|
||||
|
||||
/**
|
||||
* Flag controlling the visibility of the button.
|
||||
*/
|
||||
visible: boolean,
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* If there are permissions for audio devices.
|
||||
*/
|
||||
hasPermissions: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* Button used for audio & audio settings.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
class AudioSettingsButton extends Component<Props, State> {
|
||||
/**
|
||||
* Initializes a new {@code AudioSettingsButton} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hasPermissions: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates device permissions.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async _updatePermissions() {
|
||||
const hasPermissions = await JitsiMeetJS.mediaDevices.isDevicePermissionGranted(
|
||||
'audio',
|
||||
);
|
||||
|
||||
this.setState({
|
||||
hasPermissions
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._updatePermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { hasDevices, onAudioOptionsClick, visible } = this.props;
|
||||
const settingsDisabled = !this.state.hasPermissions || !hasDevices;
|
||||
|
||||
return visible ? (
|
||||
<AudioSettingsPopup>
|
||||
<ToolboxButtonWithIcon
|
||||
icon = { IconArrowDown }
|
||||
iconDisabled = { settingsDisabled }
|
||||
onIconClick = { onAudioOptionsClick }>
|
||||
<AudioMuteButton />
|
||||
</ToolboxButtonWithIcon>
|
||||
</AudioSettingsPopup>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that maps parts of Redux state tree into component props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
hasDevices:
|
||||
hasAvailableDevices(state, 'audioInput')
|
||||
|| hasAvailableDevices(state, 'audioOutput')
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onAudioOptionsClick: toggleAudioSettings
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(AudioSettingsButton);
|
||||
@@ -41,6 +41,68 @@ class ToolbarButton extends AbstractToolbarButton<Props> {
|
||||
tooltipPosition: 'top'
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code ToolbarButton} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onKeyDown = this._onKeyDown.bind(this);
|
||||
this._onKeyUp = this._onKeyUp.bind(this);
|
||||
}
|
||||
|
||||
_onKeyDown: (Object) => void;
|
||||
|
||||
/**
|
||||
* Handles 'Enter' key on the button to trigger onClick for accessibility.
|
||||
*
|
||||
* @param {Object} event - The key event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyDown(event) {
|
||||
// If the event coming to the dialog has been subject to preventDefault
|
||||
// we don't handle it here.
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.onClick();
|
||||
} else if (event.key === ' ') {
|
||||
// Space triggers button onKeyUp but we need to prevent default here
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyUp: (Object) => void;
|
||||
|
||||
/**
|
||||
* Handles ' '(Space) key on the button to trigger onClick for
|
||||
* accessibility.
|
||||
*
|
||||
* @param {Object} event - The key event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyUp(event) {
|
||||
// If the event coming to the dialog has been subject to preventDefault
|
||||
// we don't handle it here.
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === ' ') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the button of this {@code ToolbarButton}.
|
||||
*
|
||||
@@ -53,8 +115,13 @@ class ToolbarButton extends AbstractToolbarButton<Props> {
|
||||
return (
|
||||
<div
|
||||
aria-label = { this.props.accessibilityLabel }
|
||||
aria-pressed = { this.props.toggled }
|
||||
className = 'toolbox-button'
|
||||
onClick = { this.props.onClick }>
|
||||
onClick = { this.props.onClick }
|
||||
onKeyDown = { this._onKeyDown }
|
||||
onKeyUp = { this._onKeyUp }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
{ this.props.tooltip
|
||||
? <Tooltip
|
||||
content = { this.props.tooltip }
|
||||
|
||||
@@ -71,7 +71,7 @@ import {
|
||||
setOverflowMenuVisible,
|
||||
setToolbarHovered
|
||||
} from '../../actions';
|
||||
import AudioMuteButton from '../AudioMuteButton';
|
||||
import AudioSettingsButton from './AudioSettingsButton';
|
||||
import DownloadButton from '../DownloadButton';
|
||||
import { isToolboxVisible } from '../../functions';
|
||||
import HangupButton from '../HangupButton';
|
||||
@@ -80,7 +80,7 @@ import OverflowMenuButton from './OverflowMenuButton';
|
||||
import OverflowMenuProfileItem from './OverflowMenuProfileItem';
|
||||
import MuteEveryoneButton from './MuteEveryoneButton';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
import VideoMuteButton from '../VideoMuteButton';
|
||||
import VideoSettingsButton from './VideoSettingsButton';
|
||||
import {
|
||||
ClosedCaptionButton
|
||||
} from '../../../subtitles';
|
||||
@@ -1116,6 +1116,32 @@ class Toolbox extends Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the Audio controlling button.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderAudioButton() {
|
||||
return this._shouldShowButton('microphone')
|
||||
? <AudioSettingsButton
|
||||
key = 'asb'
|
||||
visible = { true } />
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the Video controlling button.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderVideoButton() {
|
||||
return this._shouldShowButton('camera')
|
||||
? <VideoSettingsButton
|
||||
key = 'vsb'
|
||||
visible = { true } />
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the toolbox content.
|
||||
*
|
||||
@@ -1234,12 +1260,10 @@ class Toolbox extends Component<Props, State> {
|
||||
}
|
||||
</div>
|
||||
<div className = 'button-group-center'>
|
||||
<AudioMuteButton
|
||||
visible = { this._shouldShowButton('microphone') } />
|
||||
{ this._renderAudioButton() }
|
||||
<HangupButton
|
||||
visible = { this._shouldShowButton('hangup') } />
|
||||
<VideoMuteButton
|
||||
visible = { this._shouldShowButton('camera') } />
|
||||
{ this._renderVideoButton() }
|
||||
</div>
|
||||
<div className = 'button-group-right'>
|
||||
{ buttonsRight.indexOf('localrecording') !== -1
|
||||
@@ -1303,6 +1327,7 @@ function _mapStateToProps(state) {
|
||||
let { desktopSharingEnabled } = state['features/base/conference'];
|
||||
const {
|
||||
callStatsID,
|
||||
enableFeaturesBasedOnToken,
|
||||
iAmRecorder
|
||||
} = state['features/base/config'];
|
||||
const sharedVideoStatus = state['features/shared-video'].status;
|
||||
@@ -1318,7 +1343,7 @@ function _mapStateToProps(state) {
|
||||
|
||||
let desktopSharingDisabledTooltipKey;
|
||||
|
||||
if (state['features/base/config'].enableFeaturesBasedOnToken) {
|
||||
if (enableFeaturesBasedOnToken) {
|
||||
// we enable desktop sharing if any participant already have this
|
||||
// feature enabled
|
||||
desktopSharingEnabled = getParticipants(state)
|
||||
|
||||
124
react/features/toolbox/components/web/VideoSettingsButton.js
Normal file
124
react/features/toolbox/components/web/VideoSettingsButton.js
Normal file
@@ -0,0 +1,124 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { toggleVideoSettings, VideoSettingsPopup } from '../../../settings';
|
||||
import VideoMuteButton from '../VideoMuteButton';
|
||||
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
|
||||
import { hasAvailableDevices } from '../../../base/devices';
|
||||
import { IconArrowDown } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { ToolboxButtonWithIcon } from '../../../base/toolbox';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Click handler for the small icon. Opens video options.
|
||||
*/
|
||||
onVideoOptionsClick: Function,
|
||||
|
||||
/**
|
||||
* If the user has any video devices.
|
||||
*/
|
||||
hasDevices: boolean,
|
||||
|
||||
/**
|
||||
* Flag controlling the visibility of the button.
|
||||
*/
|
||||
visible: boolean,
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* Whether the app has video permissions or not.
|
||||
*/
|
||||
hasPermissions: boolean,
|
||||
};
|
||||
|
||||
/**
|
||||
* Button used for video & video settings.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
class VideoSettingsButton extends Component<Props, State> {
|
||||
/**
|
||||
* Initializes a new {@code VideoSettingsButton} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
hasPermissions: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates device permissions.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async _updatePermissions() {
|
||||
const hasPermissions = await JitsiMeetJS.mediaDevices.isDevicePermissionGranted(
|
||||
'video',
|
||||
);
|
||||
|
||||
this.setState({
|
||||
hasPermissions
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._updatePermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { hasDevices, onVideoOptionsClick, visible } = this.props;
|
||||
const iconDisabled = !this.state.hasPermissions || !hasDevices;
|
||||
|
||||
return visible ? (
|
||||
<VideoSettingsPopup>
|
||||
<ToolboxButtonWithIcon
|
||||
icon = { IconArrowDown }
|
||||
iconDisabled = { iconDisabled }
|
||||
onIconClick = { onVideoOptionsClick }>
|
||||
<VideoMuteButton />
|
||||
</ToolboxButtonWithIcon>
|
||||
</VideoSettingsPopup>
|
||||
) : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that maps parts of Redux state tree into component props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
hasDevices: hasAvailableDevices(state, 'videoInput')
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onVideoOptionsClick: toggleVideoSettings
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(VideoSettingsButton);
|
||||
@@ -40,6 +40,8 @@ export function isToolboxVisible(state: Object) {
|
||||
timeoutID,
|
||||
visible
|
||||
} = state['features/toolbox'];
|
||||
const { audioSettingsVisible, videoSettingsVisible } = state['features/settings'];
|
||||
|
||||
return Boolean(!iAmSipGateway && (timeoutID || visible || alwaysVisible));
|
||||
return Boolean(!iAmSipGateway && (timeoutID || visible || alwaysVisible
|
||||
|| audioSettingsVisible || videoSettingsVisible));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import React, { Component } from 'react';
|
||||
import { SafeAreaView, ScrollView, Text } from 'react-native';
|
||||
|
||||
import { Avatar } from '../../base/avatar';
|
||||
import { IconInfo, IconSettings } from '../../base/icons';
|
||||
import { IconInfo, IconSettings, IconHelp } from '../../base/icons';
|
||||
import { setActiveModalId } from '../../base/modal';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantDisplayName
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
SlidingView
|
||||
} from '../../base/react';
|
||||
import { connect } from '../../base/redux';
|
||||
import { HELP_VIEW_MODAL_ID } from '../../help';
|
||||
import { setSettingsViewVisible } from '../../settings';
|
||||
|
||||
import { setSideBarVisible } from '../actions';
|
||||
@@ -25,11 +27,6 @@ import styles, { SIDEBAR_AVATAR_SIZE } from './styles';
|
||||
*/
|
||||
const PRIVACY_URL = 'https://jitsi.org/meet/privacy';
|
||||
|
||||
/**
|
||||
* The URL at which the user may send feedback.
|
||||
*/
|
||||
const SEND_FEEDBACK_URL = 'mailto:support@jitsi.org';
|
||||
|
||||
/**
|
||||
* The URL at which the terms (of service/use) are available to the user.
|
||||
*/
|
||||
@@ -72,6 +69,7 @@ class WelcomePageSideBar extends Component<Props> {
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onHideSideBar = this._onHideSideBar.bind(this);
|
||||
this._onOpenHelpPage = this._onOpenHelpPage.bind(this);
|
||||
this._onOpenSettings = this._onOpenSettings.bind(this);
|
||||
}
|
||||
|
||||
@@ -112,9 +110,9 @@ class WelcomePageSideBar extends Component<Props> {
|
||||
label = 'welcomepage.privacy'
|
||||
url = { PRIVACY_URL } />
|
||||
<SideBarItem
|
||||
icon = { IconInfo }
|
||||
label = 'welcomepage.sendFeedback'
|
||||
url = { SEND_FEEDBACK_URL } />
|
||||
icon = { IconHelp }
|
||||
label = 'welcomepage.getHelp'
|
||||
onPress = { this._onOpenHelpPage } />
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</SlidingView>
|
||||
@@ -133,6 +131,20 @@ class WelcomePageSideBar extends Component<Props> {
|
||||
this.props.dispatch(setSideBarVisible(false));
|
||||
}
|
||||
|
||||
_onOpenHelpPage: () => void;
|
||||
|
||||
/**
|
||||
* Shows the {@link HelpView}.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOpenHelpPage() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(setSideBarVisible(false));
|
||||
dispatch(setActiveModalId(HELP_VIEW_MODAL_ID));
|
||||
}
|
||||
|
||||
_onOpenSettings: () => void;
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,6 +29,9 @@ if [ ! -f certbot-auto ] ; then
|
||||
fi
|
||||
|
||||
CRON_FILE="/etc/cron.weekly/letsencrypt-renew"
|
||||
if [ ! -d "/etc/cron.weekly" ] ; then
|
||||
mkdir "/etc/cron.weekly"
|
||||
fi
|
||||
echo "#!/bin/bash" > $CRON_FILE
|
||||
echo "/usr/local/sbin/certbot-auto renew >> /var/log/le-renew.log" >> $CRON_FILE
|
||||
|
||||
|
||||
Reference in New Issue
Block a user