Compare commits

...

49 Commits

Author SHA1 Message Date
Aaron van Meerten
e48ddc28eb updated node-sass version for npm 10 2018-11-20 16:39:41 -06:00
Bettenbuk Zoltan
71edea8aac Rearrange recording feature files 2018-11-20 14:42:33 +01:00
Bettenbuk Zoltan
2b1cb75e40 [RN] Update react-native-calendar-events lib
This is required to get rid of a warning after react native update. See related commit in lib.
2018-11-19 14:35:22 +01:00
damencho
216782d606 Commit from translate.jitsi.org by user damencho.: 583 of 583 strings translated (0 fuzzy). 2018-11-14 03:33:01 +00:00
Emil Ivov
c707b82419 Replacing Atlassian with 8x8 2018-11-11 08:43:34 -06:00
Bettenbuk Zoltan
3fdf944763 Fix eslint/jsdoc warnings (doc change only!) 2018-11-08 15:52:34 +01:00
virtuacoplenny
56100d0d5c Merge pull request #3594 from mmoanis/update-docs
Update docs for AbstractRecordButton _mapStateToProps
2018-11-07 09:20:05 -08:00
Leonard Kim
486e8e35d9 ref: move all prop type declaration to flow
For the most part the changes are taking the "static propTypes" declaration off
of components and declaring them as Flow types. Sometimes to support flow some
method signatures had to be added. There are some exceptions in which more had
to be done to tame the beast:
- AbstractVideoTrack: put in additional truthy checks for videoTrack.
- Video: add truthy checks for the _videoElement ref.
- shouldRenderVideoTrack function: Some component could pass null for the
  videoTrack argument and Flow wanted that called out explicitly.
- DisplayName: Add a truthy check for the input ref before acting on it.
- NumbersList: Move array checks inline for Flow to comprehend array methods
  could be called. Add type checks in the Object.entries loop as the value is
  assumed to be a mixed type by Flow.
- AbstractToolbarButton: add additional truthy check for passed in type.
2018-11-07 17:38:10 +01:00
Bettenbuk Zoltan
554974a36d [RN] Fix YouTube channel name list 2018-11-07 16:48:56 +01:00
mmoanis
cd943319d6 Update docs for AbstractRecordButton _mapStateToProps 2018-11-06 11:36:00 +01:00
Дамян Минков
837f496e8f Moves muc definition to be last.
Fixes common problem where people following https://github.com/jitsi/jicofo#secure-domain have a syntax error, forgetting the comma after muc definition.
2018-11-06 09:55:32 +01:00
Aaron van Meerten
c43f7c8979 Merge pull request #3589 from jitsi/max-occupants
Adds max occupant module.
2018-11-03 11:37:14 -05:00
damencho
c9c9f7eac0 Adds max occupant module. 2018-11-03 10:45:59 -05:00
Leonard Kim
5ccc397e47 chore(deps): update react-i18next from 4.8.0 to 7.13.0
None of the breaking changes seemed to affect current
usage of react-i18next and light testing of features
and language switching did not produce issues.

