Compare commits

..

23 Commits

Author SHA1 Message Date
Mihai Uscat
02fb37189b fix(welcome): Add extra variables 2019-10-22 13:24:44 -07:00
Jaya Allamsetty
8fe2536996 Update LJM for taking the changes for capScreenshareBitrate 2019-10-22 13:28:14 -05:00
Paweł Domas
4b9e156c5d Generic iOS .ipa build script (#4775) 2019-10-22 12:45:28 -05:00
Bettenbuk Zoltan
9265e1ffec ui: web chat facelift 2019-10-22 13:16:00 +02:00
Bettenbuk Zoltan
d11735b04c feat: make the hangup button first 2019-10-21 19:00:12 +02:00
Saúl Ibarra Corretgé
d33b700477 rn,blank-page: refactor BlankPage
- Remove network-activity "feature"
    - It wasn't in use
    - It relied on internal React Native components, bound to break anytime
- Show an infinite loading indicator
- Style it just like the LoadConfigOverlay
    - Since it kinda represents the opposite, an "unload" then SDK is done
2019-10-21 11:17:56 +02:00
Saúl Ibarra Corretgé
97d75c2cb9 android: fix SDK release script for new dependency syntax
Skip the first character, since it's now like ^123456.0.0
2019-10-21 11:12:26 +02:00
Saúl Ibarra Corretgé
287115f4c3 deps: react-native-webrtc@latest
Fixes a crash on iOS when disposing streams.
2019-10-18 14:05:18 +02:00
Saúl Ibarra Corretgé
63a221212b ios: add ability to manually toggle WebRTC logging 2019-10-18 14:05:18 +02:00
Bettenbuk Zoltan
6a916fd0e1 fix: fix filmstrip-only toolbar 2019-10-18 13:16:15 +02:00
yanas
220691d61d Merge pull request #4762 from jitsi/fix-etherpad-follow-me
Fixes showing etherpad in follow-me mode.
2019-10-17 13:44:10 +01:00
damencho
5cafc4bcbd Fixes showing etherpad in follow-me mode. 2019-10-17 13:39:01 +01:00
yanas
b3a78dc2e6 Merge pull request #4761 from zbettenbuk/aot-icons
fix: fix and refactor AoT css
2019-10-17 12:34:13 +01:00
Bettenbuk Zoltan
bebc6eabe5 fix: fix and refactor AoT css 2019-10-17 12:15:29 +02:00
paweldomas
26dc6a4ac2 update logger and LJM to support log timestamps 2019-10-16 15:59:58 -05:00
Hristo Terezov
ff2626723a fix(HelpButton): Improvements. 2019-10-16 13:34:43 -07:00
Mihai Uscat
72bb897269 feat(DownloadOverflowButton): Implement. 2019-10-16 11:30:06 -07:00
damencho
f46387a226 Adds room name validation logic for web. 2019-10-16 17:52:24 +01:00
damencho
a4cbbccb2a Fixes loading recent lists on wrong meeting name stored.
decodeURIComponent is not needed any more and after adding a validation such meeting name should not happen to be stored.
2019-10-16 17:52:24 +01:00
damencho
3e1a008399 Adds copy icon next to the meeting url in info dialog. 2019-10-16 17:52:24 +01:00
Bettenbuk Zoltan
7e70a8c1de feat: make mobile chat messages selectable 2019-10-16 16:05:10 +02:00
Hristo Terezov
8efee04a10 feat(package.json): Node 12 support. 2019-10-16 06:34:44 -07:00
Bettenbuk Zoltan
a35099f949 feat: add chat color scheming 2019-10-16 10:38:28 +02:00
64 changed files with 2177 additions and 2427 deletions

View File

@@ -10,7 +10,7 @@ MVN_HTTP=0
DEFAULT_SDK_VERSION=$(grep sdkVersion ${THIS_DIR}/../gradle.properties | cut -d"=" -f2)
SDK_VERSION=${OVERRIDE_SDK_VERSION:-${DEFAULT_SDK_VERSION}}
RN_VERSION=$(jq -r '.dependencies."react-native"' ${THIS_DIR}/../../package.json)
JSC_VERSION="r"$(jq -r '.dependencies."jsc-android"' ${THIS_DIR}/../../node_modules/react-native/package.json | cut -d . -f 1)
JSC_VERSION="r"$(jq -r '.dependencies."jsc-android"' ${THIS_DIR}/../../node_modules/react-native/package.json | cut -d . -f 1 | cut -c 2-)
DO_GIT_TAG=${GIT_TAG:-0}
if [[ $THE_MVN_REPO == http* ]]; then

View File

@@ -419,9 +419,15 @@ var config = {
// the menu has option to flip the locally seen video for local presentations
// disableLocalVideoFlip: false
// If specified a 'Help' button will be displayed in the overflow menu with a link to the specified URL for
// user documentation.
// userDocumentationURL: 'https://docs.example.com/video-meetings.html'
// Deployment specific URLs.
// deploymentUrls: {
// // If specified a 'Help' button will be displayed in the overflow menu with a link to the specified URL for
// // user documentation.
// userDocumentationURL: 'https://docs.example.com/video-meetings.html',
// // If specified a 'Download our apps' button will be displayed in the overflow menu with a link
// // to the specified URL for an app download page.
// downloadAppsUrl: 'https://docs.example.com/our-apps.html'
// }
// List of undocumented settings used in jitsi-meet
/**

View File

@@ -82,9 +82,10 @@
#chat-recipient {
align-items: center;
background-color: $defaultWarningColor;
background-color: $chatPrivateMessageBackgroundColor;
display: flex;
flex-direction: row;
font-weight: 100;
padding: 10px;
span {
@@ -132,6 +133,7 @@
#chat-input {
border-top: 1px solid $chatInputSeparatorColor;
display: flex;
padding: 5px 10px;
* {
background-color: transparent;
@@ -152,8 +154,7 @@
box-shadow: none;
color: white;
font-size: 15px;
line-height: 30px;
padding: 5px;
padding: 10px;
overflow-y: auto;
resize: none;
width: 100%;
@@ -183,6 +184,7 @@
.display-name {
font-size: 13px;
font-weight: bold;
margin-bottom: 5px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
@@ -196,7 +198,6 @@
color: white;
margin-top: 3px;
max-width: 100%;
padding-bottom: 3px;
position: relative;
&.localuser {
@@ -219,8 +220,12 @@
}
.privatemessagenotice {
color: $defaultWarningColor;
font-style: italic;
font-size: 11px;
font-weight: 100;
}
.messagecontent {
margin: 5px 10px;
}
}
@@ -297,10 +302,6 @@
cursor: pointer;
}
#usermsg::-webkit-input-placeholder {
line-height: 30px;
}
#usermsg::-webkit-scrollbar-track-piece {
background: #3a3a3a;
}
@@ -315,6 +316,10 @@
.chatmessage {
background-color: $chatLocalMessageBackgroundColor;
border-radius: 6px 0px 6px 6px;
&.privatemessage {
background-color: $chatPrivateMessageBackgroundColor;
}
}
.display-name {
@@ -328,8 +333,9 @@
&.error {
.chatmessage {
background-color: $defaultWarningColor;
border-radius: 0px;
color: red;
font-weight: 100;
}
.display-name {
@@ -345,8 +351,17 @@
flex-direction: row;
align-items: center;
.toolbox-icon {
cursor: pointer;
.messageactions {
align-self: stretch;
border-left: 1px solid $chatActionsSeparatorColor;
display: flex;
flex-direction: column;
justify-content: center;
padding: 5px;
.toolbox-icon {
cursor: pointer;
}
}
}
}
@@ -357,6 +372,9 @@
display: inline-block;
margin-top: 3px;
color: white;
padding: 8px;
&.privatemessage {
background-color: $chatPrivateMessageBackgroundColor;
}
}
}

66
css/_mini_toolbox.scss Normal file
View File

@@ -0,0 +1,66 @@
.filmstrip-toolbox,
.always-on-top-toolbox {
background-color: $newToolbarBackgroundColor;
border-radius: 3px;
display: flex;
z-index: $toolbarZ;
.toolbox-icon {
cursor: pointer;
padding: 7px;
&.toggled {
background: $AOTToolbarButtonToggleColor;
}
&.disabled {
cursor: initial;
}
}
}
.always-on-top-toolbox {
flex-direction: row;
left: 50%;
position: absolute;
top: 10px;
transform: translateX(-50%);
.toolbox-button {
&:first-child {
.toolbox-icon {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
}
&:nth-child(2) {
svg {
fill: $hangupColor;
}
}
&:last-child {
.toolbox-icon {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
}
}
}
.filmstrip-toolbox {
flex-direction: column;
.toolbox-button {
&:nth-child(1) {
svg {
fill: $hangupColor;
}
}
.toolbox-icon {
border-radius: 3px;
}
}
}

View File

@@ -270,89 +270,6 @@
}
}
.always-on-top-toolbox,
.filmstrip-toolbox {
background-color: $newToolbarBackgroundColor;
box-sizing: border-box;
display: flex;
flex-direction: column;
z-index: $toolbarZ;
i {
cursor: pointer;
display: block;
}
i:hover {
background-color: $AOTToolbarButtonHoverColor;
}
i.toggled {
background: $AOTToolbarButtonToggleColor;
}
i.toggled:hover:not(.disabled) {
background-color: $AOTToolbarButtonHoverColor;
}
.toolbox-button {
color: $toolbarButtonColor;
cursor: pointer;
text-align: center;
}
border-radius: 3px;
}
.always-on-top-toolbox {
flex-direction: row;
left: 50%;
position: absolute;
top: 10px;
transform: translateX(-50%);
z-index: $toolbarZ;
i {
font-size: $newToolbarFontSize;
height: $newToolbarSize;
line-height: $newToolbarSize;
width: $newToolbarSize;
}
.disabled {
cursor: initial;
}
.toolbox-button:first-child i {
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
}
.toolbox-button:last-child i {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
}
.filmstrip-toolbox {
i {
font-size: 1.9em;
height: 37px;
line-height: 37px;
width: 37px;
}
.toolbox-button:first-child i {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.toolbox-button:last-child i {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
}
/**
* START of fade in animation for main toolbar
*/

View File

@@ -86,10 +86,12 @@ $modalTextColor: #333;
/**
* Chat
*/
$chatActionsSeparatorColor: rgb(173, 105, 112);
$chatHeaderBackgroundColor: rgba(42, 58, 75, 0.9);
$chatInputSeparatorColor: #A4B8D1;
$chatLocalMessageBackgroundColor: rgba(26, 108, 180, 1);
$chatRemoteMessageBackgroundColor: rgba(240, 243, 247, 0.15);
$chatLocalMessageBackgroundColor: rgb(4, 98, 178);
$chatPrivateMessageBackgroundColor: rgb(153, 69, 77);
$chatRemoteMessageBackgroundColor: rgb(86, 101, 114);
$sidebarWidth: 375px;
/**
@@ -178,8 +180,17 @@ $welcomePageHeaderTextMarginTop: 35px;
$welcomePageHeaderTextMarginBottom: 35px;
$welcomePageHeaderTextTitleMarginBottom: 16px;
$welcomePageHeaderTextTitleFontSize: 2.5rem;
$welcomePageHeaderTextTitleFontWeight: 500;
$welcomePageHeaderTextTitleLineHeight: 1.18;
$welcomePageHeaderTextTitleOpacity: 1;
$welcomePageHeaderTextDescriptionDisplay: inherit;
$welcomePageHeaderTextDescriptionFontSize: 1rem;
$welcomePageHeaderTextDescriptionFontWeight: 400;
$welcomePageHeaderTextDescriptionLineHeight: 24px;
$welcomePageHeaderTextDescriptionMarginBottom: 20px;
$welcomePageHeaderTextDescriptionAlignSelf: inherit;
$welcomePageEnterRoomWidth: 680px;
$welcomePageEnterRoomPadding: 25px 30px;
@@ -198,6 +209,8 @@ $welcomePageTabButtonsDisplay: flex;
$welcomePageTabDisplay: block;
$welcomePageButtonWidth: 51px;
$welcomePageButtonMinWidth: inherit;
$welcomePageButtonFontSize: 14px;
$welcomePageButtonHeight: 35px;
$welcomePageButtonFontWeight: inherit;
$welcomePageButtonBorderRadius: 4px;

View File

@@ -38,19 +38,21 @@ body.welcome-page {
.header-text-title {
color: $welcomePageTitleColor;
font-size: 2.5rem;
font-weight: 500;
line-height: 1.18;
font-size: $welcomePageHeaderTextTitleFontSize;
font-weight: $welcomePageHeaderTextTitleFontWeight;
line-height: $welcomePageHeaderTextTitleLineHeight;
margin-bottom: $welcomePageHeaderTextTitleMarginBottom;
opacity: $welcomePageHeaderTextTitleOpacity;
}
.header-text-description {
display: $welcomePageHeaderTextDescriptionDisplay;
color: $welcomePageDescriptionColor;
font-size: 1rem;
font-weight: 400;
line-height: 24px;
margin-bottom: 20px;
font-size: $welcomePageHeaderTextDescriptionFontSize;
font-weight: $welcomePageHeaderTextDescriptionFontWeight;
line-height: $welcomePageHeaderTextDescriptionLineHeight;
margin-bottom: $welcomePageHeaderTextDescriptionMarginBottom;
align-self: $welcomePageHeaderTextDescriptionAlignSelf;
}
#enter_room {
@@ -148,8 +150,9 @@ body.welcome-page {
.welcome-page-button {
width: $welcomePageButtonWidth;
min-width: $welcomePageButtonMinWidth;
height: $welcomePageButtonHeight;
font-size: 14px;
font-size: $welcomePageButtonFontSize;
font-weight: $welcomePageButtonFontWeight;
background: #0074E0;
border-radius: $welcomePageButtonBorderRadius;

View File

@@ -32,6 +32,7 @@ $flagsImagePath: "../images/";
@import 'overlay/overlay';
@import 'inlay';
@import 'reload_overlay/reload_overlay';
@import 'mini_toolbox';
@import 'modals/desktop-picker/desktop-picker';
@import 'modals/device-selection/device-selection';
@import 'modals/dialog';

View File

@@ -63,6 +63,8 @@
width: -webkit-max-content;
word-break: break-all;
max-width: 400px;
display: flex;
align-items: center;
}
.info-dialog-dial-in {
@@ -86,6 +88,15 @@
cursor: inherit;
}
.info-dialog-url-icon {
display: inline-block;
margin-left: 5px;
svg {
cursor: pointer;
}
}
.info-dialog-title {
font-weight: bold;
margin-bottom: 10px;
@@ -214,4 +225,10 @@
-moz-user-select: text;
-webkit-user-select: text;
}
.info-dialog-url-text-unselectable {
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
}
}

View File

@@ -19,29 +19,37 @@ server {
ssl_certificate_key /etc/jitsi/meet/jitsi-meet.example.com.key;
root /usr/share/jitsi-meet;
ssi on;
index index.html index.htm;
error_page 404 /static/404.html;
location /config.js {
location = /config.js {
alias /etc/jitsi/meet/jitsi-meet.example.com-config.js;
}
location /external_api.js {
location = /external_api.js {
alias /usr/share/jitsi-meet/libs/external_api.min.js;
}
location ~ ^/([a-zA-Z0-9=\?]+)$ {
rewrite ^/(.*)$ / break;
}
location / {
ssi on;
#ensure all static content can always be found first
location ~ ^/(libs|css|static|images|fonts|lang|sounds|connection_optimization|.well-known)/(.*)$
{
add_header 'Access-Control-Allow-Origin' '*';
alias /usr/share/jitsi-meet/$1/$2;
}
# BOSH
location /http-bind {
location = /http-bind {
proxy_pass http://localhost:5280/http-bind;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
}
location ~ ^/([^?&:’“]+)$ {
try_files $uri @root_path;
}
location @root_path {
rewrite ^/(.*)$ / break;
}
}

View File

@@ -51,7 +51,7 @@ var interfaceConfig = {
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
'tileview', 'videobackgroundblur'
'tileview', 'videobackgroundblur', 'download', 'help'
],
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],

103
ios/ci/build-ipa.sh Executable file
View File

@@ -0,0 +1,103 @@
#!/bin/bash
set -e
# Mandatory arguments with no default values provided:
# PR_REPO_SLUG - the Github name of the repo to be merged into the origin/master
# PR_BRANCH - the branch to be merged, if set to "master" no merge will happen
# IPA_DEPLOY_LOCATION - the location understandable by the "scp" command
# executed at the end of the script to deploy the output .ipa file
# LIB_JITSI_MEET_PKG (optional) - the npm package for lib-jitsi-meet which will
# be put in place of the current version in the package.json file.
#
# Other than that the script requires the following env variables to be set:
#
# DEPLOY_SSH_CERT_URL - the SSH private key used by the 'scp' command to deploy
# the .ipa. It is expected to be encrypted with the $ENCRYPTION_PASSWORD.
# ENCRYPTION_PASSWORD - the password used to decrypt certificate/key files used
# in the script.
# IOS_TEAM_ID - the team ID inserted into build-ipa-.plist.template file in
# place of "YOUR_TEAM_ID".
function echoAndExit1() {
echo $1
exit 1
}
if [ -z $PR_REPO_SLUG ]; then
echoAndExit1 "No PR_REPO_SLUG defined"
fi
if [ -z $PR_BRANCH ]; then
echoAndExit1 "No PR_BRANCH defined"
fi
if [ -z $IPA_DEPLOY_LOCATION ]; then
echoAndExit1 "No IPA_DEPLOY_LOCATION defined"
fi
echo "PR_REPO_SLUG=${PR_REPO_SLUG} PR_BRANCH=${PR_BRANCH}"
# do the marge and git log
if [ $PR_BRANCH != "master" ]; then
echo "Will merge ${PR_REPO_SLUG}/${PR_BRANCH} into master"
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git fetch origin master
git checkout master
git pull https://github.com/${PR_REPO_SLUG}.git $PR_BRANCH --no-edit
fi
# Link this lib-jitsi-meet checkout in jitsi-meet through the package.json
if [ ! -z ${LIB_JITSI_MEET_PKG} ];
then
echo "Adjusting lib-jitsi-meet package in package.json to ${LIB_JITSI_MEET_PKG}"
# escape for the sed
LIB_JITSI_MEET_PKG=$(echo $LIB_JITSI_MEET_PKG | sed -e 's/\\/\\\\/g; s/\//\\\//g; s/&/\\\&/g')
sed -i.bak -e "s/\"lib-jitsi-meet.*/\"lib-jitsi-meet\"\: \"${LIB_JITSI_MEET_PKG}\",/g" package.json
echo "Package.json lib-jitsi-meet line:"
grep lib-jitsi-meet package.json
else
echo "LIB_JITSI_MEET_PKG var not set - will not modify the package.json"
fi
git log -20 --graph --pretty=format':%C(yellow)%h%Cblue%d%Creset %s %C(white) %an, %ar%Creset'
# certificates
CERT_DIR="ios/ci/certs"
mkdir -p $CERT_DIR
curl -L -o ${CERT_DIR}/id_rsa.enc ${DEPLOY_SSH_CERT_URL}
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/id_rsa.enc -d -a -out ${CERT_DIR}/id_rsa
chmod 0600 ${CERT_DIR}/id_rsa
ssh-add ${CERT_DIR}/id_rsa
npm install
# Ever since the Apple Watch app has been added the bitcode for WebRTC needs to be downloaded in order to build successfully
./node_modules/react-native-webrtc/tools/downloadBitcode.sh
cd ios
pod install --repo-update --no-ansi
cd ..
mkdir -p /tmp/jitsi-meet/
xcodebuild archive -quiet -workspace ios/jitsi-meet.xcworkspace -scheme jitsi-meet -configuration Release -archivePath /tmp/jitsi-meet/jitsi-meet.xcarchive
sed -e "s/YOUR_TEAM_ID/${IOS_TEAM_ID}/g" ios/ci/build-ipa.plist.template > ios/ci/build-ipa.plist
IPA_EXPORT_DIR=/tmp/jitsi-meet/jitsi-meet-ipa
xcodebuild -quiet -exportArchive -archivePath /tmp/jitsi-meet/jitsi-meet.xcarchive -exportPath $IPA_EXPORT_DIR -exportOptionsPlist ios/ci/build-ipa.plist
echo "Will try deploy the .ipa to: ${IPA_DEPLOY_LOCATION}"
if [ ! -z ${SCP_PROXY_HOST} ];
then
scp -o ProxyCommand="ssh -t -A -l %r ${SCP_PROXY_HOST} -o \"StrictHostKeyChecking no\" -o \"BatchMode yes\" -W %h:%p" -o StrictHostKeyChecking=no -o LogLevel=DEBUG "${IPA_EXPORT_DIR}/jitsi-meet.ipa" "${IPA_DEPLOY_LOCATION}"
else
scp -o StrictHostKeyChecking=no -o LogLevel=DEBUG "${IPA_EXPORT_DIR}/jitsi-meet.ipa" "${IPA_DEPLOY_LOCATION}"
fi
rm -r /tmp/jitsi-meet/
rm -r $CERT_DIR

100
ios/ci/setup-certificates.sh Executable file
View File

@@ -0,0 +1,100 @@
# The script is based on tutorial written by Antonis Tsakiridis published at:
# https://medium.com/@atsakiridis/continuous-deployment-for-ios-using-travis-ci-55dcea342d9
#
# APPLE_CERT_URL - the URL pointing to Apple certificate (set to
# http://developer.apple.com/certificationauthority/AppleWWDRCA.cer by default)
# DEPLOY_SSH_CERT_URL - the SSH private key used by the 'scp' command to deploy
# the .ipa. It is expected to be encrypted with the $ENCRYPTION_PASSWORD.
# ENCRYPTION_PASSWORD - the password used to decrypt certificate/key files used
# in the script.
# IOS_DEV_CERT_KEY_URL - URL pointing to provisioning profile certificate key
# file (development-key.p12.enc from the tutorial) encrypted with the
# $ENCRYPTION_PASSWORD.
# IOS_DEV_CERT_URL - URL pointing to provisioning profile certificate file
# (development-cert.cer.enc from the tutorial) encrypted with the
# $ENCRYPTION_PASSWORD.
# IOS_DEV_PROV_PROFILE_URL - URL pointing to provisioning profile file
# (profile-development-olympus.mobileprovision.enc from the tutorial) encrypted
# IOS_DEV_WATCH_PROV_PROFILE_URL - URL pointing to watch app provisioning profile file(encrypted).
# with the $ENCRYPTION_PASSWORD.
# IOS_SIGNING_CERT_PASSWORD - the password to the provisioning profile
# certificate key (used to open development-key.p12 from the tutorial).
function echoAndExit1() {
echo $1
exit 1
}
CERT_DIR=$1
if [ -z $CERT_DIR ]; then
echoAndExit1 "First argument must be certificates directory"
fi
if [ -z $APPLE_CERT_URL ]; then
APPLE_CERT_URL="http://developer.apple.com/certificationauthority/AppleWWDRCA.cer"
fi
if [ -z $DEPLOY_SSH_CERT_URL ]; then
echoAndExit1 "DEPLOY_SSH_CERT_URL env var is not defined"
fi
if [ -z $ENCRYPTION_PASSWORD ]; then
echoAndExit1 "ENCRYPTION_PASSWORD env var is not defined"
fi
if [ -z $IOS_DEV_CERT_KEY_URL ]; then
echoAndExit1 "IOS_DEV_CERT_KEY_URL env var is not defined"
fi
if [ -z $IOS_DEV_CERT_URL ]; then
echoAndExit1 "IOS_DEV_CERT_URL env var is not defined"
fi
if [ -z $IOS_DEV_PROV_PROFILE_URL ]; then
echoAndExit1 "IOS_DEV_PROV_PROFILE_URL env var is not defined"
fi
if [ -z $IOS_DEV_WATCH_PROV_PROFILE_URL ]; then
echoAndExit1 "IOS_DEV_WATCH_PROV_PROFILE_URL env var is not defined"
fi
if [ -z $IOS_SIGNING_CERT_PASSWORD ]; then
echoAndExit1 "IOS_SIGNING_CERT_PASSWORD env var is not defined"
fi
# certificates
curl -L -o ${CERT_DIR}/AppleWWDRCA.cer 'http://developer.apple.com/certificationauthority/AppleWWDRCA.cer'
curl -L -o ${CERT_DIR}/dev-cert.cer.enc ${IOS_DEV_CERT_URL}
curl -L -o ${CERT_DIR}/dev-key.p12.enc ${IOS_DEV_CERT_KEY_URL}
curl -L -o ${CERT_DIR}/dev-profile.mobileprovision.enc ${IOS_DEV_PROV_PROFILE_URL}
curl -L -o ${CERT_DIR}/dev-watch-profile.mobileprovision.enc ${IOS_DEV_WATCH_PROV_PROFILE_URL}
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/dev-cert.cer.enc -d -a -out ${CERT_DIR}/dev-cert.cer
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/dev-key.p12.enc -d -a -out ${CERT_DIR}/dev-key.p12
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/dev-profile.mobileprovision.enc -d -a -out ${CERT_DIR}/dev-profile.mobileprovision
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/dev-watch-profile.mobileprovision.enc -d -a -out ${CERT_DIR}/dev-watch-profile.mobileprovision
security create-keychain -p $ENCRYPTION_PASSWORD ios-build.keychain
security default-keychain -s ios-build.keychain
security unlock-keychain -p $ENCRYPTION_PASSWORD ios-build.keychain
security set-keychain-settings -t 3600 -l ~/Library/Keychains/ios-build.keychain
echo "importing Apple cert"
security import ${CERT_DIR}/AppleWWDRCA.cer -k ios-build.keychain -A
echo "importing dev-cert.cer"
security import ${CERT_DIR}/dev-cert.cer -k ios-build.keychain -A
echo "importing dev-key.p12"
security import ${CERT_DIR}/dev-key.p12 -k ios-build.keychain -P $IOS_SIGNING_CERT_PASSWORD -A
echo "will set-key-partition-list"
# Fix for OS X Sierra that hungs in the codesign step
security set-key-partition-list -S apple-tool:,apple: -s -k $ENCRYPTION_PASSWORD ios-build.keychain > /dev/null
echo "done set-key-partition-list"
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp "${CERT_DIR}/dev-profile.mobileprovision" ~/Library/MobileDevice/Provisioning\ Profiles/
cp "${CERT_DIR}/dev-watch-profile.mobileprovision" ~/Library/MobileDevice/Provisioning\ Profiles/

View File

@@ -24,6 +24,7 @@
#import "ReactUtils.h"
#import <RNGoogleSignin/RNGoogleSignin.h>
#import <WebRTC/RTCLogging.h>
@implementation JitsiMeet {
@@ -54,6 +55,11 @@
// Register a log handler for React.
registerReactLogHandler();
#if 0
// Enable WebRTC logs
RTCSetMinDebugLogLevel(RTCLoggingSeverityInfo);
#endif
}
return self;

View File

@@ -37,6 +37,7 @@ set -e
# IOS_TEAM_ID - the team ID inserted into build-ipa-.plist.template file in
# place of "YOUR_TEAM_ID".
# Travis will not print the last echo if there's no sleep 1
function echoSleepAndExit1() {
echo $1
@@ -57,10 +58,6 @@ if [ -z $IPA_DEPLOY_LOCATION ]; then
echoSleepAndExit1 "No IPA_DEPLOY_LOCATION defined"
fi
if [ -z $APPLE_CERT_URL ]; then
APPLE_CERT_URL="http://developer.apple.com/certificationauthority/AppleWWDRCA.cer"
fi
echo "PR_REPO_SLUG=${PR_REPO_SLUG} PR_BRANCH=${PR_BRANCH}"
# do the marge and git log
@@ -88,47 +85,17 @@ fi
git log -20 --graph --pretty=format':%C(yellow)%h%Cblue%d%Creset %s %C(white) %an, %ar%Creset'
# certificates
#certificates
CERT_DIR="ios/travis-ci/certs"
mkdir $CERT_DIR
mkdir -p $CERT_DIR
./ios/ci/setup-certificates.sh $CERT_DIR
curl -L -o ${CERT_DIR}/AppleWWDRCA.cer 'http://developer.apple.com/certificationauthority/AppleWWDRCA.cer'
curl -L -o ${CERT_DIR}/dev-cert.cer.enc ${IOS_DEV_CERT_URL}
curl -L -o ${CERT_DIR}/dev-key.p12.enc ${IOS_DEV_CERT_KEY_URL}
curl -L -o ${CERT_DIR}/dev-profile.mobileprovision.enc ${IOS_DEV_PROV_PROFILE_URL}
curl -L -o ${CERT_DIR}/dev-watch-profile.mobileprovision.enc ${IOS_DEV_WATCH_PROV_PROFILE_URL}
curl -L -o ${CERT_DIR}/id_rsa.enc ${DEPLOY_SSH_CERT_URL}
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/dev-cert.cer.enc -d -a -out ${CERT_DIR}/dev-cert.cer
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/dev-key.p12.enc -d -a -out ${CERT_DIR}/dev-key.p12
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/dev-profile.mobileprovision.enc -d -a -out ${CERT_DIR}/dev-profile.mobileprovision
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/dev-watch-profile.mobileprovision.enc -d -a -out ${CERT_DIR}/dev-watch-profile.mobileprovision
openssl aes-256-cbc -k "$ENCRYPTION_PASSWORD" -in ${CERT_DIR}/id_rsa.enc -d -a -out ${CERT_DIR}/id_rsa
chmod 0600 ${CERT_DIR}/id_rsa
security create-keychain -p $ENCRYPTION_PASSWORD ios-build.keychain
security default-keychain -s ios-build.keychain
security unlock-keychain -p $ENCRYPTION_PASSWORD ios-build.keychain
security set-keychain-settings -t 3600 -l ~/Library/Keychains/ios-build.keychain
echo "importing Apple cert"
security import ${CERT_DIR}/AppleWWDRCA.cer -k ios-build.keychain -A
echo "importing dev-cert.cer"
security import ${CERT_DIR}/dev-cert.cer -k ios-build.keychain -A
echo "importing dev-key.p12"
security import ${CERT_DIR}/dev-key.p12 -k ios-build.keychain -P $IOS_SIGNING_CERT_PASSWORD -A
echo "will set-key-partition-list"
# Fix for OS X Sierra that hungs in the codesign step
security set-key-partition-list -S apple-tool:,apple: -s -k $ENCRYPTION_PASSWORD ios-build.keychain > /dev/null
echo "done set-key-partition-list"
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp "${CERT_DIR}/dev-profile.mobileprovision" ~/Library/MobileDevice/Provisioning\ Profiles/
cp "${CERT_DIR}/dev-watch-profile.mobileprovision" ~/Library/MobileDevice/Provisioning\ Profiles/
ssh-add ${CERT_DIR}/id_rsa
npm install
@@ -136,24 +103,21 @@ npm install
./node_modules/react-native-webrtc/tools/downloadBitcode.sh
cd ios
pod update
pod install
pod install --repo-update --no-ansi
cd ..
mkdir -p /tmp/jitsi-meet/
xcodebuild archive -quiet -workspace ios/jitsi-meet.xcworkspace -scheme jitsi-meet -configuration Release -archivePath /tmp/jitsi-meet/jitsi-meet.xcarchive
sed -e "s/YOUR_TEAM_ID/${IOS_TEAM_ID}/g" ios/travis-ci/build-ipa.plist.template > ios/travis-ci/build-ipa.plist
sed -e "s/YOUR_TEAM_ID/${IOS_TEAM_ID}/g" ios/ci/build-ipa.plist.template > ios/ci/build-ipa.plist
IPA_EXPORT_DIR=/tmp/jitsi-meet/jitsi-meet-ipa
xcodebuild -quiet -exportArchive -archivePath /tmp/jitsi-meet/jitsi-meet.xcarchive -exportPath $IPA_EXPORT_DIR -exportOptionsPlist ios/travis-ci/build-ipa.plist
xcodebuild -quiet -exportArchive -archivePath /tmp/jitsi-meet/jitsi-meet.xcarchive -exportPath $IPA_EXPORT_DIR -exportOptionsPlist ios/ci/build-ipa.plist
echo "Will try deploy the .ipa to: ${IPA_DEPLOY_LOCATION}"
ssh-add ${CERT_DIR}/id_rsa
if [ ! -z ${SCP_PROXY_HOST} ];
then
scp -o ProxyCommand="ssh -t -A -l %r ${SCP_PROXY_HOST} -o \"StrictHostKeyChecking no\" -o \"BatchMode yes\" -W %h:%p" -o StrictHostKeyChecking=no -o LogLevel=DEBUG "${IPA_EXPORT_DIR}/jitsi-meet.ipa" "${IPA_DEPLOY_LOCATION}"

View File

@@ -570,6 +570,7 @@
"cc": "Toggle subtitles",
"chat": "Toggle chat window",
"document": "Toggle shared document",
"download": "Download our apps",
"feedback": "Leave feedback",
"fullScreen": "Toggle full screen",
"hangup": "Leave the call",
@@ -609,6 +610,7 @@
"closeChat": "Close chat",
"documentClose": "Close shared document",
"documentOpen": "Open shared document",
"download": "Download our apps",
"enterFullScreen": "View full screen",
"enterTileView": "Enter tile view",
"exitFullScreen": "Exit full screen",
@@ -725,7 +727,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",
"onlyAsciiAllowed": "Meeting name should only contain latin characters and numbers.",
"roomNameAllowedChars": "Meeting name should not contain any of these characters: ?, &, :, ', \", %, #.",
"go": "GO",
"join": "JOIN",
"info": "Info",

3042
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -49,7 +49,7 @@
"i18next-browser-languagedetector": "3.0.1",
"i18next-xhr-backend": "3.0.0",
"jQuery-Impromptu": "github:trentrichardson/jQuery-Impromptu#v6.0.0",
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#a885cc98688ef2c3972284bda901596a26ffee52",
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#5ec92357570dc8f0b7ffc1528820721c84c6af8b",
"jquery": "3.4.0",
"jquery-contextmenu": "2.4.5",
"jquery-i18next": "1.2.1",
@@ -57,7 +57,7 @@
"js-utils": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
"jsrsasign": "8.0.12",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#f9808adb8eb523bae3318f9f8ef49b544651485f",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#a6fd643e060dc63833ce2d291903b8233202a90d",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.13",
"moment": "2.19.4",
@@ -80,7 +80,7 @@
"react-native-svg-transformer": "0.13.0",
"react-native-swipeout": "2.3.6",
"react-native-watch-connectivity": "0.2.0",
"react-native-webrtc": "github:jitsi/react-native-webrtc#047b019a7ce1ec93ab4a2f6796e997d7a02e8e5d",
"react-native-webrtc": "github:react-native-webrtc/react-native-webrtc#a12a6cdfdefe53d03b388394e4cf10966bd99fca",
"react-native-webview": "7.4.1",
"react-redux": "7.1.0",
"react-textarea-autosize": "7.1.0",
@@ -122,14 +122,14 @@
"imports-loader": "0.7.1",
"jetifier": "1.6.4",
"metro-react-native-babel-preset": "0.56.0",
"node-sass": "4.10.0",
"node-sass": "4.12.0",
"precommit-hook": "3.0.0",
"string-replace-loader": "2.1.1",
"style-loader": "0.19.0",
"webpack": "4.26.1",
"webpack": "4.27.1",
"webpack-bundle-analyzer": "3.4.1",
"webpack-cli": "3.1.2",
"webpack-dev-server": "3.1.14"
"webpack-dev-server": "3.8.2"
},
"engines": {
"node": ">=8.0.0",

View File

@@ -16,7 +16,7 @@ import {
} from '../../base/dialog';
import { translate } from '../../base/i18n';
import { JitsiConnectionErrors } from '../../base/lib-jitsi-meet';
import { StyleType } from '../../base/styles';
import type { StyleType } from '../../base/styles';
import { authenticateAndUpgradeRole, cancelLogin } from '../actions';
import styles from './styles';

View File

@@ -153,6 +153,8 @@ class ColorSchemeRegistry {
const colorScheme = toState(stateful)['features/base/color-scheme'];
return {
...defaultScheme._defaultTheme,
...colorScheme._defaultTheme,
...defaultScheme[componentName],
...colorScheme[componentName]
}[colorDefinition];

View File

@@ -6,18 +6,26 @@ import { ColorPalette, getRGBAFormat } from '../styles';
* The default color scheme of the application.
*/
export default {
'BottomSheet': {
'_defaultTheme': {
// Generic app theme colors that are used accross the entire app.
// All scheme definitions below inherit these values.
background: 'rgb(255, 255, 255)',
icon: '#1c2025',
label: '#1c2025'
icon: 'rgb(28, 32, 37)',
text: 'rgb(28, 32, 37)'
},
'Chat': {
displayName: 'rgb(94, 109, 121)',
localMsgBackground: 'rgb(215, 230, 249)',
privateMsgBackground: 'rgb(250, 219, 219)',
privateMsgNotice: 'rgb(186, 39, 58)',
remoteMsgBackground: 'rgb(241, 242, 246)',
replyBorder: 'rgb(219, 197, 200)',
replyIcon: 'rgb(94, 109, 121)'
},
'Dialog': {
background: 'rgb(255, 255, 255)',
border: 'rgba(0, 3, 6, 0.6)',
buttonBackground: ColorPalette.blue,
buttonLabel: ColorPalette.white,
icon: '#1c2025',
text: '#1c2025'
buttonLabel: ColorPalette.white
},
'Header': {
background: ColorPalette.blue,
@@ -30,8 +38,7 @@ export default {
background: 'rgb(42, 58, 75)'
},
'LoadConfigOverlay': {
background: 'rgb(249, 249, 249)',
text: 'rgb(28, 32, 37)'
background: 'rgb(249, 249, 249)'
},
'Thumbnail': {
activeParticipantHighlight: 'rgb(81, 214, 170)',

View File

@@ -147,7 +147,7 @@ ColorSchemeRegistry.register('BottomSheet', {
* Style for the label in a generic item rendered in the menu.
*/
labelStyle: {
color: schemeColor('label'),
color: schemeColor('text'),
flexShrink: 1,
fontSize: MD_FONT_SIZE,
marginLeft: 32,

View File

@@ -23,6 +23,11 @@ type Props = {
*/
id?: string,
/**
* Function to invoke on click.
*/
onClick?: Function,
/**
* The size of the icon (if not provided by the style object).
*/
@@ -53,6 +58,7 @@ export default function Icon(props: Props) {
className,
color,
id,
onClick,
size,
src: IconComponent,
style
@@ -69,6 +75,7 @@ export default function Icon(props: Props) {
return (
<Container
className = { `jitsi-icon ${className}` }
onClick = { onClick }
style = { restStyle }>
<IconComponent
fill = { calculatedColor }

View 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="M4 4C4 2.89543 4.89543 2 6 2H14C15.1046 2 16 2.89543 16 4H6V18C4.89543 18 4 17.1046 4 16V4ZM10 8V20H18V8H10ZM10 6H18C19.1046 6 20 6.89543 20 8V20C20 21.1046 19.1046 22 18 22H10C8.89543 22 8 21.1046 8 20V8C8 6.89543 8.89543 6 10 6Z" fill="#5E6D7A"/>
</svg>

After

Width:  |  Height:  |  Size: 401 B

View 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 13.3705V5C11 4.44772 11.4477 4 12 4C12.5523 4 13 4.44772 13 5V13.4667L15.631 10.5433C16.0005 10.1328 16.6328 10.0995 17.0433 10.469C17.4538 10.8384 17.4871 11.4707 17.1176 11.8812L12.1064 17.4492C12.0727 17.4867 12.0139 17.4867 11.9802 17.4492L6.96897 11.8812C6.59951 11.4707 6.63278 10.8384 7.04329 10.469C7.4538 10.0995 8.08609 10.1328 8.45555 10.5433L11 13.3705ZM20 15C20 14.4477 20.4477 14 21 14C21.5523 14 22 14.4477 22 15V21C22 21.5523 21.5523 22 21 22H3C2.96548 22 2.93137 21.9983 2.89776 21.9948C2.3935 21.9436 2 21.5178 2 21V15C2 14.4477 2.44772 14 3 14C3.55228 14 4 14.4477 4 15V20H20V15Z" fill="#A4B8D1"/>
</svg>

After

Width:  |  Height:  |  Size: 773 B

View File

@@ -18,11 +18,13 @@ export { default as IconClose } from './close.svg';
export { default as IconClosedCaption } from './closed_caption.svg';
export { default as IconConnectionActive } from './gsm-bars.svg';
export { default as IconConnectionInactive } from './ninja.svg';
export { default as IconCopy } from './copy.svg';
export { default as IconDeviceBluetooth } from './bluetooth.svg';
export { default as IconDeviceEarpiece } from './phone-talk.svg';
export { default as IconDeviceHeadphone } from './headset.svg';
export { default as IconDeviceSpeaker } from './volume.svg';
export { default as IconDominantSpeaker } from './dominant-speaker.svg';
export { default as IconDownload } from './download.svg';
export { default as IconEventNote } from './event_note.svg';
export { default as IconExitFullScreen } from './exit-full-screen.svg';
export { default as IconFeedback } from './feedback.svg';

View File

@@ -63,13 +63,13 @@ export default class JitsiMeetLogStorage {
for (let i = 0, len = logEntries.length; i < len; i++) {
const logEntry = logEntries[i];
if (typeof logEntry === 'object') {
// Aggregated message
logMessage += `(${logEntry.count}) ${logEntry.text}\n`;
} else {
// Regular message
logMessage += `${logEntry}\n`;
if (logEntry.timestamp) {
logMessage += `${logEntry.timestamp} `;
}
if (logEntry.count > 1) {
logMessage += `(${logEntry.count}) `;
}
logMessage += `${logEntry.text}\n`;
}
logMessage += '"}';

View File

@@ -39,7 +39,8 @@ function buildTransport() {
'warn',
'error'
].reduce((logger, logName) => {
logger[logName] = (...args: Array<string>) => {
logger[logName] = (timestamp: string, ...args: Array<string>) => {
// It ignores the timestamp argument, because LogBridge will add it on the native side anyway
const nargs = args.map(arg => {
if (arg instanceof Error) {
const errorBody = {

View File

@@ -12,7 +12,7 @@ import {
} from '../../media';
import { Container, TintedView } from '../../react';
import { connect } from '../../redux';
import { StyleType } from '../../styles';
import type { StyleType } from '../../styles';
import { TestHint } from '../../testing/components';
import { getTrackByMediaTypeAndParticipant } from '../../tracks';

View File

@@ -45,7 +45,7 @@ export default class Linkify extends Component<Props> {
return (
<ReactLinkify
componentDecorator = { this._componentDecorator }>
<Text>
<Text selectable = { true }>
{ this.props.children }
</Text>
</ReactLinkify>

View File

@@ -4,6 +4,8 @@ import { PureComponent } from 'react';
import { getLocalizedDateFormatter } from '../../base/i18n';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../constants';
/**
* Formatter string to display the message timestamp.
*/
@@ -65,7 +67,7 @@ export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
_getMessageText() {
const { message } = this.props;
return message.messageType === 'error'
return message.messageType === MESSAGE_TYPE_ERROR
? this.props.t('chat.error', {
error: message.message
})
@@ -81,7 +83,7 @@ export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
const { message, t } = this.props;
return t('chat.privateNotice', {
recipient: message.messageType === 'local' ? message.recipient : t('chat.you')
recipient: message.messageType === MESSAGE_TYPE_LOCAL ? message.recipient : t('chat.you')
});
}
}

View File

@@ -6,7 +6,7 @@ import { getParticipantDisplayName } from '../../base/participants';
import { setPrivateMessageRecipient } from '../actions';
type Props = {
export type Props = {
/**
* Function used to translate i18n labels.
@@ -27,7 +27,7 @@ type Props = {
/**
* Abstract class for the {@code MessageRecipient} component.
*/
export default class AbstractMessageRecipient extends PureComponent<Props> {
export default class AbstractMessageRecipient<P: Props> extends PureComponent<P> {
}

View File

@@ -3,15 +3,16 @@
import React from 'react';
import { KeyboardAvoidingView, SafeAreaView } from 'react-native';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { translate } from '../../../base/i18n';
import { HeaderWithNavigation, SlidingView } from '../../../base/react';
import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
import AbstractChat, {
_mapDispatchToProps,
_mapStateToProps,
type Props
_mapStateToProps as _abstractMapStateToProps,
type Props as AbstractProps
} from '../AbstractChat';
import ChatInputBar from './ChatInputBar';
@@ -19,6 +20,14 @@ 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.
@@ -41,6 +50,8 @@ class Chat extends AbstractChat<Props> {
* @inheritdoc
*/
render() {
const { _styles } = this.props;
return (
<SlidingView
onHide = { this._onClose }
@@ -52,7 +63,7 @@ class Chat extends AbstractChat<Props> {
<HeaderWithNavigation
headerLabelKey = 'chat.title'
onPressBack = { this._onClose } />
<SafeAreaView style = { styles.backdrop }>
<SafeAreaView style = { _styles.backdrop }>
<MessageContainer messages = { this.props._messages } />
<MessageRecipient />
<ChatInputBar onSend = { this.props._onSendMessage } />
@@ -80,4 +91,17 @@ class Chat extends AbstractChat<Props> {
}
}
/**
* 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));

View File

@@ -4,16 +4,28 @@ import React from 'react';
import { Text, View } from 'react-native';
import { Avatar } from '../../../base/avatar';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { translate } from '../../../base/i18n';
import { Linkify } from '../../../base/react';
import { connect } from '../../../base/redux';
import { type StyleType } from '../../../base/styles';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL } from '../../constants';
import { replaceNonUnicodeEmojis } from '../../functions';
import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
import AbstractChatMessage, { type Props as AbstractProps } from '../AbstractChatMessage';
import PrivateMessageButton from '../PrivateMessageButton';
import styles from './styles';
type Props = AbstractProps & {
/**
* The color-schemed stylesheet of the feature.
*/
_styles: StyleType
};
/**
* Renders a single chat message.
*/
@@ -24,55 +36,58 @@ class ChatMessage extends AbstractChatMessage<Props> {
* @inheritdoc
*/
render() {
const { message } = this.props;
const localMessage = message.messageType === 'local';
const { _styles, message } = this.props;
const localMessage = message.messageType === MESSAGE_TYPE_LOCAL;
const { privateMessage } = message;
// Style arrays that need to be updated in various scenarios, such as
// error messages or others.
const detailsWrapperStyle = [
styles.detailsWrapper
];
const textWrapperStyle = [
styles.textWrapper
const messageBubbleStyle = [
styles.messageBubble
];
if (localMessage) {
// This is a message sent by the local participant.
// The wrapper needs to be aligned to the right.
detailsWrapperStyle.push(styles.ownMessageDetailsWrapper);
// The bubble needs to be differently styled.
textWrapperStyle.push(styles.ownTextWrapper);
} else if (message.messageType === 'error') {
// The bubble needs to be differently styled.
textWrapperStyle.push(styles.systemTextWrapper);
// The bubble needs some additional styling
messageBubbleStyle.push(_styles.localMessageBubble);
} else if (message.messageType === MESSAGE_TYPE_ERROR) {
// This is a system message.
// The bubble needs some additional styling
messageBubbleStyle.push(styles.systemMessageBubble);
} else {
// This is a remote message sent by a remote participant.
// The bubble needs some additional styling
messageBubbleStyle.push(_styles.remoteMessageBubble);
}
if (privateMessage) {
messageBubbleStyle.push(_styles.privateMessageBubble);
}
return (
<View style = { styles.messageWrapper } >
{ this._renderAvatar() }
<View style = { detailsWrapperStyle }>
<View style = { styles.replyWrapper }>
<View style = { textWrapperStyle } >
{
this.props.showDisplayName
&& this._renderDisplayName()
}
<View style = { messageBubbleStyle }>
<View style = { styles.textWrapper } >
{ this._renderDisplayName() }
<Linkify linkStyle = { styles.chatLink }>
{ replaceNonUnicodeEmojis(this._getMessageText()) }
</Linkify>
{
message.privateMessage
&& this._renderPrivateNotice()
}
{ this._renderPrivateNotice() }
</View>
{ message.privateMessage && !localMessage
&& <PrivateMessageButton
participantID = { message.id }
reply = { true }
showLabel = { false }
toggledStyles = { styles.replyStyles } /> }
{ this._renderPrivateReplyButton() }
</View>
{ this.props.showTimestamp && this._renderTimestamp() }
{ this._renderTimestamp() }
</View>
</View>
);
@@ -104,37 +119,77 @@ class ChatMessage extends AbstractChatMessage<Props> {
}
/**
* Renders the display name of the sender.
* Renders the display name of the sender if necessary.
*
* @returns {React$Element<*>}
* @returns {React$Element<*> | null}
*/
_renderDisplayName() {
const { _styles, message, showDisplayName } = this.props;
if (!showDisplayName) {
return null;
}
return (
<Text style = { styles.displayName }>
{ this.props.message.displayName }
<Text style = { _styles.displayName }>
{ message.displayName }
</Text>
);
}
/**
* Renders the message privacy notice.
* Renders the message privacy notice, if necessary.
*
* @returns {React$Element<*>}
* @returns {React$Element<*> | null}
*/
_renderPrivateNotice() {
const { _styles, message } = this.props;
if (!message.privateMessage) {
return null;
}
return (
<Text style = { styles.privateNotice }>
<Text style = { _styles.privateNotice }>
{ this._getPrivateNoticeMessage() }
</Text>
);
}
/**
* Renders the time at which the message was sent.
* Renders the private reply button, if necessary.
*
* @returns {React$Element<*>}
* @returns {React$Element<*> | null}
*/
_renderPrivateReplyButton() {
const { _styles, message } = this.props;
const { messageType, privateMessage } = message;
if (!privateMessage || messageType === MESSAGE_TYPE_LOCAL) {
return null;
}
return (
<View style = { _styles.replyContainer }>
<PrivateMessageButton
participantID = { message.id }
reply = { true }
showLabel = { false }
toggledStyles = { _styles.replyStyles } />
</View>
);
}
/**
* Renders the time at which the message was sent, if necessary.
*
* @returns {React$Element<*> | null}
*/
_renderTimestamp() {
if (!this.props.showTimestamp) {
return null;
}
return (
<Text style = { styles.timeText }>
{ this._getFormattedTimestamp() }
@@ -143,4 +198,16 @@ class ChatMessage extends AbstractChatMessage<Props> {
}
}
export default translate(ChatMessage);
/**
* Maps part of the redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
_styles: ColorSchemeRegistry.get(state, 'Chat')
};
}
export default translate(connect(_mapStateToProps)(ChatMessage));

View File

@@ -3,6 +3,8 @@
import React, { Component } from 'react';
import { FlatList } from 'react-native';
import { MESSAGE_TYPE_LOCAL, MESSAGE_TYPE_REMOTE } from '../../constants';
import ChatMessage from './ChatMessage';
import styles from './styles';
@@ -73,11 +75,11 @@ export default class ChatMessageGroup extends Component<Props> {
<ChatMessage
message = { message }
showAvatar = {
this.props.messages[0].messageType !== 'local'
this.props.messages[0].messageType !== MESSAGE_TYPE_LOCAL
&& index === this.props.messages.length - 1
}
showDisplayName = {
this.props.messages[0].messageType === 'remote'
this.props.messages[0].messageType === MESSAGE_TYPE_REMOTE
&& index === this.props.messages.length - 1
}
showTimestamp = { index === 0 } />

View File

@@ -3,28 +3,37 @@
import React from 'react';
import { Text, TouchableHighlight, View } from 'react-native';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { translate } from '../../../base/i18n';
import { Icon, IconCancelSelection } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { type StyleType } from '../../../base/styles';
import AbstractMessageRecipient, {
_mapDispatchToProps,
_mapStateToProps
_mapStateToProps as _abstractMapStateToProps,
type Props as AbstractProps
} from '../AbstractMessageRecipient';
import styles from './styles';
type Props = AbstractProps & {
/**
* The color-schemed stylesheet of the feature.
*/
_styles: StyleType
};
/**
* Class to implement the displaying of the recipient of the next message.
*/
class MessageRecipient extends AbstractMessageRecipient {
class MessageRecipient extends AbstractMessageRecipient<Props> {
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { _privateMessageRecipient } = this.props;
const { _privateMessageRecipient, _styles } = this.props;
if (!_privateMessageRecipient) {
return null;
@@ -33,8 +42,8 @@ class MessageRecipient extends AbstractMessageRecipient {
const { t } = this.props;
return (
<View style = { styles.messageRecipientContainer }>
<Text style = { styles.messageRecipientText }>
<View style = { _styles.messageRecipientContainer }>
<Text style = { _styles.messageRecipientText }>
{ t('chat.messageTo', {
recipient: _privateMessageRecipient
}) }
@@ -42,11 +51,24 @@ class MessageRecipient extends AbstractMessageRecipient {
<TouchableHighlight onPress = { this.props._onRemovePrivateMessageRecipient }>
<Icon
src = { IconCancelSelection }
style = { styles.messageRecipientCancelIcon } />
style = { _styles.messageRecipientCancelIcon } />
</TouchableHighlight>
</View>
);
}
}
/**
* 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)(MessageRecipient));

View File

@@ -1,7 +1,10 @@
// @flow
import { ColorSchemeRegistry, schemeColor } from '../../../base/color-scheme';
import { BoxModel, ColorPalette } from '../../../base/styles';
const BUBBLE_RADIUS = 8;
/**
* The styles of the feature chat.
*
@@ -20,14 +23,6 @@ export default {
width: 32
},
/**
* Background of the chat screen.
*/
backdrop: {
backgroundColor: ColorPalette.white,
flex: 1
},
chatContainer: {
alignItems: 'stretch',
flex: 1,
@@ -47,14 +42,6 @@ export default {
flexDirection: 'column'
},
/**
* The text node for the display name.
*/
displayName: {
color: 'rgb(118, 136, 152)',
fontSize: 13
},
/**
* A special padding to avoid issues on some devices (such as Android devices with custom suggestions bar).
*/
@@ -76,35 +63,16 @@ export default {
height: 48
},
messageBubble: {
alignItems: 'center',
borderRadius: BUBBLE_RADIUS,
flexDirection: 'row'
},
messageContainer: {
flex: 1
},
messageRecipientCancelIcon: {
color: ColorPalette.white,
fontSize: 18
},
messageRecipientContainer: {
alignItems: 'center',
backgroundColor: ColorPalette.warning,
flexDirection: 'row',
padding: BoxModel.padding
},
messageRecipientText: {
color: ColorPalette.white,
flex: 1
},
/**
* The message text itself.
*/
messageText: {
color: 'rgb(28, 32, 37)',
fontSize: 15
},
/**
* Wrapper View for the entire block.
*/
@@ -123,34 +91,11 @@ export default {
alignItems: 'flex-end'
},
/**
* Style modifier for the {@code textWrapper} for own messages.
*/
ownTextWrapper: {
backgroundColor: 'rgb(210, 231, 249)',
borderTopLeftRadius: 8,
borderTopRightRadius: 0
},
replyWrapper: {
alignItems: 'center',
flexDirection: 'row'
},
replyStyles: {
iconStyle: {
color: 'rgb(118, 136, 152)',
fontSize: 22,
margin: BoxModel.margin / 2
}
},
privateNotice: {
color: ColorPalette.warning,
fontSize: 13,
fontStyle: 'italic'
},
sendButtonIcon: {
color: ColorPalette.darkGrey,
fontSize: 22
@@ -159,7 +104,7 @@ export default {
/**
* Style modifier for system (error) messages.
*/
systemTextWrapper: {
systemMessageBubble: {
backgroundColor: 'rgb(247, 215, 215)'
},
@@ -168,9 +113,6 @@ export default {
*/
textWrapper: {
alignItems: 'flex-start',
backgroundColor: 'rgb(240, 243, 247)',
borderRadius: 8,
borderTopLeftRadius: 0,
flexDirection: 'column',
padding: 9
},
@@ -183,3 +125,73 @@ export default {
fontSize: 13
}
};
ColorSchemeRegistry.register('Chat', {
/**
* Background of the chat screen.
*/
backdrop: {
backgroundColor: schemeColor('background'),
flex: 1
},
/**
* The text node for the display name.
*/
displayName: {
color: schemeColor('displayName'),
fontSize: 13
},
localMessageBubble: {
backgroundColor: schemeColor('localMsgBackground'),
borderTopRightRadius: 0
},
messageRecipientCancelIcon: {
color: schemeColor('icon'),
fontSize: 18
},
messageRecipientContainer: {
alignItems: 'center',
backgroundColor: schemeColor('privateMsgBackground'),
flexDirection: 'row',
padding: BoxModel.padding
},
messageRecipientText: {
color: schemeColor('text'),
flex: 1
},
privateNotice: {
color: schemeColor('privateMsgNotice'),
fontSize: 11,
marginTop: 6
},
privateMessageBubble: {
backgroundColor: schemeColor('privateMsgBackground')
},
remoteMessageBubble: {
backgroundColor: schemeColor('remoteMsgBackground'),
borderTopLeftRadius: 0
},
replyContainer: {
alignSelf: 'stretch',
borderLeftColor: schemeColor('replyBorder'),
borderLeftWidth: 1,
justifyContent: 'center'
},
replyStyles: {
iconStyle: {
color: schemeColor('replyIcon'),
fontSize: 22,
padding: 8
}
}
});

View File

@@ -4,6 +4,7 @@ import React from 'react';
import Transition from 'react-transition-group/Transition';
import { translate } from '../../../base/i18n';
import { Icon, IconClose } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractChat, {
@@ -137,7 +138,9 @@ class Chat extends AbstractChat<Props> {
<div className = 'chat-header'>
<div
className = 'chat-close'
onClick = { this.props._onToggleChat }>X</div>
onClick = { this.props._onToggleChat }>
<Icon src = { IconClose } />
</div>
</div>
);
}

View File

@@ -7,6 +7,8 @@ import { toArray } from 'react-emoji-render';
import { translate } from '../../../base/i18n';
import { Linkify } from '../../../base/react';
import { MESSAGE_TYPE_LOCAL } from '../../constants';
import AbstractChatMessage, {
type Props
} from '../AbstractChatMessage';
@@ -39,19 +41,25 @@ class ChatMessage extends AbstractChatMessage<Props> {
return (
<div className = 'chatmessage-wrapper'>
<div className = 'replywrapper'>
<div className = 'chatmessage'>
{ this.props.showDisplayName && this._renderDisplayName() }
<div className = 'usermessage'>
{ processedMessage }
<div className = { `chatmessage ${message.privateMessage ? 'privatemessage' : ''}` }>
<div className = 'replywrapper'>
<div className = 'messagecontent'>
{ this.props.showDisplayName && this._renderDisplayName() }
<div className = 'usermessage'>
{ processedMessage }
</div>
{ message.privateMessage && this._renderPrivateNotice() }
</div>
{ message.privateMessage && this._renderPrivateNotice() }
{ message.privateMessage && message.messageType !== MESSAGE_TYPE_LOCAL
&& (
<div className = 'messageactions'>
<PrivateMessageButton
participantID = { message.id }
reply = { true }
showLabel = { false } />
</div>
) }
</div>
{ message.privateMessage && message.messageType !== 'local'
&& <PrivateMessageButton
participantID = { message.id }
reply = { true }
showLabel = { false } /> }
</div>
{ this.props.showTimestamp && this._renderTimestamp() }
</div>

View File

@@ -2,6 +2,8 @@
import React from 'react';
import { MESSAGE_TYPE_REMOTE } from '../../constants';
import AbstractMessageContainer, { type Props }
from '../AbstractMessageContainer';
@@ -61,7 +63,7 @@ export default class MessageContainer extends AbstractMessageContainer {
return (
<ChatMessageGroup
className = { messageType || 'remote' }
className = { messageType || MESSAGE_TYPE_REMOTE }
key = { index }
messages = { group } />
);

View File

@@ -8,13 +8,14 @@ import { connect } from '../../../base/redux';
import AbstractMessageRecipient, {
_mapDispatchToProps,
_mapStateToProps
_mapStateToProps,
type Props
} from '../AbstractMessageRecipient';
/**
* Class to implement the displaying of the recipient of the next message.
*/
class MessageRecipient extends AbstractMessageRecipient {
class MessageRecipient extends AbstractMessageRecipient<Props> {
/**
* Implements {@code PureComponent#render}.
*

View File

@@ -1,3 +1,5 @@
// @flow
/**
* The audio ID of the audio element for which the {@link playAudio} action is
* triggered when new chat message is received.
@@ -5,3 +7,18 @@
* @type {string}
*/
export const INCOMING_MSG_SOUND_ID = 'INCOMING_MSG_SOUND';
/**
* The {@code messageType} of error (system) messages.
*/
export const MESSAGE_TYPE_ERROR = 'error';
/**
* The {@code messageType} of local messages.
*/
export const MESSAGE_TYPE_LOCAL = 'local';
/**
* The {@code messageType} of remote messages.
*/
export const MESSAGE_TYPE_REMOTE = 'remote';

View File

@@ -22,7 +22,7 @@ import { isButtonEnabled, showToolbox } from '../toolbox';
import { SEND_MESSAGE, SET_PRIVATE_MESSAGE_RECIPIENT } from './actionTypes';
import { addMessage, clearMessages, toggleChat } from './actions';
import { ChatPrivacyDialog } from './components';
import { INCOMING_MSG_SOUND_ID } from './constants';
import { INCOMING_MSG_SOUND_ID, MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, MESSAGE_TYPE_REMOTE } from './constants';
import { INCOMING_MSG_SOUND_FILE } from './sounds';
declare var APP: Object;
@@ -194,7 +194,7 @@ function _addChatMsgListener(conference, store) {
function _handleChatError({ dispatch }, error) {
dispatch(addMessage({
hasRead: true,
messageType: 'error',
messageType: MESSAGE_TYPE_ERROR,
message: error,
privateMessage: false,
timestamp: Date.now()
@@ -231,7 +231,7 @@ function _handleReceivedMessage({ dispatch, getState }, { id, message, nick, pri
displayName,
hasRead,
id,
messageType: participant.local ? 'local' : 'remote',
messageType: participant.local ? MESSAGE_TYPE_LOCAL : MESSAGE_TYPE_REMOTE,
message,
privateMessage,
recipient: getParticipantDisplayName(state, localParticipant.id),
@@ -285,7 +285,7 @@ function _persistSentPrivateMessage({ dispatch, getState }, recipientID, message
displayName,
hasRead: true,
id: localParticipant.id,
messageType: 'local',
messageType: MESSAGE_TYPE_LOCAL,
message,
privateMessage: true,
recipient: getParticipantDisplayName(getState, recipientID),
@@ -323,7 +323,7 @@ function _shouldSendPrivateMessageTo(state, action): ?string {
const lastMessage = navigator.product === 'ReactNative'
? messages[0] : messages[messages.length - 1];
if (lastMessage.messageType === 'local') {
if (lastMessage.messageType === MESSAGE_TYPE_LOCAL) {
// The sender is probably aware of any private messages as already sent
// a message since then. Doesn't make sense to display the notice now.
return undefined;
@@ -339,7 +339,7 @@ function _shouldSendPrivateMessageTo(state, action): ?string {
const now = Date.now();
const recentPrivateMessages = messages.filter(
message =>
message.messageType !== 'local'
message.messageType !== MESSAGE_TYPE_LOCAL
&& message.privateMessage
&& message.timestamp + PRIVACY_NOTICE_TIMEOUT > now);
const recentPrivateMessage = navigator.product === 'ReactNative'

View File

@@ -40,12 +40,12 @@ class Toolbar extends Component<Props> {
<div
className = 'filmstrip-toolbox'
id = 'new-toolbox'>
<AudioMuteButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('microphone') } />
<HangupButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('hangup') } />
<AudioMuteButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('microphone') } />
<VideoMuteButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('camera') } />

View File

@@ -139,7 +139,6 @@ function _onFollowMeCommand(attributes = {}, id, store) {
// For now gate etherpad checks behind a web-app check to be extra safe
// against calling a web-app global.
if (typeof APP !== 'undefined'
&& state['features/etherpad'].initialized
&& oldState.sharedDocumentVisible !== attributes.sharedDocumentVisible) {
const isEtherpadVisible = attributes.sharedDocumentVisible === 'true';
const documentManager = APP.UI.getSharedDocumentManager();

View File

@@ -7,7 +7,7 @@ import { setPassword } from '../../../../base/conference';
import { getInviteURL } from '../../../../base/connection';
import { Dialog } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { Icon, IconInfo } from '../../../../base/icons';
import { Icon, IconInfo, IconCopy } from '../../../../base/icons';
import { connect } from '../../../../base/redux';
import {
isLocalParticipantModerator,
@@ -136,6 +136,7 @@ type State = {
*/
class InfoDialog extends Component<Props, State> {
_copyElement: ?Object;
_copyUrlElement: ?Object;
/**
* Implements React's {@link Component#getDerivedStateFromProps()}.
@@ -197,12 +198,14 @@ class InfoDialog extends Component<Props, State> {
// Bind event handlers so they are only bound once for every instance.
this._onClickURLText = this._onClickURLText.bind(this);
this._onCopyInviteURL = this._onCopyInviteURL.bind(this);
this._onCopyInviteInfo = this._onCopyInviteInfo.bind(this);
this._onCopyInviteUrl = this._onCopyInviteUrl.bind(this);
this._onPasswordRemove = this._onPasswordRemove.bind(this);
this._onPasswordSubmit = this._onPasswordSubmit.bind(this);
this._onTogglePasswordEditState
= this._onTogglePasswordEditState.bind(this);
this._setCopyElement = this._setCopyElement.bind(this);
this._setCopyUrlElement = this._setCopyUrlElement.bind(this);
}
/**
@@ -239,12 +242,18 @@ class InfoDialog extends Component<Props, State> {
<span className = 'spacer'>&nbsp;</span>
<span className = 'info-value'>
<a
className = 'info-dialog-url-text'
className = 'info-dialog-url-text info-dialog-url-text-unselectable'
href = { this.props._inviteURL }
onClick = { this._onClickURLText } >
{ decodeURI(this._getURLToDisplay()) }
</a>
</span>
<span className = 'info-dialog-url-icon'>
<Icon
onClick = { this._onCopyInviteUrl }
size = { 18 }
src = { IconCopy } />
</span>
</div>
<div className = 'info-dialog-dial-in'>
{ this._renderDialInDisplay() }
@@ -262,7 +271,7 @@ class InfoDialog extends Component<Props, State> {
<div className = 'info-dialog-action-link'>
<a
className = 'info-copy'
onClick = { this._onCopyInviteURL }>
onClick = { this._onCopyInviteInfo }>
{ t('dialog.copy') }
</a>
</div>
@@ -275,6 +284,12 @@ class InfoDialog extends Component<Props, State> {
ref = { this._setCopyElement }
tabIndex = '-1'
value = { this._getTextToCopy() } />
<textarea
className = 'info-dialog-copy-element'
readOnly = { true }
ref = { this._setCopyUrlElement }
tabIndex = '-1'
value = { this.props._inviteURL } />
</div>
);
@@ -365,7 +380,7 @@ class InfoDialog extends Component<Props, State> {
event.preventDefault();
}
_onCopyInviteURL: () => void;
_onCopyInviteInfo: () => void;
/**
* Callback invoked to copy the contents of {@code this._copyElement} to the
@@ -374,7 +389,7 @@ class InfoDialog extends Component<Props, State> {
* @private
* @returns {void}
*/
_onCopyInviteURL() {
_onCopyInviteInfo() {
try {
if (!this._copyElement) {
throw new Error('No element to copy from.');
@@ -388,6 +403,28 @@ class InfoDialog extends Component<Props, State> {
}
}
_onCopyInviteUrl: () => void;
/**
* Callback invoked to copy the contents of {@code this._copyUrlElement} to the clipboard.
*
* @private
* @returns {void}
*/
_onCopyInviteUrl() {
try {
if (!this._copyUrlElement) {
throw new Error('No element to copy from.');
}
this._copyUrlElement && this._copyUrlElement.select();
document.execCommand('copy');
this._copyUrlElement && this._copyUrlElement.blur();
} catch (err) {
logger.error('error when copying the text', err);
}
}
_onPasswordRemove: () => void;
/**
@@ -565,6 +602,21 @@ class InfoDialog extends Component<Props, State> {
_setCopyElement(element: Object) {
this._copyElement = element;
}
_setCopyUrlElement: () => void;
/**
* Sets the internal reference to the DOM/HTML element backing the React
* {@code Component} input.
*
* @param {HTMLInputElement} element - The DOM/HTML element for this
* {@code Component}'s input.
* @private
* @returns {void}
*/
_setCopyUrlElement(element: Object) {
this._copyUrlElement = element;
}
}
/**

View File

@@ -577,5 +577,12 @@ export function _decodeRoomURI(url: string) {
roomUrl = decodeURI(roomUrl);
}
// Handles a special case where the room name has % encoded, the decoded will have
// % followed by a char (non-digit) which is not a valid URL and room name ... so we do not
// want to show this decoded
if (roomUrl.match(/.*%[^\d].*/)) {
return url;
}
return roomUrl;
}

View File

@@ -1,37 +0,0 @@
/**
* The type of redux action to add a network request to the redux store/state.
*
* {
* type: _ADD_NETWORK_REQUEST,
* request: Object
* }
*
* @protected
*/
export const _ADD_NETWORK_REQUEST = '_ADD_NETWORK_REQUEST';
/**
* The type of redux action to remove all network requests from the redux
* store/state.
*
* {
* type: _REMOVE_ALL_NETWORK_REQUESTS,
* }
*
* @protected
*/
export const _REMOVE_ALL_NETWORK_REQUESTS
= '_REMOVE_ALL_NETWORK_REQUESTS';
/**
* The type of redux action to remove a network request from the redux
* store/state.
*
* {
* type: _REMOVE_NETWORK_REQUEST,
* request: Object
* }
*
* @protected
*/
export const _REMOVE_NETWORK_REQUEST = '_REMOVE_NETWORK_REQUEST';

View File

@@ -1,55 +0,0 @@
// @flow
import React, { Component } from 'react';
import { LoadingIndicator } from '../../../base/react';
import { connect } from '../../../base/redux';
/**
* The type of the React {@code Component} props of
* {@link NetworkActivityIndicator}.
*/
type Props = {
/**
* Indicates whether there is network activity i.e. ongoing network
* requests.
*/
_networkActivity: boolean
};
/**
* The React {@code Component} which renders a progress indicator when there
* are ongoing network requests.
*/
class NetworkActivityIndicator extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return this.props._networkActivity ? <LoadingIndicator /> : null;
}
}
/**
* Maps (parts of) the redux state to the React {@code Component} props of
* {@code NetworkActivityIndicator}.
*
* @param {Object} state - The redux state.
* @private
* @returns {{
* _networkActivity: boolean
* }}
*/
function _mapStateToProps(state) {
const { requests } = state['features/network-activity'];
return {
_networkActivity: Boolean(requests && requests.size)
};
}
export default connect(_mapStateToProps)(NetworkActivityIndicator);

View File

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

View File

@@ -1,4 +0,0 @@
export * from './components';
import './middleware';
import './reducer';

View File

@@ -1,81 +0,0 @@
/* @flow */
import XHRInterceptor from 'react-native/Libraries/Network/XHRInterceptor';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
import { MiddlewareRegistry } from '../../base/redux';
import {
_ADD_NETWORK_REQUEST,
_REMOVE_ALL_NETWORK_REQUESTS,
_REMOVE_NETWORK_REQUEST
} from './actionTypes';
/**
* Middleware which captures app startup and conference actions in order to
* clear the image cache.
*
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) {
case APP_WILL_MOUNT:
_startNetInterception(store);
break;
case APP_WILL_UNMOUNT:
_stopNetInterception(store);
break;
}
return result;
});
/**
* Starts intercepting network requests. Only XHR requests are supported at the
* moment.
*
* Ongoing request information is kept in redux, and it's removed as soon as a
* given request is complete.
*
* @param {Object} store - The redux store.
* @private
* @returns {void}
*/
function _startNetInterception({ dispatch }) {
XHRInterceptor.setOpenCallback((method, url, xhr) => dispatch({
type: _ADD_NETWORK_REQUEST,
request: xhr,
// The following are not really necessary read anywhere at the time of
// this writing but are provided anyway if the reducer chooses to
// remember them:
method,
url
}));
XHRInterceptor.setResponseCallback((...args) => dispatch({
type: _REMOVE_NETWORK_REQUEST,
// XXX The XHR is the last argument of the responseCallback.
request: args[args.length - 1]
}));
XHRInterceptor.enableInterception();
}
/**
* Stops intercepting network requests.
*
* @param {Object} store - The redux store.
* @private
* @returns {void}
*/
function _stopNetInterception({ dispatch }) {
XHRInterceptor.disableInterception();
dispatch({
type: _REMOVE_ALL_NETWORK_REQUESTS
});
}

View File

@@ -1,60 +0,0 @@
// @flow
import { ReducerRegistry, set } from '../../base/redux';
import {
_ADD_NETWORK_REQUEST,
_REMOVE_ALL_NETWORK_REQUESTS,
_REMOVE_NETWORK_REQUEST
} from './actionTypes';
/**
* The default/initial redux state of the feature network-activity.
*
* @type {{
* requests: Map
* }}
*/
const DEFAULT_STATE = {
/**
* The ongoing network requests i.e. the network request which have been
* added to the redux store/state and have not been removed.
*
* @type {Map}
*/
requests: new Map()
};
ReducerRegistry.register(
'features/network-activity',
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case _ADD_NETWORK_REQUEST: {
const {
type, // eslint-disable-line no-unused-vars
request: key,
...value
} = action;
const requests = new Map(state.requests);
requests.set(key, value);
return set(state, 'requests', requests);
}
case _REMOVE_ALL_NETWORK_REQUESTS:
return set(state, 'requests', DEFAULT_STATE.requests);
case _REMOVE_NETWORK_REQUEST: {
const { request: key } = action;
const requests = new Map(state.requests);
requests.delete(key);
return set(state, 'requests', requests);
}
}
return state;
});

View File

@@ -18,7 +18,7 @@ export function toDisplayableList(recentList) {
date: item.date,
duration: item.duration,
time: [ item.date ],
title: decodeURIComponent(parseURIString(item.conference).room),
title: parseURIString(item.conference).room,
url: item.conference
};
}));

View File

@@ -2,4 +2,5 @@
* The URL that is the main landing page for YouTube live streaming and should
* have a user's live stream key.
*/
export const YOUTUBE_LIVE_DASHBOARD_URL = 'https://www.youtube.com/live_dashboard';
export const YOUTUBE_LIVE_DASHBOARD_URL
= 'https://www.youtube.com/live_dashboard';

View File

@@ -1,13 +1,12 @@
// @flow
import React from 'react';
import { Text, TextInput, TouchableOpacity, View } from 'react-native';
import { Linking, Text, TextInput, TouchableOpacity, View } from 'react-native';
import { _abstractMapStateToProps } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { connect } from '../../../../base/redux';
import { StyleType } from '../../../../base/styles';
import { openURLInBrowser } from '../../../../base/util';
import AbstractStreamKeyForm, {
type Props as AbstractProps
@@ -121,7 +120,7 @@ class StreamKeyForm extends AbstractStreamKeyForm<Props> {
const { helpURL } = this;
if (typeof helpURL === 'string') {
openURLInBrowser(helpURL);
Linking.openURL(helpURL);
}
}
}

View File

@@ -2,6 +2,7 @@
import React, { Component } from 'react';
import {
Linking,
Text,
TouchableHighlight,
TouchableOpacity,
@@ -12,7 +13,6 @@ import { _abstractMapStateToProps } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { connect } from '../../../../base/redux';
import { StyleType } from '../../../../base/styles';
import { openURLInBrowser } from '../../../../base/util';
import { YOUTUBE_LIVE_DASHBOARD_URL } from '../constants';
@@ -153,7 +153,7 @@ class StreamKeyPicker extends Component<Props, State> {
* @returns {void}
*/
_onOpenYoutubeDashboard() {
openURLInBrowser(YOUTUBE_LIVE_DASHBOARD_URL);
Linking.openURL(YOUTUBE_LIVE_DASHBOARD_URL);
}
_onStreamPick: string => Function

View File

@@ -0,0 +1,56 @@
// @flow
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { translate } from '../../base/i18n';
import { IconDownload } from '../../base/icons';
import { connect } from '../../base/redux';
import { openURLInBrowser } from '../../base/util';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox';
type Props = AbstractButtonProps & {
/**
* The URL to the applications page.
*/
_downloadAppsUrl: string
};
/**
* Implements an {@link AbstractButton} to open the applications page in a new window.
*/
class DownloadButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.download';
icon = IconDownload;
label = 'toolbar.download';
/**
* Handles clicking / pressing the button, and opens a new window with the user documentation.
*
* @private
* @returns {void}
*/
_handleClick() {
sendAnalytics(createToolbarEvent('download.pressed'));
openURLInBrowser(this.props._downloadAppsUrl);
}
}
/**
* Maps part of the redux state to the component's props.
*
* @param {Object} state - The redux store/state.
* @returns {Object}
*/
function _mapStateToProps(state: Object) {
const { downloadAppsUrl } = state['features/base/config'].deploymentUrls || {};
const visible = typeof downloadAppsUrl === 'string';
return {
_downloadAppsUrl: downloadAppsUrl,
visible
};
}
export default translate(connect(_mapStateToProps)(DownloadButton));

View File

@@ -44,7 +44,7 @@ class HelpButton extends AbstractButton<Props, *> {
* @returns {Object}
*/
function _mapStateToProps(state: Object) {
const { userDocumentationURL } = state['features/base/config'];
const { userDocumentationURL } = state['features/base/config'].deploymentUrls || {};
const visible = typeof userDocumentationURL === 'string';
return {

View File

@@ -69,6 +69,7 @@ import {
setToolbarHovered
} from '../../actions';
import AudioMuteButton from '../AudioMuteButton';
import DownloadButton from '../DownloadButton';
import { isToolboxVisible } from '../../functions';
import HangupButton from '../HangupButton';
import HelpButton from '../HelpButton';
@@ -975,6 +976,10 @@ class Toolbox extends Component<Props, State> {
key = 'shortcuts'
onClick = { this._onToolbarOpenKeyboardShortcuts }
text = { t('toolbar.shortcuts') } />,
this._shouldShowButton('download')
&& <DownloadButton
key = 'download'
showLabel = { true } />,
this._shouldShowButton('help')
&& <HelpButton
key = 'help'

View File

@@ -1,18 +1,27 @@
// @flow
import React, { Component } from 'react';
import { View } from 'react-native';
import type { Dispatch } from 'redux';
import { ColorSchemeRegistry } from '../../base/color-scheme';
import { LoadingIndicator } from '../../base/react';
import { connect } from '../../base/redux';
import { StyleType } from '../../base/styles';
import { destroyLocalTracks } from '../../base/tracks';
import { NetworkActivityIndicator } from '../../mobile/network-activity';
import LocalVideoTrackUnderlay from './LocalVideoTrackUnderlay';
import styles from './styles';
/**
* The type of React {@code Component} props of {@link BlankPage}.
*/
type Props = {
/**
* The color schemed style of the component.
*/
_styles: StyleType,
dispatch: Dispatch<any>
};
@@ -40,12 +49,32 @@ class BlankPage extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { _styles } = this.props;
return (
<LocalVideoTrackUnderlay>
<NetworkActivityIndicator />
</LocalVideoTrackUnderlay>
<View
style = { [
styles.blankPageWrapper,
_styles.loadingOverlayWrapper
] }>
<LoadingIndicator
color = { _styles.indicatorColor }
size = 'large' />
</View>
);
}
}
export default connect()(BlankPage);
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
_styles: ColorSchemeRegistry.get(state, 'LoadConfigOverlay')
};
}
export default connect(_mapStateToProps)(BlankPage);

View File

@@ -12,6 +12,12 @@ import { SettingsButton, SETTINGS_TABS } from '../../settings';
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
import Tabs from './Tabs';
/**
* The pattern used to validate room name.
* @type {string}
*/
export const ROOM_NAME_VALIDATE_PATTERN_STR = '^[^?&:\u0022\u0027%#]+$';
/**
* The Web container rendering the welcome page.
*
@@ -53,6 +59,8 @@ class WelcomePage extends AbstractWelcomePage {
*/
this._additionalContentRef = null;
this._roomInputRef = null;
/**
* The HTML Element used as the container for additional toolbar content. Used
* for directly appending the additional content template to the dom.
@@ -88,6 +96,7 @@ class WelcomePage extends AbstractWelcomePage {
this._onRoomChange = this._onRoomChange.bind(this);
this._setAdditionalContentRef
= this._setAdditionalContentRef.bind(this);
this._setRoomInputRef = this._setRoomInputRef.bind(this);
this._setAdditionalToolbarContentRef
= this._setAdditionalToolbarContentRef.bind(this);
this._onTabSelected = this._onTabSelected.bind(this);
@@ -184,9 +193,10 @@ class WelcomePage extends AbstractWelcomePage {
className = 'enter-room-input'
id = 'enter_room_field'
onChange = { this._onRoomChange }
pattern = '^[a-zA-Z0-9=\?]+$'
pattern = { ROOM_NAME_VALIDATE_PATTERN_STR }
placeholder = { this.state.roomPlaceholder }
title = { t('welcomepage.onlyAsciiAllowed') }
ref = { this._setRoomInputRef }
title = { t('welcomepage.roomNameAllowedChars') }
type = 'text'
value = { this.state.room } />
</form>
@@ -194,7 +204,7 @@ class WelcomePage extends AbstractWelcomePage {
<div
className = 'welcome-page-button'
id = 'enter_room_button'
onClick = { this._onJoin }>
onClick = { this._onFormSubmit }>
{ t('welcomepage.go') }
</div>
</div>
@@ -219,7 +229,9 @@ class WelcomePage extends AbstractWelcomePage {
_onFormSubmit(event) {
event.preventDefault();
this._onJoin();
if (!this._roomInputRef || this._roomInputRef.reportValidity()) {
this._onJoin();
}
}
/**
@@ -311,6 +323,18 @@ class WelcomePage extends AbstractWelcomePage {
this._additionalToolbarContentRef = el;
}
/**
* Sets the internal reference to the HTMLInputElement used to hold the
* welcome page input room element.
*
* @param {HTMLInputElement} el - The HTMLElement for the input of the room name on the welcome page.
* @private
* @returns {void}
*/
_setRoomInputRef(el) {
this._roomInputRef = el;
}
/**
* Returns whether or not additional content should be displayed below
* the welcome page's header for entering a room name.

View File

@@ -1,5 +1,7 @@
// @flow
import { StyleSheet } from 'react-native';
import { BoxModel, ColorPalette } from '../../base/styles';
export const PLACEHOLDER_TEXT_COLOR = 'rgba(255, 255, 255, 0.3)';
@@ -38,6 +40,17 @@ export default {
flexDirection: 'row'
},
/**
* View that is rendered when there is no welcome page.
*/
blankPageWrapper: {
...StyleSheet.absoluteFillObject,
alignItems: 'center',
flex: 1,
flexDirection: 'column',
justifyContent: 'center'
},
/**
* Join button style.
*/