This update is a pre-requisite for removing deprecated react
lifecycle methods, as older versions of react-i18next
have a higher order component that uses the deprecated
componentWillMount, and that issue has been fixed since 7.8.0.
2018-10-29 20:49:53 +01:00
hristoterezov
00cd82d976 fix(analytics-ga): Ignore some events 2018-10-22 16:18:47 -05:00
hristoterezov
61deb74444 fix(welcome-page): Change text 2018-10-22 16:18:30 -05:00
Hristo Terezov
b30008e3a5 feat(welcome-page): Redesign. (#3559)
* feat(welcome-page): Redesign.

* Style adjustments.
2018-10-22 13:49:18 -05:00
bgrozev
62b6737a3f fix: Filter more events for google analytics. (#3557) 2018-10-18 17:15:27 -05:00
damencho
cd77a9176c Make sure we do only one replacement, not one over another for messages. 2018-10-18 23:37:07 +02:00
damencho
2a61968566 Fixes small video's top toolbar background in dark theme. 2018-10-18 09:51:10 +02:00
yanas
be4813e10d Revert "feat(cleanup): remove no longer used FontAwesome"
This reverts commit d3c5756f7a.
2018-10-17 16:27:18 -05:00
Saúl Ibarra Corretgé
ae890dc093 deps: update react-native-webrtc (M69) 2018-10-16 16:32:26 +02:00
Saúl Ibarra Corretgé
9407f562f6 [iOS] Simplify dynamically loading fonts 2018-10-16 14:33:48 +02:00
Saúl Ibarra Corretgé
011a46ce2d [RN] Don't bundle fonts we don't use 2018-10-16 14:33:48 +02:00
Saúl Ibarra Corretgé
8e0bd36ece deps: update react-native-vector-icons 2018-10-16 14:33:48 +02:00
Saúl Ibarra Corretgé
6f8743af3a doc: update Google SignIn integration documentation 2018-10-16 14:01:11 +02:00
paweldomas
58d220d645 deps: update LJM to fix grey overlay
Updates LJM to fix grey overlay with "user is having connectivity
issues" caused by remote track overwrite.
2018-10-16 09:58:42 +02:00
Saúl Ibarra Corretgé
d3c5756f7a feat(cleanup): remove no longer used FontAwesome 2018-10-15 14:14:25 -05:00
Saúl Ibarra Corretgé
5ff1ce5a60 [iOS] Don't show google signin button on iOS <= 10
It doesn't seem to work properly.
2018-10-12 13:17:29 -05:00
Saúl Ibarra Corretgé
843f08f38e [RN] Don't show a beta label for recordings 2018-10-12 12:03:32 -05:00
Saúl Ibarra Corretgé
418575136f [RN] Don't use webClientId on mobile
That is only required if we'd want our backend to authenticate on behalf of our
users. If the app is to authenticate directly it's not needed.
2018-10-12 13:08:20 +02:00
Saúl Ibarra Corretgé
8c97ce2ee9 deps: update react-native-google-signin 2018-10-12 13:08:20 +02:00
Saúl Ibarra Corretgé
b2245729cc [iOS] Update Podfile.lock 2018-10-12 13:08:20 +02:00
Saúl Ibarra Corretgé
cc2b5a261b deps: update lib-jitsi-meet 2018-10-10 18:07:36 +02:00
Saúl Ibarra Corretgé
13c4ec884b deps: update package-lock.json
It's impossible to avoid conflicts on a long lived PR like this. Sigh.
2018-10-10 18:07:36 +02:00
Saúl Ibarra Corretgé
7162080d00 feat(flow): tame the beast 2018-10-10 18:07:36 +02:00
Saúl Ibarra Corretgé
b71adbdf70 deps: update React Native to version 0.57 2018-10-10 18:07:36 +02:00
Saúl Ibarra Corretgé
2ae2f04f0a feat(eslint): tame the beast 2018-10-10 18:07:36 +02:00
Saúl Ibarra Corretgé
4424c456a9 deps: update eslint
This is required due to the Babel update.
2018-10-10 18:07:36 +02:00
Saúl Ibarra Corretgé
cfa1e2f90d deps: update to Babel 7 2018-10-10 18:07:36 +02:00
Saúl Ibarra Corretgé
d290d28248 feat(Audio): fix react warning 2018-10-10 18:07:36 +02:00
Saúl Ibarra Corretgé
0474031a78 deps: update react and styled-components 2018-10-10 18:07:36 +02:00
paweldomas
a57a5ca49d chore(Travis): use XCode 10 image 2018-10-09 17:54:40 -05:00
Bettenbuk Zoltan
d8c1f107da [RN] Add swipe to delete feature 2018-10-09 13:35:06 +02:00
Saúl Ibarra Corretgé
9d27c36d80 config: remove no longer used option 2018-10-08 11:31:47 -05:00
Saúl Ibarra Corretgé
057b300074 feat(Participant): reuse avatar URL generation logic
It was moved to js-utils, so make use of it.
2018-10-05 17:15:00 +02:00
Bettenbuk Zoltan
e164a23cf0 [RN] Fix start recording dialog after dropbox changes 2018-10-04 12:10:28 -05:00
Bettenbuk Zoltan
61456b0d99 Handle all day events in calendar 2018-10-04 16:21:51 +02:00
damencho
df55448a2c Fixes chat image links to reflect html base. 2018-10-03 14:44:33 -05:00
243 changed files with 7901 additions and 7364 deletions

View File

@@ -38,7 +38,23 @@ node_modules/react-native/flow-github/
[options]
emoji=true
esproposal.optional_chaining=enable
esproposal.nullish_coalescing=enable
module.system=haste
module.system.haste.use_name_reducers=true
# get basename
module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1'
# strip .js or .js.flow suffix
module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1'
# strip .ios suffix
module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1'
module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1'
module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1'
module.system.haste.paths.blacklist=.*/__tests__/.*
module.system.haste.paths.blacklist=.*/__mocks__/.*
module.system.haste.paths.blacklist=<PROJECT_ROOT>/node_modules/react-native/Libraries/Animated/src/polyfills/.*
module.system.haste.paths.whitelist=<PROJECT_ROOT>/node_modules/react-native/Libraries/.*
munge_underscores=true
@@ -67,4 +83,4 @@ module.file_ext=.jsx
module.file_ext=.json
[version]
^0.67.0
^0.78.0

View File

@@ -1,4 +1,4 @@
osx_image: xcode9.4
osx_image: xcode10
language: objective-c
script:
- "./ios/travis-ci/build-ipa.sh"

View File

@@ -130,7 +130,7 @@ equivalent to these of a direct one-to-one WebRTC call. This is what's unique to
Jitsi Meet in terms of security.
The [meet.jit.si](https://meet.jit.si) service is maintained by the Jitsi team
at [Atlassian](https://atlassian.com).
at [8x8](https://8x8.com).
## Mobile app
Jitsi Meet is also available as a React Native app for Android and iOS.

View File

@@ -126,9 +126,12 @@
return;
}
// The e2e rtt are not useful in GA, and there are too many of them.
// We just filter them out for now.
if (event.action === 'e2e_rtt') {
const ignoredEvents
= [ 'e2e_rtt', 'rtp.stats', 'rtt.by.region', 'available.device',
'stream.switch.delay', 'ice.state.changed', 'ice.duration' ];
// Temporary removing some of the events that are too noisy.
if (ignoredEvents.indexOf(event.action) !== -1) {
return;
}

View File

@@ -2,6 +2,7 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
applicationId 'org.jitsi.meet'
@@ -44,9 +45,10 @@ android {
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.2.0'
compile 'com.google.android.gms:play-services-auth:15.0.0'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "com.android.support:support-v4:${rootProject.ext.supportLibVersion}"
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
implementation 'com.google.android.gms:play-services-auth:15.0.0'
implementation project(':sdk')

View File

@@ -7,7 +7,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.android.tools.build:gradle:3.1.4'
classpath 'com.google.gms:google-services:3.2.1'
// NOTE: Do not place your application dependencies here; they belong
@@ -153,10 +153,11 @@ allprojects {
}
ext {
buildToolsVersion = "26.0.2"
compileSdkVersion = 26
buildToolsVersion = "27.0.3"
compileSdkVersion = 27
minSdkVersion = 21
targetSdkVersion = 26
supportLibVersion = "27.1.1"
// The Maven artifact groupdId of the third-party react-native modules which
// Jitsi Meet SDK for Android depends on and which are not available in
@@ -179,3 +180,8 @@ subprojects { subproject ->
}
}
}
task wrapper(type: Wrapper) {
gradleVersion = '4.4'
distributionUrl = distributionUrl.replace("bin", "all")
}

View File

@@ -17,5 +17,4 @@
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.useDeprecatedNdk=true
version=1
version=1

Binary file not shown.

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

110
android/gradlew vendored
View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
##############################################################################
##
@@ -6,47 +6,6 @@
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
@@ -61,9 +20,49 @@ while [ -h "$PRG" ] ; do
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -90,7 +89,7 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@@ -114,6 +113,7 @@ fi
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
@@ -154,11 +154,19 @@ if $cygwin ; then
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
APP_ARGS=$(save "$@")
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

14
android/gradlew.bat vendored
View File

@@ -8,14 +8,14 @@
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@@ -46,10 +46,9 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
@@ -60,11 +59,6 @@ set _SKIP=2
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line

View File

@@ -19,136 +19,144 @@ android {
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
implementation fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:27.0.2'
compile 'com.dropbox.core:dropbox-core-sdk:3.0.8'
compile 'com.facebook.react:react-native:+'
implementation "com.android.support:support-v4:${rootProject.ext.supportLibVersion}"
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
compile project(':react-native-background-timer')
compile project(':react-native-fast-image')
compile(project(":react-native-google-signin")) {
implementation 'com.dropbox.core:dropbox-core-sdk:3.0.8'
api 'com.facebook.react:react-native:+'
implementation project(':react-native-background-timer')
implementation project(':react-native-calendar-events')
implementation(project(':react-native-fast-image')) {
exclude group: 'com.android.support'
}
implementation(project(":react-native-google-signin")) {
exclude group: 'com.google.android.gms'
exclude group: 'com.android.support'
}
compile project(':react-native-immersive')
compile project(':react-native-keep-awake')
compile project(':react-native-linear-gradient')
compile project(':react-native-locale-detector')
compile project(':react-native-sound')
compile project(':react-native-vector-icons')
compile project(':react-native-webrtc')
compile project(':react-native-calendar-events')
implementation project(':react-native-immersive')
implementation project(':react-native-keep-awake')
implementation project(':react-native-linear-gradient')
implementation project(':react-native-locale-detector')
implementation project(':react-native-sound')
implementation project(':react-native-vector-icons')
implementation project(':react-native-webrtc')
testCompile 'junit:junit:4.12'
testImplementation 'junit:junit:4.12'
}
// Build process helpers
//
void runBefore(String dependentTaskName, Task task) {
Task dependentTask = tasks.findByPath(dependentTaskName);
if (dependentTask != null) {
dependentTask.dependsOn task
// Here we bundle all assets, resources and React files. We cannot use the
// react.gradle file provided by react-native because it's designed to be used
// in an application (it taps into applicationVariants, but the SDK is a library
// so we need libraryVariants instead).
android.libraryVariants.all { def variant ->
// Create variant and target names
def targetName = variant.name.capitalize()
def targetPath = variant.dirName
// React js bundle directories
def jsBundleDir = file("$buildDir/generated/assets/react/${targetPath}")
def resourcesDir = file("$buildDir/generated/res/react/${targetPath}")
def jsBundleFile = file("$jsBundleDir/index.android.bundle")
def currentBundleTask = tasks.create(
name: "bundle${targetName}JsAndAssets",
type: Exec) {
group = "react"
description = "bundle JS and assets for ${targetName}."
// Create dirs if they are not there (e.g. the "clean" task just ran)
doFirst {
jsBundleDir.deleteDir()
jsBundleDir.mkdirs()
resourcesDir.deleteDir()
resourcesDir.mkdirs()
}
// Set up inputs and outputs so gradle can cache the result
def reactRoot = file("${projectDir}/../../")
inputs.files fileTree(dir: reactRoot, excludes: ["android/**", "ios/**"])
outputs.dir jsBundleDir
outputs.dir resourcesDir
// Set up the call to the react-native cli
workingDir reactRoot
// Set up dev mode
def devEnabled = !targetName.toLowerCase().contains("release")
// Run the bundler
commandLine(
"node",
"node_modules/react-native/local-cli/cli.js",
"bundle",
"--platform", "android",
"--dev", "${devEnabled}",
"--reset-cache",
"--entry-file", "index.android.js",
"--bundle-output", jsBundleFile,
"--assets-dest", resourcesDir)
// Disable bundling on dev builds
enabled !devEnabled
}
}
gradle.projectsEvaluated {
android.buildTypes.all { buildType ->
def buildNameCapitalized = "${buildType.name.capitalize()}"
def bundlePath = "${buildDir}/intermediates/bundles/${buildType.name}"
currentBundleTask.ext.generatedResFolders = files(resourcesDir).builtBy(currentBundleTask)
currentBundleTask.ext.generatedAssetsFolders = files(jsBundleDir).builtBy(currentBundleTask)
// Bundle fonts in react-native-vector-icons.
variant.registerGeneratedResFolders(currentBundleTask.generatedResFolders)
variant.mergeResources.dependsOn(currentBundleTask)
def assetsDir = variant.mergeAssets.outputDir
variant.mergeAssets.doLast {
// Bundle fonts
//
def currentFontTask = tasks.create(
name: "copy${buildNameCapitalized}Fonts",
type: Copy) {
copy {
from("${projectDir}/../../fonts/jitsi.ttf")
from("${projectDir}/../../node_modules/react-native-vector-icons/Fonts/")
into("${bundlePath}/assets/fonts")
into("${assetsDir}/fonts")
}
currentFontTask.dependsOn("merge${buildNameCapitalized}Resources")
currentFontTask.dependsOn("merge${buildNameCapitalized}Assets")
runBefore("processArmeabi-v7a${buildNameCapitalized}Resources", currentFontTask)
runBefore("processX86${buildNameCapitalized}Resources", currentFontTask)
runBefore("processUniversal${buildNameCapitalized}Resources", currentFontTask)
runBefore("process${buildNameCapitalized}Resources", currentFontTask)
def currentSoundsTask = tasks.create(
name: "copy${buildNameCapitalized}Sounds",
type: Copy) {
from("${projectDir}/../../sounds/joined.wav")
from("${projectDir}/../../sounds/left.wav")
from("${projectDir}/../../sounds/outgoingRinging.wav")
from("${projectDir}/../../sounds/outgoingStart.wav")
from("${projectDir}/../../sounds/recordingOn.mp3")
from("${projectDir}/../../sounds/recordingOff.mp3")
from("${projectDir}/../../sounds/rejected.wav")
into("${bundlePath}/assets/sounds")
}
currentSoundsTask.dependsOn("merge${buildNameCapitalized}Resources")
currentSoundsTask.dependsOn("merge${buildNameCapitalized}Assets")
runBefore("processArmeabi-v7a${buildNameCapitalized}Resources", currentSoundsTask)
runBefore("processX86${buildNameCapitalized}Resources", currentSoundsTask)
runBefore("processUniversal${buildNameCapitalized}Resources", currentSoundsTask)
runBefore("process${buildNameCapitalized}Resources", currentSoundsTask)
// Bundle JavaScript and React resources.
// (adapted from react-native/react.gradle)
// Bundle sounds
//
// React JS bundle directories
def jsBundleDir = file("${bundlePath}/assets")
def resourcesDir = file("${bundlePath}/res/merged")
def jsBundleFile = file("${jsBundleDir}/index.android.bundle")
// Bundle task name for variant.
def bundleJsAndAssetsTaskName = "bundle${buildNameCapitalized}JsAndAssets"
def currentBundleTask = tasks.create(
name: bundleJsAndAssetsTaskName,
type: Exec) {
// Set up inputs and outputs so gradle can cache the result.
def reactRoot = file("${projectDir}/../../")
inputs.files fileTree(dir: reactRoot, excludes: ['android/**', 'ios/**'])
outputs.dir jsBundleDir
outputs.dir resourcesDir
// Set up the call to the react-native cli.
workingDir reactRoot
// Create JS bundle
def devEnabled = !buildNameCapitalized.toLowerCase().contains('release')
commandLine(
'node',
'node_modules/react-native/local-cli/cli.js',
'bundle',
'--assets-dest', resourcesDir,
'--bundle-output', jsBundleFile,
'--dev', "${devEnabled}",
'--entry-file', 'index.android.js',
'--platform', 'android',
'--reset-cache')
// Disable bundling on dev builds
enabled !devEnabled
copy {
from("${projectDir}/../../sounds/joined.wav")
from("${projectDir}/../../sounds/left.wav")
from("${projectDir}/../../sounds/outgoingRinging.wav")
from("${projectDir}/../../sounds/outgoingStart.wav")
from("${projectDir}/../../sounds/recordingOn.mp3")
from("${projectDir}/../../sounds/recordingOff.mp3")
from("${projectDir}/../../sounds/rejected.wav")
into("${assetsDir}/sounds")
}
// Hook bundle${productFlavor}${buildType}JsAndAssets into the android build process
currentBundleTask.dependsOn("merge${buildNameCapitalized}Resources")
currentBundleTask.dependsOn("merge${buildNameCapitalized}Assets")
// Copy React assets
//
if (currentBundleTask.enabled) {
copy {
from(jsBundleDir)
into(assetsDir)
}
}
}
runBefore("processArmeabi-v7a${buildNameCapitalized}Resources", currentBundleTask)
runBefore("processX86${buildNameCapitalized}Resources", currentBundleTask)
runBefore("processUniversal${buildNameCapitalized}Resources", currentBundleTask)
runBefore("process${buildNameCapitalized}Resources", currentBundleTask)
variant.mergeResources.doLast {
// Copy React resources
//
if (currentBundleTask.enabled) {
copy {
from(resourcesDir)
into(variant.mergeResources.outputDir)
}
}
}
}
publishing {
publications {
aarArchive(MavenPublication) {

View File

@@ -18,9 +18,6 @@ var config = {
// XMPP domain.
domain: 'jitsi-meet.example.com',
// XMPP MUC domain. FIXME: use XEP-0030 to discover it.
muc: 'conference.jitsi-meet.example.com'
// When using authentication, domain for guest users.
// anonymousdomain: 'guest.example.com',
@@ -35,6 +32,9 @@ var config = {
// Focus component domain. Defaults to focus.<domain>.
// focus: 'focus.jitsi-meet.example.com',
// XMPP MUC domain. FIXME: use XEP-0030 to discover it.
muc: 'conference.jitsi-meet.example.com'
},
// BOSH URL. FIXME: use XEP-0156 to discover it.
@@ -241,10 +241,6 @@ var config = {
// Disable hiding of remote thumbnails when in a 1-on-1 conference call.
// disable1On1Mode: false,
// The minimum value a video's height (or width, whichever is smaller) needs
// to be in order to be considered high-definition.
minHDHeight: 540,
// Default language for the user interface.
// defaultLanguage: 'en',

117
css/_meetings_list.scss Normal file
View File

@@ -0,0 +1,117 @@
.meetings-list {
font-size: 14px;
color: #253858;
line-height: 20px;
text-align: left;
text-overflow: ellipsis;
display: flex;
flex-direction: column;
position: relative;
width: 100%;
height: 100%;
overflow: auto;
.meetings-list-empty {
text-align: center;
align-items: center;
justify-content: center;
display: flex;
flex-grow: 1;
flex-direction: column;
.description {
font-size: 16px;
padding: 20px;
}
}
.button {
background: #0074E0;
border-radius: 4px;
color: #FFFFFF;
display: flex;
justify-content: center;
align-items: center;
padding: 5px 10px;
cursor: pointer;
}
.item {
background: rgba(255,255,255,0.50);
box-sizing: border-box;
display: inline-flex;
margin-top: 5px;
min-height: 92px;
width: 100%;
word-break: break-word;
display: flex;
flex-direction: row;
text-align: left;
&:first-child {
margin-top: 0px;
}
.left-column {
display: flex;
flex-direction: column;
width: 140px;
flex-grow: 0;
padding-left: 30px;
padding-top: 25px;
.date {
font-weight: bold;
padding-bottom: 5px;
}
}
.right-column {
display: flex;
flex-direction: column;
flex-grow: 1;
padding-left: 30px;
padding-top: 25px;
.title {
font-size: 16px;
font-weight: bold;
padding-bottom: 5px;
}
}
.actions {
display: flex;
align-items: center;
justify-content: center;
flex-grow: 0;
padding-right: 30px;
}
&.with-click-handler {
cursor: pointer;
}
&.with-click-handler:hover {
background-color: #75A7E7;
}
.add-button {
width: 30px;
height: 30px;
padding: 0px;
}
i {
cursor: inherit;
}
.join-button {
display: none;
}
&:hover .join-button {
display: block
}
}
}

View File

@@ -144,7 +144,7 @@ $watermarkHeight: 74px;
/**
* Welcome page variables.
*/
$welcomePageDescriptionColor: #E6EDFA;
$welcomePageDescriptionColor: #fff;
$welcomePageFontFamily: inherit;
$welcomePageHeaderBackground: #1D69D4;
$welcomePageHeaderBackground: linear-gradient(-90deg, #1251AE 0%, #0074FF 50%, #1251AE 100%);
$welcomePageTitleColor: #fff;

View File

@@ -86,7 +86,7 @@
* wrapper needed before we're able to move all top toolbar indicators
* creation to react.
*/
.ckAJgx {
.sc-ifAKCX {
background: none;
}
}

View File

@@ -4,7 +4,7 @@ body.welcome-page {
}
.welcome {
background-color: $welcomePageHeaderBackground;
background-image: $welcomePageHeaderBackground;
display: flex;
flex-direction: column;
font-family: $welcomePageFontFamily;
@@ -24,8 +24,8 @@ body.welcome-page {
.header-text {
display: flex;
flex-direction: column;
margin-top: $watermarkHeight + 80;
margin-bottom: 36px;
margin-top: $watermarkHeight + 35;
margin-bottom: 35px;
max-width: calc(100% - 40px);
width: 650px;
z-index: $zindex2;
@@ -35,7 +35,6 @@ body.welcome-page {
color: $welcomePageTitleColor;
font-size: 2.5rem;
font-weight: 500;
letter-spacing: 0;
line-height: 1.18;
margin-bottom: 16px;
}
@@ -49,41 +48,118 @@ body.welcome-page {
}
#enter_room {
align-items: center;
display: flex;
align-items: center;
max-width: calc(100% - 40px);
margin-bottom: 20px;
position: relative;
width: 650px;
width: 680px;
z-index: $zindex2;
background-color: #fff;
padding: 25px 30px;
.enter-room-input {
display: inline-block;
margin-right: 8px;
.enter-room-input-container {
width: 100%;
padding-right: 8px;
padding-bottom: 5px;
text-align: left;
color: #253858;
height: fit-content;
border-width: 0px 0px 2px 0px;
border-style: solid;
border-image: linear-gradient(to right, #dee1e6, #fff) 1;
.enter-room-title {
font-size: 18px;
font-weight: bold;
padding-bottom: 5px;
}
.enter-room-input {
border: none;
display: inline-block;
width: 100%;
font-size: 14px;
}
::placeholder {
color: #253858;
}
}
}
.tab-container {
font-size: 16px;
position: relative;
text-align: left;
width: 650px;
min-height: 354px;
width: 710px;
background: #75A7E7;
display: flex;
flex-direction: column;
.tab-content{
margin: 5px 0px;
overflow: hidden;
flex-grow: 1;
position: relative;
> * {
position: absolute;
}
}
.tab-buttons {
font-size: 18px;
color: #FFFFFF;
display: flex;
flex-grow: 0;
flex-direction: row;
min-height: 54px;
width: 100%;
.tab {
text-align: center;
background: rgba(9,30,66,0.37);
height: 55px;
line-height: 54px;
flex-grow: 1;
cursor: pointer;
&.selected, &:hover {
background: rgba(9,30,66,0.71);
}
&:last-child {
margin-left: 1px;
}
}
}
}
}
.welcome-page-button {
font-size: 16px;
width: 51px;
height: 35px;
font-size: 14px;
background: #0074E0;
border-radius: 4px;
color: #FFFFFF;
text-align: center;
vertical-align: middle;
line-height: 35px;
cursor: pointer;
}
.welcome-page-settings {
color: $welcomePageDescriptionColor;
position: absolute;
right: 10px;
top: 32px;
right: 32px;
z-index: $zindex2;
* {
cursor: pointer;
font-size: 32px;
}
}

View File

@@ -76,6 +76,7 @@
@import 'modals/invite/add-people';
@import 'deep-linking/main';
@import 'transcription-subtitles';
@import '_meetings_list.scss';
@import 'navigate_section_list';
@import 'third-party-branding/google';
@import 'third-party-branding/microsoft';

View File

@@ -1,22 +1,22 @@
# Setting up Google Authentication
- Create a Firebase project here: https://firebase.google.com/. You'll need a
signed Android build for that, that can be a debug auto-signed build too, just
retrieve the signing hash.
signed Android build for that, that can be a debug self-signed build too, just
retrieve the signing hash. The key hash of an already signed ap can be obtained
as follows (on macOS): ```keytool -list -printcert -jarfile the-app.apk```
- Place the generated ```google-services.json``` file in ```android/app```
for Android and the ```GoogleService-Info.plist``` into ```ios/app/src``` for
iOS (you can stop at that step, no need for the driver and the code changes they
suggest in the wizard).
- You may want to exclude these files in YOUR GIT config (do not exclude them in
the ```.gitignore``` of the application itself!).
- Your WEB and iOS client IDs are auto generated during the Firebase project
creation. Find them in the Google Developer console:
https://console.developers.google.com/
- Make sure your config reflects these IDs so then the Redux state of the
feature ```features/base/config``` contains variables
```googleApiApplicationClientID``` and ```googleApiIOSClientID``` with the
respective values.
- Add your iOS client ID as an application URL schema into
```ios/app/src/Info.plist``` (replacing placeholder).
- Enable YouTube API access on the developer console (see above) for live
- Your web client ID is auto generated during the Firebase project
creation. Find them in the Google Developer console
(https://console.developers.google.com/)
- Make sure your config reflects this ID by setting
```googleApiApplicationClientID``` in config.js.
- Add your iOS client ID (the REVERSED_CLIENT_ID in the plist file) as an
application URL schema into ```ios/app/src/Info.plist```
(replacing placeholder).
- Enable YouTube API access on the developer console (see above) to enable live
streaming.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 889 B

View File

@@ -1,13 +1,13 @@
PODS:
- boost-for-react-native (1.63.0)
- DoubleConversion (1.1.5)
- DoubleConversion (1.1.6)
- FLAnimatedImage (1.0.12)
- Folly (2016.09.26.00):
- Folly (2016.10.31.00):
- boost-for-react-native
- DoubleConversion
- glog
- glog (0.3.4)
- GoogleSignIn (4.2.0):
- glog (0.3.5)
- GoogleSignIn (4.3.0):
- "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)"
- "GoogleToolboxForMac/NSString+URLArguments (~> 2.1)"
- GTMOAuth2 (~> 1.0)
@@ -27,9 +27,9 @@ PODS:
- GTMSessionFetcher/Core (1.2.0)
- GTMSessionFetcher/Full (1.2.0):
- GTMSessionFetcher/Core (= 1.2.0)
- ObjectiveDropboxOfficial (3.9.1)
- React (0.55.4):
- React/Core (= 0.55.4)
- ObjectiveDropboxOfficial (3.9.2)
- React (0.57.1):
- React/Core (= 0.57.1)
- react-native-background-timer (2.0.0):
- React
- react-native-calendar-events (1.6.2):
@@ -43,48 +43,48 @@ PODS:
- React
- react-native-locale-detector (1.0.0):
- React
- react-native-webrtc (1.63.0):
- react-native-webrtc (1.67.1):
- React
- React/Core (0.55.4):
- yoga (= 0.55.4.React)
- React/CxxBridge (0.55.4):
- Folly (= 2016.09.26.00)
- React/Core (0.57.1):
- yoga (= 0.57.1.React)
- React/CxxBridge (0.57.1):
- Folly (= 2016.10.31.00)
- React/Core
- React/cxxreact
- React/cxxreact (0.55.4):
- React/cxxreact (0.57.1):
- boost-for-react-native (= 1.63.0)
- Folly (= 2016.09.26.00)
- Folly (= 2016.10.31.00)
- React/jschelpers
- React/jsinspector
- React/DevSupport (0.55.4):
- React/DevSupport (0.57.1):
- React/Core
- React/RCTWebSocket
- React/fishhook (0.55.4)
- React/jschelpers (0.55.4):
- Folly (= 2016.09.26.00)
- React/fishhook (0.57.1)
- React/jschelpers (0.57.1):
- Folly (= 2016.10.31.00)
- React/PrivateDatabase
- React/jsinspector (0.55.4)
- React/PrivateDatabase (0.55.4)
- React/RCTActionSheet (0.55.4):
- React/jsinspector (0.57.1)
- React/PrivateDatabase (0.57.1)
- React/RCTActionSheet (0.57.1):
- React/Core
- React/RCTAnimation (0.55.4):
- React/RCTAnimation (0.57.1):
- React/Core
- React/RCTBlob (0.55.4):
- React/RCTBlob (0.57.1):
- React/Core
- React/RCTImage (0.55.4):
- React/RCTImage (0.57.1):
- React/Core
- React/RCTNetwork
- React/RCTLinkingIOS (0.55.4):
- React/RCTLinkingIOS (0.57.1):
- React/Core
- React/RCTNetwork (0.55.4):
- React/RCTNetwork (0.57.1):
- React/Core
- React/RCTText (0.55.4):
- React/RCTText (0.57.1):
- React/Core
- React/RCTWebSocket (0.55.4):
- React/RCTWebSocket (0.57.1):
- React/Core
- React/fishhook
- React/RCTBlob
- RNGoogleSignin (1.0.0-rc3):
- RNGoogleSignin (1.0.0-rc6):
- GoogleSignIn
- React
- RNSound (0.10.9):
@@ -92,13 +92,13 @@ PODS:
- RNSound/Core (= 0.10.9)
- RNSound/Core (0.10.9):
- React/Core
- RNVectorIcons (4.4.2):
- RNVectorIcons (6.0.2):
- React
- SDWebImage/Core (4.4.2)
- SDWebImage/GIF (4.4.2):
- FLAnimatedImage (~> 1.0)
- SDWebImage/Core
- yoga (0.55.4.React)
- yoga (0.57.1.React)
DEPENDENCIES:
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
@@ -169,16 +169,16 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
DoubleConversion: e22e0762848812a87afd67ffda3998d9ef29170c
DoubleConversion: bb338842f62ab1d708ceb63ec3d999f0f3d98ecd
FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
Folly: 211775e49d8da0ca658aebc8eab89d642935755c
glog: 1de0bb937dccdc981596d3b5825ebfb765017ded
GoogleSignIn: 591e46382014e591269f862ba6e7bc0fbd793532
Folly: c89ac2d5c6ab169cd7397ef27485c44f35f742c7
glog: e8acf0ebbf99759d3ff18c86c292a5898282dcde
GoogleSignIn: 11183592dc63e105475c7305a325045ff95e02b7
GoogleToolboxForMac: 91c824d21e85b31c2aae9bb011c5027c9b4e738f
GTMOAuth2: c77fe325e4acd453837e72d91e3b5f13116857b2
GTMSessionFetcher: 0c4baf0a73acd0041bf9f71ea018deedab5ea84e
ObjectiveDropboxOfficial: 274ce69d66286c94416daf1da5237c55e105e8c0
React: aa2040dbb6f317b95314968021bd2888816e03d5
ObjectiveDropboxOfficial: aa792e0556ceb7b72955fa29a2709072f6e35fd9
React: 1fe0eb13d90b625d94c3b117c274dcfd2e760e11
react-native-background-timer: 63dcbf37dbcf294b5c6c071afcdc661fa06a7594
react-native-calendar-events: fe6fbc8ed337a7423c98f2c9012b25f20444de09
react-native-fast-image: cba3d9bf9c2cf8ddb643d887a686c53a5dd90a2c
@@ -187,9 +187,9 @@ SPEC CHECKSUMS:
react-native-webrtc: 31b6d3f1e3e2ce373aa43fd682b04367250f807d
RNGoogleSignin: 44debd8c359a662c0e2d585952e88b985bf78008
RNSound: b360b3862d3118ed1c74bb9825696b5957686ac4
RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44
RNVectorIcons: 8c52e1e8da1153613fdef44748e865c25556cb9c
SDWebImage: 624d6e296c69b244bcede364c72ae0430ac14681
yoga: a23273df0088bf7f2bb7e5d7b00044ea57a2a54a
yoga: b1ce48b6cf950b98deae82838f5173ea7cf89e85
PODFILE CHECKSUM: cf8276ba4b0933b24c6082a25a5f4eabe0ba4ea6

View File

@@ -138,7 +138,7 @@
83CBB9F71A601CBA00E9B192 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0920;
LastUpgradeCheck = 1000;
ORGANIZATIONNAME = Facebook;
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
@@ -327,12 +327,14 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
@@ -383,12 +385,14 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0920"
LastUpgradeVersion = "1000"
version = "1.3">
<BuildAction
parallelizeBuildables = "NO"
@@ -40,7 +40,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
@@ -60,7 +59,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View File

@@ -3,14 +3,17 @@
# This script is executed from Xcode to start the React packager for Debug
# targets.
export RCT_METRO_PORT="${RCT_METRO_PORT:=8081}"
echo "export RCT_METRO_PORT=${RCT_METRO_PORT}" > "${SRCROOT}/../../node_modules/react-native/scripts/.packager.env"
if [[ "$CONFIGURATION" = "Debug" ]]; then
if nc -w 5 -z localhost 8081 ; then
if ! curl -s "http://localhost:8081/status" | grep -q "packager-status:running" ; then
echo "Port 8081 already in use, packager is either not running or not running correctly"
if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then
if ! curl -s "http://localhost:${RCT_METRO_PORT}/status" | grep -q "packager-status:running" ; then
echo "Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly"
exit 2
fi
else
open -g "$SRCROOT/../../node_modules/react-native/scripts/launchPackager.command" || echo "Can't start packager automatically"
fi
fi

View File

@@ -14,7 +14,6 @@
0B49424520AD8DBD00BD2DE0 /* outgoingStart.wav in Resources */ = {isa = PBXBuildFile; fileRef = 0B49424320AD8DBD00BD2DE0 /* outgoingStart.wav */; };
0B49424620AD8DBD00BD2DE0 /* outgoingRinging.wav in Resources */ = {isa = PBXBuildFile; fileRef = 0B49424420AD8DBD00BD2DE0 /* outgoingRinging.wav */; };
0B7C2CFD200F51D60060D076 /* LaunchOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B7C2CFC200F51D60060D076 /* LaunchOptions.m */; };
0B93EF7B1EC608550030D24D /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B93EF7A1EC608550030D24D /* CoreText.framework */; };
0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */; };
0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */; };
0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */; };
@@ -114,7 +113,6 @@
files = (
0BB9AD791F5EC6D7001C08DB /* Intents.framework in Frameworks */,
0BB9AD771F5EC6CE001C08DB /* CallKit.framework in Frameworks */,
0B93EF7B1EC608550030D24D /* CoreText.framework in Frameworks */,
0F65EECE1D95DA94561BB47E /* libPods-JitsiMeet.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -285,6 +283,7 @@
0BD906E31EC0C00300C8C18E /* Resources */,
0BCA49651EC4B77500B793EE /* Package React bundle */,
C7BC10B338C94EEB98048E64 /* [CP] Copy Pods Resources */,
0B64F7BE2175DFEA005009CD /* Remove unneeded fonts */,
);
buildRules = (
);
@@ -301,7 +300,7 @@
0BD906DC1EC0C00300C8C18E /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0920;
LastUpgradeCheck = 1000;
ORGANIZATIONNAME = Jitsi;
TargetAttributes = {
0BD906E41EC0C00300C8C18E = {
@@ -350,6 +349,24 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
0B64F7BE2175DFEA005009CD /* Remove unneeded fonts */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Remove unneeded fonts";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# We need to manually do this because react-native-vecotr-icons lists fonts as resources in the Pod spec file\n# so they are automatically added.\n\nshopt -s extglob\n\nrm -f ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/!(jitsi).ttf\n";
};
0BCA49651EC4B77500B793EE /* Package React bundle */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -391,10 +408,14 @@
"${SRCROOT}/../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet-resources.sh",
"${PODS_ROOT}/GTMOAuth2/Source/Touch/GTMOAuth2ViewTouch.xib",
"${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Feather.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Foundation.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf",
@@ -407,10 +428,14 @@
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GTMOAuth2ViewTouch.nib",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AntDesign.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Feather.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Solid.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Foundation.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Ionicons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialCommunityIcons.ttf",
@@ -470,6 +495,7 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -477,6 +503,7 @@
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
@@ -529,6 +556,7 @@
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
@@ -536,6 +564,7 @@
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
@@ -561,6 +590,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0920"
LastUpgradeVersion = "1000"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
@@ -26,7 +26,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
@@ -37,7 +36,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View File

@@ -18,11 +18,6 @@
<string>1.9.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>JitsiMeetFonts</key>
<array>
<string>FontAwesome.ttf</string>
<string>jitsi.ttf</string>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>dbapi-2</string>

View File

@@ -14,7 +14,6 @@
* limitations under the License.
*/
#import <CoreText/CoreText.h>
#import <Intents/Intents.h>
#include <mach/mach_time.h>
@@ -53,35 +52,6 @@ RCTFatalHandler _RCTFatal = ^(NSError *error) {
}
};
/**
* Helper function to dynamically load custom fonts. The `UIAppFonts` key in the
* plist file doesn't work for frameworks, so fonts have to be manually loaded.
*/
void loadCustomFonts(Class clazz) {
NSBundle *bundle = [NSBundle bundleForClass:clazz];
NSArray *fonts = [bundle objectForInfoDictionaryKey:@"JitsiMeetFonts"];
for (NSString *item in fonts) {
NSString *fontName = [item stringByDeletingPathExtension];
NSString *fontExt = [item pathExtension];
NSString *fontPath = [bundle pathForResource:fontName ofType:fontExt];
NSData *inData = [NSData dataWithContentsOfFile:fontPath];
CFErrorRef error;
CGDataProviderRef provider
= CGDataProviderCreateWithCFData((__bridge CFDataRef)inData);
CGFontRef font = CGFontCreateWithDataProvider(provider);
if (!CTFontManagerRegisterGraphicsFont(font, &error)) {
CFStringRef errorDescription = CFErrorCopyDescription(error);
NSLog(@"Failed to load font: %@", errorDescription);
CFRelease(errorDescription);
}
CFRelease(font);
CFRelease(provider);
}
}
/**
* Helper function to register a fatal error handler for React. Our handler
* won't kill the process, it will swallow JS errors and print stack traces
@@ -410,9 +380,6 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
= [[RCTBridgeWrapper alloc] initWithLaunchOptions:_launchOptions];
views = [NSMapTable strongToWeakObjectsMapTable];
// Dynamically load custom bundled fonts.
loadCustomFonts(self.class);
// Register a fatal error handler for React.
registerFatalErrorHandler();
});

View File

@@ -1,11 +1,18 @@
{
"en": "Anglais",
"az": "Azerbaïdjanais",
"bg": "Bulgare",
"cs": "Tchèque",
"de": "Allemand",
"el": "Grec",
"eo": "Espéranto",
"es": "Espagnol",
"fr": "Français",
"hy": "Arménien",
"it": "Italien",
"ja": "Japonais",
"ko": "Coréen",
"nb": "Norvégien Bokmal",
"oc": "Occitan",
"pl": "Polonais",
"ptBR": "Portugais (Brésil)",
@@ -14,7 +21,6 @@
"sl": "Slovène",
"sv": "Suédois",
"tr": "Turc",
"zhCN": "Chinois (Chine)",
"nb": "Norvégien Bokmal",
"eo": "Espéranto"
"vi": "Vietnamien",
"zhCN": "Chinois (Chine)"
}

View File

@@ -1,6 +1,5 @@
{
"contactlist": "__count__ Membres",
"contactlist_plural": "",
"contactlist_plural": "__count__ Membres",
"passwordSetRemotely": "défini par un autre membre",
"poweredby": "Produit par",
"inviteUrlDefaultMsg": "Votre conférence est en cours de création...",
@@ -23,6 +22,7 @@
"react-nativeGrantPermissions": "Sélectionnez <b><i>Autoriser</i></b> lorsque votre navigateur demande des autorisations.",
"chromeGrantPermissions": "Sélectionnez <b><i>Autoriser</i></b> lorsque votre navigateur demande des autorisations.",
"androidGrantPermissions": "Sélectionnez <b><i>Autoriser</i></b> lorsque votre navigateur demande des autorisations.",
"electronGrantPermissions": "Merci d'autoriser le partage de votre camera et microphone",
"firefoxGrantPermissions": "Sélectionnez <b><i>Partager le périphérique sélectionné</i></b> lorsque votre navigateur demande des autorisations.",
"operaGrantPermissions": "Sélectionnez <b><i>Autoriser</i></b> lorsque votre navigateur demande des autorisations.",
"iexplorerGrantPermissions": "Sélectionnez <b><i>OK</i></b> quand le navigateur demande les permissions.",
@@ -35,57 +35,42 @@
"raiseHand": "Lever ou baisser la main",
"pushToTalk": "Appuyer pour parler",
"toggleScreensharing": "Basculer entre la caméra et le partage d'écran",
"toggleFilmstrip": "Afficher ou cacher la vidéo",
"toggleShortcuts": "Afficher ou masquer ce menu d'aide",
"toggleFilmstrip": "Afficher ou masquer les vignettes vidéos",
"toggleShortcuts": "Afficher ou masquer les raccourcis clavier",
"focusLocal": "Épingler ma vidéo",
"focusRemote": "Épingler la vidéo des autres",
"focusRemote": "Épingler la vidéo de quelqu'un d'autre",
"toggleChat": "Ouvrir ou fermer le panneau de conversation",
"mute": "Activer ou désactiver le microphone",
"fullScreen": "Activer ou Désactiver le plein écran",
"fullScreen": "Activer / Désactiver le mode plein écran",
"videoMute": "Démarrer ou arrêter votre caméra",
"showSpeakerStats": "Afficher les statistiques de l'interlocuteur"
"showSpeakerStats": "Afficher les statistiques de l'interlocuteur",
"localRecording": "Afficher ou masquer les commandes de l'enregistrement local"
},
"welcomepage": {
"disable": "Ne plus afficher cette page",
"feature1": {
"content": "Aucun téléchargement requis. __app__ s'utilise directement depuis votre navigateur. Partager simplement l'URL de votre conférence avec les autres pour commencer.",
"title": "Simple à utiliser"
"accessibilityLabel": {
"join": "Touchez pour rejoindre",
"roomname": "Saisissez un nom de salle"
},
"feature2": {
"content": "Les vidéo conférences à plusieurs participants nécessitent moins de 128 kbps. Le partage d'écran et les conférences avec seulement de l'audio sont possibles avec beaucoup moins de débit.",
"title": "Bande passante faible"
},
"feature3": {
"content": "__app__ est sous licence Apache. Vous êtes libre de télécharger, d'utiliser, de modifier et de partager __app__ selon cette licence libre.",
"title": "Open source"
},
"feature4": {
"content": "Il n'y a pas de limitation sur le nombre d'utilisateurs ou de conférences. Seules la puissance et la bande passante du serveur sont des facteurs limitants.",
"title": "Nombre d'utilisateurs illimité"
},
"feature5": {
"content": "C'est facile de partager votre écran avec d'autres personnes. __app__ est idéal pour les présentations en ligne, les cours, et les sessions de support technique.",
"title": "Partage d'écran"
},
"feature6": {
"content": "Besoin de confidentialité ? Les salles de conférence __app__ peuvent être sécurisées par un mot de passe pour exclure les invités non désirées, et prévenir des interruptions.",
"title": "Salles sécurisées"
},
"feature7": {
"content": "__app__ propose Etherpad, un éditeur de texte collaboratif en temps réel qui est parfait pour les procès-verbaux, l'édition d'articles et plus encore.",
"title": "Notes partagées"
},
"feature8": {
"content": "Apprenez plus au sujet de vos utilisateurs avec une intégration facile de Piwik, Google Analytics et d'autres systèmes de statistiques et supervision d'utilisation.",
"title": "Statistiques d'utilisation"
"appDescription": "Allez-y, chat vidéo avec toute l'équipe. En fait, invitez tout le monde que vous connaissez. __app__ est une solution de visioconférence entièrement cryptée et 100% open source que vous pouvez utiliser toute la journée, tous les jours, gratuitement— aucun compte requis.",
"audioVideoSwitch": {
"audio": "Voix",
"video": "Vidéo"
},
"calendar": "Calendrier",
"connectCalendarText": "Connectez votre calendrier pour voir toutes vos réunion dans __app__. Ajoutez les réunions __app__ dans votre calendrier pour les lancer en un seul clic.",
"connectCalendarButton": "Connecter votre calendrier",
"enterRoomTitle": "Démarrer une nouvelle réunion",
"go": "Créer",
"join": "REJOINDRE",
"privacy": "Confidentialité",
"recentList": "Récent",
"recentListDelete": "Supprimer",
"recentListEmpty": "Votre liste récente est actuellement vide. Discuter avec votre équipe et vous trouverez toutes vos réunions récentes ici.",
"roomname": "Saisissez un nom de salle",
"roomnamePlaceHolder": "nom de la conférence",
"roomnameHint": "Entrez le nom ou l'URL de la salle que vous souhaitez rejoindre. Vous pouvez faire un nom, laissez les gens que vous rencontrerez le savoir afin qu'ils entrent le même nom.",
"sendFeedback": "Envoyer votre avis",
"terms": "Termes"
"terms": "Termes",
"title": "Vidéoconférence Sécurisée, entièrement en vedette et gratuite"
},
"startupoverlay": {
"policyText": " ",
@@ -97,52 +82,95 @@
"rejoinKeyTitle": "Rejoindre"
},
"toolbar": {
"accessibilityLabel": {
"audioOnly": "Activer/désactiver le mode voix uniquement",
"audioRoute": "",
"callQuality": "Accorder la qualité des appels",
"chat": "Afficher/masquer la discussion instantanée",
"cc": "Activer/désactiver les sous-titres",
"document": "Activer/désactiver le document partagé",
"feedback": "Laisser des commentaires",
"fullScreen": "Activer/désactiver le plein écran",
"hangup": "Quitter la conversation",
"invite": "Inviter des participants",
"localRecording": "Activer/désactiver les contrôles d'enregistrement local",
"lockRoom": "Activer/Désactiver le verrouillage de la session",
"moreActions": "Activer/désactiver le menu d'actions supplémentaires",
"moreActionsMenu": "Menu d'actions supplémentaires",
"mute": "Activer/désactiver l'audio",
"pip": "Activer/désactiver le mode Picture in Picture",
"profile": "Éditer votre profil",
"raiseHand": "Lever/baisser la main",
"recording": "Activer/désactiver l'enregistrement",
"Settings": "Afficher/masquer le menu des paramètres",
"sharedvideo": "Démarrer/arrêter le partage de vidéo Youtube",
"shareRoom": "Inviter quelqu'un",
"shareYourScreen": "Activer/désactiver le partage décran",
"shortcuts": "Afficher/masquer les raccourcis",
"speakerStats": "Afficher/cacher les statistiques de parole",
"toggleCamera": "Activer/désactiver la caméra",
"tileView": "Activer/désactiver la vue mosaïque",
"videomute": "Activer/désactiver la vidéo"
},
"addPeople": "Ajouter des personnes à votre appel",
"audioonly": "Activer / Désactiver le mode audio uniquement (économiser de la bande passante)",
"audioOnlyOn": "Activer/désactiver le mode audio uniquement (économiser de la bande passante)",
"audioOnlyOff": "Désactiver le mode audio uniquement",
"audioRoute": "Sélectionner le périphérique audio",
"callQuality": "Accorder la qualité des appels",
"enterFullScreen": "Afficher en plein écran",
"exitFullScreen": "Quitter le mode plein écran",
"feedback": "Laisser des commentaires",
"moreActions": "Plus d'actions",
"mute": "Muet / Actif",
"videomute": "Démarrer / Arrêter la caméra",
"authenticate": "Authentifiez-vous",
"lock": "Verrouiller / déverrouiller la conférence",
"invite": "Partager le lien",
"chat": "Ouvrir / Fermer le chat",
"etherpad": "Ouvrir / Fermer le document partagé",
"documentOpen": "Ouvrir le document partagé",
"documentClose": "Fermer le document partagé",
"shareRoom": "Partager le salon",
"sharedvideo": "Partager une vidéo YouTube",
"sharescreen": "Démarrer / Arrêter le partage d'écran",
"stopSharedVideo": "Arrêter la vidéo YouTube",
"fullscreen": "Activer / Désactiver le plein écran",
"sip": "Appeler un numéro SIP",
"Settings": "Paramètres",
"hangup": "Quitter",
"login": "Connexion",
"logout": "Déconnexion",
"dialpad": "Ouvrir / Fermer le pavé numérique",
"sharedVideoMutedPopup": "Votre vidéo a été coupée pour que vous puissiez parler aux autres participants.",
"toggleCamera": "Activer/désactiver la caméra",
"micMutedPopup": "Votre microphone a été coupé afin que vous puissiez profiter de la vidéo partagée",
"talkWhileMutedPopup": "Vous voulez parler? Vous êtes en muet.",
"unableToUnmutePopup": "Vous ne pouvez pas réactiver votre microphone pendant que la vidéo partagée est activée.",
"cameraDisabled": "La camera n'est pas disponible",
"micDisabled": "Le microphone n'est pas disponible",
"filmstrip": "Afficher / Masquer les vidéos",
"pip": "Entrer en mode Picture-in-Picture",
"profile": "Éditer votre profil",
"raiseHand": "Lever / Baisser la main"
},
"unsupportedBrowser": {
"appNotInstalled": "Rejoignez cette réunion avec __app__ sur votre téléphone.",
"downloadApp": "Télécharger l'application",
"openApp": "Continuer sur __app__"
},
"bottomtoolbar": {
"chat": "Ouvrir / fermer le chat",
"filmstrip": "Afficher / cacher les vidéos",
"contactlist": "Voir et inviter des participants"
"raiseHand": "Lever / Baisser la main",
"shortcuts": "Afficher les raccourcis",
"speakerStats": "Statistiques de l'interlocuteur",
"tileViewToggle": "Activer/désactiver la vue mosaïque",
"invite": "Inviter des participants"
},
"chat": {
"nickname": {
"title": "Saisissez un pseudonyme dans le champ ci-dessous",
"popover": "Choisissez un pseudonyme"
},
"error": "Erreur : votre message \"__originalText__\" n'a pas été envoyé. Raison : __error__",
"messagebox": "Saisissez votre texte..."
},
"settings": {
"calendar": {
"about": "L'intégration de __appName__ avec votre calendrier permet daccéder de manière sécurisée aux événement à venir.",
"disconnect": "Se déconnecter",
"microsoftSignIn": "Se connecter avec Microsoft",
"signedIn": "Accès aux événements du calendrier __email__. Cliquez sur le bouton se déconnecter ci-dessous pour arrêter l'accès aux événements du calendrier.",
"title": "Calendrier"
},
"title": "Paramètres",
"update": "Mise à jour",
"name": "Nom",
@@ -152,11 +180,15 @@
"selectMic": "Microphone",
"selectAudioOutput": "Sortie audio",
"followMe": "Tout le monde me suit",
"language": "Langue",
"loggedIn": "Connecté en tant que __name__",
"noDevice": "Aucun",
"cameraAndMic": "Caméra et microphone",
"moderator": "MODÉRATEUR",
"moderator": "Moderateur",
"more": "Plus",
"password": "DÉFINIR UN MOT DE PASSE",
"audioVideo": "AUDIO ET VIDÉO"
"audioVideo": "AUDIO ET VIDÉO",
"devices": "Périphériques"
},
"profile": {
"title": "Profil",
@@ -176,7 +208,9 @@
},
"connectionindicator": {
"header": "État de la connexion",
"connectedTo": "Connecté à :",
"bitrate": "Débit :",
"bridgeCount": "Nombre de serveurs :",
"packetloss": "Perte de paquets :",
"resolution": "Résolution :",
"framerate": "Images par seconde",
@@ -216,15 +250,20 @@
"focus": "Focus de conférence",
"focusFail": "__component__ n'est pas disponible - réessayez dans __ms__ sec",
"grantedTo": "Droits modérateur accordés à __to__ !",
"grantedToUnknown": "Droits modérateur accordés à $t(notify.somebody)!",
"muted": "Vous avez commencé la conversation en muet.",
"mutedTitle": "Vous êtes en muet !",
"raisedHand": "Aimerait prendre la parole."
"raisedHand": "Aimerait prendre la parole.",
"suboptimalExperienceTitle": "Avertissement du navigateur",
"suboptimalExperienceDescription": "Eer ... nous craignons que votre expérience avec __appName__ ne sera pas aussi excellente que nous le pensions. Nous cherchons des moyens d'améliorer cela, mais jusque là, essayez d'utiliser l'un des <a href='static/recommendedBrowsers.html' target='_blank'>navigateurs entièrement pris en charge</a>."
},
"dialog": {
"add": "Ajouter",
"accessibilityLabel": {
"liveStreaming": "Diffusion en direct"
},
"allow": "Autoriser",
"confirm": "Confirmer",
"kickMessage": "Oups! Vous avez été renvoyé de la réunion !",
"kickTitle": "Viré de la conférence",
"popupErrorTitle": "Pop-up bloquée",
"popupError": "Votre navigateur bloque les fenêtres pop-up. Veuillez autoriser les fenêtres pop-up dans les paramètres de votre navigateur.",
"passwordErrorTitle": "Problème avec le mot de passe",
@@ -237,7 +276,6 @@
"copy": "Copier",
"contactSupport": "Contacter le support",
"error": "Erreur",
"createPassword": "Créer un mot de passe",
"detectext": "Une erreur est survenue pendant la détection de l'extension de partage d'écran.",
"failedpermissions": "Échec d'obtention des permissions pour utiliser le micro et/ou la caméra.",
"conferenceReloadTitle": "Malheureusement, un problème est survenu",
@@ -248,6 +286,7 @@
"rejoinNow": "Rejoindre maintenant",
"maxUsersLimitReachedTitle": "Le nombre maximal de participants est atteint",
"maxUsersLimitReached": "Le nombre maximal de participants est atteint. La conférence est complète. Merci de contacter le propriétaire du salon ou réessayer plus tard.",
"lockRoom": "Verrouiller la réunion",
"lockTitle": "Échec du verrouillage",
"lockMessage": "Impossible de verrouiller la conférence.",
"warning": "Avertissement",
@@ -288,10 +327,6 @@
"Save": "Sauvegarder",
"recording": "Enregistrement",
"recordingToken": "Saisissez un jeton d'enregistrement",
"passwordCheck": "Voulez-vous vraiment supprimer votre mot de passe ?",
"passwordMsg": "Saisissez un mot de passe pour verrouiller la conférence",
"shareLink": "Partager le lien de la conférence",
"yourPassword": "Saisissez un nouveau mot de passe",
"Back": "Retour",
"serviceUnavailable": "Service indisponible",
"gracefulShutdown": "Le service est actuellement en maintenance. Réessayez plus tard.",
@@ -299,29 +334,31 @@
"reservationError": "Erreur du système de réservation",
"reservationErrorMsg": "Code d'erreur: __code__, message: __msg__",
"password": "Saisir le mot de passe",
"unlockRoom": "Déverrouiller la réunion",
"userPassword": "mot de passe utilisateur",
"token": "jeton",
"tokenAuthFailedTitle": "Échec de l'authentification",
"tokenAuthFailed": "Désolé, vous n'êtes pas autorisé à rejoindre cette conversation.",
"displayNameRequired": "Un nom d'utilisateur est requis",
"enterDisplayName": "Veuillez saisir votre nom",
"extensionRequired": "Extension requise :",
"firefoxExtensionPrompt": "Vous devez installer une extension Firefox pour utiliser le partage d'écran. Merci d'essayer de nouveau après l'installation <a href='__url__'>depuis ce lien</a> !",
"feedbackHelp": "Vos retours nous permettrons d'améliorer notre expérience vidéo.",
"feedbackQuestion": "Informez-nous à propos de votre appel !",
"thankYou": "Merci d'avoir utilisé __appName__ !",
"sorryFeedback": "Nous sommes désolés d'apprendre cela. Voulez-vous nous en dire plus ?",
"liveStreaming": "Direct",
"streamKey": "Stream name/key",
"startLiveStreaming": "Commencer le direct",
"streamKey": "Clé Live stream",
"startLiveStreaming": "Démarrer la diffusion en direct",
"startRecording": "Commencer l'enregistrement",
"stopStreamingWarning": "Désirez-vous vraiment arrêter le direct?",
"stopRecordingWarning": "Désirez-vous vraiment arrêter l'enregistrement?",
"stopLiveStreaming": "Arrêter le direct",
"stopLiveStreaming": "Arrêter la diffusion en direct",
"stopRecording": "Arrêter l'enregistrement",
"doNotShowMessageAgain": "Ne plus afficher ce message",
"permissionDenied": "Permission refusée",
"screenSharingFailedToInstall": "Oups! Votre extension de partage d'écran n'a pas pu être installée.",
"screenSharingFailedToInstallTitle": "L'extension de partage d'écran n'a pas pu être installée",
"screenSharingFirefoxPermissionDeniedError": "Quelque chose s'est mal passé pendant que nous essayions de partager votre écran. S'il vous plaît assurez-vous que vous nous avez donné la permission de le faire.",
"screenSharingFirefoxPermissionDeniedTitle": "Oups! Nous ne pouvions pas démarrer le partage d'écran!",
"screenSharingPermissionDeniedError": "Oups! Une erreur s'est produite avec vos autorisations d'extension de partage d'écran. Veuillez rafraîchir et réessayer.",
"cameraUnsupportedResolutionError": "Votre appareil ne prend pas en charge la résolution vidéo requise.",
"cameraUnknownError": "Vous ne pouvez pas utiliser la caméra pour une raison inconnue.",
@@ -344,6 +381,10 @@
"muteParticipantTitle": "Couper le micro de ce participant?",
"muteParticipantBody": "Vous ne pourrez plus réactiver leurs micros, mais ils peuvent l'activer par eux-même à tout moment.",
"muteParticipantButton": "Couper le micro",
"liveStreamingDisabledTooltip": "La diffusion en direct est désactivé",
"liveStreamingDisabledForGuestTooltip": "Les invités ne peuvent démarrer la diffusion en direct.",
"recordingDisabledTooltip": "L'enregistrement est désactivé.",
"recordingDisabledForGuestTooltip": "Les invités ne peuvent enregistrer.",
"remoteControlTitle": "Contrôle de bureau à distance",
"remoteControlRequestMessage": "Voulez-vous autoriser __user__ à contrôler votre bureau?",
"remoteControlShareScreenWarning": "Si vous appuyez sur \"Autoriser\" vous allez partager votre écran!",
@@ -354,8 +395,11 @@
"remoteControlStopMessage": "La prise en main à distance est terminée!",
"close": "Fermer",
"shareYourScreen": "Partagez votre écran",
"shareYourScreenDisabled": "Le partage décran est désactivé.",
"shareYourScreenDisabledForGuest": "Les invités ne peuvent partager l'écran",
"yourEntireScreen": "Votre écran entier",
"applicationWindow": "Fenêtre d'application"
"applicationWindow": "Fenêtre d'application",
"transcribing": "Transcription"
},
"email": {
"sharedKey": [
@@ -385,6 +429,22 @@
],
"and": "et"
},
"share": {
"mainText": [
"Cliquez sur le lien suivant pour rejoindre une conférence :",
"__roomUrl__"
],
"dialInfoText": [
"",
"",
"=====",
"",
"Voulez-vous appeler depuis votre téléphone?",
"",
"__defaultDialInNumber__Cliquez sur ce lien pour voir les numéros de téléphone pour cette conférence",
"__dialInfoPageUrl__"
]
},
"connection": {
"ERROR": "Erreur",
"CONNECTING": "Connexion en cours",
@@ -398,30 +458,67 @@
"ATTACHED": "Attachée"
},
"recording": {
"beta": "BETA",
"busy": "Nous sommes en train de libérer les ressources d'enregistrement. Réessayez dans quelques minutes.",
"busyTitle": "Tous les enregistreurs sont actuellement occupés",
"buttonTooltip": "Démarrer / Arrêter l'enregistrement",
"error": "Échec de l'enregistrement. Veuillez réessayer.",
"expandedOff": "L'enregistrement a été arrêté",
"expandedOn": "Cette conférence est actuellement en cours d'enregistrement.",
"expandedPending": "Démarrage de l'enregistrement...",
"failedToStart": "L'enregistrement n'as pas réussi à démarrer",
"live": "DIRECT",
"off": "Enregistrement arrêté",
"on": "Enregistrement",
"pending": "Enregistrement en attente de participant...",
"pending": "Préparation de l'enregistrement de la réunion...",
"rec": "REC",
"authDropboxText": "Téléchargement vers Dropbox",
"serviceName": "Service d'enregistrement",
"signOut": "Se déconnecter",
"signIn": "s'identifier",
"loggedIn": "Connecté en tant que __userName__",
"availableSpace": "Espace disponible: __spaceLeft_ Mo (approximativement __duration__ minutes d'enregistrement)",
"startRecordingBody": "Voulez-vous vraiment démarrer l'enregistrement?",
"unavailable": "Oups! Le __serviceName__ est actuellement indisponible. Nous travaillons sur la résolution du problème. Veuillez réessayer plus tard.",
"unavailableTitle": "Enregistrement indisponible"
},
"transcribing": {
"pending": "Préparation de la transcription de la réunion...",
"off": "La transcription désactivée",
"error": "Échec de la transcription. Veuillez réessayer.",
"expandedLabel": "La transcription est actuellement activée",
"failedToStart": "Échec de démarrage de la transcription",
"tr": "TR",
"labelToolTip": "La transcription de la réunion est en cours",
"ccButtonTooltip": "Afficher/masquer les sous-titres",
"start": "Afficher/masquer les sous-titres",
"stop": "Désactiver le sous-titrage"
},
"liveStreaming": {
"busy": "Nous travaillons sur la libération des ressources de Streaming. Veuillez réessayez dans quelques minutes.",
"busyTitle": "Tous les streamers sont actuellement occupés",
"buttonTooltip": "Démarrer / Arrêter le Stream",
"changeSignIn": "Changer de compte.",
"choose": "Choisir un flux live",
"chooseCTA": "Choisissez une option de diffusion. Vous êtes actuellement connecté comme __email__.",
"enterStreamKey": "Entrez votre clé de flux live Youtube ici",
"error": "Le Streaming a échoué. Veuillez réessayer.",
"errorAPI": "Une erreur s'est produite lors de l'accès à vos diffusions YouTube. Veuillez réessayer de vous connecter.",
"errorLiveStreamNotEnabled": "La diffusion en direct n'est pas activée pour __email__. Merci de l'activer ou de vous connecter avec un compte où elle est déjà activée.",
"expandedOff": "La diffusion en direct a été arrêtée",
"expandedOn": "La conférence est en cours de diffusion sur YouTube.",
"expandedPending": "La diffusion en direct a commencé...",
"failedToStart": "Le Streaming n'as pas réussi à démarrer",
"off": "Le Streaming a été arrêter",
"off": "Le Streaming a été arrêté",
"on": "Direct",
"pending": "Commencer le direct...",
"serviceName": "Service de diffusion en direct",
"streamIdRequired": "Merci de renseigner le stream id pour lancer le streaming.",
"streamIdHelp": "Où puis-je trouver ceci?",
"signedInAs": "Vous êtes connecté en tant que :",
"signIn": "Se connecter avec Google",
"signOut": "Se déconnecter",
"signInCTA": "Connectez vous ou entrez votre clé de flux live provenant de Youtube.",
"start": "Démarrer la diffusion en direct",
"streamIdHelp": "Qu'est-ce que c'est?",
"unavailableTitle": "Le Streaming est indisponible"
},
"videoSIPGW": {
@@ -449,28 +546,20 @@
"noPermission": "Permission non accordée",
"previewUnavailable": "Aperçu non disponible",
"selectADevice": "Sélectionner un périphérique",
"testAudio": "Son de test"
},
"invite": {
"addPassword": "Ajouter un mot de passe",
"callNumber": "Appeler le __number__",
"enterID": "Saisissez l'identifiant: __conferenceID__ suivi de # pour rejoindre avec un téléphone",
"howToDialIn": "Pour rejoindre avec un téléphone, utilisez un des des numéros suivants et l'identifiant de la conférence",
"hidePassword": "Cacher le mot de passe",
"inviteTo": "inviter des participants à __conferenceName__",
"invitedYouTo": "__userName__ vous a invité(e) à la conférence __inviteURL__",
"invitePeople": "Inviter",
"locked": "Cet appel est verrouillé. les nouveaux interlocuteurs devraient avoir le lien et saisir le mot de passe pour rejoindre.",
"showPassword": "Afficher le mot de passe",
"unlocked": "Cet appel est verrouillé. Tout nouveau participant avec un lien peut rejoindre l'appel."
"testAudio": "Lire un audio de test"
},
"videoStatus": {
"audioOnly": "VOIX",
"audioOnlyExpanded": "Vous êtes en mode audio uniquement. Ce mode économise de la bande passant mais vous ne pourrez pas voir la vidéo des autres participants.",
"callQuality": "Qualité de l'appel",
"hd": "HD",
"hdTooltip": "Regardez la vidéo en haute définition",
"highDefinition": "Haute définition",
"labelTooltipVideo": "Qualité vidéo actuelle",
"labelTooltipAudioOnly": "Mode audio uniquement activé",
"labelTooiltipNoVideo": "Aucune vidéo",
"labelTooltipVideo": "Qualité vidéo actuelle",
"ld": "BD",
"ldTooltip": "Regardez la vidéo en basse définition",
"lowDefinition": "Basse définition",
"onlyAudioAvailable": "Seul l'audio est disponible",
"onlyAudioSupported": "Nous ne supportons que l'audio sur ce navigateur.",
@@ -478,21 +567,31 @@
"p2pVideoQualityDescription": "En mode peer to peer, la qualité d'appel reçue ne peut être basculée qu'entre haut et audio. Les autres paramètres ne seront pas respectés tant que l'on n'aura pas quitté peer to peer.",
"recHighDefinitionOnly": "Va préférer la haute définition",
"sd": "MD",
"sdTooltip": "Regardez la vidéo en définition standard",
"standardDefinition": "Moyenne Définition",
"qualityButtonTip": "Changer la qualité de vidéo reçue"
},
"dialOut": {
"dial": "Composer",
"dialOut": "Appeler #",
"statusMessage": "est maintenant __status__",
"enterPhone": "Saisissez un numéro de téléphone",
"phoneNotAllowed": "Désolé, nous ne supportons pas encore cette destination!"
"statusMessage": "est maintenant __status__"
},
"addPeople": {
"add": "Ajouter",
"add": "Inviter",
"countryNotSupported": "Nous ne supportons pas encore cette destination.",
"countryReminder": "Appel hors États-Unis? Veuillez commencer avec le code du pays!",
"disabled": "Vous ne pouvez pas inviter quelqu'un.",
"footerText": "Appels sortants désactivés",
"invite": "Inviter",
"loading": "Rechercher des personnes et des numéros de téléphone",
"loadingNumber": "Validation du numéro de téléphone",
"loadingPeople": "Recherche de personnes à inviter",
"noResults": "Aucun résultat de recherche correspondant",
"searchPlaceholder": "Rechercher des personnes et des salons à ajouter",
"title": "Ajouter des personnes à votre appel",
"noValidNumbers": "Veuillez entrer un numéro de téléphone",
"notAvailable": "Vous ne pouvez pas inviter quelqu'un.",
"searchNumbers": "Ajouter des numéros de téléphone",
"searchPeople": "Rechercher une personne",
"searchPeopleAndNumbers": "Rechercher des personnes ou ajouter leurs numéros de téléphone",
"telephone": "Téléphone: __number__",
"title": "Inviter une personne à cette réunion",
"failedToAdd": "Échec de l'ajout de membres"
},
"inlineDialogFailure": {
@@ -511,14 +610,131 @@
"average": "Moyen",
"bad": "Mauvais",
"good": "Bien",
"rateExperience": "Veuillez évaluer votre réunion.",
"detailsLabel": "Dites nous en plus à ce sujet.",
"rateExperience": "Veuillez évaluer votre expérience.",
"veryBad": "Très mauvais",
"veryGood": "Très bon"
},
"info": {
"copy": "Copier le lien",
"invite": "Inviter à __app__",
"title": "Informations sur la conférence",
"tooltip": "Obtenir des informations d'accès"
"accessibilityLabel": "Afficher les informations",
"addPassword": "Ajouter un mot de passe",
"cancelPassword": "Annuler mot de passe",
"conferenceURL": "Lien:",
"country": "Pays",
"dialANumber": "Pour rejoindre votre réunion, composez l'un de ces numéros, puis entrez ce code PIN: __conferenceID __ #",
"dialInNumber": "Composer:",
"dialInConferenceID": "PIN:",
"dialInNotSupported": "Désolé, les appels entrants ne sont pas supportés.",
"genericError": "Oups, quelque chose a mal tourné.",
"inviteLiveStream": "Pour voir la diffusion en direct de cette réunion, cliquez sur ce lien : __url__",
"invitePhone": "Pour joindre par téléphone, composez __number__ et saisissez ce code PIN : __conferenceID__ #",
"invitePhoneAlternatives": "Pour voir plus de numéros de téléphone, suivez ce lien : __url__",
"inviteURL": "Pour rejoindre la vidéoconférence, cliquez sur ce lien : __url__",
"liveStreamURL": "Diffusion en direct :",
"moreNumbers": "Plus de numéros ",
"noNumbers": "Numéros à composer non trouvés",
"noPassword": "Aucun",
"noRoom": "Aucune réunion n'a été spécifiée pour l'appel entrant.",
"numbers": "Numéros d'appel",
"password": "Mot de passe:",
"title": "Partager",
"tooltip": "Partager le lien et les informations de connexion pour cette conférence"
},
"settingsView": {
"alertOk": "D'accord",
"alertTitle": "Avertissement",
"alertURLText": "L'URL du serveur est invalide",
"conferenceSection": "Conférence",
"displayName": "Pseudo",
"email": "Email",
"header": "Paramètres",
"profileSection": "Profil",
"serverURL": "URL du serveur",
"startWithAudioMuted": "Commencez avec la vidéo en sourdine",
"startWithVideoMuted": "Commencez avec la vidéo en sourdine"
},
"calendarSync": {
"addMeetingURL": "Ajouter un lien de conférence",
"confirmAddLink": "Voulez-vous ajouter un lien Jitsi à cet événement?",
"confirmAddLinkTitle": "Calendrier",
"join": "Joindre",
"joinTooltip": "Rejoindre la réunion",
"nextMeeting": "prochaine réunion",
"noEvents": "Il n'y a pas dévénement à venir.",
"ongoingMeeting": "La réunion en cours",
"permissionButton": "Afficher les réglages",
"permissionMessage": "La permission du calendrier est requise pour afficher vos réunions dans l'application.",
"refresh": "Rafraîchir le calendrier",
"today": "Aujourd'hui"
},
"recentList": {
"joinPastMeeting": "Rejoindre une réunion précédente"
},
"sectionList": {
"pullToRefresh": "Tirer pour recharger"
},
"deepLinking": {
"title": "Lancement de votre réunion dans __app __ en cours...",
"description": "Rien ne s'est passé? Nous avons essayé de lancer votre réunion dans l'application de bureau __app__. Essayez à nouveau ou lancez-la dans l'application web __app__.",
"tryAgainButton": "Réessayez sur le bureau",
"launchWebButton": "Lancer dans le navigateur",
"appNotInstalled": "Vous avez besoin de l'application mobile __app__ pour participer à cette réunion avec votre téléphone.",
"downloadApp": "Télécharger l'application",
"openApp": "Continuer vers l'application"
},
"presenceStatus": {
"invited": "Invité(e)",
"ringing": "Appel en cours...",
"calling": "Appel...",
"initializingCall": "Lancement de l'appel...",
"connected": "Connecté",
"connecting": "Connexion en cours...",
"connecting2": "Connexion en cours*...",
"disconnected": "Déconnecté",
"busy": "Occupé",
"rejected": "Rejeté",
"ignored": "Ignoré",
"expired": "Expiré"
},
"dateUtils": {
"today": "Aujourd'hui",
"yesterday": "Hier",
"earlier": "Plus tôt"
},
"incomingCall": {
"answer": "Répondre",
"audioCallTitle": "Appel entrant",
"decline": "Rejeter",
"productLabel": "de Jitsi Meet",
"videoCallTitle": "Appel vidéo entrant"
},
"localRecording": {
"localRecording": "Enregistrement local",
"dialogTitle": "Commandes de l'enregistrement local",
"start": "Démarrer l'enregistrement",
"stop": "Arrêter l'enregistrement",
"moderator": "Moderateur",
"me": "Moi",
"duration": "Durée",
"durationNA": "N/A",
"encoding": "Encodage",
"participantStats": "Statistiques du participant",
"participant": "Participant",
"sessionToken": "Token de la session",
"clientState": {
"on": "Actif",
"off": "Inactif",
"unknown": "Inconnu"
},
"messages": {
"engaged": "Enregistrement local engagé.",
"finished": "L'enregistrement de la session __token__ s'est terminé. Merci d'envoyer le fichier au modérateur.",
"finishedModerator": "L'enregistrement de la session __token__ s'est terminé. La piste a bien été sauvegardée. Merci de demander aux autres participants de soumettre leurs enregistrements.",
"notModerator": "Vous n'êtes pas le modérateur. Vous ne pouvez pas démarrer ou arrêter un enregistrement local."
},
"yes": "Oui",
"no": "Non",
"label": "ENR-LOC",
"labelToolTip": "L'enregistrement local est engagé"
}
}

View File

@@ -59,16 +59,18 @@
"calendar": "Calendar",
"connectCalendarText": "Connect your calendar to view all your meetings in __app__. Plus, add __app__ meetings to your calendar and start them with one click.",
"connectCalendarButton": "Connect your calendar",
"enterRoomTitle": "Start a new meeting",
"go": "GO",
"join": "JOIN",
"privacy": "Privacy",
"recentList": "Recent",
"recentListDelete": "Delete",
"recentListEmpty": "Your recent list is currently empty. Chat with your team and you will find all your recent meetings here.",
"roomname": "Enter room name",
"roomnameHint": "Enter the name or URL of the room you want to join. You may make a name up, just let the people you are meeting know it so that they enter the same name.",
"sendFeedback": "Send feedback",
"terms": "Terms",
"title": "More secure, more flexible, and completely free video conferencing."
"title": "Secure, fully featured, and completely free video conferencing"
},
"startupoverlay": {
"policyText": " ",

View File

@@ -112,7 +112,7 @@ function initCommands() {
const { name } = request;
switch (name) {
case 'invite': // eslint-disable-line no-case-declarations
case 'invite': {
const { invitees } = request;
if (!Array.isArray(invitees) || invitees.length === 0) {
@@ -143,6 +143,7 @@ function initCommands() {
});
});
break;
}
case 'is-audio-muted':
callback(APP.conference.isLocalAudioMuted());
break;

View File

@@ -114,10 +114,10 @@ function parseArguments(args) {
switch (typeof firstArg) {
case 'string': // old arguments format
case undefined: // eslint-disable-line no-case-declarations
// not sure which format but we are trying to parse the old
// format because if the new format is used everything will be undefined
// anyway.
case undefined: {
// Not sure which format but we are trying to parse the old
// format because if the new format is used everything will be undefined
// anyway.
const [
roomName,
width,
@@ -141,6 +141,7 @@ function parseArguments(args) {
jwt,
onload
};
}
case 'object': // new arguments format
return args[0];
default:
@@ -461,55 +462,57 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* the event and value - the listener.
* Currently we support the following
* events:
* incomingMessage - receives event notifications about incoming
* {@code incomingMessage} - receives event notifications about incoming
* messages. The listener will receive object with the following structure:
* {{
* 'from': from,//JID of the user that sent the message
* 'nick': nick,//the nickname of the user that sent the message
* 'message': txt//the text of the message
* }}
* outgoingMessage - receives event notifications about outgoing
* {@code outgoingMessage} - receives event notifications about outgoing
* messages. The listener will receive object with the following structure:
* {{
* 'message': txt//the text of the message
* }}
* displayNameChanged - receives event notifications about display name
* change. The listener will receive object with the following structure:
* {@code displayNameChanged} - receives event notifications about display
* name change. The listener will receive object with the following
* structure:
* {{
* jid: jid,//the JID of the participant that changed his display name
* displayname: displayName //the new display name
* }}
* participantJoined - receives event notifications about new participant.
* {@code participantJoined} - receives event notifications about new
* participant.
* The listener will receive object with the following structure:
* {{
* jid: jid //the jid of the participant
* }}
* participantLeft - receives event notifications about the participant that
* left the room.
* {@code participantLeft} - receives event notifications about the
* participant that left the room.
* The listener will receive object with the following structure:
* {{
* jid: jid //the jid of the participant
* }}
* video-conference-joined - receives event notifications about the local
* user has successfully joined the video conference.
* {@code video-conference-joined} - receives event notifications about the
* local user has successfully joined the video conference.
* The listener will receive object with the following structure:
* {{
* roomName: room //the room name of the conference
* }}
* video-conference-left - receives event notifications about the local user
* has left the video conference.
* {@code video-conference-left} - receives event notifications about the
* local user has left the video conference.
* The listener will receive object with the following structure:
* {{
* roomName: room //the room name of the conference
* }}
* screenSharingStatusChanged - receives event notifications about
* {@code screenSharingStatusChanged} - receives event notifications about
* turning on/off the local user screen sharing.
* The listener will receive object with the following structure:
* {{
* on: on //whether screen sharing is on
* }}
* readyToClose - all hangup operations are completed and Jitsi Meet is
* ready to be disposed.
* {@code readyToClose} - all hangup operations are completed and Jitsi Meet
* is ready to be disposed.
* @returns {void}
*
* @deprecated
@@ -536,11 +539,12 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
/**
* Executes command. The available commands are:
* displayName - sets the display name of the local participant to the value
* passed in the arguments array.
* toggleAudio - mutes / unmutes audio with no arguments.
* toggleVideo - mutes / unmutes video with no arguments.
* toggleFilmStrip - hides / shows the filmstrip with no arguments.
* {@code displayName} - Sets the display name of the local participant to
* the value passed in the arguments array.
* {@code toggleAudio} - Mutes / unmutes audio with no arguments.
* {@code toggleVideo} - Mutes / unmutes video with no arguments.
* {@code toggleFilmStrip} - Hides / shows the filmstrip with no arguments.
*
* If the command doesn't require any arguments the parameter should be set
* to empty array or it may be omitted.
*
@@ -561,13 +565,13 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
/**
* Executes commands. The available commands are:
* displayName - sets the display name of the local participant to the value
* passed in the arguments array.
* toggleAudio - mutes / unmutes audio. no arguments
* toggleVideo - mutes / unmutes video. no arguments
* toggleFilmStrip - hides / shows the filmstrip. no arguments
* toggleChat - hides / shows chat. no arguments.
* toggleShareScreen - starts / stops screen sharing. no arguments.
* {@code displayName} - Sets the display name of the local participant to
* the value passed in the arguments array.
* {@code toggleAudio} - Mutes / unmutes audio. No arguments.
* {@code toggleVideo} - Mutes / unmutes video. No arguments.
* {@code toggleFilmStrip} - Hides / shows the filmstrip. No arguments.
* {@code toggleChat} - Hides / shows chat. No arguments.
* {@code toggleShareScreen} - Starts / stops screen sharing. No arguments.
*
* @param {Object} commandList - The object with commands to be executed.
* The keys of the object are the commands that will be executed and the

View File

@@ -87,6 +87,7 @@ class Etherpad extends LargeContainer {
this.container.appendChild(iframe);
iframe.onload = function() {
// eslint-disable-next-line no-self-assign
document.domain = document.domain;
bubbleIframeMouseMove(iframe);

5351
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -47,62 +47,70 @@
"jquery-contextmenu": "2.4.5",
"jquery-i18next": "1.2.0",
"js-md5": "0.6.1",
"js-utils": "github:jitsi/js-utils#446497893023aa8dec403e0e4e35a22cae6bc87d",
"jsc-android": "224109.1.0",
"jsrsasign": "8.0.12",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#ba0dc564cba5b49aa55cdce0f8aae19e9e475d8e",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#34f728456eb5e8e9817bdaf999e4702bac2ee6ce",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.4",
"moment": "2.19.4",
"moment-duration-format": "2.2.2",
"postis": "2.2.0",
"prop-types": "15.6.0",
"react": "16.3.1",
"react-dom": "16.3.1",
"react-i18next": "4.8.0",
"react-native": "0.55.4",
"react": "16.5.0",
"react-dom": "16.5.0",
"react-i18next": "7.13.0",
"react-native": "0.57.1",
"react-native-background-timer": "2.0.0",
"react-native-calendar-events": "github:wmcmahan/react-native-calendar-events#cb2731db6684a49b4343e09de7f9c2fcc68bcd9b",
"react-native-calendar-events": "github:wmcmahan/react-native-calendar-events#056807286da610d884fb6b4c8ca187a767b261f7",
"react-native-callstats": "3.53.4",
"react-native-fast-image": "github:jitsi/react-native-fast-image#1f8c93a5584869848d75cc9b946beb9688efe285",
"react-native-google-signin": "1.0.0-rc3",
"react-native-google-signin": "1.0.0-rc6",
"react-native-immersive": "1.1.0",
"react-native-keep-awake": "2.0.6",
"react-native-linear-gradient": "2.4.0",
"react-native-locale-detector": "github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9",
"react-native-prompt": "1.0.0",
"react-native-sound": "0.10.9",
"react-native-vector-icons": "4.4.2",
"react-native-webrtc": "github:jitsi/react-native-webrtc#be3de15bb988cfabbb62cd4b3b06f4c920ee5ba0",
"react-native-swipeout": "2.3.6",
"react-native-vector-icons": "6.0.2",
"react-native-webrtc": "github:jitsi/react-native-webrtc#6322a9b5a38ce590cfaea4041072ea87c8dbf558",
"react-redux": "5.0.7",
"react-transition-group": "2.4.0",
"redux": "4.0.0",
"redux-thunk": "2.2.0",
"styled-components": "1.4.6",
"styled-components": "3.4.9",
"uuid": "3.1.0",
"xmldom": "0.1.27"
},
"devDependencies": {
"babel-core": "6.26.0",
"babel-eslint": "8.0.3",
"babel-loader": "7.1.2",
"babel-preset-env": "1.6.1",
"babel-preset-react": "6.24.1",
"babel-preset-stage-1": "6.24.1",
"@babel/core": "7.1.2",
"@babel/preset-env": "7.1.0",
"@babel/preset-flow": "7.0.0",
"@babel/preset-react": "7.0.0",
"@babel/plugin-proposal-class-properties": "7.1.0",
"@babel/plugin-proposal-export-default-from": "7.0.0",
"@babel/plugin-proposal-export-namespace-from": "7.0.0",
"@babel/plugin-transform-flow-strip-types": "7.0.0",
"@babel/runtime": "7.1.2",
"babel-eslint": "10.0.1",
"babel-loader": "8.0.4",
"clean-css": "3.4.25",
"css-loader": "0.28.7",
"eslint": "4.12.1",
"eslint": "5.6.1",
"eslint-config-jitsi": "github:jitsi/eslint-config-jitsi#7474f6668515eb5852f1273dc5a50b940a550d3f",
"eslint-plugin-flowtype": "2.39.1",
"eslint-plugin-import": "2.8.0",
"eslint-plugin-jsdoc": "3.2.0",
"eslint-plugin-react": "7.5.1",
"eslint-plugin-react-native": "3.2.0",
"eslint-plugin-flowtype": "2.50.3",
"eslint-plugin-import": "2.14.0",
"eslint-plugin-jsdoc": "3.8.0",
"eslint-plugin-react": "7.11.1",
"eslint-plugin-react-native": "3.3.0",
"expose-loader": "0.7.4",
"file-loader": "1.1.5",
"flow-bin": "0.67.1",
"flow-bin": "0.78.0",
"imports-loader": "0.7.1",
"node-sass": "4.8.3",
"metro-react-native-babel-preset": "0.47.0",
"node-sass": "4.10.0",
"precommit-hook": "3.0.0",
"string-replace-loader": "1.3.0",
"style-loader": "0.19.0",

View File

@@ -4,5 +4,8 @@ module.exports = {
'eslint-config-jitsi/jsdoc',
'eslint-config-jitsi/react',
'.eslintrc-react-native.js'
]
],
'rules': {
'react/no-deprecated': 0
}
};

View File

@@ -413,7 +413,7 @@ export function createRemoteVideoMenuButtonEvent(buttonName, attributes) {
/**
* Creates an event indicating that an action related to screen sharing
* occurred (e.g. it was started or stopped).
* occurred (e.g. It was started or stopped).
*
* @param {string} action - The action which occurred.
* @returns {Object} The event in a format suitable for sending via
@@ -466,7 +466,7 @@ export function createSharedVideoEvent(action, attributes = {}) {
* Creates an event associated with a shortcut being pressed, released or
* triggered. By convention, where appropriate an attribute named 'enable'
* should be used to indicate the action which resulted by the shortcut being
* pressed (e.g. whether screen sharing was enabled or disabled).
* pressed (e.g. Whether screen sharing was enabled or disabled).
*
* @param {string} shortcut - The identifier of the shortcut which produced
* an action.
@@ -512,7 +512,7 @@ export function createStartAudioOnlyEvent(audioOnly) {
*
* @param {string} source - The source of the configuration, 'local' or
* 'remote' depending on whether it comes from the static configuration (i.e.
* config.js) or comes dynamically from Jicofo.
* {@code config.js}) or comes dynamically from Jicofo.
* @param {boolean} audioMute - Whether the configuration requests that audio
* is muted.
* @param {boolean} videoMute - Whether the configuration requests that video
@@ -573,7 +573,7 @@ export function createSyncTrackStateEvent(mediaType, muted) {
* Creates an event associated with a toolbar button being clicked/pressed. By
* convention, where appropriate an attribute named 'enable' should be used to
* indicate the action which resulted by the shortcut being pressed (e.g.
* whether screen sharing was enabled or disabled).
* Whether screen sharing was enabled or disabled).
*
* @param {string} buttonName - The identifier of the toolbar button which was
* clicked/pressed.
@@ -595,9 +595,9 @@ export function createToolbarEvent(buttonName, attributes = {}) {
* Creates an event which indicates that a local track was muted.
*
* @param {string} mediaType - The track's media type ('audio' or 'video').
* @param {string} reason - The reason the track was muted (e.g. it was
* @param {string} reason - The reason the track was muted (e.g. It was
* triggered by the "initial mute" option, or a previously muted track was
* replaced (e.g. when a new device was used)).
* replaced (e.g. When a new device was used)).
* @param {boolean} muted - Whether the track was muted or unmuted.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.

View File

@@ -128,10 +128,10 @@ export class AbstractApp extends BaseApp<Props, *> {
}
/**
* Navigates this {@code AbstractApp} to (i.e. opens) a specific URL.
* Navigates this {@code AbstractApp} to (i.e. Opens) a specific URL.
*
* @param {Object|string} url - The URL to navigate this {@code AbstractApp}
* to (i.e. the URL to open).
* to (i.e. The URL to open).
* @protected
* @returns {void}
*/

View File

@@ -1,4 +1,5 @@
import PropTypes from 'prop-types';
/* @flow */
import React, { Component } from 'react';
/**
@@ -13,27 +14,24 @@ const AUDIO_LEVEL_DOTS = 5;
*/
const CENTER_DOT_INDEX = Math.floor(AUDIO_LEVEL_DOTS / 2);
/**
* The type of the React {@code Component} props of {@link AudioLevelIndicator}.
*/
type Props = {
/**
* The current audio level to display. The value should be a number between
* 0 and 1.
*/
audioLevel: number
};
/**
* Creates a ReactElement responsible for drawing audio levels.
*
* @extends {Component}
*/
class AudioLevelIndicator extends Component {
/**
* {@code AudioLevelIndicator}'s property types.
*
* @static
*/
static propTypes = {
/**
* The current audio level to display. The value should be a number
* between 0 and 1.
*
* @type {number}
*/
audioLevel: PropTypes.number
};
class AudioLevelIndicator extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*

View File

@@ -23,7 +23,7 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
* password + guest access configuration. Refer to {@link LoginDialog} for more
* info.
*
* @param {string} id - The XMPP user's ID (e.g. user@domain.com).
* @param {string} id - The XMPP user's ID (e.g. {@code user@domain.com}).
* @param {string} password - The XMPP user's password.
* @param {JitsiConference} conference - The conference for which the local
* participant's role will be upgraded.

View File

@@ -1,4 +1,5 @@
import PropTypes from 'prop-types';
/* @flow */
import React, { Component } from 'react';
import { Text, TextInput, View } from 'react-native';
import { connect as reduxConnect } from 'react-redux';
@@ -11,6 +12,65 @@ import { JitsiConnectionErrors } from '../../base/lib-jitsi-meet';
import { authenticateAndUpgradeRole, cancelLogin } from '../actions';
import styles from './styles';
/**
* The type of the React {@link Component} props of {@link LoginDialog}.
*/
type Props = {
/**
* {@link JitsiConference} that needs authentication - will hold a valid
* value in XMPP login + guest access mode.
*/
_conference: Object,
/**
* The server hosts specified in the global config.
*/
_configHosts: Object,
/**
* Indicates if the dialog should display "connecting" status message.
*/
_connecting: boolean,
/**
* The error which occurred during login/authentication.
*/
_error: Object,
/**
* The progress in the floating range between 0 and 1 of the authenticating
* and upgrading the role of the local participant/user.
*/
_progress: number,
/**
* Redux store dispatch method.
*/
dispatch: Dispatch<*>,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* The type of the React {@link Component} state of {@link LoginDialog}.
*/
type State = {
/**
* The user entered password for the conference.
*/
password: string,
/**
* The user entered local participant name.
*/
username: string
};
/**
* Dialog asks user for username and password.
*
@@ -38,58 +98,14 @@ import styles from './styles';
* See {@link https://github.com/jitsi/jicofo#secure-domain} for a description
* of the configuration parameters.
*/
class LoginDialog extends Component {
/**
* LoginDialog component's property types.
*
* @static
*/
static propTypes = {
/**
* {@link JitsiConference} that needs authentication - will hold a valid
* value in XMPP login + guest access mode.
*/
_conference: PropTypes.object,
/**
*
*/
_configHosts: PropTypes.object,
/**
* Indicates if the dialog should display "connecting" status message.
*/
_connecting: PropTypes.bool,
/**
* The error which occurred during login/authentication.
*/
_error: PropTypes.object,
/**
* The progress in the floating range between 0 and 1 of the
* authenticating and upgrading the role of the local participant/user.
*/
_progress: PropTypes.number,
/**
* Redux store dispatch method.
*/
dispatch: PropTypes.func,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
class LoginDialog extends Component<Props, State> {
/**
* Initializes a new LoginDialog instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
constructor(props: Props) {
super(props);
this.state = {
@@ -119,7 +135,7 @@ class LoginDialog extends Component {
} = this.props;
let messageKey;
let messageOptions;
const messageOptions = {};
if (progress && progress < 1) {
messageKey = 'connection.FETCH_SESSION_ID';
@@ -142,7 +158,6 @@ class LoginDialog extends Component {
}
} else if (name) {
messageKey = 'dialog.connectErrorWithMsg';
messageOptions || (messageOptions = {});
messageOptions.msg = `${name} ${error.message}`;
}
}
@@ -170,7 +185,7 @@ class LoginDialog extends Component {
<Text style = { styles.dialogText }>
{
messageKey
? t(messageKey, messageOptions || {})
? t(messageKey, messageOptions)
: connecting
? t('connection.CONNECTING')
: ''
@@ -181,6 +196,8 @@ class LoginDialog extends Component {
);
}
_onUsernameChange: (string) => void;
/**
* Called when user edits the username.
*
@@ -194,6 +211,8 @@ class LoginDialog extends Component {
});
}
_onPasswordChange: (string) => void;
/**
* Called when user edits the password.
*
@@ -207,6 +226,8 @@ class LoginDialog extends Component {
});
}
_onCancel: () => void;
/**
* Notifies this LoginDialog that it has been dismissed by cancel.
*
@@ -217,6 +238,8 @@ class LoginDialog extends Component {
this.props.dispatch(cancelLogin());
}
_onLogin: () => void;
/**
* Notifies this LoginDialog that the login button (OK) has been pressed by
* the user.

View File

@@ -198,8 +198,8 @@ export function getNearestReceiverVideoQualityLevel(availableHeight: number) {
}
/**
* Handle an error thrown by the backend (i.e. lib-jitsi-meet) while
* manipulating a conference participant (e.g. pin or select participant).
* Handle an error thrown by the backend (i.e. {@code lib-jitsi-meet}) while
* manipulating a conference participant (e.g. Pin or select participant).
*
* @param {Error} err - The Error which was thrown by the backend while
* manipulating a conference participant and which is to be handled.

View File

@@ -28,8 +28,13 @@ import { VIDEO_QUALITY_LEVELS } from './constants';
import { isRoomValid } from './functions';
const DEFAULT_STATE = {
conference: undefined,
joining: undefined,
leaving: undefined,
locked: undefined,
maxReceiverVideoQuality: VIDEO_QUALITY_LEVELS.HIGH,
password: undefined,
passwordRequired: undefined,
preferredReceiverVideoQuality: VIDEO_QUALITY_LEVELS.HIGH
};

View File

@@ -71,7 +71,7 @@ export type ConnectionFailedError = {
/**
* Opens new connection.
*
* @param {string} [id] - The XMPP user's ID (e.g. user@server.com).
* @param {string} [id] - The XMPP user's ID (e.g. {@code user@server.com}).
* @param {string} [password] - The XMPP user's password.
* @returns {Function}
*/

View File

@@ -4,7 +4,7 @@ import { toState } from '../redux';
/**
* Retrieves a simplified version of the conference/location URL stripped of URL
* params (i.e. query/search and hash) which should be used for sending invites.
* params (i.e. Query/search and hash) which should be used for sending invites.
*
* @param {Function|Object} stateOrGetState - The redux state or redux's
* {@code getState} function.

View File

@@ -1,35 +1,35 @@
import PropTypes from 'prop-types';
/* @flow */
import React, { Component } from 'react';
import { connect } from 'react-redux';
/**
* The type of the React {@code Component} props of {@link DialogContainer}.
*/
type Props = {
/**
* The component to render.
*/
_component: Function,
/**
* The props to pass to the component that will be rendered.
*/
_componentProps: Object,
/**
* True if the UI is in a compact state where we don't show dialogs.
*/
_reducedUI: boolean
};
/**
* Implements a DialogContainer responsible for showing all dialogs. We will
* need a separate container so we can handle multiple dialogs by showing them
* simultaneously or queuing them.
*/
export class DialogContainer extends Component {
/**
* DialogContainer component's property types.
*
* @static
*/
static propTypes = {
/**
* The component to render.
*/
_component: PropTypes.func,
/**
* The props to pass to the component that will be rendered.
*/
_componentProps: PropTypes.object,
/**
* True if the UI is in a compact state where we don't show dialogs.
*/
_reducedUI: PropTypes.bool
};
export class DialogContainer extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*

View File

@@ -1,7 +1,8 @@
// @flow
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { Container, Text } from '../../react';
import { dialog as styles } from './styles';
@@ -31,9 +32,9 @@ export default class DialogContent extends Component<Props> {
: children;
return (
<View style = { styles.dialogContainer }>
<Container style = { styles.dialogContainer }>
{ childrenComponent }
</View>
</Container>
);
}
}

View File

@@ -0,0 +1,5 @@
/**
* Placeholder styles for web to be able to use cross platform components
* unmodified such as {@code DialogContent}.
*/
export const dialog = {};

View File

@@ -1,3 +1,5 @@
import { Platform } from 'react-native';
// FIXME The import of react-native-vector-icons makes the file native-specific
// but the file's name and/or location (within the directory structure) don't
// reflect that, it suggests the file is platform-independent.
@@ -10,3 +12,8 @@ import icoMoonConfig from './jitsi.json';
* Creates the Jitsi icon set from the ico moon project config file.
*/
export const Icon = createIconSetFromIcoMoon(icoMoonConfig);
// Dynamically load font on iOS
if (Platform.OS === 'ios') {
Icon.loadFont('jitsi.ttf');
}

View File

@@ -18,7 +18,7 @@ declare var interfaceConfig: Object;
* @public
* @type {Array<string>}
*/
export const LANGUAGES = Object.keys(LANGUAGES_RESOURCES);
export const LANGUAGES: Array<string> = Object.keys(LANGUAGES_RESOURCES);
/**
* The default language.

View File

@@ -32,7 +32,7 @@ const SOCK_STREAM = 1; /* stream socket */
* The RTCPeerConnection provided by react-native-webrtc fires onaddstream
* before it remembers remotedescription (and thus makes it available to API
* clients). Because that appears to be a problem for lib-jitsi-meet which has
* been successfully running on Chrome, Firefox, etc. for a very long
* been successfully running on Chrome, Firefox and others for a very long
* time, attempt to meet its expectations (by extending RTCPPeerConnection).
*
* @class

View File

@@ -39,9 +39,10 @@ function _getCommonPrototype(a, b) {
}
/**
* Implements an absolute minimum of the common logic of Document.querySelector
* and Element.querySelector. Implements the most simple of selectors necessary
* to satisfy the call sites at the time of this writing i.e. select by tagName.
* Implements an absolute minimum of the common logic of
* {@code Document.querySelector} and {@code Element.querySelector}. Implements
* the most simple of selectors necessary to satisfy the call sites at the time
* of this writing (i.e. Select by tagName).
*
* @param {Node} node - The Node which is the root of the tree to query.
* @param {string} selectors - The group of CSS selectors to match on.

View File

@@ -1,4 +1,5 @@
import PropTypes from 'prop-types';
/* @flow */
import React, { Component } from 'react';
import { trackVideoStarted } from '../../tracks';
@@ -6,51 +7,71 @@ import { trackVideoStarted } from '../../tracks';
import { shouldRenderVideoTrack } from '../functions';
import { Video } from './_';
/**
* The type of the React {@code Component} props of {@link AbstractVideoTrack}.
*/
export type Props = {
/**
* The Redux dispatch function.
*/
dispatch: Dispatch<*>,
/**
* Callback to invoke when the {@link Video} of {@code AbstractVideoTrack}
* is clicked/pressed.
*/
onPress?: Function,
/**
* The Redux representation of the participant's video track.
*/
videoTrack?: Object,
/**
* Whether or not video should be rendered after knowing video playback has
* started.
*/
waitForVideoStarted?: boolean,
/**
* The z-order of the Video of AbstractVideoTrack in the stacking space of
* all Videos. For more details, refer to the zOrder property of the Video
* class for React Native.
*/
zOrder?: number,
/**
* Indicates whether zooming (pinch to zoom and/or drag) is enabled.
*/
zoomEnabled?: boolean
};
/**
* The type of the React {@code Component} state of {@link AbstractVideoTrack}.
*/
type State = {
/**
* The Redux representation of the participant's video track.
*/
videoTrack: Object | null
};
/**
* Implements a React {@link Component} that renders video element for a
* specific video track.
*
* @abstract
*/
export default class AbstractVideoTrack extends Component {
/**
* AbstractVideoTrack component's property types.
*
* @static
*/
static propTypes = {
dispatch: PropTypes.func,
/**
* Callback to invoke when the {@link Video} of
* {@code AbstractVideoTrack} is clicked/pressed.
*/
onPress: PropTypes.func,
videoTrack: PropTypes.object,
waitForVideoStarted: PropTypes.bool,
/**
* The z-order of the Video of AbstractVideoTrack in the stacking space
* of all Videos. For more details, refer to the zOrder property of the
* Video class for React Native.
*/
zOrder: PropTypes.number,
/**
* Indicates whether zooming (pinch to zoom and/or drag) is enabled.
*/
zoomEnabled: PropTypes.bool
};
export default class AbstractVideoTrack<P: Props> extends Component<P, State> {
/**
* Initializes a new AbstractVideoTrack instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
constructor(props: P) {
super(props);
this.state = {
@@ -69,7 +90,7 @@ export default class AbstractVideoTrack extends Component {
* receive.
* @returns {void}
*/
componentWillReceiveProps(nextProps) {
componentWillReceiveProps(nextProps: P) {
const oldValue = this.state.videoTrack;
const newValue = _falsy2null(nextProps.videoTrack);
@@ -88,7 +109,7 @@ export default class AbstractVideoTrack extends Component {
const { videoTrack } = this.state;
let render;
if (this.props.waitForVideoStarted) {
if (this.props.waitForVideoStarted && videoTrack) {
// That's the complex case: we have to wait for onPlaying before we
// render videoTrack. The complexity comes from the fact that
// onPlaying will come after we render videoTrack.
@@ -110,14 +131,15 @@ export default class AbstractVideoTrack extends Component {
render = shouldRenderVideoTrack(videoTrack, false);
}
const stream
= render ? videoTrack.jitsiTrack.getOriginalStream() : null;
const stream = render && videoTrack
? videoTrack.jitsiTrack.getOriginalStream() : null;
// Actual zoom is currently only enabled if the stream is a desktop
// stream.
const zoomEnabled
= this.props.zoomEnabled
&& stream
&& videoTrack
&& videoTrack.videoType === 'desktop';
return (
@@ -131,6 +153,8 @@ export default class AbstractVideoTrack extends Component {
);
}
_onVideoPlaying: () => void;
/**
* Handler for case when video starts to play.
*

View File

@@ -1,6 +1,5 @@
// @flow
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { RTCView } from 'react-native-webrtc';
@@ -9,60 +8,58 @@ import { Pressable } from '../../../react';
import styles from './styles';
import VideoTransform from './VideoTransform';
/**
* The type of the React {@code Component} props of {@link Video}.
*/
type Props = {
mirror: boolean,
onPlaying: Function,
/**
* Callback to invoke when the {@code Video} is clicked/pressed.
*/
onPress: Function,
stream: Object,
/**
* Similarly to the CSS property z-index, specifies the z-order of this
* Video in the stacking space of all Videos. When Videos overlap,
* zOrder determines which one covers the other. A Video with a larger
* zOrder generally covers a Video with a lower one.
*
* Non-overlapping Videos may safely share a z-order (because one does
* not have to cover the other).
*
* The support for zOrder is platform-dependent and/or
* implementation-specific. Thus, specifying a value for zOrder is to be
* thought of as giving a hint rather than as imposing a requirement.
* For example, video renderers such as Video are commonly implemented
* using OpenGL and OpenGL views may have different numbers of layers in
* their stacking space. Android has three: a layer bellow the window
* (aka default), a layer bellow the window again but above the previous
* layer (aka media overlay), and above the window. Consequently, it is
* advisable to limit the number of utilized layers in the stacking
* space to the minimum sufficient for the desired display. For example,
* a video call application usually needs a maximum of two zOrder
* values: 0 for the remote video(s) which appear in the background, and
* 1 for the local video(s) which appear above the remote video(s).
*/
zOrder: number,
/**
* Indicates whether zooming (pinch to zoom and/or drag) is enabled.
*/
zoomEnabled: boolean
};
/**
* The React Native {@link Component} which is similar to Web's
* {@code HTMLVideoElement} and wraps around react-native-webrtc's
* {@link RTCView}.
*/
export default class Video extends Component<*> {
/**
* {@code Video} component's property types.
*
* @static
*/
static propTypes = {
mirror: PropTypes.bool,
onPlaying: PropTypes.func,
/**
* Callback to invoke when the {@code Video} is clicked/pressed.
*/
onPress: PropTypes.func,
stream: PropTypes.object,
/**
* Similarly to the CSS property z-index, specifies the z-order of this
* Video in the stacking space of all Videos. When Videos overlap,
* zOrder determines which one covers the other. A Video with a larger
* zOrder generally covers a Video with a lower one.
*
* Non-overlapping Videos may safely share a z-order (because one does
* not have to cover the other).
*
* The support for zOrder is platform-dependent and/or
* implementation-specific. Thus, specifying a value for zOrder is to be
* thought of as giving a hint rather than as imposing a requirement.
* For example, video renderers such as Video are commonly implemented
* using OpenGL and OpenGL views may have different numbers of layers in
* their stacking space. Android has three: a layer bellow the window
* (aka default), a layer bellow the window again but above the previous
* layer (aka media overlay), and above the window. Consequently, it is
* advisable to limit the number of utilized layers in the stacking
* space to the minimum sufficient for the desired display. For example,
* a video call application usually needs a maximum of two zOrder
* values: 0 for the remote video(s) which appear in the background, and
* 1 for the local video(s) which appear above the remote video(s).
*/
zOrder: PropTypes.number,
/**
* Indicates whether zooming (pinch to zoom and/or drag) is enabled.
*/
zoomEnabled: PropTypes.bool
};
export default class Video extends Component<Props> {
/**
* React Component method that executes once component is mounted.
*

View File

@@ -1,8 +1,11 @@
/* @flow */
import React from 'react';
import { View } from 'react-native';
import { connect } from 'react-redux';
import AbstractVideoTrack from '../AbstractVideoTrack';
import type { Props } from '../AbstractVideoTrack';
import styles from './styles';
/**
@@ -10,14 +13,7 @@ import styles from './styles';
*
* @extends AbstractVideoTrack
*/
class VideoTrack extends AbstractVideoTrack {
/**
* VideoTrack component's property types.
*
* @static
*/
static propTypes = AbstractVideoTrack.propTypes
class VideoTrack extends AbstractVideoTrack<Props> {
/**
* Renders the video element for the associated video track.
*

View File

@@ -41,11 +41,9 @@ export default class Audio extends AbstractAudio {
* @returns {ReactElement}
*/
render() {
const loop = this.props.loop ? 'true' : null;
return (
<audio
loop = { loop }
loop = { Boolean(this.props.loop) }
onCanPlayThrough = { this._onCanPlayThrough }
preload = 'auto'

View File

@@ -1,12 +1,42 @@
import PropTypes from 'prop-types';
/* @flow */
import React, { Component } from 'react';
/**
* The type of the React {@code Component} props of {@link Video}.
*/
type Props = {
/**
* CSS classes to add to the video element.
*/
className: string,
/**
* The value of the id attribute of the video. Used by the torture tests to
* locate video elements.
*/
id: string,
/**
* Optional callback to invoke once the video starts playing.
*/
onVideoPlaying: Function,
/**
* The JitsiLocalTrack to display.
*/
videoTrack: ?Object
};
/**
* Component that renders a video element for a passed in video track.
*
* @extends Component
*/
class Video extends Component {
class Video extends Component<Props> {
_videoElement: ?Object;
/**
* Default values for {@code Video} component's properties.
*
@@ -18,41 +48,13 @@ class Video extends Component {
id: ''
};
/**
* {@code Video} component's property types.
*
* @static
*/
static propTypes = {
/**
* CSS classes to add to the video element.
*/
className: PropTypes.string,
/**
* The value of the id attribute of the video. Used by the torture tests
* to locate video elements.
*/
id: PropTypes.string,
/**
* Optional callback to invoke once the video starts playing.
*/
onVideoPlaying: PropTypes.func,
/**
* The JitsiLocalTrack to display.
*/
videoTrack: PropTypes.object
};
/**
* Initializes a new {@code Video} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
constructor(props: Props) {
super(props);
/**
@@ -78,8 +80,10 @@ class Video extends Component {
* @returns {void}
*/
componentDidMount() {
this._videoElement.volume = 0;
this._videoElement.onplaying = this._onVideoPlaying;
if (this._videoElement) {
this._videoElement.volume = 0;
this._videoElement.onplaying = this._onVideoPlaying;
}
this._attachTrack(this.props.videoTrack);
}
@@ -98,13 +102,13 @@ class Video extends Component {
/**
* Updates the video display only if a new track is added. This component's
* updating is blackboxed from React to prevent re-rendering of video
* element, as the lib uses track.attach(videoElement) instead.
* element, as the lib uses {@code track.attach(videoElement)} instead.
*
* @inheritdoc
* @returns {boolean} - False is always returned to blackbox this component.
* @returns {boolean} - False is always returned to blackbox this component
* from React.
*/
shouldComponentUpdate(nextProps) {
shouldComponentUpdate(nextProps: Props) {
const currentJitsiTrack = this.props.videoTrack
&& this.props.videoTrack.jitsiTrack;
const nextJitsiTrack = nextProps.videoTrack
@@ -167,6 +171,8 @@ class Video extends Component {
}
}
_onVideoPlaying: () => void;
/**
* Invokes the onvideoplaying callback if defined.
*
@@ -179,6 +185,8 @@ class Video extends Component {
}
}
_setVideoElement: () => void;
/**
* Sets an instance variable for the component's video element so it can be
* referenced later for attaching and detaching a JitsiLocalTrack.

View File

@@ -1,51 +1,49 @@
import PropTypes from 'prop-types';
/* @flow */
import React from 'react';
import { connect } from 'react-redux';
import AbstractVideoTrack from '../AbstractVideoTrack';
import type { Props as AbstractVideoTrackProps } from '../AbstractVideoTrack';
import Video from './Video';
/**
* The type of the React {@code Component} props of {@link VideoTrack}.
*/
type Props = {
...AbstractVideoTrackProps,
/**
* CSS classes to add to the video element.
*/
className: string,
/**
* The value of the id attribute of the video. Used by the torture tests
* to locate video elements.
*/
id: string
};
/**
* Component that renders a video element for a passed in video track and
* notifies the store when the video has started playing.
*
* @extends AbstractVideoTrack
*/
class VideoTrack extends AbstractVideoTrack {
class VideoTrack extends AbstractVideoTrack<Props> {
/**
* Default values for {@code VideoTrack} component's properties.
*
* @static
*/
static defaultProps = {
...AbstractVideoTrack.defaultProps,
className: '',
id: ''
};
/**
* {@code VideoTrack} component's property types.
*
* @static
*/
static propTypes = {
...AbstractVideoTrack.propTypes,
/**
* CSS classes to add to the video element.
*/
className: PropTypes.string,
/**
* The value of the id attribute of the video. Used by the torture tests
* to locate video elements.
*/
id: PropTypes.string
};
/**
* Renders the video element.
*
@@ -62,6 +60,8 @@ class VideoTrack extends AbstractVideoTrack {
videoTrack = { this.props.videoTrack } />
);
}
_onVideoPlaying: () => void;
}
export default connect()(VideoTrack);

View File

@@ -58,7 +58,7 @@ export function isVideoMutedByUser(stateful: Function | Object) {
* otherwise, false.
*/
export function shouldRenderVideoTrack(
videoTrack: { muted: boolean, videoStarted: boolean },
videoTrack: ?{ muted: boolean, videoStarted: boolean },
waitForVideoStarted: boolean) {
return (
videoTrack

View File

@@ -146,11 +146,11 @@ export default class Avatar extends Component<Props, State> {
}
/**
* Notifies this {@code Component} that it will be unmounted and destroyed
* and, most importantly, that it should no longer call
* {@link #setState(Object)}. {@code Avatar} needs it because it downloads
* images via {@link ImageCache} which will asynchronously notify about
* success.
* Notifies this {@code Component} that it will be unmounted and destroyed,
* and most importantly, that it should no longer call
* {@link #setState(Object)}. The {@code Avatar} needs it because it
* downloads images via {@link ImageCache} which will asynchronously notify
* about success.
*
* @inheritdoc
* @returns {void}

View File

@@ -1,5 +1,5 @@
// @flow
import md5 from 'js-md5';
import { getAvatarURL as _getAvatarURL } from 'js-utils/avatar';
import { toState } from '../redux';
@@ -42,40 +42,18 @@ export function getAvatarURL({ avatarID, avatarURL, email, id }: {
return avatarURL;
}
let key = email || avatarID;
let urlPrefix;
let urlSuffix;
// The deployment is allowed to choose the avatar service which is to
// generate the random avatars.
const avatarService
= typeof interfaceConfig === 'object'
&& interfaceConfig.RANDOM_AVATAR_URL_PREFIX
? {
urlPrefix: interfaceConfig.RANDOM_AVATAR_URL_PREFIX,
urlSuffix: interfaceConfig.RANDOM_AVATAR_URL_SUFFIX }
: undefined;
// If the ID looks like an e-mail address, we'll use Gravatar because it
// supports e-mail addresses.
if (key && key.indexOf('@') > 0) {
urlPrefix = 'https://www.gravatar.com/avatar/';
urlSuffix = '?d=wavatar&size=200';
} else {
// Otherwise, we do not have much a choice but a random avatar (fetched
// from a configured avatar service).
if (!key) {
key = id;
if (!key) {
return undefined;
}
}
// The deployment is allowed to choose the avatar service which is to
// generate the random avatars.
urlPrefix
= typeof interfaceConfig === 'object'
&& interfaceConfig.RANDOM_AVATAR_URL_PREFIX;
if (urlPrefix) {
urlSuffix = interfaceConfig.RANDOM_AVATAR_URL_SUFFIX;
} else {
// Otherwise, use a default (meeples, of course).
urlPrefix = 'https://abotars.jitsi.net/meeple/';
urlSuffix = '';
}
}
return urlPrefix + md5.hex(key.trim().toLowerCase()) + urlSuffix;
// eslint-disable-next-line object-property-newline
return _getAvatarURL({ avatarID, email, id }, avatarService);
}
/**
@@ -149,8 +127,9 @@ export function getParticipantCount(stateful: Object | Function) {
/**
* Returns participant's display name.
* FIXME: remove the hardcoded strings once interfaceConfig is stored in redux
* and merge with a similarly named method in conference.js.
*
* FIXME: Remove the hardcoded strings once interfaceConfig is stored in redux
* and merge with a similarly named method in {@code conference.js}.
*
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state.

View File

@@ -1,5 +1,6 @@
/* @flow */
import InlineDialog from '@atlaskit/inline-dialog';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
/**
@@ -28,13 +29,68 @@ function _mapPositionToPaddingClass(position = 'left') {
return DIALOG_TO_PADDING_POSITION[position.split(' ')[0]];
}
/**
* The type of the React {@code Component} props of {@link Popover}.
*/
type Props = {
/**
* A child React Element to use as the trigger for showing the dialog.
*/
children: React$Node,
/**
* Additional CSS classnames to apply to the root of the {@code Popover}
* component.
*/
className: string,
/**
* The ReactElement to display within the dialog.
*/
content: Object,
/**
* Whether displaying of the popover should be prevented.
*/
disablePopover: boolean,
/**
* An id attribute to apply to the root of the {@code Popover}
* component.
*/
id: string,
/**
* Callback to invoke when the popover has opened.
*/
onPopoverOpen: Function,
/**
* From which side of the dialog trigger the dialog should display. The
* value will be passed to {@code InlineDialog}.
*/
position: string
};
/**
* The type of the React {@code Component} state of {@link Popover}.
*/
type State = {
/**
* Whether or not the {@code InlineDialog} should be displayed.
*/
showDialog: boolean
};
/**
* Implements a React {@code Component} for showing an {@code InlineDialog} on
* mouseenter of the trigger and contents, and hiding the dialog on mouseleave.
*
* @extends Component
*/
class Popover extends Component {
class Popover extends Component<Props, State> {
/**
* Default values for {@code Popover} component's properties.
*
@@ -45,66 +101,16 @@ class Popover extends Component {
id: ''
};
/**
* {@code Popover} component's property types.
*
* @static
*/
static propTypes = {
/**
* A child React Element to use as the trigger for showing the dialog.
*/
children: PropTypes.object,
/**
* Additional CSS classnames to apply to the root of the {@code Popover}
* component.
*/
className: PropTypes.string,
/**
* The ReactElement to display within the dialog.
*/
content: PropTypes.object,
/**
* Whether displaying of the popover should be prevented.
*/
disablePopover: PropTypes.bool,
/**
* An id attribute to apply to the root of the {@code Popover}
* component.
*/
id: PropTypes.string,
/**
* Callback to invoke when the popover has opened.
*/
onPopoverOpen: PropTypes.func,
/**
* From which side of the dialog trigger the dialog should display. The
* value will be passed to {@code InlineDialog}.
*/
position: PropTypes.string
};
/**
* Initializes a new {@code Popover} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
constructor(props: Props) {
super(props);
this.state = {
/**
* Whether or not the {@code InlineDialog} should be displayed.
*
* @type {boolean}
*/
showDialog: false
};
@@ -136,6 +142,8 @@ class Popover extends Component {
);
}
_onHideDialog: () => void;
/**
* Stops displaying the {@code InlineDialog}.
*
@@ -146,6 +154,8 @@ class Popover extends Component {
this.setState({ showDialog: false });
}
_onShowDialog: () => void;
/**
* Displays the {@code InlineDialog} and calls any registered onPopoverOpen
* callbacks.

View File

@@ -17,6 +17,11 @@ export type Item = {
*/
elementAfter?: ?ComponentType<any>,
/**
* Unique ID of the item.
*/
id: Object | string,
/**
* Item title
*/

View File

@@ -58,7 +58,6 @@ export type Props = {
visible?: ?boolean
};
/**
* Abstract (base) class for container of React {@link Component} children with
* a style.
@@ -95,6 +94,7 @@ export default class AbstractContainer<P: Props> extends Component<P> {
...filteredProps
} = props || this.props;
// $FlowFixMe
return React.createElement(type, filteredProps, children);
}
}

View File

@@ -0,0 +1,22 @@
// @flow
import { Component } from 'react';
/**
* Abstract component that defines a refreshable page to be rendered by
* {@code PagedList}.
*/
export default class AbstractPage<P> extends Component<P> {
/**
* Method to be overriden by the implementing classes to refresh the data
* content of the component.
*
* Note: It is a static method as the {@code Component} may not be
* initialized yet when the UI invokes refresh (e.g. Tab change).
*
* @returns {void}
*/
static refresh() {
// No implementation in abstract class.
}
}

View File

@@ -43,7 +43,13 @@ type Props = {
/**
* An array of sections
*/
sections: Array<Section>
sections: Array<Section>,
/**
* Optional array of on-slide actions this list should support. For details
* see https://github.com/dancormier/react-native-swipeout.
*/
slideActions?: Array<Object>
};
/**
@@ -85,8 +91,8 @@ class NavigateSectionList extends Component<Props> {
}
/**
* Implements React's Component.render.
* Note: we don't use the refreshing value yet, because refreshing of these
* Implements React's {@code Component.render}.
* Note: We don't use the refreshing value yet, because refreshing of these
* lists is super quick, no need to complicate the code - yet.
*
* @inheritdoc
@@ -205,7 +211,8 @@ class NavigateSectionList extends Component<Props> {
key = { key }
onPress = { url ? this._onPress(url) : undefined }
secondaryAction = {
url ? undefined : this._onSecondaryAction(id) } />
url ? undefined : this._onSecondaryAction(id) }
slideActions = { this.props.slideActions } />
);
}

View File

@@ -1,2 +1,3 @@
export * from './_';
export { default as AbstractPage } from './AbstractPage';
export { default as NavigateSectionList } from './NavigateSectionList';

View File

@@ -1,184 +0,0 @@
// @flow
import React, { Component } from 'react';
import { View } from 'react-native';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link AbstractPagedList}.
*/
type Props = {
/**
* The zero-based index of the page that should be rendered (selected) by
* default.
*/
defaultPage: number,
/**
* Indicates if the list is disabled or not.
*/
disabled: boolean,
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* Callback to execute on page change.
*/
onSelectPage: ?Function,
/**
* The pages of the PagedList component to be rendered.
*
* Note: An element's {@code component} may be {@code undefined} and then it
* won't need to be rendered.
*/
pages: Array<{
component: ?Object,
icon: string | number,
title: string
}>
};
/**
* The type of the React {@code Component} state of {@link AbstractPagedList}.
*/
type State = {
/**
* The currently selected page.
*/
pageIndex: number
};
/**
* Abstract class containing the platform independent logic of the paged lists.
*/
export default class AbstractPagedList extends Component<Props, State> {
/**
* Initializes a new {@code AbstractPagedList} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.state = {
pageIndex: this._validatePageIndex(props.defaultPage)
};
// Bind event handlers so they are only bound once per instance.
this._maybeRefreshSelectedPage
= this._maybeRefreshSelectedPage.bind(this);
}
/**
* Implements React's {@link Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this._maybeRefreshSelectedPage(false);
}
/**
* Renders the component.
*
* @inheritdoc
*/
render() {
const { disabled } = this.props;
const pages = this.props.pages.filter(({ component }) => component);
return (
<View
style = { [
styles.pagedListContainer,
disabled ? styles.pagedListContainerDisabled : null
] }>
{
pages.length > 1
? this._renderPagedList(disabled)
: pages.length === 1
? React.createElement(
// $FlowExpectedError
/* type */ pages[0].component,
/* props */ {
disabled,
style: styles.pagedList
})
: null
}
</View>
);
}
_maybeRefreshSelectedPage: ?boolean => void;
/**
* Components that this PagedList displays may have a refresh function to
* refresh its content when displayed (or based on custom logic). This
* function invokes this logic if it's present.
*
* @private
* @param {boolean} isInteractive - If true this refresh was caused by
* direct user interaction, false otherwise.
* @returns {void}
*/
_maybeRefreshSelectedPage(isInteractive: boolean = true) {
const selectedPage = this.props.pages[this.state.pageIndex];
let component;
if (selectedPage && (component = selectedPage.component)) {
const { refresh } = component;
typeof refresh === 'function'
&& refresh.call(component, this.props.dispatch, isInteractive);
}
}
_renderPagedList: boolean => React$Node;
_selectPage: number => void;
/**
* Sets the selected page.
*
* @param {number} pageIndex - The index of the selected page.
* @protected
* @returns {void}
*/
_selectPage(pageIndex: number) {
// eslint-disable-next-line no-param-reassign
pageIndex = this._validatePageIndex(pageIndex);
const { onSelectPage } = this.props;
typeof onSelectPage === 'function' && onSelectPage(pageIndex);
this.setState({ pageIndex }, this._maybeRefreshSelectedPage);
}
_validatePageIndex: number => number;
/**
* Validates the requested page index and returns a safe value.
*
* @private
* @param {number} pageIndex - The requested page index.
* @returns {number}
*/
_validatePageIndex(pageIndex) {
// pageIndex may point to a non-existing page if some of the pages are
// disabled (their component property is undefined).
const maxPageIndex
= this.props.pages.filter(({ component }) => component).length - 1;
return Math.max(0, Math.min(maxPageIndex, pageIndex));
}
}

View File

@@ -58,7 +58,7 @@ export default class Container<P: Props> extends AbstractContainer<P> {
accessibilityLabel,
accessible,
onPress: onClick,
underlayColor
...touchFeedback && { underlayColor }
},
element);
}

View File

@@ -1,47 +1,47 @@
import PropTypes from 'prop-types';
/* @flow */
import React, { Component } from 'react';
import { Linking } from 'react-native';
import Text from './Text';
/**
* The type of the React {@code Component} props of {@link Link}.
*/
type Props = {
/**
* The children to be displayed within this Link.
*/
children: React$Node,
/**
* Notifies that this Link failed to open the URL associated with it.
*/
onLinkingOpenURLRejected: Function,
/**
* The CSS style to be applied to this Link for the purposes of display.
*/
style: Object,
/**
* The URL to be opened when this Link is clicked/pressed.
*/
url: string
};
/**
* Implements a (hyper)link to a URL in the fashion of the HTML anchor element
* and its href attribute.
*/
export default class Link extends Component {
/**
* {@code Link} component's property types.
*
* @static
*/
static propTypes = {
/**
* The children to be displayed within this Link.
*/
children: PropTypes.node,
/**
* Notifies that this Link failed to open the URL associated with it.
*/
onLinkingOpenURLRejected: PropTypes.func,
/**
* The CSS style to be applied to this Link for the purposes of display.
*/
style: PropTypes.object,
/**
* The URL to be opened when this Link is clicked/pressed.
*/
url: PropTypes.string
};
export default class Link extends Component<Props> {
/**
* Initializes a new Link instance.
*
* @param {Object} props - Component properties.
*/
constructor(props) {
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
@@ -77,6 +77,8 @@ export default class Link extends Component {
onRejected && onRejected(reason);
}
_onPress: () => void;
/**
* Handles press on this Link. Opens the URL associated with this Link.
*

View File

@@ -1,6 +1,9 @@
// @flow
import React, { Component } from 'react';
import Swipeout from 'react-native-swipeout';
import { ColorPalette } from '../../../styles';
import Container from './Container';
import Text from './Text';
@@ -22,7 +25,13 @@ type Props = {
/**
* Function to be invoked when secondary action was performed on an Item.
*/
secondaryAction: ?Function
secondaryAction: ?Function,
/**
* Optional array of on-slide actions this list should support. For details
* see https://github.com/dancormier/react-native-swipeout.
*/
slideActions?: Array<Object>
}
/**
@@ -129,37 +138,59 @@ export default class NavigateSectionListItem extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { colorBase, lines, title } = this.props.item;
const { slideActions } = this.props;
const { id, colorBase, lines, title } = this.props.item;
const avatarStyles = {
...styles.avatar,
...this._getAvatarColor(colorBase)
};
let right;
// NOTE: The {@code Swipeout} component has an onPress prop encapsulated
// in the {@code right} array, but we need to bind it to the ID of the
// item too.
if (slideActions) {
right = [];
for (const slideAction of slideActions) {
right.push({
backgroundColor: slideAction.backgroundColor,
onPress: slideAction.onPress.bind(undefined, id),
text: slideAction.text
});
}
}
return (
<Container
onClick = { this.props.onPress }
style = { styles.listItem }
underlayColor = { UNDERLAY_COLOR }>
<Container style = { styles.avatarContainer }>
<Container style = { avatarStyles }>
<Text style = { styles.avatarContent }>
{title.substr(0, 1).toUpperCase()}
</Text>
<Swipeout
backgroundColor = { ColorPalette.transparent }
right = { right }>
<Container
onClick = { this.props.onPress }
style = { styles.listItem }
underlayColor = { UNDERLAY_COLOR }>
<Container style = { styles.avatarContainer }>
<Container style = { avatarStyles }>
<Text style = { styles.avatarContent }>
{title.substr(0, 1).toUpperCase()}
</Text>
</Container>
</Container>
<Container style = { styles.listItemDetails }>
<Text
numberOfLines = { 1 }
style = { [
styles.listItemText,
styles.listItemTitle
] }>
{title}
</Text>
{this._renderItemLines(lines)}
</Container>
{ this.props.secondaryAction
&& this._renderSecondaryAction() }
</Container>
<Container style = { styles.listItemDetails }>
<Text
numberOfLines = { 1 }
style = {{
...styles.listItemText,
...styles.listItemTitle
}}>
{title}
</Text>
{this._renderItemLines(lines)}
</Container>
{ this.props.secondaryAction && this._renderSecondaryAction() }
</Container>
</Swipeout>
);
}
}

View File

@@ -1,199 +0,0 @@
// @flow
import React from 'react';
import { Text, TouchableOpacity, View, ViewPagerAndroid } from 'react-native';
import { connect } from 'react-redux';
import { Icon } from '../../../font-icons';
import AbstractPagedList from './AbstractPagedList';
import styles from './styles';
/**
* An Android specific component to render a paged list.
*
* @extends PagedList
*/
class PagedList extends AbstractPagedList {
/**
* A reference to the viewpager.
*/
_viewPager: ViewPagerAndroid;
/**
* Initializes a new {@code PagedList} instance.
*
* @inheritdoc
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onIconPress = this._onIconPress.bind(this);
this._getIndicatorStyle = this._getIndicatorStyle.bind(this);
this._onPageSelected = this._onPageSelected.bind(this);
this._setViewPager = this._setViewPager.bind(this);
}
_onIconPress: number => Function;
/**
* Constructs a function to be used as a callback for the icons in the tab
* bar.
*
* @param {number} pageIndex - The index of the page to activate via the
* callback.
* @private
* @returns {Function}
*/
_onIconPress(pageIndex) {
return () => {
this._viewPager.setPage(pageIndex);
this._selectPage(pageIndex);
};
}
_getIndicatorStyle: number => Object;
/**
* Constructs the style of an indicator.
*
* @param {number} indicatorIndex - The index of the indicator.
* @private
* @returns {Object}
*/
_getIndicatorStyle(indicatorIndex) {
if (this.state.pageIndex === indicatorIndex) {
return styles.pageIndicatorActive;
}
return null;
}
_onPageSelected: Object => void;
/**
* Updates the index of the currently selected page, based on the native
* event received from the {@link ViewPagerAndroid} component.
*
* @param {Object} event - The native event of the callback.
* @private
* @returns {void}
*/
_onPageSelected({ nativeEvent: { position } }) {
if (this.state.pageIndex !== position) {
this._selectPage(position);
}
}
/**
* Renders a single page of the page list.
*
* @private
* @param {Object} page - The page to render.
* @param {number} index - The index of the rendered page.
* @param {boolean} disabled - Renders the page disabled.
* @returns {React$Node}
*/
_renderPage(page, index, disabled) {
return page.component
? <View key = { index }>
{
React.createElement(
page.component,
{
disabled
})
}
</View>
: null;
}
/**
* Renders a page indicator (icon) for the page.
*
* @private
* @param {Object} page - The page the indicator is rendered for.
* @param {number} index - The index of the page the indicator is rendered
* for.
* @param {boolean} disabled - Renders the indicator disabled.
* @returns {React$Node}
*/
_renderPageIndicator(page, index, disabled) {
return page.component
? <TouchableOpacity
disabled = { disabled }
key = { index }
onPress = { this._onIconPress(index) }
style = { styles.pageIndicator } >
<View style = { styles.pageIndicator }>
<Icon
name = { page.icon }
style = { [
styles.pageIndicatorIcon,
this._getIndicatorStyle(index)
] } />
<Text
style = { [
styles.pageIndicatorText,
this._getIndicatorStyle(index)
] }>
{ page.title }
</Text>
</View>
</TouchableOpacity>
: null;
}
/**
* Renders the paged list if multiple pages are to be rendered. This is the
* platform dependent part of the component.
*
* @param {boolean} disabled - True if the rendered lists should be
* disabled.
* @returns {ReactElement}
*/
_renderPagedList(disabled) {
const { defaultPage, pages } = this.props;
return (
<View style = { styles.pagedListContainer }>
<ViewPagerAndroid
initialPage = { defaultPage }
onPageSelected = { this._onPageSelected }
peekEnabled = { true }
ref = { this._setViewPager }
style = { styles.pagedList }>
{
pages.map((page, index) => this._renderPage(
page, index, disabled
))
}
</ViewPagerAndroid>
<View style = { styles.pageIndicatorContainer }>
{
pages.map((page, index) => this._renderPageIndicator(
page, index, disabled
))
}
</View>
</View>
);
}
_setViewPager: Object => void;
/**
* Sets the {@link ViewPagerAndroid} instance.
*
* @param {ViewPagerAndroid} viewPager - The {@code ViewPagerAndroid}
* instance.
* @private
* @returns {void}
*/
_setViewPager(viewPager) {
this._viewPager = viewPager;
}
}
export default connect()(PagedList);

View File

@@ -1,100 +0,0 @@
// @flow
import React from 'react';
import { TabBarIOS } from 'react-native';
import { connect } from 'react-redux';
import AbstractPagedList from './AbstractPagedList';
import styles from './styles';
/**
* An iOS specific component to render a paged list.
*
* @extends PagedList
*/
class PagedList extends AbstractPagedList {
/**
* Initializes a new {@code PagedList} instance.
*
* @inheritdoc
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onTabSelected = this._onTabSelected.bind(this);
}
_onTabSelected: number => Function;
/**
* Constructs a callback to update the selected tab when the bottom bar icon
* is pressed.
*
* @param {number} tabIndex - The selected tab.
* @private
* @returns {Function}
*/
_onTabSelected(tabIndex) {
return () => super._selectPage(tabIndex);
}
_renderPage: (Object, number, boolean) => React$Node
/**
* Renders a single page of the page list.
*
* @private
* @param {Object} page - The page to render.
* @param {number} index - The index of the rendered page.
* @param {boolean} disabled - Renders the page disabled.
* @returns {React$Node}
*/
_renderPage(page, index, disabled) {
const { pageIndex } = this.state;
return page.component
? <TabBarIOS.Item
icon = { page.icon }
key = { index }
onPress = { this._onTabSelected(index) }
selected = { pageIndex === index }
title = { page.title }>
{
React.createElement(
page.component,
{
disabled
})
}
</TabBarIOS.Item>
: null;
}
/**
* Renders the paged list if multiple pages are to be rendered. This is the
* platform dependent part of the component.
*
* @param {boolean} disabled - True if the rendered lists should be
* disabled.
* @returns {ReactElement}
*/
_renderPagedList(disabled) {
const { pages } = this.props;
return (
<TabBarIOS
itemPositioning = 'fill'
style = { styles.pagedList }>
{
pages.map((page, index) => this._renderPage(
page, index, disabled
))
}
</TabBarIOS>
);
}
}
export default connect()(PagedList);

View File

@@ -0,0 +1,286 @@
// @flow
import React, { Component } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { connect } from 'react-redux';
import { Icon } from '../../../font-icons';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link PagedList}.
*/
type Props = {
/**
* The zero-based index of the page that should be rendered (selected) by
* default.
*/
defaultPage: number,
/**
* Indicates if the list is disabled or not.
*/
disabled: boolean,
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
* Callback to execute on page change.
*/
onSelectPage: ?Function,
/**
* The pages of the PagedList component to be rendered.
*
* NOTE 1: An element's {@code component} may be {@code undefined} and then
* it won't need to be rendered.
*
* NOTE 2: There must be at least one page available and enabled.
*/
pages: Array<{
component: ?Object,
icon: string | number,
title: string
}>
};
/**
* The type of the React {@code Component} state of {@link PagedList}.
*/
type State = {
/**
* The currently selected page.
*/
pageIndex: number
};
/**
* A component that renders a paged list.
*
* @extends PagedList
*/
class PagedList extends Component<Props, State> {
/**
* Initializes a new {@code PagedList} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.state = {
pageIndex: this._validatePageIndex(props.defaultPage)
};
// Bind event handlers so they are only bound once per instance.
this._maybeRefreshSelectedPage
= this._maybeRefreshSelectedPage.bind(this);
}
/**
* Renders the component.
*
* @inheritdoc
*/
render() {
const { disabled } = this.props;
const pages = this.props.pages.filter(({ component }) => component);
return (
<View
style = { [
styles.pagedListContainer,
disabled ? styles.pagedListContainerDisabled : null
] }>
{
pages.length > 1
? this._renderPagedList(disabled)
: React.createElement(
// $FlowExpectedError
/* type */ pages[0].component,
/* props */ {
disabled,
style: styles.pagedList
})
}
</View>
);
}
/**
* Constructs the style of an indicator.
*
* @param {number} indicatorIndex - The index of the indicator.
* @private
* @returns {Object}
*/
_getIndicatorStyle(indicatorIndex) {
if (this.state.pageIndex === indicatorIndex) {
return styles.pageIndicatorActive;
}
return null;
}
_maybeRefreshSelectedPage: ?boolean => void;
/**
* Components that this PagedList displays may have a refresh function to
* refresh its content when displayed (or based on custom logic). This
* function invokes this logic if it's present.
*
* @private
* @param {boolean} isInteractive - If true this refresh was caused by
* direct user interaction, false otherwise.
* @returns {void}
*/
_maybeRefreshSelectedPage(isInteractive: boolean = true) {
const selectedPage = this.props.pages[this.state.pageIndex];
let component;
if (selectedPage && (component = selectedPage.component)) {
const { refresh } = component;
refresh.call(component, this.props.dispatch, isInteractive);
}
}
/**
* Sets the selected page.
*
* @param {number} pageIndex - The index of the selected page.
* @protected
* @returns {void}
*/
_onSelectPage(pageIndex: number) {
return () => {
// eslint-disable-next-line no-param-reassign
pageIndex = this._validatePageIndex(pageIndex);
const { onSelectPage } = this.props;
onSelectPage && onSelectPage(pageIndex);
this.setState({ pageIndex }, this._maybeRefreshSelectedPage);
};
}
/**
* Renders a single page of the page list.
*
* @private
* @param {Object} page - The page to render.
* @param {boolean} disabled - Renders the page disabled.
* @returns {React$Node}
*/
_renderPage(page, disabled) {
if (!page.component) {
return null;
}
return (
<View style = { styles.pageContainer }>
{
React.createElement(
page.component,
{
disabled
})
}
</View>
);
}
/**
* Renders the paged list if multiple pages are to be rendered.
*
* @param {boolean} disabled - True if the rendered lists should be
* disabled.
* @returns {ReactElement}
*/
_renderPagedList(disabled) {
const { pages } = this.props;
const { pageIndex } = this.state;
return (
<View style = { styles.pagedListContainer }>
{
this._renderPage(pages[pageIndex], disabled)
}
<View style = { styles.pageIndicatorContainer }>
{
pages.map((page, index) => this._renderPageIndicator(
page, index, disabled
))
}
</View>
</View>
);
}
/**
* Renders a page indicator (icon) for the page.
*
* @private
* @param {Object} page - The page the indicator is rendered for.
* @param {number} index - The index of the page the indicator is rendered
* for.
* @param {boolean} disabled - Renders the indicator disabled.
* @returns {React$Node}
*/
_renderPageIndicator(page, index, disabled) {
if (!page.component) {
return null;
}
return (
<TouchableOpacity
disabled = { disabled }
key = { index }
onPress = { this._onSelectPage(index) }
style = { styles.pageIndicator } >
<View style = { styles.pageIndicator }>
<Icon
name = { page.icon }
style = { [
styles.pageIndicatorIcon,
this._getIndicatorStyle(index)
] } />
<Text
style = { [
styles.pageIndicatorText,
this._getIndicatorStyle(index)
] }>
{ page.title }
</Text>
</View>
</TouchableOpacity>
);
}
/**
* Validates the requested page index and returns a safe value.
*
* @private
* @param {number} pageIndex - The requested page index.
* @returns {number}
*/
_validatePageIndex(pageIndex) {
// pageIndex may point to a non-existing page if some of the pages are
// disabled (their component property is undefined).
const maxPageIndex
= this.props.pages.filter(({ component }) => component).length - 1;
return Math.max(0, Math.min(maxPageIndex, pageIndex));
}
}
export default connect()(PagedList);

View File

@@ -73,6 +73,13 @@ const HEADER_STYLES = {
*/
const PAGED_LIST_STYLES = {
/**
* Outermost container of a page in {@code PagedList}.
*/
pageContainer: {
flex: 1
},
/**
* Style of the page indicator (Android).
*/
@@ -214,7 +221,7 @@ const SECTION_LIST_STYLES = {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
paddingVertical: 5
padding: 5
},
listItemDetails: {
@@ -239,7 +246,8 @@ const SECTION_LIST_STYLES = {
backgroundColor: 'rgba(255, 255, 255, 0.2)',
flex: 1,
flexDirection: 'row',
padding: 5
paddingVertical: 5,
paddingHorizontal: 10
},
listSectionText: {

View File

@@ -9,13 +9,6 @@ import type { Props } from '../AbstractContainer';
* @extends AbstractContainer
*/
export default class Container<P: Props> extends AbstractContainer<P> {
/**
* {@code Container} component's property types.
*
* @static
*/
static propTypes = AbstractContainer.propTypes;
/**
* Implements React's {@link Component#render()}.
*

View File

@@ -1,34 +1,32 @@
/* @flow */
import Button from '@atlaskit/button';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { translate } from '../../../i18n';
declare var interfaceConfig: Object;
/**
* The type of the React {@code Component} props of {@link InlineDialogFailure}.
*/
type Props = {
/**
* Allows to retry the call that previously didn't succeed.
*/
onRetry: Function,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* Inline dialog that represents a failure and allows a retry.
*/
class InlineDialogFailure extends Component<*> {
/**
* {@code InlineDialogFailure}'s property types.
*
* @static
*/
static propTypes = {
/**
* Allows to retry the call that previously didn't succeed.
*/
onRetry: PropTypes.func,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
class InlineDialogFailure extends Component<Props> {
/**
* Renders the content of this component.
*

View File

@@ -0,0 +1,201 @@
// @flow
import React, { Component } from 'react';
import {
getLocalizedDateFormatter,
getLocalizedDurationFormatter
} from '../../../i18n';
import Container from './Container';
import Text from './Text';
type Props = {
/**
* Indicates if the list is disabled or not.
*/
disabled: boolean,
/**
* Indicates if the URL should be hidden or not.
*/
hideURL: boolean,
/**
* Function to be invoked when an item is pressed. The item's URL is passed.
*/
onPress: Function,
/**
* Rendered when the list is empty. Should be a rendered element.
*/
listEmptyComponent: Object,
/**
* An array of meetings.
*/
meetings: Array<Object>,
/**
* Defines what happens when an item in the section list is clicked
*/
onItemClick: Function
};
/**
* Generates a date string for a given date.
*
* @param {Object} date - The date.
* @private
* @returns {string}
*/
function _toDateString(date) {
return getLocalizedDateFormatter(date).format('MMM Do, YYYY');
}
/**
* Generates a time (interval) string for a given times.
*
* @param {Array<Date>} times - Array of times.
* @private
* @returns {string}
*/
function _toTimeString(times) {
if (times && times.length > 0) {
return (
times
.map(time => getLocalizedDateFormatter(time).format('LT'))
.join(' - '));
}
return undefined;
}
/**
* Implements a React/Web {@link Component} for displaying a list with
* meetings.
*
* @extends Component
*/
export default class MeetingsList extends Component<Props> {
/**
* Constructor of the MeetingsList component.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onPress = this._onPress.bind(this);
this._renderItem = this._renderItem.bind(this);
}
/**
* Renders the content of this component.
*
* @returns {React.ReactNode}
*/
render() {
const { listEmptyComponent, meetings } = this.props;
/**
* If there are no recent meetings we don't want to display anything
*/
if (meetings) {
return (
<Container
className = 'meetings-list'>
{
meetings.length === 0
? listEmptyComponent
: meetings.map(this._renderItem)
}
</Container>
);
}
return null;
}
_onPress: string => Function;
/**
* Returns a function that is used in the onPress callback of the items.
*
* @param {string} url - The URL of the item to navigate to.
* @private
* @returns {Function}
*/
_onPress(url) {
const { disabled, onPress } = this.props;
if (!disabled && url && typeof onPress === 'function') {
return () => onPress(url);
}
return null;
}
_renderItem: (Object, number) => React$Node;
/**
* Renders an item for the list.
*
* @param {Object} meeting - Information about the meeting.
* @param {number} index - The index of the item.
* @returns {Node}
*/
_renderItem(meeting, index) {
const {
date,
duration,
elementAfter,
time,
title,
url
} = meeting;
const { hideURL = false } = this.props;
const onPress = this._onPress(url);
const rootClassName
= `item ${
onPress ? 'with-click-handler' : 'without-click-handler'}`;
return (
<Container
className = { rootClassName }
key = { index }
onClick = { onPress }>
<Container className = 'left-column'>
<Text className = 'date'>
{ _toDateString(date) }
</Text>
<Text>
{ _toTimeString(time) }
</Text>
</Container>
<Container className = 'right-column'>
<Text className = 'title'>
{ title }
</Text>
{
hideURL || !url ? null : (
<Text>
{ url }
</Text>)
}
{
typeof duration === 'number' ? (
<Text>
{ getLocalizedDurationFormatter(duration) }
</Text>) : null
}
</Container>
<Container className = 'actions'>
{ elementAfter || null }
</Container>
</Container>
);
}
}

View File

@@ -1,7 +1,8 @@
// @flow
import { MultiSelectStateless } from '@atlaskit/multi-select';
import AKInlineDialog from '@atlaskit/inline-dialog';
import _debounce from 'lodash/debounce';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import InlineDialogFailure from './InlineDialogFailure';
@@ -9,118 +10,127 @@ import InlineDialogFailure from './InlineDialogFailure';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* A MultiSelect that is also auto-completing.
* The type of the React {@code Component} props of
* {@link MultiSelectAutocomplete}.
*/
class MultiSelectAutocomplete extends Component {
type Props = {
/**
* {@code MultiSelectAutocomplete} component's property types.
*
* @static
* The default value of the selected item.
*/
static propTypes = {
/**
* The default value of the selected item.
*/
defaultValue: PropTypes.array,
defaultValue: Array<Object>,
/**
* Optional footer to show as a last element in the results.
* Should be of type {content: <some content>}
*/
footer: PropTypes.object,
/**
* Optional footer to show as a last element in the results.
* Should be of type {content: <some content>}
*/
footer: Object,
/**
* Indicates if the component is disabled.
*/
isDisabled: PropTypes.bool,
/**
* Indicates if the component is disabled.
*/
isDisabled: boolean,
/**
* Text to display while a query is executing.
*/
loadingMessage: PropTypes.string,
/**
* Text to display while a query is executing.
*/
loadingMessage: string,
/**
* The text to show when no matches are found.
*/
noMatchesFound: PropTypes.string,
/**
* The text to show when no matches are found.
*/
noMatchesFound: string,
/**
* The function called immediately before a selection has been actually
* selected. Provides an opportunity to do any formatting.
*/
onItemSelected: PropTypes.func,
/**
* The function called immediately before a selection has been actually
* selected. Provides an opportunity to do any formatting.
*/
onItemSelected: Function,
/**
* The function called when the selection changes.
*/
onSelectionChange: PropTypes.func,
/**
* The function called when the selection changes.
*/
onSelectionChange: Function,
/**
* The placeholder text of the input component.
*/
placeholder: PropTypes.string,
/**
* The placeholder text of the input component.
*/
placeholder: string,
/**
* The service providing the search.
*/
resourceClient: PropTypes.shape({
makeQuery: PropTypes.func,
parseResults: PropTypes.func
}).isRequired,
/**
* The service providing the search.
*/
resourceClient: { makeQuery: Function, parseResults: Function },
/**
* Indicates if the component should fit the container.
*/
shouldFitContainer: PropTypes.bool,
/**
* Indicates if the component should fit the container.
*/
shouldFitContainer: boolean,
/**
* Indicates if we should focus.
*/
shouldFocus: PropTypes.bool
};
/**
* Indicates if we should focus.
*/
shouldFocus: boolean
};
/**
* The type of the React {@code Component} state of
* {@link MultiSelectAutocomplete}.
*/
type State = {
/**
* Indicates if the dropdown is open.
*/
isOpen: boolean,
/**
* The text that filters the query result of the search.
*/
filterValue: string,
/**
* Indicates if the component is currently loading results.
*/
loading: boolean,
/**
* Indicates if there was an error.
*/
error: boolean,
/**
* The list of result items.
*/
items: Array<Object>,
/**
* The list of selected items.
*/
selectedItems: Array<Object>
};
/**
* A MultiSelect that is also auto-completing.
*/
class MultiSelectAutocomplete extends Component<Props, State> {
/**
* Initializes a new {@code MultiSelectAutocomplete} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
constructor(props: Props) {
super(props);
const defaultValue = this.props.defaultValue || [];
this.state = {
/**
* Indicates if the dropdown is open.
*/
isOpen: false,
/**
* The text that filters the query result of the search.
*/
filterValue: '',
/**
* Indicates if the component is currently loading results.
*/
loading: false,
/**
* Indicates if there was an error.
*/
error: false,
/**
* The list of result items.
*/
items: [],
/**
* The list of selected items.
*/
selectedItems: [ ...defaultValue ]
};
@@ -137,7 +147,7 @@ class MultiSelectAutocomplete extends Component {
* having been selected.
* @returns {void}
*/
setSelectedItems(selectedItems = []) {
setSelectedItems(selectedItems: Array<Object> = []) {
this.setState({ selectedItems });
}
@@ -177,6 +187,8 @@ class MultiSelectAutocomplete extends Component {
);
}
_onFilterChange: (string) => void;
/**
* Sets the state and sends a query on filter change.
*
@@ -198,6 +210,8 @@ class MultiSelectAutocomplete extends Component {
}
}
_onRetry: () => void;
/**
* Retries the query on retry.
*
@@ -208,6 +222,8 @@ class MultiSelectAutocomplete extends Component {
this._sendQuery(this.state.filterValue);
}
_onSelectionChange: (Object) => void;
/**
* Updates the selected items when a selection event occurs.
*
@@ -258,6 +274,8 @@ class MultiSelectAutocomplete extends Component {
);
}
_sendQuery: (string) => void;
/**
* Sends a query to the resourceClient.
*

View File

@@ -1,6 +1,5 @@
/* @flow */
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -17,32 +16,71 @@ const _RIGHT_WATERMARK_STYLE = {
backgroundImage: 'url(images/rightwatermark.png)'
};
/**
* The type of the React {@code Component} props of {@link Watermarks}.
*/
type Props = {
/**
* Whether or not the current user is logged in through a JWT.
*/
_isGuest: boolean,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* The type of the React {@code Component} state of {@link Watermarks}.
*/
type State = {
/**
* The url to open when clicking the brand watermark.
*/
brandWatermarkLink: string,
/**
* The url to open when clicking the Jitsi watermark.
*/
jitsiWatermarkLink: string,
/**
* Whether or not the brand watermark should be displayed.
*/
showBrandWatermark: boolean,
/**
* Whether or not the Jitsi watermark should be displayed.
*/
showJitsiWatermark: boolean,
/**
* Whether or not the Jitsi watermark should be displayed for users not
* logged in through a JWT.
*/
showJitsiWatermarkForGuests: boolean,
/**
* Whether or not the show the "powered by Jitsi.org" link.
*/
showPoweredBy: boolean
};
/**
* A Web Component which renders watermarks such as Jits, brand, powered by,
* etc.
*/
class Watermarks extends Component<*, *> {
static propTypes = {
_isGuest: PropTypes.bool,
t: PropTypes.func
};
state = {
brandWatermarkLink: String,
jitsiWatermarkLink: String,
showBrandWatermark: Boolean,
showJitsiWatermark: Boolean,
showJitsiWatermarkForGuests: Boolean,
showPoweredBy: Boolean
};
class Watermarks extends Component<Props, State> {
/**
* Initializes a new Watermarks instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Object) {
constructor(props: Props) {
super(props);
let showBrandWatermark;

View File

@@ -1,5 +1,6 @@
export { default as Container } from './Container';
export { default as LoadingIndicator } from './LoadingIndicator';
export { default as MeetingsList } from './MeetingsList';
export { default as MultiSelectAutocomplete } from './MultiSelectAutocomplete';
export { default as NavigateSectionListEmptyComponent } from
'./NavigateSectionListEmptyComponent';

View File

@@ -113,7 +113,7 @@ function _set(
/**
* Returns redux state from the specified {@code stateful} which is presumed to
* be related to the redux state (e.g. the redux store, the redux
* be related to the redux state (e.g. The redux store, the redux
* {@code getState} function).
*
* @param {Function|Object} stateful - The entity such as the redux store or the

View File

@@ -1,11 +1,17 @@
// @flow
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from '../constants';
/**
* The type of the React {@code Component} props of {@link AspectRatioAware}.
*/
type Props = {
aspectRatio: ASPECT_RATIO_NARROW | ASPECT_RATIO_WIDE
};
/**
* Determines whether a specific React {@code Component} decorated into an
* {@link AspectRatioAware} has {@link ASPECT_RATIO_NARROW} as the value of its
@@ -34,20 +40,7 @@ export function makeAspectRatioAware(
/**
* Renders {@code WrappedComponent} with the React prop {@code aspectRatio}.
*/
class AspectRatioAware extends Component<*> {
/**
* Properties of the aspect ratio aware wrapper.
*/
static propTypes = {
/**
* Either {@link ASPECT_RATIO_NARROW} or {@link ASPECT_RATIO_WIDE}.
*/
aspectRatio: PropTypes.oneOf([
ASPECT_RATIO_NARROW,
ASPECT_RATIO_WIDE
])
}
class AspectRatioAware extends Component<Props> {
/**
* Implement's React render method to wrap the nested component.
*

View File

@@ -57,7 +57,7 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
* Retrieves the legacy profile values regardless of it's being in pre or
* post-flattening format.
*
* FIXME: Let's remove this after a predefined time (e.g. by July 2018) to avoid
* FIXME: Let's remove this after a predefined time (e.g. By July 2018) to avoid
* garbage in the source.
*
* @private

View File

@@ -73,7 +73,7 @@ export default class Storage {
* Returns the value associated with a specific key in this {@code Storage}
* in an async manner. The method is required for the cases where we need
* the stored data but we're not sure yet whether this {@code Storage} is
* already initialized (e.g. on app start).
* already initialized (e.g. On app start).
*
* @param {string} key - The name of the key to retrieve the value of.
* @returns {Promise}

View File

@@ -26,6 +26,7 @@ export const ColorPalette = {
lightGrey: '#AAAAAA',
lighterGrey: '#EEEEEE',
red: '#D00000',
transparent: 'rgba(0, 0, 0, 0)',
white: 'white',
/**

View File

@@ -418,7 +418,7 @@ function _addTracks(tracks) {
* @returns {Promise} - A {@code Promise} resolved once all
* {@code gumProcess.cancel()} {@code Promise}s are settled because all we care
* about here is to be sure that the {@code getUserMedia} callbacks have
* completed (i.e. returned from the native side).
* completed (i.e. Returned from the native side).
*/
function _cancelGUMProcesses(getState) {
const logError

View File

@@ -1,6 +1,5 @@
// @flow
import Button from '@atlaskit/button';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Tooltip from '@atlaskit/tooltip';
@@ -65,12 +64,11 @@ class AddMeetingUrlButton extends Component<Props> {
render() {
return (
<Tooltip content = { this.props.t('calendarSync.addMeetingURL') }>
<Button
appearance = 'primary'
onClick = { this._onClick }
type = 'button'>
<div
className = 'button add-button'
onClick = { this._onClick }>
<i className = { 'icon-add' } />
</Button>
</div>
</Tooltip>
);
}
@@ -92,4 +90,3 @@ class AddMeetingUrlButton extends Component<Props> {
}
export default translate(connect()(AddMeetingUrlButton));

View File

@@ -1,16 +1,18 @@
// @flow
import React, { Component } from 'react';
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { connect } from 'react-redux';
import { openSettings } from '../../mobile/permissions';
import { translate } from '../../base/i18n';
import { AbstractPage } from '../../base/react';
import { refreshCalendar } from '../actions';
import { isCalendarEnabled } from '../functions';
import styles from './styles';
import BaseCalendarList from './BaseCalendarList';
import CalendarListContent from './CalendarListContent';
/**
* The tyoe of the React {@code Component} props of {@link CalendarList}.
@@ -36,7 +38,7 @@ type Props = {
/**
* Component to display a list of events from the (mobile) user's calendar.
*/
class CalendarList extends Component<Props> {
class CalendarList extends AbstractPage<Props> {
/**
* Initializes a new {@code CalendarList} instance.
*
@@ -50,6 +52,21 @@ class CalendarList extends Component<Props> {
= this._getRenderListEmptyComponent.bind(this);
}
/**
* Public API method for {@code Component}s rendered in
* {@link AbstractPagedList}. When invoked, refreshes the calendar entries
* in the app.
*
* @param {Function} dispatch - The Redux dispatch function.
* @param {boolean} isInteractive - If true this refresh was caused by
* direct user interaction, false otherwise.
* @public
* @returns {void}
*/
static refresh(dispatch, isInteractive) {
dispatch(refreshCalendar(false, isInteractive));
}
/**
* Implements React's {@link Component#render}.
*
@@ -59,10 +76,10 @@ class CalendarList extends Component<Props> {
const { disabled } = this.props;
return (
BaseCalendarList
? <BaseCalendarList
CalendarListContent
? <CalendarListContent
disabled = { disabled }
renderListEmptyComponent
listEmptyComponent
= { this._getRenderListEmptyComponent() } />
: null
);

View File

@@ -1,11 +1,11 @@
// @flow
import Button from '@atlaskit/button';
import Spinner from '@atlaskit/spinner';
import React, { Component } from 'react';
import React from 'react';
import { connect } from 'react-redux';
import { translate } from '../../base/i18n';
import { AbstractPage } from '../../base/react';
import { openSettingsDialog, SETTINGS_TABS } from '../../settings';
import {
createCalendarClickedEvent,
@@ -15,7 +15,7 @@ import {
import { refreshCalendar } from '../actions';
import { isCalendarEnabled } from '../functions';
import BaseCalendarList from './BaseCalendarList';
import CalendarListContent from './CalendarListContent';
declare var interfaceConfig: Object;
@@ -53,7 +53,7 @@ type Props = {
/**
* Component to display a list of events from the user's calendar.
*/
class CalendarList extends Component<Props> {
class CalendarList extends AbstractPage<Props> {
/**
* Initializes a new {@code CalendarList} instance.
*
@@ -78,10 +78,10 @@ class CalendarList extends Component<Props> {
const { disabled } = this.props;
return (
BaseCalendarList
? <BaseCalendarList
CalendarListContent
? <CalendarListContent
disabled = { disabled }
renderListEmptyComponent
listEmptyComponent
= { this._getRenderListEmptyComponent() } />
: null
);
@@ -101,21 +101,18 @@ class CalendarList extends Component<Props> {
if (_hasIntegrationSelected && _hasLoadedEvents) {
return (
<div className = 'navigate-section-list-empty'>
<div className = 'meetings-list-empty'>
<div>{ t('calendarSync.noEvents') }</div>
<Button
appearance = 'primary'
className = 'calendar-button'
id = 'connect_calendar_button'
onClick = { this._onRefreshEvents }
type = 'button'>
<div
className = 'button'
onClick = { this._onRefreshEvents }>
{ t('calendarSync.refresh') }
</Button>
</div>
</div>
);
} else if (_hasIntegrationSelected && !_hasLoadedEvents) {
return (
<div className = 'navigate-section-list-empty'>
<div className = 'meetings-list-empty'>
<Spinner
invertColor = { true }
isCompleting = { false }
@@ -125,20 +122,17 @@ class CalendarList extends Component<Props> {
}
return (
<div className = 'navigate-section-list-empty'>
<p className = 'header-text-description'>
<div className = 'meetings-list-empty'>
<p className = 'description'>
{ t('welcomepage.connectCalendarText', {
app: interfaceConfig.APP_NAME
}) }
</p>
<Button
appearance = 'primary'
className = 'calendar-button'
id = 'connect_calendar_button'
onClick = { this._onOpenSettings }
type = 'button'>
<div
className = 'button'
onClick = { this._onOpenSettings }>
{ t('welcomepage.connectCalendarButton') }
</Button>
</div>
</div>
);
}

View File

@@ -13,15 +13,12 @@ import { getLocalizedDateFormatter, translate } from '../../base/i18n';
import { NavigateSectionList } from '../../base/react';
import { refreshCalendar, openUpdateCalendarEventDialog } from '../actions';
import { isCalendarEnabled } from '../functions';
import AddMeetingUrlButton from './AddMeetingUrlButton';
import JoinButton from './JoinButton';
/**
* The type of the React {@code Component} props of
* {@link BaseCalendarList}.
* {@link CalendarListContent}.
*/
type Props = {
@@ -43,7 +40,7 @@ type Props = {
/**
*
*/
renderListEmptyComponent: Function,
listEmptyComponent: React$Node,
/**
* The translate function.
@@ -54,7 +51,7 @@ type Props = {
/**
* Component to display a list of events from a connected calendar.
*/
class BaseCalendarList extends Component<Props> {
class CalendarListContent extends Component<Props> {
/**
* Default values for the component's props.
*/
@@ -63,26 +60,7 @@ class BaseCalendarList extends Component<Props> {
};
/**
* Public API method for {@code Component}s rendered in
* {@link AbstractPagedList}. When invoked, refreshes the calendar entries
* in the app.
*
* Note: It is a static method as the {@code Component} may not be
* initialized yet when the UI invokes refresh (e.g. {@link TabBarIOS} tab
* change).
*
* @param {Function} dispatch - The Redux dispatch function.
* @param {boolean} isInteractive - If true this refresh was caused by
* direct user interaction, false otherwise.
* @public
* @returns {void}
*/
static refresh(dispatch, isInteractive) {
dispatch(refreshCalendar(false, isInteractive));
}
/**
* Initializes a new {@code BaseCalendarList} instance.
* Initializes a new {@code CalendarListContent} instance.
*
* @inheritdoc
*/
@@ -90,7 +68,6 @@ class BaseCalendarList extends Component<Props> {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onJoinPress = this._onJoinPress.bind(this);
this._onPress = this._onPress.bind(this);
this._onRefresh = this._onRefresh.bind(this);
this._onSecondaryAction = this._onSecondaryAction.bind(this);
@@ -117,7 +94,7 @@ class BaseCalendarList extends Component<Props> {
* @inheritdoc
*/
render() {
const { disabled, renderListEmptyComponent } = this.props;
const { disabled, listEmptyComponent } = this.props;
return (
<NavigateSectionList
@@ -126,27 +103,11 @@ class BaseCalendarList extends Component<Props> {
onRefresh = { this._onRefresh }
onSecondaryAction = { this._onSecondaryAction }
renderListEmptyComponent
= { renderListEmptyComponent }
= { listEmptyComponent }
sections = { this._toDisplayableList() } />
);
}
_onJoinPress: (Object, string) => Function;
/**
* Handles the list's navigate action.
*
* @private
* @param {Object} event - The click event.
* @param {string} url - The url string to navigate to.
* @returns {void}
*/
_onJoinPress(event, url) {
event.stopPropagation();
this._onPress(url, 'calendar.meeting.join');
}
_onPress: (string, string) => Function;
/**
@@ -154,7 +115,7 @@ class BaseCalendarList extends Component<Props> {
*
* @private
* @param {string} url - The url string to navigate to.
* @param {string} analyticsEventName - Тhe name of the analytics event.
* @param {string} analyticsEventName - Тhe name of the analytics event
* associated with this action.
* @returns {void}
*/
@@ -217,13 +178,6 @@ class BaseCalendarList extends Component<Props> {
*/
_toDisplayableItem(event) {
return {
elementAfter: event.url
? <JoinButton
onPress = { this._onJoinPress }
url = { event.url } />
: (<AddMeetingUrlButton
calendarId = { event.calendarId }
eventId = { event.id } />),
id: event.id,
key: `${event.id}-${event.startDate}`,
lines: [
@@ -318,5 +272,5 @@ function _mapStateToProps(state: Object) {
}
export default isCalendarEnabled()
? translate(connect(_mapStateToProps)(BaseCalendarList))
? translate(connect(_mapStateToProps)(CalendarListContent))
: undefined;

View File

@@ -0,0 +1,177 @@
// @flow
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { appNavigate } from '../../app';
import {
createCalendarClickedEvent,
createCalendarSelectedEvent,
sendAnalytics
} from '../../analytics';
import { MeetingsList } from '../../base/react';
import { isCalendarEnabled } from '../functions';
import AddMeetingUrlButton from './AddMeetingUrlButton';
import JoinButton from './JoinButton';
/**
* The type of the React {@code Component} props of
* {@link CalendarListContent}.
*/
type Props = {
/**
* The calendar event list.
*/
_eventList: Array<Object>,
/**
* Indicates if the list is disabled or not.
*/
disabled: boolean,
/**
* The Redux dispatch function.
*/
dispatch: Function,
/**
*
*/
listEmptyComponent: React$Node,
};
/**
* Component to display a list of events from a connected calendar.
*/
class CalendarListContent extends Component<Props> {
/**
* Default values for the component's props.
*/
static defaultProps = {
_eventList: []
};
/**
* Initializes a new {@code CalendarListContent} instance.
*
* @inheritdoc
*/
constructor(props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onJoinPress = this._onJoinPress.bind(this);
this._onPress = this._onPress.bind(this);
this._toDisplayableItem = this._toDisplayableItem.bind(this);
}
/**
* Implements React's {@link Component#componentDidMount()}. Invoked
* immediately after this component is mounted.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
sendAnalytics(createCalendarSelectedEvent());
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { disabled, listEmptyComponent } = this.props;
const { _eventList = [] } = this.props;
const meetings = _eventList.map(this._toDisplayableItem);
return (
<MeetingsList
disabled = { disabled }
listEmptyComponent = { listEmptyComponent }
meetings = { meetings }
onPress = { this._onPress } />
);
}
_onJoinPress: (Object, string) => Function;
/**
* Handles the list's navigate action.
*
* @private
* @param {Object} event - The click event.
* @param {string} url - The url string to navigate to.
* @returns {void}
*/
_onJoinPress(event, url) {
event.stopPropagation();
this._onPress(url, 'calendar.meeting.join');
}
_onPress: (string, string) => Function;
/**
* Handles the list's navigate action.
*
* @private
* @param {string} url - The url string to navigate to.
* @param {string} analyticsEventName - Тhe name of the analytics event
* associated with this action.
* @returns {void}
*/
_onPress(url, analyticsEventName = 'calendar.meeting.tile') {
sendAnalytics(createCalendarClickedEvent(analyticsEventName));
this.props.dispatch(appNavigate(url));
}
_toDisplayableItem: Object => Object;
/**
* Creates a displayable object from an event.
*
* @param {Object} event - The calendar event.
* @private
* @returns {Object}
*/
_toDisplayableItem(event) {
return {
elementAfter: event.url
? <JoinButton
onPress = { this._onJoinPress }
url = { event.url } />
: (<AddMeetingUrlButton
calendarId = { event.calendarId }
eventId = { event.id } />),
date: event.startDate,
time: [ event.startDate, event.endDate ],
description: event.url,
title: event.title,
url: event.url
};
}
}
/**
* Maps redux state to component props.
*
* @param {Object} state - The redux state.
* @returns {{
* _eventList: Array<Object>
* }}
*/
function _mapStateToProps(state: Object) {
return {
_eventList: state['features/calendar-sync'].events
};
}
export default isCalendarEnabled()
? connect(_mapStateToProps)(CalendarListContent)
: undefined;

View File

@@ -1,6 +1,5 @@
// @flow
import Button from '@atlaskit/button';
import React, { Component } from 'react';
import Tooltip from '@atlaskit/tooltip';
@@ -58,13 +57,11 @@ class JoinButton extends Component<Props> {
return (
<Tooltip
content = { t('calendarSync.joinTooltip') }>
<Button
appearance = 'primary'
className = 'join-button'
onClick = { this._onClick }
type = 'button'>
<div
className = 'button join-button'
onClick = { this._onClick }>
{ t('calendarSync.join') }
</Button>
</div>
</Tooltip>
);
}
@@ -84,4 +81,3 @@ class JoinButton extends Component<Props> {
}
export default translate(JoinButton);

View File

@@ -7,11 +7,30 @@ import { APP_LINK_SCHEME, parseURIString } from '../base/util';
import { MAX_LIST_LENGTH } from './constants';
const logger = require('jitsi-meet-logger').getLogger(__filename);
const ALLDAY_EVENT_LENGTH = 23 * 60 * 60 * 1000;
/**
* Returns true of the calendar entry is to be displayed in the app, false
* otherwise.
*
* @param {Object} entry - The calendar entry.
* @returns {boolean}
*/
function _isDisplayableCalendarEntry(entry) {
// Entries are displayable if:
// - Ends in the future (future or ongoing events)
// - Is not an all day event and there is only one attendee (these events
// are usually placeholder events that don't need to be shown.)
return entry.endDate > Date.now()
&& !((entry.allDay
|| entry.endDate - entry.startDate > ALLDAY_EVENT_LENGTH)
&& (!entry.attendees || entry.attendees.length < 2));
}
/**
* Updates the calendar entries in redux when new list is received. The feature
* calendar-sync doesn't display all calendar events, it displays unique
* title, URL, and start time tuples i.e. it doesn't display subsequent
* title, URL, and start time tuples, and it doesn't display subsequent
* occurrences of recurring events, and the repetitions of events coming from
* multiple calendars.
*
@@ -29,13 +48,12 @@ export function _updateCalendarEntries(events: Array<Object>) {
// eslint-disable-next-line no-invalid-this
const { dispatch, getState } = this;
const knownDomains = getState()['features/base/known-domains'];
const now = Date.now();
const entryMap = new Map();
for (const event of events) {
const entry = _parseCalendarEntry(event, knownDomains);
if (entry && entry.endDate > now) {
if (entry && _isDisplayableCalendarEntry(entry)) {
// As was stated above, we don't display subsequent occurrences of
// recurring events, and the repetitions of events coming from
// multiple calendars.
@@ -111,6 +129,8 @@ function _parseCalendarEntry(event, knownDomains) {
);
} else {
return {
allDay: event.allDay,
attendees: event.attendees,
calendarId: event.calendarId,
endDate,
id: event.id,

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