Compare commits

...

123 Commits
4635 ... 4706

Author SHA1 Message Date
Saúl Ibarra Corretgé
c3a41b8cf3 fix(avatar) refactor preloading to avoid CORS issues
Fixes: https://github.com/jitsi/jitsi-meet/issues/8510

This basically reverts
a3fb996ff0
while retaining the same properties that prompted it's original intent, namely
avoiding sending the Referrer header.
2021-02-10 14:32:56 +01:00
damencho
f4d0ec1bb4 fix(load-test): Uses websocket if available and adds room param to the connection url. 2021-02-09 15:48:06 -06:00
tmoldovan8x8
ef6b641802 bugfix(ios) changes the participantInfo completion handler reference to strong.
When the method was called from Swift they were collected before calling them.
2021-02-09 18:03:25 +02:00
Saúl Ibarra Corretgé
579acbc570 feat(embed-meeting) add autoplay permission to iframe sample 2021-02-09 08:01:51 -06:00
niteshletxsoft
bca9a12df1 feat(external_ap) add api call to get live stream url 2021-02-09 12:43:38 +01:00
Shoolpani Dubey
a57b967f2e fix(external_api) add autoplay capabilities to created iframe
Should fix https://github.com/jitsi/jitsi-meet/issues/7037
2021-02-09 11:22:42 +01:00
Hristo Terezov
299927fcad docs: Add comment for initial GUM timeout values. 2021-02-08 15:53:38 -06:00
Hristo Terezov
7dc899ace1 ref(DeviceSelectionPopup): remove. 2021-02-08 15:53:38 -06:00
Hristo Terezov
a6c6cd6c56 fix: Add GUM timeout & improve device permissions 2021-02-08 15:53:38 -06:00
Hristo Terezov
7dc45c28a2 fix(AudioSlider): removed when volume is 0 2021-02-08 15:25:17 -06:00
Hristo Terezov
a215f9706a chore(deps) lib-jitsi-meet@latest
* fix(GUM): improve permissions logic.
* feat(GUM): timeout.
* ref: Remove pinEndpoint. (#1440)

30c8795770...84357ce1a8
2021-02-08 14:52:25 -06:00
Jonathan Lennox
4beca0d5dd Set receiver video constraint in load-test script, as-if tile view. (#8567) 2021-02-08 13:35:03 -05:00
Vlad Piersec
bfc4b2ac6f feat(vpaas): Send billing id to prosody 2021-02-08 12:49:55 +02:00
kazan417
53bdaa7928 fix(lang) update Russian translation 2021-02-07 12:44:20 +01:00
Tobias Kneidl
ba18b12024 add module proxy_wstunnel 2021-02-06 22:49:12 -06:00
Tobias Kneidl
a1438f1f21 change websocket url from http:// to ws:// 2021-02-06 22:49:04 -06:00
Julian1203
c856c20513 Update main-de.json
Small improvements (again)
2021-02-06 07:27:55 -06:00
Julian1203
cf92c964b4 Update main-de.json
Small improvements (again)
2021-02-05 22:15:51 -06:00
Julian1203
e69529867e Update main-de.json
Small improvements for the gender-neutral version and other small improvements.
2021-02-05 16:15:39 -06:00
Jaya Allamsetty
fd313c1af7 fix(tests): Add more checks so that test don't error out. 2021-02-05 16:30:58 -05:00
Jonathan Lennox
73c3feb8fa Improve load-test script. (#8563)
* In load-test, merge URL params into config.

* Honor config.testing.noAutoPlayVideo in load-test.
2021-02-05 16:00:28 -05:00
Дамян Минков
67a01364d3 Move load-test to resources (#8560)
* fix: Move load-test to resources.

* squash: Updates load-test dependencies.

* squash: Fix load-test build.
2021-02-05 09:12:45 -06:00
Saúl Ibarra Corretgé
7a64bf006e misc(tools) add script for updating mobile apps versions 2021-02-05 11:56:44 +01:00
Saúl Ibarra Corretgé
e5ea96fd4c feat(rn) update SDK version to 3.1.0 2021-02-05 11:56:44 +01:00
Saúl Ibarra Corretgé
c8ad04d0ff misc(tools) add script for updating SDK version 2021-02-05 11:56:44 +01:00
Saúl Ibarra Corretgé
c56afde00c feat(dev) bind to 0.0.0.0 on the dev server by default 2021-02-05 11:15:20 +01:00
tmoldovan8x8
9ed1969f7e feat(android) adds ability to disable the requestFocus on Android 2021-02-05 09:05:55 +02:00
Jonathan Lennox
12680c35ca Add lightweight load-test webpage, disabled by default (#8514)
Co-authored-by: Hristo Terezov <hristo@jitsi.org>
Co-authored-by: damencho <damencho@jitsi.org>
2021-02-04 18:04:36 -05:00
Jaya Allamsetty
0138f23755 feat(conference): Enable forced reload of client on bridge failure.
* feat(conference): Enable forced reload of client on bridge failure.
Force the client to reload when the bridge that is handling the media goes down.
This mitigates issues seen on the bridge because of a client re-joining the call with the same endpointId, BWE issues, etc.
This behavior is configurable through 'enableForcedReload' setting in config.js.
The client skips the pre-join page when the page reloads.

* squash: refactor the restart logic.

* squash: fix description

* squash: dispatch conferenceWillLeave action before reload.
2021-02-04 12:33:18 -05:00
Marc Seitz
16d88a288f feat: add ipados to list of Platform.OS (#8205)
* feat: add ipados list of Platform.OS
2021-02-04 10:34:44 -05:00
Jaya Allamsetty
210c4857fd deps: Update latest@lib-jitsi-meet.
Add the ability to configure different max bitrates for VP8 and VP9.
Set max bitrate for presenter to 2500 Kbps irrespective of the configured max bitrates for video.
479dd98...77978f0.
2021-02-04 09:50:32 -05:00
tmoldovan8x8
dca96f25f3 feat(mobile) adds feature flags for audioMute, videoMute and overflow… (#8537) 2021-02-04 15:32:09 +02:00
Mihai-Andrei Uscat
b69e93a900 fix(Safari): Fix mobile double tapping for toolbar and overflow.
* Create generic tooltip wrapper for mobile usability.
* Change overflow menu icon/font/padding sizes.
* Change overflow drawer expand icon.
2021-02-04 15:24:25 +02:00
tmoldovan8x8
d2568b874b feat(mobile) adds ability to retrieve participantsInfo array 2021-02-04 14:26:35 +02:00
Juan Searle
20c6115c38 Fix suspicious URL on Persian localization file 2021-02-03 20:39:08 -06:00
OctopusET
68b8ee5961 fix(lang) update korean translation 2021-02-03 15:52:59 +01:00
Andrei Gavrilescu
9895a04609 feat(rtcstats): send meeting uuid to rtcstats (#8526)
* send meeting uuid to rtcstats

* change ret description

* fix flow error

* update lib-jitsi-meet version
2021-02-03 12:28:39 +02:00
Andrzej Moskal
87f688dc8f fix(android) add ability to localize notification actions strings 2021-02-03 10:58:05 +01:00
Jaya Allamsetty
c58657c759 fix(test): Make sure test doesn't error out. 2021-02-01 11:49:45 -05:00
Ali Kazemkhanloo
687106818a Add Persian to languages-fa.json 2021-02-01 07:49:58 -06:00
Ali Kazemkhanloo
f228b4ecc1 Add Persian language to the list of languages 2021-02-01 07:49:58 -06:00
kichinosukey
8582b25d28 typo fix 2021-02-01 07:46:32 -06:00
Vlad Piersec
9418dbc2b1 fix(recents-list): Order recents by last used 2021-02-01 13:30:34 +02:00
Jonas Rittershofer
19bf027b8b Include xmpp and colibri proxy for apache
Signed-off-by: Jonas Rittershofer <jotoeri@users.noreply.github.com>
2021-01-30 10:06:22 -06:00
Mihai-Andrei Uscat
c370c05701 fix(Filmstrip): Prevent Toolbox from being shown indefinitely when hovering filmstrip 2021-01-29 15:34:37 +02:00
Emil Ivov
f034f179ff Merge pull request #8507 from saghul/readme-jaas
fix(docs) add JaaS link to README
2021-01-29 06:57:51 -06:00
Saúl Ibarra Corretgé
b94b18770c fix(docs) add JaaS link to README 2021-01-29 13:54:27 +01:00
Mihai-Andrei Uscat
3f93726c41 fix(Safari): Fix zoomed in mobile interface and cropped tile 2021-01-29 12:55:24 +02:00
Titus-Andrei Moldovan
af8072d9d2 chore(mobile) changes the name for screenShares to remoteScreenShares to better reflect it's content 2021-01-29 12:45:53 +02:00
Titus-Andrei Moldovan
45f4643469 feat(mobile) adds ToggleScreenShare event and action. 2021-01-29 12:45:53 +02:00
Avram Tudor
745879c447 Merge pull request #8503 from jitsi/tavram/dropbox
feat(external_api) allow dropbox option to be overwritten
2021-01-29 12:30:03 +02:00
Mihai-Andrei Uscat
4aab5e2054 feat(Dialog): Make dialog close button more mobile friendly.
* Remove pointless custom headers, fall back to ModalHeader instead.
2021-01-29 12:22:43 +02:00
Tudor-Ovidiu Avram
69971a0e90 feat(external_api) allow dropbox option to be overwritten 2021-01-29 11:44:16 +02:00
Pawel Domas
7c90f75ec9 fix(conference.js): crash on undefined
While on the prejoin screen, the local tracks are managed by the redux store and not conference.js, so localAudio is undefined.
2021-01-28 21:59:44 -05:00
damencho
bf714c1c8b feat: Allow star for room in moderated tenants. 2021-01-28 16:28:39 -06:00
Avram Tudor
8414e9d99f Merge pull request #8495 from jitsi/tavram/chat-updated
feat(external_api) add event for chat updates (unread counter, open s…
2021-01-28 16:42:06 +02:00
Tudor-Ovidiu Avram
dcaad41e69 feat(external_api) add event for chat updates (unread counter, open state) 2021-01-28 11:41:27 +02:00
Jonathan Lennox
63f0166f75 Add mime type mapping for wasm to default Debian nginx config. 2021-01-27 13:42:37 -06:00
Дамян Минков
79f3756d33 feat: Adds option to set ws keepalive url through config. (#8487)
* feat: Adds option to set ws keepalive url through config.

* chore(deps) lib-jitsi-meet@latest

* feat: Adds option to set ws keepalive url through config.
* fix VADAudioAnalyser: catch error

be18ff34be...9fdde46694
2021-01-27 11:13:32 -06:00
damencho
5ac30262a5 fix(debian): Improves handling db_get. 2021-01-26 09:49:03 -06:00
damencho
09315fa653 fix: Adds luasec as dependency.
This is in Recommended but we need it.
2021-01-26 09:49:03 -06:00
damencho
5f0dd903f6 fix(debian): Fixes enforce_apache option. 2021-01-26 09:49:03 -06:00
Steffen Kolmer
ef7d425859 feat(ui) update AtlasKit components 2021-01-26 16:47:55 +01:00
bgrozev
a9bb8e5e81 fix: Use internal_hashed everywhere. (#8485) 2021-01-26 09:33:41 -06:00
Mihai-Andrei Uscat
8cf4e15b23 Add config flag for tile responsiveness 2021-01-26 13:42:57 +02:00
Mihai-Andrei Uscat
db84889143 feat(tiles): Add responsive behaviour.
* Enforce fixed column number at various width breakpoints.
* Bring back the filmstrip at small sizes but hide it.
* Change default maximum columns to 7.
2021-01-26 13:42:57 +02:00
Ali Karpuzoglu
6ca3c6e43a fix(misc) typo 2021-01-26 12:38:49 +01:00
DeBuXer
6fd280a960 fix(lang) update Dutch translation 2021-01-26 11:27:42 +01:00
Jonas Rittershofer
6f9e65d348 Allow to enforce Apache via debconf
Signed-off-by: Jonas Rittershofer <jotoeri@users.noreply.github.com>
2021-01-25 16:27:01 -06:00
damencho
f1e06bff7b fix: Lobby display name set when preJoin is disabled. Fixes #8415. 2021-01-25 16:03:54 -06:00
Jaya Allamsetty
1cf7a361e9 feat: Implement aggressive layer suspension on RN.
RN doesn't support RTCRtpSender yet. Therefore, media is suspended on RN by changing the media direction in the SDP whenever the client receives an ideal height of 0 for sender constraints on the bridge channel.
LJM update - 3570339360...be18ff34be.
2021-01-25 15:21:16 -05:00
okyanusoz
e7990baa7d Fixed Turkish translation issues 2021-01-25 13:36:32 -06:00
Cedric Roijakkers
d35708815d fix(lang) corrected Dutch translation of toggleCamera 2021-01-22 17:39:20 +01:00
Jaya Allamsetty
270e52e402 deps(ljm): Restore local connection status.
831716c160...3570339360.
2021-01-22 10:44:01 -05:00
Saúl Ibarra Corretgé
635d283d5a chore(deps) react-native-callstats@3.70.1
Support processing stats in the spec-compliant format.
2021-01-22 14:06:10 +01:00
Balu
4affc68b50 Update main-de.json
Added missing "user" dialog parameter translation. Tried to stay gender neutral.
2021-01-22 06:24:42 -06:00
chipechop
dca262620b Update languages-it.json 2021-01-22 06:24:11 -06:00
as0bit
12c8258f56 Update main-de.json
lang: add missing fields to German translation
2021-01-22 06:22:22 -06:00
tmoldovan8x8
6a6aeb1d95 feat(mobile) adds more feature flags (#8450)
Features flags added:  
-tile-view.enabled
-filmstrip.enabled
-notifications.enabled
-toolbox.enabled
2021-01-22 12:03:39 +02:00
Дамян Минков
01c55bdb15 feat: Uses mod_external_services supporting urn:xmpp:extdisco:2. (#8455)
* feat: Uses mod_external_services supporting urn:xmpp:extdisco:2.

The old mod_turncredentials.lua is left to continue working for those using old installs.
New install will start using the new module which will no longer be needed with prosody 0.12.

https://hg.prosody.im/prosody-modules/file/4841cf3fded5/mod_external_services/mod_external_services.lua

* squash: Updates ljm to support urn:xmpp:extdisco:2.
2021-01-21 16:14:00 -06:00
bgrozev
5f891fd060 debian: Do not read jicofosecret. (#8454) 2021-01-21 13:19:39 -06:00
chipechop
a39905883f Update main-it.json
Added 4 missing lines in italian translation
2021-01-20 12:01:54 -06:00
Saúl Ibarra Corretgé
fe78f104bc feat(android) set compile/target SDK versions to 30 2021-01-20 15:14:09 +01:00
Saúl Ibarra Corretgé
9c13603489 feat(android) update native dependencies 2021-01-20 15:14:09 +01:00
tmoldovan8x8
61037b982b feat(mobile) adds ability to send and receive text messages (#8425) 2021-01-20 14:06:45 +02:00
Jaya Allamsetty
df21ec6f04 chore(deps) lib-jitsi-meet@latest (#8437)
https://github.com/jitsi/lib-jitsi-meet/compare/...94ac35ae818093896e639e74f5fc389b488206a0
2021-01-19 12:45:33 -06:00
Vlad Piersec
23574e9edc fix(vpaas): Store billing id in parent lolcaStorage on Safari 2021-01-18 09:56:03 +02:00
Dennis Scheiba
ec3130af0e make translation gender neutral 2021-01-16 21:56:22 -06:00
Filipe dos Santos Gundim
3b692dc502 fix join label in pt-br 2021-01-16 21:36:00 -06:00
Дамян Минков
6689aa3700 feat: Detects shard changed when using websockets.
* feat: Detects shard changed when using websockets. (#1462)

1009693f2e...cb484cf48c
2021-01-15 09:51:43 -06:00
Avram Tudor
13bc9863cb Merge pull request #8426 from jitsi/tavram/gradient-condition
fix(subject) remove gradient if no info in topbar
2021-01-15 16:26:19 +02:00
Tudor-Ovidiu Avram
7ff332b2bb fix(subject) remove gradient if no info in topbar 2021-01-15 16:02:50 +02:00
Mihai-Andrei Uscat
8aae2065dc fix(Toolbox): Fix toolbox display when accessing it via keyboard 2021-01-15 13:43:09 +02:00
Jaya Allamsetty
b65e61f633 feat: Add new codec selection mechanism.
When an endpoint that doesn't support the preferred codec (VP9) joins a conference, all the other endpoints fallback to VP8 until the endpoint leaves the call.
2021-01-14 18:01:38 -05:00
Saúl Ibarra Corretgé
88f1c218eb fix(rn,stats) fix incorrect bitrate calculation on mobile
Stats timestamps were incorrectly formatted, fixed upstream.

Fixes: https://github.com/jitsi/jitsi-meet/issues/8367
2021-01-14 14:59:22 +01:00
Boris Grozev
f6df76ab10 fix: Fix broken postinst reported by @wsldankers. 2021-01-14 07:55:50 -06:00
Saúl Ibarra Corretgé
85f1701393 fix(tile-view) avoid covering the logo
Reverts a8b2e6ffb3
2021-01-13 20:29:20 +01:00
damencho
2f7ff37472 fix: Fixes #8396, wrong ssi includes in offline static page. 2021-01-13 09:11:10 -06:00
Mihai-Andrei Uscat
c752ea13f1 feat(overflow): Add responsive drawer at small screen width.
* Implement opening toolbar and participant overflows as drawers when below certain width.
* Fix dial-in copy button displaying incorrectly.
2021-01-13 16:07:22 +01:00
tmoldovan8x8
5ef60c3a7d [WIP] adds BroadcastService (#8336)
feat(external_api) exposes more events from JS to native and adds the ability to send actions from native to JS.
2021-01-13 15:48:29 +02:00
hmuresan
1196ede961 feat(external-api) set privateMessage flag on incoming-message 2021-01-13 13:33:55 +01:00
Saúl Ibarra Corretgé
12877c7fce fix(settings) remove legacy compatibility code 2021-01-13 13:32:54 +01:00
Avram Tudor
c6bb600d4c Merge pull request #8400 from jitsi/tavram/shortcuts
feat(external_api) allow shortcuts to be disabled
2021-01-13 12:39:27 +02:00
Avram Tudor
845e23a947 Merge pull request #8399 from jitsi/tavram/overflowmenu
fix(menu) do not display overflow menu button if no items
2021-01-13 12:39:11 +02:00
Tudor-Ovidiu Avram
55ebb60f85 feat(external_api) allow shortcuts to be disabled 2021-01-13 12:10:27 +02:00
Tudor-Ovidiu Avram
8d3d94f568 fix(menu) do not display overflow menu button if no items 2021-01-13 11:55:38 +02:00
Avram Tudor
be24772e57 Merge pull request #8397 from jitsi/tavram/disable-jaas-directory
fix(jaas) disable directory integration
2021-01-13 11:38:05 +02:00
Tudor-Ovidiu Avram
a807f804a9 fix(jaas) disable directory integration 2021-01-13 11:13:04 +02:00
Hristo Terezov
db48dc3ed3 fix(Thumbnail): volume & audioLevel default values 2021-01-12 13:42:31 -06:00
Hristo Terezov
9bae7099dd fix(filmstrip): Import from base/tracks. 2021-01-12 13:42:31 -06:00
Hristo Terezov
e990f6984a fix(Thumbnail): Improve naming. 2021-01-12 13:42:31 -06:00
Hristo Terezov
9f321c988e style(isVideoPlayable): improve readability. 2021-01-12 13:42:31 -06:00
Hristo Terezov
2e5e9a3f79 fix(AudioTrack):Add check for NaN value for volume 2021-01-12 13:42:31 -06:00
Hristo Terezov
fdb8f76b90 fix(Thumbnail): imports. 2021-01-12 13:42:31 -06:00
Hristo Terezov
3d97bef308 style(Thumbnail): improve readability
Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
2021-01-12 13:42:31 -06:00
Hristo Terezov
5a55c7b965 style(AudioTrack): improve readability
Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
2021-01-12 13:42:31 -06:00
Hristo Terezov
e161cbc4bd fix(SmallVideo): computeDisplayModeInput
Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
2021-01-12 13:42:31 -06:00
Hristo Terezov
51e381a0b1 ref(Thumbnail): Create React component. 2021-01-12 13:42:31 -06:00
bgrozev
d8dd644f38 make sure extra plugin paths are enabled (#8390)
* fix: Make sure extra plugin_paths are not commented out.

* fix: Do not use "-e" as backup suffix.
2021-01-12 13:25:20 -06:00
Saúl Ibarra Corretgé
e30b2e14a5 fix(rn) stop room name generator when field is focused
Fixes: https://github.com/jitsi/jitsi-meet/issues/8307
2021-01-12 17:36:09 +01:00
Avram Tudor
7f1894dd57 Merge pull request #8379 from horymury/hmuresan/broadcast-screenshare
feat(external_api) add command and event listener for CS
2021-01-12 17:49:06 +02:00
hmuresan
4dda508708 feat(external_api) add command and event listener for CS 2021-01-12 17:23:40 +02:00
damencho
69c6463476 chore(deps) lib-jitsi-meet@latest
* fix: Disabling lobby when using tenant.

87c6e37475...7896fc8b92
2021-01-12 08:22:14 -06:00
251 changed files with 13860 additions and 4438 deletions

View File

@@ -16,9 +16,12 @@ WEBPACK_DEV_SERVER = ./node_modules/.bin/webpack-dev-server
all: compile deploy clean
compile:
compile: compile-load-test
$(WEBPACK) -p
compile-load-test:
${NPM} install --prefix resources/load-test && ${NPM} run build --prefix resources/load-test
clean:
rm -fr $(BUILD_DIR)
@@ -39,8 +42,6 @@ deploy-appbundle:
$(BUILD_DIR)/external_api.min.map \
$(BUILD_DIR)/flacEncodeWorker.min.js \
$(BUILD_DIR)/flacEncodeWorker.min.map \
$(BUILD_DIR)/device_selection_popup_bundle.min.js \
$(BUILD_DIR)/device_selection_popup_bundle.min.map \
$(BUILD_DIR)/dial_in_info_bundle.min.js \
$(BUILD_DIR)/dial_in_info_bundle.min.map \
$(BUILD_DIR)/alwaysontop.min.js \

View File

@@ -6,6 +6,8 @@ The Jitsi Meet client runs in your browser, without installing anything else on
Jitsi Meet allows very efficient collaboration. Users can stream their desktop or only some windows. It also supports shared document editing with Etherpad.
**NOTE:** If you are looking for Jitsi as a Service (JaaS) please start [here](https://jaas.8x8.vc).
## Installation
On the client side, no installation is necessary. You just point your browser to the URL of your deployment. This section is about installing a Jitsi Meet suite on your server and hosting your own conferencing service.

View File

@@ -38,7 +38,7 @@ import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.Map;
import java.util.HashMap;
/**
* The one and only Activity that the Jitsi Meet app needs. The
@@ -183,8 +183,8 @@ public class MainActivity extends JitsiMeetActivity {
}
@Override
public void onConferenceTerminated(Map<String, Object> data) {
Log.d(TAG, "Conference terminated: " + data);
protected void onConferenceTerminated(HashMap<String, Object> extraData) {
Log.d(TAG, "Conference terminated: " + extraData);
}
// Activity lifecycle method overrides

View File

@@ -10,17 +10,17 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.2'
classpath 'com.google.gms:google-services:4.3.3'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.3.0'
classpath 'com.android.tools.build:gradle:4.1.2'
classpath 'com.google.gms:google-services:4.3.4'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1'
}
}
ext {
buildToolsVersion = "29.0.3"
compileSdkVersion = 29
buildToolsVersion = "30.0.3"
compileSdkVersion = 30
minSdkVersion = 23
targetSdkVersion = 29
targetSdkVersion = 30
supportLibVersion = "28.0.0"
// The Maven artifact groupdId of the third-party react-native modules which

View File

@@ -26,4 +26,4 @@ android.useAndroidX=true
android.enableJetifier=true
appVersion=21.0.0
sdkVersion=3.0.0
sdkVersion=3.1.0

View File

@@ -36,6 +36,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.fragment:fragment:1.2.5'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
//noinspection GradleDynamicVersion
@@ -46,6 +47,7 @@ dependencies {
implementation 'com.dropbox.core:dropbox-core-sdk:3.0.8'
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.squareup.duktape:duktape-android:1.3.0'
implementation 'com.google.code.gson:gson:2.8.6'
if (rootProject.ext.libreBuild) {
implementation(project(':react-native-device-info')) {

View File

@@ -99,6 +99,7 @@ public abstract class BaseReactView<ListenerT>
* The listener (e.g. {@link JitsiMeetViewListener}) instance for reporting
* events occurring in Jitsi Meet.
*/
@Deprecated
private ListenerT listener;
/**
@@ -167,6 +168,7 @@ public abstract class BaseReactView<ListenerT>
*
* @return The listener set on this {@code BaseReactView}.
*/
@Deprecated
public ListenerT getListener() {
return listener;
}
@@ -179,8 +181,10 @@ public abstract class BaseReactView<ListenerT>
* @param data - The details of the event associated with/specific to the
* specified {@code name}.
*/
@Deprecated
protected abstract void onExternalAPIEvent(String name, ReadableMap data);
@Deprecated
protected void onExternalAPIEvent(
Map<String, Method> listenerMethods,
String name, ReadableMap data) {
@@ -215,6 +219,7 @@ public abstract class BaseReactView<ListenerT>
*
* @param listener The listener to set on this {@code BaseReactView}.
*/
@Deprecated
public void setListener(ListenerT listener) {
this.listener = listener;
}

View File

@@ -0,0 +1,87 @@
package org.jitsi.meet.sdk;
import android.content.Intent;
import android.os.Bundle;
import com.facebook.react.bridge.WritableNativeMap;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.util.HashMap;
/**
* Wraps the name and extra data for events that were broadcasted locally.
*/
public class BroadcastAction {
private static final String TAG = BroadcastAction.class.getSimpleName();
private final Type type;
private final HashMap<String, Object> data;
public BroadcastAction(Intent intent) {
this.type = Type.buildTypeFromAction(intent.getAction());
this.data = buildDataFromBundle(intent.getExtras());
}
public Type getType() {
return this.type;
}
public HashMap<String, Object> getData() {
return this.data;
}
public WritableNativeMap getDataAsWritableNativeMap() {
WritableNativeMap nativeMap = new WritableNativeMap();
for (String key : this.data.keySet()) {
try {
// TODO add support for different types of objects
nativeMap.putString(key, this.data.get(key).toString());
} catch (Exception e) {
JitsiMeetLogger.w(TAG + " invalid extra data in event", e);
}
}
return nativeMap;
}
private static HashMap<String, Object> buildDataFromBundle(Bundle bundle) {
HashMap<String, Object> map = new HashMap<>();
if (bundle != null) {
for (String key : bundle.keySet()) {
map.put(key, bundle.get(key));
}
}
return map;
}
enum Type {
SET_AUDIO_MUTED("org.jitsi.meet.SET_AUDIO_MUTED"),
HANG_UP("org.jitsi.meet.HANG_UP"),
SEND_ENDPOINT_TEXT_MESSAGE("org.jitsi.meet.SEND_ENDPOINT_TEXT_MESSAGE"),
TOGGLE_SCREEN_SHARE("org.jitsi.meet.TOGGLE_SCREEN_SHARE"),
RETRIEVE_PARTICIPANTS_INFO("org.jitsi.meet.RETRIEVE_PARTICIPANTS_INFO");
private final String action;
Type(String action) {
this.action = action;
}
public String getAction() {
return action;
}
private static Type buildTypeFromAction(String action) {
for (Type type : Type.values()) {
if (type.action.equalsIgnoreCase(action)) {
return type;
}
}
return null;
}
}
}

View File

@@ -0,0 +1,30 @@
package org.jitsi.meet.sdk;
import android.content.Context;
import android.content.Intent;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.facebook.react.bridge.ReadableMap;
/**
* Class used to emit events through the LocalBroadcastManager, called when events
* from JS occurred. Takes an action name from JS, builds and broadcasts the {@link BroadcastEvent}
*/
public class BroadcastEmitter {
private final LocalBroadcastManager localBroadcastManager;
public BroadcastEmitter(Context context) {
localBroadcastManager = LocalBroadcastManager.getInstance(context);
}
public void sendBroadcast(String name, ReadableMap data) {
BroadcastEvent event = new BroadcastEvent(name, data);
Intent intent = event.buildIntent();
if (intent != null) {
localBroadcastManager.sendBroadcast(intent);
}
}
}

View File

@@ -0,0 +1,142 @@
package org.jitsi.meet.sdk;
import android.content.Intent;
import android.os.Bundle;
import com.facebook.react.bridge.ReadableMap;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.util.HashMap;
/**
* Wraps the name and extra data for the events that occur on the JS side and are
* to be broadcasted.
*/
public class BroadcastEvent {
private static final String TAG = BroadcastEvent.class.getSimpleName();
private final Type type;
private final HashMap<String, Object> data;
public BroadcastEvent(String name, ReadableMap data) {
this.type = Type.buildTypeFromName(name);
this.data = data.toHashMap();
}
public BroadcastEvent(Intent intent) {
this.type = Type.buildTypeFromAction(intent.getAction());
this.data = buildDataFromBundle(intent.getExtras());
}
public Type getType() {
return this.type;
}
public HashMap<String, Object> getData() {
return this.data;
}
public Intent buildIntent() {
if (type != null && type.action != null) {
Intent intent = new Intent(type.action);
for (String key : this.data.keySet()) {
try {
intent.putExtra(key, this.data.get(key).toString());
} catch (Exception e) {
JitsiMeetLogger.w(TAG + " invalid extra data in event", e);
}
}
return intent;
}
return null;
}
private static HashMap<String, Object> buildDataFromBundle(Bundle bundle) {
if (bundle != null) {
try {
HashMap<String, Object> map = new HashMap<>();
for (String key : bundle.keySet()) {
map.put(key, bundle.get(key));
}
return map;
} catch (Exception e) {
JitsiMeetLogger.w(TAG + " invalid extra data", e);
}
}
return null;
}
public enum Type {
CONFERENCE_JOINED("org.jitsi.meet.CONFERENCE_JOINED"),
CONFERENCE_TERMINATED("org.jitsi.meet.CONFERENCE_TERMINATED"),
CONFERENCE_WILL_JOIN("org.jitsi.meet.CONFERENCE_WILL_JOIN"),
AUDIO_MUTED_CHANGED("org.jitsi.meet.AUDIO_MUTED_CHANGED"),
PARTICIPANT_JOINED("org.jitsi.meet.PARTICIPANT_JOINED"),
PARTICIPANT_LEFT("org.jitsi.meet.PARTICIPANT_LEFT"),
ENDPOINT_TEXT_MESSAGE_RECEIVED("org.jitsi.meet.ENDPOINT_TEXT_MESSAGE_RECEIVED"),
SCREEN_SHARE_TOGGLED("org.jitsi.meet.SCREEN_SHARE_TOGGLED"),
PARTICIPANTS_INFO_RETRIEVED("org.jitsi.meet.PARTICIPANTS_INFO_RETRIEVED");
private static final String CONFERENCE_WILL_JOIN_NAME = "CONFERENCE_WILL_JOIN";
private static final String CONFERENCE_JOINED_NAME = "CONFERENCE_JOINED";
private static final String CONFERENCE_TERMINATED_NAME = "CONFERENCE_TERMINATED";
private static final String AUDIO_MUTED_CHANGED_NAME = "AUDIO_MUTED_CHANGED";
private static final String PARTICIPANT_JOINED_NAME = "PARTICIPANT_JOINED";
private static final String PARTICIPANT_LEFT_NAME = "PARTICIPANT_LEFT";
private static final String ENDPOINT_TEXT_MESSAGE_RECEIVED_NAME = "ENDPOINT_TEXT_MESSAGE_RECEIVED";
private static final String SCREEN_SHARE_TOGGLED_NAME = "SCREEN_SHARE_TOGGLED";
private static final String PARTICIPANTS_INFO_RETRIEVED_NAME = "PARTICIPANTS_INFO_RETRIEVED";
private final String action;
Type(String action) {
this.action = action;
}
public String getAction() {
return action;
}
private static Type buildTypeFromAction(String action) {
for (Type type : Type.values()) {
if (type.action.equalsIgnoreCase(action)) {
return type;
}
}
return null;
}
private static Type buildTypeFromName(String name) {
switch (name) {
case CONFERENCE_WILL_JOIN_NAME:
return CONFERENCE_WILL_JOIN;
case CONFERENCE_JOINED_NAME:
return CONFERENCE_JOINED;
case CONFERENCE_TERMINATED_NAME:
return CONFERENCE_TERMINATED;
case AUDIO_MUTED_CHANGED_NAME:
return AUDIO_MUTED_CHANGED;
case PARTICIPANT_JOINED_NAME:
return PARTICIPANT_JOINED;
case PARTICIPANT_LEFT_NAME:
return PARTICIPANT_LEFT;
case ENDPOINT_TEXT_MESSAGE_RECEIVED_NAME:
return ENDPOINT_TEXT_MESSAGE_RECEIVED;
case SCREEN_SHARE_TOGGLED_NAME:
return SCREEN_SHARE_TOGGLED;
case PARTICIPANTS_INFO_RETRIEVED_NAME:
return PARTICIPANTS_INFO_RETRIEVED;
}
return null;
}
}
}

View File

@@ -0,0 +1,26 @@
package org.jitsi.meet.sdk;
import android.content.Intent;
public class BroadcastIntentHelper {
public static Intent buildSetAudioMutedIntent(boolean muted) {
Intent intent = new Intent(BroadcastAction.Type.SET_AUDIO_MUTED.getAction());
intent.putExtra("muted", muted);
return intent;
}
public static Intent buildHangUpIntent() {
return new Intent(BroadcastAction.Type.HANG_UP.getAction());
}
public static Intent buildSendEndpointTextMessageIntent(String to, String message) {
Intent intent = new Intent(BroadcastAction.Type.SEND_ENDPOINT_TEXT_MESSAGE.getAction());
intent.putExtra("to", to);
intent.putExtra("message", message);
return intent;
}
public static Intent buildToggleScreenShareIntent() {
return new Intent(BroadcastAction.Type.TOGGLE_SCREEN_SHARE.getAction());
}
}

View File

@@ -0,0 +1,34 @@
package org.jitsi.meet.sdk;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
/**
* Listens for {@link BroadcastAction}s on LocalBroadcastManager. When one occurs,
* it emits it to JS.
*/
public class BroadcastReceiver extends android.content.BroadcastReceiver {
public BroadcastReceiver(Context context) {
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);
IntentFilter intentFilter = new IntentFilter();
for (BroadcastAction.Type type : BroadcastAction.Type.values()) {
intentFilter.addAction(type.getAction());
}
localBroadcastManager.registerReceiver(this, intentFilter);
}
@Override
public void onReceive(Context context, Intent intent) {
BroadcastAction action = new BroadcastAction(intent);
String actionName = action.getType().getAction();
ReactInstanceManagerHolder.emitEvent(actionName, action.getDataAsWritableNativeMap());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
* Copyright @ 2017-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,6 +24,9 @@ import com.facebook.react.module.annotations.ReactModule;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.util.HashMap;
import java.util.Map;
/**
* Module implementing an API for sending events from JavaScript to native code.
*/
@@ -35,6 +38,9 @@ class ExternalAPIModule
private static final String TAG = NAME;
private final BroadcastEmitter broadcastEmitter;
private final BroadcastReceiver broadcastReceiver;
/**
* Initializes a new module instance. There shall be a single instance of
* this module throughout the lifetime of the app.
@@ -44,6 +50,11 @@ class ExternalAPIModule
*/
public ExternalAPIModule(ReactApplicationContext reactContext) {
super(reactContext);
broadcastEmitter = new BroadcastEmitter(reactContext);
broadcastReceiver = new BroadcastReceiver(reactContext);
ParticipantsService.init(reactContext);
}
/**
@@ -56,6 +67,25 @@ class ExternalAPIModule
return NAME;
}
/**
* Gets a mapping with the constants this module is exporting.
*
* @return a {@link Map} mapping the constants to be exported with their
* values.
*/
@Override
public Map<String, Object> getConstants() {
Map<String, Object> constants = new HashMap<>();
constants.put("SET_AUDIO_MUTED", BroadcastAction.Type.SET_AUDIO_MUTED.getAction());
constants.put("HANG_UP", BroadcastAction.Type.HANG_UP.getAction());
constants.put("SEND_ENDPOINT_TEXT_MESSAGE", BroadcastAction.Type.SEND_ENDPOINT_TEXT_MESSAGE.getAction());
constants.put("TOGGLE_SCREEN_SHARE", BroadcastAction.Type.TOGGLE_SCREEN_SHARE.getAction());
constants.put("RETRIEVE_PARTICIPANTS_INFO", BroadcastAction.Type.RETRIEVE_PARTICIPANTS_INFO.getAction());
return constants;
}
/**
* Dispatches an event that occurred on the JavaScript side of the SDK to
* the specified {@link BaseReactView}'s listener.
@@ -79,7 +109,8 @@ class ExternalAPIModule
JitsiMeetLogger.d(TAG + " Sending event: " + name + " with data: " + data);
try {
view.onExternalAPIEvent(name, data);
} catch(Exception e) {
broadcastEmitter.sendBroadcast(name, data);
} catch (Exception e) {
JitsiMeetLogger.e(e, TAG + " onExternalAPIEvent: error sending event");
}
}

View File

@@ -16,33 +16,41 @@
package org.jitsi.meet.sdk;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.facebook.react.modules.core.PermissionListener;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.util.Map;
import java.util.HashMap;
/**
* A base activity for SDK users to embed. It uses {@link JitsiMeetFragment} to do the heavy
* lifting and wires the remaining Activity lifecycle methods so it works out of the box.
*/
public class JitsiMeetActivity extends FragmentActivity
implements JitsiMeetActivityInterface, JitsiMeetViewListener {
implements JitsiMeetActivityInterface {
protected static final String TAG = JitsiMeetActivity.class.getSimpleName();
private static final String ACTION_JITSI_MEET_CONFERENCE = "org.jitsi.meet.CONFERENCE";
private static final String JITSI_MEET_CONFERENCE_OPTIONS = "JitsiMeetConferenceOptions";
private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
onBroadcastReceived(intent);
}
};
// Helpers for starting the activity
//
@@ -68,8 +76,7 @@ public class JitsiMeetActivity extends FragmentActivity
setContentView(R.layout.activity_jitsi_meet);
// Listen for conference events.
getJitsiView().setListener(this);
registerForBroadcastMessages();
if (!extraInitialize()) {
initialize();
@@ -91,6 +98,8 @@ public class JitsiMeetActivity extends FragmentActivity
}
JitsiMeetOngoingConferenceService.abort(this);
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver);
super.onDestroy();
}
@@ -113,8 +122,8 @@ public class JitsiMeetActivity extends FragmentActivity
public void join(@Nullable String url) {
JitsiMeetConferenceOptions options
= new JitsiMeetConferenceOptions.Builder()
.setRoom(url)
.build();
.setRoom(url)
.build();
join(options);
}
@@ -138,7 +147,8 @@ public class JitsiMeetActivity extends FragmentActivity
}
}
private @Nullable JitsiMeetConferenceOptions getConferenceOptions(Intent intent) {
private @Nullable
JitsiMeetConferenceOptions getConferenceOptions(Intent intent) {
String action = intent.getAction();
if (Intent.ACTION_VIEW.equals(action)) {
@@ -157,7 +167,7 @@ public class JitsiMeetActivity extends FragmentActivity
* Helper function called during activity initialization. If {@code true} is returned, the
* initialization is delayed and the {@link JitsiMeetActivity#initialize()} method is not
* called. In this case, it's up to the subclass to call the initialize method when ready.
*
* <p>
* This is mainly required so we do some extra initialization in the Jitsi Meet app.
*
* @return {@code true} if the initialization will be delayed, {@code false} otherwise.
@@ -172,6 +182,37 @@ public class JitsiMeetActivity extends FragmentActivity
join(getConferenceOptions(getIntent()));
}
protected void onConferenceJoined(HashMap<String, Object> extraData) {
JitsiMeetLogger.i("Conference joined: " + extraData);
// Launch the service for the ongoing notification.
JitsiMeetOngoingConferenceService.launch(this);
}
protected void onConferenceTerminated(HashMap<String, Object> extraData) {
JitsiMeetLogger.i("Conference terminated: " + extraData);
finish();
}
protected void onConferenceWillJoin(HashMap<String, Object> extraData) {
JitsiMeetLogger.i("Conference will join: " + extraData);
}
protected void onParticipantJoined(HashMap<String, Object> extraData) {
try {
JitsiMeetLogger.i("Participant joined: ", extraData);
} catch (Exception e) {
JitsiMeetLogger.w("Invalid participant joined extraData", e);
}
}
protected void onParticipantLeft(HashMap<String, Object> extraData) {
try {
JitsiMeetLogger.i("Participant left: ", extraData);
} catch (Exception e) {
JitsiMeetLogger.w("Invalid participant left extraData", e);
}
}
// Activity lifecycle methods
//
@@ -223,24 +264,37 @@ public class JitsiMeetActivity extends FragmentActivity
JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
// JitsiMeetViewListener
//
private void registerForBroadcastMessages() {
IntentFilter intentFilter = new IntentFilter();
@Override
public void onConferenceJoined(Map<String, Object> data) {
JitsiMeetLogger.i("Conference joined: " + data);
// Launch the service for the ongoing notification.
JitsiMeetOngoingConferenceService.launch(this);
for (BroadcastEvent.Type type : BroadcastEvent.Type.values()) {
intentFilter.addAction(type.getAction());
}
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, intentFilter);
}
@Override
public void onConferenceTerminated(Map<String, Object> data) {
JitsiMeetLogger.i("Conference terminated: " + data);
finish();
}
private void onBroadcastReceived(Intent intent) {
if (intent != null) {
BroadcastEvent event = new BroadcastEvent(intent);
@Override
public void onConferenceWillJoin(Map<String, Object> data) {
JitsiMeetLogger.i("Conference will join: " + data);
switch (event.getType()) {
case CONFERENCE_JOINED:
onConferenceJoined(event.getData());
break;
case CONFERENCE_WILL_JOIN:
onConferenceWillJoin(event.getData());
break;
case CONFERENCE_TERMINATED:
onConferenceTerminated(event.getData());
break;
case PARTICIPANT_JOINED:
onParticipantJoined(event.getData());
break;
case PARTICIPANT_LEFT:
onParticipantLeft(event.getData());
break;
}
}
}
}

View File

@@ -21,11 +21,13 @@ import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.IBinder;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
/**
* This class implements an Android {@link Service}, a foreground one specifically, and it's
@@ -35,19 +37,18 @@ import org.jitsi.meet.sdk.log.JitsiMeetLogger;
* See: https://developer.android.com/guide/components/services
*/
public class JitsiMeetOngoingConferenceService extends Service
implements OngoingConferenceTracker.OngoingConferenceListener {
implements OngoingConferenceTracker.OngoingConferenceListener {
private static final String TAG = JitsiMeetOngoingConferenceService.class.getSimpleName();
static final class Actions {
static final String START = TAG + ":START";
static final String HANGUP = TAG + ":HANGUP";
}
private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver();
private boolean isAudioMuted;
static void launch(Context context) {
OngoingNotification.createOngoingConferenceNotificationChannel();
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
intent.setAction(Actions.START);
intent.setAction(Action.START.getName());
ComponentName componentName;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -70,11 +71,16 @@ public class JitsiMeetOngoingConferenceService extends Service
super.onCreate();
OngoingConferenceTracker.getInstance().addListener(this);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BroadcastEvent.Type.AUDIO_MUTED_CHANGED.getAction());
LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(broadcastReceiver, intentFilter);
}
@Override
public void onDestroy() {
OngoingConferenceTracker.getInstance().removeListener(this);
LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(broadcastReceiver);
super.onDestroy();
}
@@ -86,26 +92,37 @@ public class JitsiMeetOngoingConferenceService extends Service
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
final String action = intent.getAction();
if (Actions.START.equals(action)) {
Notification notification = OngoingNotification.buildOngoingConferenceNotification();
if (notification == null) {
final String actionName = intent.getAction();
final Action action = Action.fromName(actionName);
switch (action) {
case UNMUTE:
case MUTE:
Intent muteBroadcastIntent = BroadcastIntentHelper.buildSetAudioMutedIntent(action == Action.MUTE);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(muteBroadcastIntent);
break;
case START:
Notification notification = OngoingNotification.buildOngoingConferenceNotification(isAudioMuted);
if (notification == null) {
stopSelf();
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
} else {
startForeground(OngoingNotification.NOTIFICATION_ID, notification);
JitsiMeetLogger.i(TAG + " Service started");
}
break;
case HANGUP:
JitsiMeetLogger.i(TAG + " Hangup requested");
Intent hangupBroadcastIntent = BroadcastIntentHelper.buildHangUpIntent();
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(hangupBroadcastIntent);
stopSelf();
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
} else {
startForeground(OngoingNotification.NOTIFICATION_ID, notification);
JitsiMeetLogger.i(TAG + " Service started");
}
} else if (Actions.HANGUP.equals(action)) {
JitsiMeetLogger.i(TAG + " Hangup requested");
// Abort all ongoing calls
if (AudioModeModule.useConnectionService()) {
ConnectionService.abortConnections();
}
stopSelf();
} else {
JitsiMeetLogger.w(TAG + " Unknown action received: " + action);
stopSelf();
break;
default:
JitsiMeetLogger.w(TAG + " Unknown action received: " + action);
stopSelf();
break;
}
return START_NOT_STICKY;
@@ -118,4 +135,46 @@ public class JitsiMeetOngoingConferenceService extends Service
JitsiMeetLogger.i(TAG + "Service stopped");
}
}
public enum Action {
START(TAG + ":START"),
HANGUP(TAG + ":HANGUP"),
MUTE(TAG + ":MUTE"),
UNMUTE(TAG + ":UNMUTE");
private final String name;
Action(String name) {
this.name = name;
}
public static Action fromName(String name) {
for (Action action : Action.values()) {
if (action.name.equalsIgnoreCase(name)) {
return action;
}
}
return null;
}
public String getName() {
return name;
}
}
private class BroadcastReceiver extends android.content.BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
isAudioMuted = Boolean.parseBoolean(intent.getStringExtra("muted"));
Notification notification = OngoingNotification.buildOngoingConferenceNotification(isAudioMuted);
if (notification == null) {
stopSelf();
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
} else {
startForeground(OngoingNotification.NOTIFICATION_ID, notification);
JitsiMeetLogger.i(TAG + " Service started");
}
}
}
}

View File

@@ -197,6 +197,7 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener>
* by/associated with the specified {@code name}.
*/
@Override
@Deprecated
protected void onExternalAPIEvent(String name, ReadableMap data) {
onExternalAPIEvent(LISTENER_METHODS, name, data);
}

View File

@@ -21,6 +21,7 @@ import java.util.Map;
/**
* Interface for listening to events coming from Jitsi Meet.
*/
@Deprecated
public interface JitsiMeetViewListener {
/**
* Called when a conference was joined.

View File

@@ -32,6 +32,7 @@ import java.util.regex.Pattern;
* Utility methods for helping with transforming {@link ExternalAPIModule}
* events into listener methods. Used with descendants of {@link BaseReactView}.
*/
@Deprecated
public final class ListenerUtils {
/**
* Extracts the methods defined in a listener and creates a mapping of this

View File

@@ -23,13 +23,14 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.util.Random;
/**
* Helper class for creating the ongoing notification which is used with
* {@link JitsiMeetOngoingConferenceService}. It allows the user to easily get back to the app
@@ -43,7 +44,6 @@ class OngoingNotification {
static final int NOTIFICATION_ID = new Random().nextInt(99999) + 10000;
static void createOngoingConferenceNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
@@ -56,7 +56,7 @@ class OngoingNotification {
}
NotificationManager notificationManager
= (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
= (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel
= notificationManager.getNotificationChannel(CHANNEL_ID);
@@ -73,7 +73,7 @@ class OngoingNotification {
notificationManager.createNotificationChannel(channel);
}
static Notification buildOngoingConferenceNotification() {
static Notification buildOngoingConferenceNotification(boolean isMuted) {
Context context = ReactInstanceManagerHolder.getCurrentActivity();
if (context == null) {
JitsiMeetLogger.w(TAG + " Cannot create notification: no current context");
@@ -83,12 +83,7 @@ class OngoingNotification {
Intent notificationIntent = new Intent(context, context.getClass());
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
NotificationCompat.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder = new NotificationCompat.Builder(context, CHANNEL_ID);
} else {
builder = new NotificationCompat.Builder(context);
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID);
builder
.setCategory(NotificationCompat.CATEGORY_CALL)
@@ -99,21 +94,28 @@ class OngoingNotification {
.setOngoing(true)
.setAutoCancel(false)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setUsesChronometer(true)
.setOnlyAlertOnce(true)
.setSmallIcon(context.getResources().getIdentifier("ic_notification", "drawable", context.getPackageName()));
// Add a "hang-up" action only if we are using ConnectionService.
if (AudioModeModule.useConnectionService()) {
Intent hangupIntent = new Intent(context, JitsiMeetOngoingConferenceService.class);
hangupIntent.setAction(JitsiMeetOngoingConferenceService.Actions.HANGUP);
PendingIntent hangupPendingIntent
= PendingIntent.getService(context, 0, hangupIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action hangupAction = new NotificationCompat.Action(0, "Hang up", hangupPendingIntent);
NotificationCompat.Action hangupAction = createAction(context, JitsiMeetOngoingConferenceService.Action.HANGUP, R.string.ongoing_notification_action_hang_up);
builder.addAction(hangupAction);
}
JitsiMeetOngoingConferenceService.Action toggleAudioAction = isMuted
? JitsiMeetOngoingConferenceService.Action.UNMUTE : JitsiMeetOngoingConferenceService.Action.MUTE;
int toggleAudioTitle = isMuted ? R.string.ongoing_notification_action_unmute : R.string.ongoing_notification_action_mute;
NotificationCompat.Action audioAction = createAction(context, toggleAudioAction, toggleAudioTitle);
builder.addAction(hangupAction);
builder.addAction(audioAction);
return builder.build();
}
private static NotificationCompat.Action createAction(Context context, JitsiMeetOngoingConferenceService.Action action, @StringRes int titleId) {
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
intent.setAction(action.getName());
PendingIntent pendingIntent
= PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
String title = context.getString(titleId);
return new NotificationCompat.Action(0, title, pendingIntent);
}
}

View File

@@ -0,0 +1,27 @@
package org.jitsi.meet.sdk;
import com.google.gson.annotations.SerializedName;
public class ParticipantInfo {
@SerializedName("participantId")
public String id;
@SerializedName("displayName")
public String displayName;
@SerializedName("avatarUrl")
public String avatarUrl;
@SerializedName("email")
public String email;
@SerializedName("name")
public String name;
@SerializedName("isLocal")
public boolean isLocal;
@SerializedName("role")
public String role;
}

View File

@@ -0,0 +1,90 @@
package org.jitsi.meet.sdk;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.annotation.Nullable;
public class ParticipantsService extends android.content.BroadcastReceiver {
private static final String TAG = ParticipantsService.class.getSimpleName();
private static final String REQUEST_ID = "requestId";
private final Map<String, WeakReference<ParticipantsInfoCallback>> participantsInfoCallbackMap = new HashMap<>();
private static ParticipantsService instance;
@Nullable
public static ParticipantsService getInstance() {
return instance;
}
private ParticipantsService(Context context) {
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BroadcastEvent.Type.PARTICIPANTS_INFO_RETRIEVED.getAction());
localBroadcastManager.registerReceiver(this, intentFilter);
}
static void init(Context context) {
instance = new ParticipantsService(context);
}
public void retrieveParticipantsInfo(ParticipantsInfoCallback participantsInfoCallback) {
String callbackKey = UUID.randomUUID().toString();
this.participantsInfoCallbackMap.put(callbackKey, new WeakReference<>(participantsInfoCallback));
String actionName = BroadcastAction.Type.RETRIEVE_PARTICIPANTS_INFO.getAction();
WritableMap data = Arguments.createMap();
data.putString(REQUEST_ID, callbackKey);
ReactInstanceManagerHolder.emitEvent(actionName, data);
}
@Override
public void onReceive(Context context, Intent intent) {
BroadcastEvent event = new BroadcastEvent(intent);
switch (event.getType()) {
case PARTICIPANTS_INFO_RETRIEVED:
try {
List<ParticipantInfo> participantInfoList = new Gson().fromJson(
event.getData().get("participantsInfo").toString(),
new TypeToken<ArrayList<ParticipantInfo>>() {
}.getType());
ParticipantsInfoCallback participantsInfoCallback = this.participantsInfoCallbackMap.get(event.getData().get(REQUEST_ID).toString()).get();
if (participantsInfoCallback != null) {
participantsInfoCallback.onReceived(participantInfoList);
this.participantsInfoCallbackMap.remove(participantsInfoCallback);
}
} catch (Exception e) {
JitsiMeetLogger.w(TAG + "error parsing participantsList", e);
}
break;
}
}
public interface ParticipantsInfoCallback {
void onReceived(List<ParticipantInfo> participantInfoList);
}
}

View File

@@ -3,4 +3,7 @@
<string name="dropbox_app_key"></string>
<string name="ongoing_notification_title">Ongoing meeting</string>
<string name="ongoing_notification_text">You are currently in a meeting. Tap to return to it.</string>
<string name="ongoing_notification_action_hang_up">Hang up</string>
<string name="ongoing_notification_action_mute">Mute</string>
<string name="ongoing_notification_action_unmute">Unmute</string>
</resources>

View File

@@ -33,6 +33,7 @@ import {
conferenceLeft,
conferenceSubjectChanged,
conferenceTimestampChanged,
conferenceUniqueIdSet,
conferenceWillJoin,
conferenceWillLeave,
dataChannelOpened,
@@ -503,6 +504,11 @@ export default {
let tryCreateLocalTracks;
// On Electron there is no permission prompt for granting permissions. That's why we don't need to
// spend much time displaying the overlay screen. If GUM is not resolved withing 15 seconds it will
// probably never resolve.
const timeout = browser.isElectron() ? 15000 : 60000;
// FIXME is there any simpler way to rewrite this spaghetti below ?
if (options.startScreenSharing) {
tryCreateLocalTracks = this._createDesktopTrack()
@@ -511,7 +517,10 @@ export default {
return [ desktopStream ];
}
return createLocalTracksF({ devices: [ 'audio' ] }, true)
return createLocalTracksF({
devices: [ 'audio' ],
timeout
}, true)
.then(([ audioStream ]) =>
[ desktopStream, audioStream ])
.catch(error => {
@@ -525,7 +534,10 @@ export default {
errors.screenSharingError = error;
return requestedAudio
? createLocalTracksF({ devices: [ 'audio' ] }, true)
? createLocalTracksF({
devices: [ 'audio' ],
timeout
}, true)
: [];
})
.catch(error => {
@@ -537,15 +549,33 @@ export default {
// Resolve with no tracks
tryCreateLocalTracks = Promise.resolve([]);
} else {
tryCreateLocalTracks = createLocalTracksF({ devices: initialDevices }, true)
tryCreateLocalTracks = createLocalTracksF({
devices: initialDevices,
timeout
}, true)
.catch(err => {
if (requestedAudio && requestedVideo) {
// Try audio only...
errors.audioAndVideoError = err;
if (err.name === JitsiTrackErrors.TIMEOUT && !browser.isElectron()) {
// In this case we expect that the permission prompt is still visible. There is no point of
// executing GUM with different source. Also at the time of writting the following
// inconsistency have been noticed in some browsers - if the permissions prompt is visible
// and another GUM is executed the prompt does not change its content but if the user
// clicks allow the user action isassociated with the latest GUM call.
errors.audioOnlyError = err;
errors.videoOnlyError = err;
return [];
}
return (
createLocalTracksF({ devices: [ 'audio' ] }, true));
createLocalTracksF({
devices: [ 'audio' ],
timeout
}, true));
} else if (requestedAudio && !requestedVideo) {
errors.audioOnlyError = err;
@@ -566,7 +596,10 @@ export default {
// Try video only...
return requestedVideo
? createLocalTracksF({ devices: [ 'video' ] }, true)
? createLocalTracksF({
devices: [ 'video' ],
timeout
}, true)
: [];
})
.catch(err => {
@@ -1865,6 +1898,10 @@ export default {
APP.store.dispatch(conferenceLeft(room, ...args));
});
room.on(
JitsiConferenceEvents.CONFERENCE_UNIQUE_ID_SET,
(...args) => APP.store.dispatch(conferenceUniqueIdSet(room, ...args)));
room.on(
JitsiConferenceEvents.AUTH_STATUS_CHANGED,
(authEnabled, authLogin) =>
@@ -2014,7 +2051,6 @@ export default {
formattedDisplayName
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME)
});
APP.UI.changeDisplayName(id, formattedDisplayName);
}
);
room.on(
@@ -2053,10 +2089,7 @@ export default {
(...args) => APP.store.dispatch(lockStateChanged(room, ...args)));
room.on(JitsiConferenceEvents.KICKED, participant => {
APP.UI.hideStats();
APP.store.dispatch(kickedOut(room, participant));
// FIXME close
});
room.on(JitsiConferenceEvents.PARTICIPANT_KICKED, (kicker, kicked) => {
@@ -2252,7 +2285,7 @@ export default {
return this.useAudioStream(stream);
})
.then(() => {
if (hasDefaultMicChanged) {
if (this.localAudio && hasDefaultMicChanged) {
// workaround for the default device to be shown as selected in the
// settings even when the real device id was passed to gUM because of the
// above mentioned chrome bug.
@@ -2386,14 +2419,11 @@ export default {
_onConferenceJoined() {
APP.UI.initConference();
APP.keyboardshortcut.init();
if (!config.disableShortcuts) {
APP.keyboardshortcut.init();
}
APP.store.dispatch(conferenceJoined(room));
const displayName
= APP.store.getState()['features/base/settings'].displayName;
APP.UI.changeDisplayName('localVideoContainer', displayName);
},
/**
@@ -2595,7 +2625,7 @@ export default {
// Use the new stream or null if we failed to obtain it.
return useStream(tracks.find(track => track.getType() === mediaType) || null)
.then(() => {
if (hasDefaultMicChanged) {
if (this.localAudio && hasDefaultMicChanged) {
// workaround for the default device to be shown as selected in the
// settings even when the real device id was passed to gUM because of
// the above mentioned chrome bug.
@@ -2871,10 +2901,6 @@ export default {
APP.store.dispatch(updateSettings({
displayName: formattedNickname
}));
if (room) {
APP.UI.changeDisplayName(id, formattedNickname);
}
},
/**

View File

@@ -261,9 +261,16 @@ var config = {
// // the available bandwidth calculated by the browser, but it will be capped by the values specified here.
// // This is currently not implemented on app based clients on mobile.
// maxBitratesVideo: {
// low: 200000,
// standard: 500000,
// high: 1500000
// VP8 : {
// low: 200000,
// standard: 500000,
// high: 1500000
// },
// VP9: {
// low: 100000,
// standard: 300000,
// high: 1200000
// }
// },
//
// // The options can be used to override default thresholds of video thumbnail heights corresponding to
@@ -318,6 +325,11 @@ var config = {
// TCC sequence numbers starting from 0.
// enableIceRestart: false,
// Enables forced reload of the client when the call is migrated as a result of
// the bridge going down. Currently enabled by default as call migration through
// session-terminate is causing siganling issues when Octo is enabled.
// enableForcedReload: true,
// Use TURN/UDP servers for the jitsi-videobridge connection (by default
// we filter out TURN/UDP because it is usually not needed since the
// bridge itself is reachable via UDP)
@@ -326,6 +338,9 @@ var config = {
// UI
//
// Disables responsive tiles.
// disableResponsiveTiles: false,
// Hides lobby button
// hideLobbyButton: false,
@@ -336,6 +351,9 @@ var config = {
// will be joined when no room is specified.
enableWelcomePage: true,
// Disable app shortcuts that are registered upon joining a conference
// disableShortcuts: false,
// Disable initial browser getUserMedia requests.
// This is useful for scenarios where users might want to start a conference for screensharing only
// disableInitialGUM: false,
@@ -693,6 +711,8 @@ var config = {
forceTurnRelay
hiddenDomain
ignoreStartMuted
websocketKeepAlive
websocketKeepAliveUrl
*/
/**
@@ -717,6 +737,7 @@ var config = {
// 'dialog.reservationError',
// 'dialog.serviceUnavailable', // shown when server is not reachable
// 'dialog.sessTerminated', // shown when there is a failed conference session
// 'dialog.sessionRestarted', // show when a client reload is initiated because of bridge migration
// 'dialog.tokenAuthFailed', // show when an invalid jwt is used
// 'dialog.transcribing', // transcribing notifications (pending, off)
// 'dialOut.statusMessage', // shown when dial out status is updated.

View File

@@ -94,6 +94,10 @@ function connect(id, password, roomName) {
// in future). It's included for the time being for Jitsi Meet and lib-jitsi-meet versions interoperability.
connectionConfig.serviceUrl = connectionConfig.bosh = serviceUrl;
if (connectionConfig.websocketKeepAliveUrl) {
connectionConfig.websocketKeepAliveUrl += `?room=${roomName}`;
}
const connection = new JitsiMeetJS.JitsiConnection(null, jwt, connectionConfig);
if (config.iAmRecorder) {

View File

@@ -2,49 +2,58 @@
* Move the @atlaskit/flag container up a little bit so it does not cover the
* toolbar with the first notification.
*/
.jIMojv{
.atlaskit-portal > #notifications-container {
bottom: calc(#{$newToolbarSizeWithPadding}) !important;
}
/**
* Disable the slide-in animation for @atlaskit/flag due to the animation
* repeating for each queued flag once it becomes the top flag.
*/
.mIBKA:first-child {
animation: cbfRuT 0s !important;
-webkit-animation: cbfRuT 0s !important;
}
.modal-dialog-form {
/**
* Update the @atlaskit/dropdown-menu trigger wrapper to make sure it looks
* click-able.
*/
.cjJUnw {
cursor: pointer;
}
/**
* Override @atlaskit/dropdown-menu styling when in a modal because the
* dropdown backgrounds clash with the modal backgrounds.
*/
.cksvax[data-role=droplistContent] {
border: 1px solid #455166;
.dropdown-menu div[style*="transform"] {
outline: 1px solid #455166;
}
}
/**
* Override @atlaskit/modal-dialog header styling
*/
.atlaskit-portal [role="dialog"] header {
.jitsi-icon {
cursor: pointer;
}
.jitsi-icon svg {
fill: #B8C7E0;
}
}
/**
* Make header close button more easily tappable on mobile.
*/
.mobile-browser .atlaskit-portal [role="dialog"] header .jitsi-icon {
display: grid;
place-items: center;
height: 48px;
width: 48px;
background: #2a3a4b;
border-radius: 3px;
}
/**
* Override @atlaskit/theme styling for the top toolbar so it displays over
* the video thumbnail while obscuring as little as possible.
*/
.videocontainer .tOoji {
.videocontainer__toptoolbar > div > div {
background: none;
}
/**
* Override @atlaskit/InlineDialog styling for the overflowmenu so it displays
* with the correct height.
* Keep overflow menu within screen vertical bounds and make it scrollable.
*/
.toolbox-button-wth-dialog .eYJELv {
max-height: initial;
}
.toolbox-button-wth-dialog > div:nth-child(2) {
max-height: calc(100vh - #{$newToolbarSizeWithPadding} - 46px);
overflow-y: auto;
}

View File

@@ -143,8 +143,8 @@
top: 18px;
}
// Override @atlaskit/InlineDialog container which is made with styled components
& > div > div:nth-child(2) > div > div {
// Override @atlaskit/InlineDialog container which is made with styled components
& > div:nth-child(2) {
outline: none;
padding: 0;
}

View File

@@ -45,20 +45,12 @@ body {
* pad the modal container in order for the modals to be centered
* while also taking the chat size into consideration.
*/
@media (min-width: 480px + $sidebarWidth) {
.shift-right [class^="Modal__FillScreen"] {
@media (min-width: 581px) {
.shift-right .atlaskit-portal > div:not(.Tooltip) {
padding-left: $sidebarWidth;
}
}
/**
* Similarly, we offset the notifications when the chat is open by
* padding the container.
*/
.shift-right [class^="styledFlagGroup-"] {
padding-left: $sidebarWidth;
}
.jitsi-icon svg {
fill: white;
}

View File

@@ -103,25 +103,22 @@
position: relative;
width: 100%;
z-index: 1;
display: flex;
justify-content: space-between;
padding: 16px;
align-items: center;
box-sizing: border-box;
color: #fff;
font-weight: 600;
font-size: 24px;
line-height: 32px;
.chat-close {
align-items: center;
bottom: 8px;
color: white;
.jitsi-icon {
cursor: pointer;
display: flex;
font-size: 18px;
height: 40px;
justify-content: center;
line-height: 15px;
padding: 4px;
position: absolute;
right: 5px;
width: 40px;
}
&:hover {
color: rgba(255, 255, 255, 0.8);
}
.jitsi-icon > svg {
fill: #A4B8D1;
}
}
@@ -184,6 +181,10 @@
text-overflow: ellipsis;
overflow: hidden;
}
@media (max-width: 580px) {
display: none !important;
}
}
.chatmessage {
@@ -389,6 +390,7 @@
&-header {
display: flex;
justify-content: space-between;
align-items: center;
margin: 16px 16px 24px;
width: calc(100% - 32px);
box-sizing: border-box;
@@ -397,8 +399,11 @@
font-size: 24px;
line-height: 32px;
.jitsi-icon > svg {
.jitsi-icon {
cursor: pointer;
}
.jitsi-icon > svg {
fill: #A4B8D1;
}
}
@@ -407,3 +412,15 @@
width: 100%;
}
}
/**
* Make header close button more easily tappable on mobile.
*/
.mobile-browser .chat-dialog-header .jitsi-icon {
display: grid;
place-items: center;
height: 48px;
width: 48px;
background: #2a3a4b;
border-radius: 3px;
}

View File

@@ -69,7 +69,7 @@
}
// Override @Atlaskit/inline-dialog styles
.cpick-container > div > div:nth-child(2) > div > div {
.cpick-container > div:nth-child(2) {
outline: none;
padding: 8px 0 0 0;
}

134
css/_drawer.scss Normal file
View File

@@ -0,0 +1,134 @@
.drawer-portal {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: $drawerZ;
}
.drawer-menu {
padding: 0 16px;
max-height: 50vh;
background: #242528;
border-radius: 16px 16px 0 0;
overflow-y: auto;
&.expanded {
max-height: 80vh;
}
.drawer-toggle {
display: flex;
justify-content: center;
align-items: center;
height: 44px;
cursor: pointer;
@media (hover: hover) and (pointer: fine) {
&:hover {
background-color: $overflowMenuItemHoverBG;
}
}
svg {
fill: none;
}
}
.popupmenu {
margin: auto;
width: 100%;
}
.popupmenu__item {
height: 48px;
}
&#{&} .overflow-menu {
margin: auto;
font-size: 1.2em;
list-style-type: none;
padding: 0;
.overflow-menu-item {
box-sizing: border-box;
height: 48px;
padding: 12px 16px;
align-items: center;
color: $overflowMenuItemColor;
cursor: pointer;
display: flex;
font-size: 16px;
div {
display: flex;
flex-direction: row;
align-items: center;
}
@media (hover: hover) and (pointer: fine) {
&:hover {
background-color: $overflowMenuItemHoverBG;
color: $overflowMenuItemHoverColor;
}
}
&.unclickable {
cursor: default;
}
@media (hover: hover) and (pointer: fine) {
&.unclickable:hover {
background: inherit;
}
}
&.disabled {
cursor: initial;
color: #3b475c;
}
}
.beta-tag {
background: $overflowMenuItemColor;
border-radius: 2px;
color: $overflowMenuBG;
font-size: 11px;
font-weight: bold;
margin-left: 8px;
padding: 0 6px;
}
.overflow-menu-item-icon {
margin-right: 16px;
i {
display: inline;
font-size: 24px;
}
@media (hover: hover) and (pointer: fine) {
i:hover {
background-color: initial;
}
}
img {
max-width: 24px;
max-height: 24px;
}
svg {
fill: #B8C7E0 !important;
height: 20px;
width: 20px;
}
}
.profile-text {
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}

View File

@@ -129,7 +129,7 @@
}
&-dropdown-container {
& > div > div:nth-child(2) > div > div {
& > div:nth-child(2) {
background: #fff;
padding: 0;
}

View File

@@ -143,16 +143,6 @@
@media only screen and (max-width: $verySmallScreen) {
@include very-small-button-size();
#videoResolutionLabel {
display: none;
}
.vertical-filmstrip .filmstrip {
display: none;
}
.chrome-extension-banner {
display: none;
}
}
&.shift-right {
@@ -177,23 +167,23 @@
}
@media (min-width: 480px) and (max-width: 580px) {
.shift-right [class^="Modal__PositionerAbsolute"] {
.shift-right .focus-lock > div > div {
@include full-size-modal-positioner();
}
.shift-right [class^="Modal__Dialog-"] {
.shift-right .focus-lock [role="dialog"] {
@include full-size-modal-dialog();
}
}
@media (min-width: 580px) and (max-width: 680px) {
.mobile-browser {
&.shift-right [class^="Modal__PositionerAbsolute"] {
&.shift-right .focus-lock > div > div {
@include full-size-modal-positioner();
}
&.shift-right [class^="Modal__Dialog-"] {
&.shift-right .focus-lock [role="dialog"] {
@include full-size-modal-dialog();
}
}
}
}

View File

@@ -14,12 +14,15 @@
text-overflow: ellipsis;
box-sizing: border-box;
white-space: nowrap;
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
&.visible {
top: 0px;
}
&.gradient {
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
}
&-text {
vertical-align: middle;
}

View File

@@ -42,9 +42,11 @@
display: none;
}
&.shift-right {
margin-left: $sidebarWidth;
width: calc(100% - #{$sidebarWidth});
@media (min-width: 581px) {
&.shift-right {
margin-left: $sidebarWidth;
width: calc(100% - #{$sidebarWidth});
}
}
.toolbox-background {
@@ -88,31 +90,35 @@
margin: 0px 4px;
width: 38px;
height: 38px;
&:hover {
background-color: #daebfa;
border: 1px solid #daebfa;
@media (hover: hover) and (pointer: fine) {
&:hover {
background-color: #daebfa;
border: 1px solid #daebfa;
}
}
&.toggled {
background: #2a3a4b;
border: 1px solid #5e6d7a;
svg {
fill: #fff;
}
&:hover {
background-color: #5e6d7a;
@media (hover: hover) and (pointer: fine) {
&:hover {
background-color: #5e6d7a;
}
}
}
&.disabled, .disabled & {
cursor: initial;
color: #fff;
background-color: #a4b8d1;
}
svg {
fill: #5e6d7a;
}
@@ -124,9 +130,11 @@
border: 1px solid $hangupColor;
width: 40px;
height: 40px;
&:hover {
background-color: $hangupColor;
@media (hover: hover) and (pointer: fine) {
&:hover {
background-color: $hangupColor;
}
}
svg {
@@ -157,8 +165,9 @@
cursor: pointer;
display: flex;
font-size: 14px;
height: 22px;
height: 40px;
padding: 5px 12px;
box-sizing: border-box;
div {
display: flex;
@@ -166,16 +175,20 @@
align-items: center;
}
&:hover {
background-color: $overflowMenuItemHoverBG;
color: $overflowMenuItemHoverColor;
@media (hover: hover) and (pointer: fine) {
&:hover {
background-color: $overflowMenuItemHoverBG;
color: $overflowMenuItemHoverColor;
}
}
&.unclickable {
cursor: default;
}
&.unclickable:hover {
background: inherit;
@media (hover: hover) and (pointer: fine) {
&.unclickable:hover {
background: inherit;
}
}
&.disabled {
cursor: initial;
@@ -194,15 +207,17 @@
}
.overflow-menu-item-icon {
margin-right: 10px;
margin-right: 16px;
i {
display: inline;
font-size: 24px;
}
i:hover {
background-color: initial;
@media (hover: hover) and (pointer: fine) {
i:hover {
background-color: initial;
}
}
img {
@@ -212,6 +227,8 @@
svg {
fill: #B8C7E0 !important;
height: 20px;
width: 20px;
}
}
@@ -259,10 +276,16 @@
justify-content: center;
width: $newToolbarSize;
&:hover, &.toggled {
@media (hover: hover) and (pointer: fine) {
&:hover {
background: $newToolbarButtonHoverColor;
}
}
&.toggled {
background: $newToolbarButtonHoverColor;
}
&.disabled {
cursor: initial !important;
background-color: #a4b8d1 !important;

View File

@@ -121,6 +121,7 @@ $poweredByZ: 100;
$ringingZ: 300;
$sideToolbarContainerZ: 300;
$toolbarZ: 350;
$drawerZ: 351;
$tooltipsZ: 401;
$dropdownMaskZ: 900;
$dropdownZ: 901;

View File

@@ -63,7 +63,7 @@
}
// Override @atlaskit/InlineDialog container which is made with styled components
& > div > div:nth-child(2) > div > div {
& > div:nth-child(2) {
outline: none;
padding: 0;
}

View File

@@ -182,10 +182,12 @@
z-index: $zindex2;
}
&.shift-right {
&#largeVideoContainer {
margin-left: $sidebarWidth;
width: calc(100% - #{$sidebarWidth});
@media (min-width: 581px) {
&.shift-right {
&#largeVideoContainer {
margin-left: $sidebarWidth;
width: calc(100% - #{$sidebarWidth});
}
}
}
}

View File

@@ -15,7 +15,7 @@
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100vh;
height: 100%;
width: 100vw;
}
@@ -47,12 +47,14 @@
width: 100%;
z-index: $filmstripVideosZ;
&.shift-right {
margin-left: $sidebarWidth;
width: calc(100% - #{$sidebarWidth});
@media (min-width: 581px) {
&.shift-right {
margin-left: $sidebarWidth;
width: calc(100% - #{$sidebarWidth});
#filmstripRemoteVideos {
width: calc(100vw - #{$sidebarWidth});
#filmstripRemoteVideos {
width: calc(100vw - #{$sidebarWidth});
}
}
}
}
@@ -86,6 +88,7 @@
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
flex-shrink: 0;
margin-top: auto;
margin-bottom: auto;
justify-content: center;
@@ -100,6 +103,15 @@
video {
object-fit: contain;
}
/**
* Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants.
*/
@media only screen and (max-width: 500px) {
video {
object-fit: cover;
}
}
}
.has-overflow#filmstripRemoteVideosContainer {

View File

@@ -22,7 +22,8 @@
display: none;
}
#remoteConnectionMessage {
#remoteConnectionMessage,
.watermark {
z-index: $filmstripVideosZ + 1;
}

View File

@@ -103,5 +103,6 @@ $flagsImagePath: "../images/";
@import 'e2ee';
@import 'responsive';
@import 'connection-status';
@import 'drawer';
/* Modules END */

View File

@@ -39,7 +39,7 @@
.device-selector-trigger-text {
overflow: hidden;
margin-left: 8px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;

View File

@@ -2,22 +2,6 @@
&-dialog {
display: flex;
flex-direction: column;
&-header {
display: flex;
justify-content: space-between;
margin: 16px 16px 24px;
width: calc(100% - 32px);
color: #fff;
font-weight: 600;
font-size: 24px;
line-height: 32px;
& > div > svg {
cursor: pointer;
fill: #A4B8D1;
}
}
}
&-copy {

View File

@@ -50,6 +50,12 @@
}
}
.dial-in-number {
display: flex;
justify-content: space-between;
padding-right: 8px;
}
.dial-in-numbers-list {
margin-top: 20px;
font-size: 12px;

View File

@@ -32,8 +32,10 @@
line-height: 24px;
cursor: pointer;
&:hover {
background: #278ADF;
@media (hover: hover) and (pointer: fine) {
&:hover {
background: #278ADF;
}
}
&-text {
@@ -47,22 +49,6 @@
font-size: 15px;
line-height: 24px;
&.header {
display: flex;
justify-content: space-between;
margin: 16px 16px 24px;
width: calc(100% - 32px);
color: #fff;
font-weight: 600;
font-size: 24px;
line-height: 32px;
& > div > svg {
cursor: pointer;
fill: #A4B8D1;
}
}
&.separator {
margin: 24px 0 24px -20px;
padding: 0 20px;
@@ -135,7 +121,6 @@
.dial-in-copy {
display: inline-block;
vertical-align: middle;
margin-left: 21px;
cursor: pointer;
}
}

View File

@@ -23,6 +23,10 @@
padding: 20px 0px 4px 0px;
}
input[type="checkbox"] + svg + span {
color: #9FB0CC;
}
.calendar-tab,
.more-tab,
.profile-edit {

2
debian/control vendored
View File

@@ -33,7 +33,7 @@ Description: Configuration for web serving of Jitsi Meet
Package: jitsi-meet-prosody
Architecture: all
Depends: openssl, prosody | prosody-trunk | prosody-0.11
Depends: openssl, prosody | prosody-trunk | prosody-0.11, lua-sec
Replaces: jitsi-meet-tokens
Description: Prosody configuration for Jitsi Meet
Jitsi Meet is a WebRTC JavaScript application that uses Jitsi

View File

@@ -60,15 +60,6 @@ case "$1" in
JICOFO_AUTH_PASSWORD="$RET"
fi
db_get jicofo/jicofosecret
if [ -z "$RET" ] ; then
# if secret is missing generate it, and store it
JICOFO_SECRET=`generateRandomPassword`
db_set jicofo/jicofosecret "$JICOFO_SECRET"
else
JICOFO_SECRET="$RET"
fi
JICOFO_AUTH_DOMAIN="auth.$JVB_HOSTNAME"
# detect dpkg-reconfigure, just delete old links
@@ -107,7 +98,6 @@ case "$1" in
mkdir -p /etc/prosody/conf.d/
cp /usr/share/jitsi-meet-prosody/prosody.cfg.lua-jvb.example $PROSODY_HOST_CONFIG
sed -i "s/jitmeet.example.com/$JVB_HOSTNAME/g" $PROSODY_HOST_CONFIG
sed -i "s/focusSecret/$JICOFO_SECRET/g" $PROSODY_HOST_CONFIG
sed -i "s/focusUser/$JICOFO_AUTH_USER/g" $PROSODY_HOST_CONFIG
sed -i "s/__turnSecret__/$TURN_SECRET/g" $PROSODY_HOST_CONFIG
if [ ! -f /etc/prosody/conf.d/$JVB_HOSTNAME.cfg.lua ]; then
@@ -150,7 +140,14 @@ case "$1" in
# Component "focus.jitmeet.example.com" "client_proxy"
# target_address = "focus@auth.jitmeet.example.com"
if grep -q "Component \"focus.$JVB_HOSTNAME\"" $PROSODY_HOST_CONFIG && ! grep "Component \"focus.$JVB_HOSTNAME\" \"client_proxy\"" $PROSODY_HOST_CONFIG ;then
sed -i -e "s/Component \"focus.$JVB_HOSTNAME\"/Component \"focus.$JVB_HOSTNAME\" \"client_proxy\"\n target_address = \"$JICOFO_AUTH_USER@auth.$JVB_HOSTNAME\"/" $PROSODY_HOST_CONFIG
sed -i "s/Component \"focus.$JVB_HOSTNAME\"/Component \"focus.$JVB_HOSTNAME\" \"client_proxy\"\n target_address = \"$JICOFO_AUTH_USER@auth.$JVB_HOSTNAME\"/g" $PROSODY_HOST_CONFIG
PROSODY_CONFIG_PRESENT="false"
fi
# Old versions of jitsi-meet-prosody come with the extra plugin path commented out (https://github.com/jitsi/jitsi-meet/commit/e11d4d3101e5228bf956a69a9e8da73d0aee7949)
# Make sure it is uncommented, as it contains required modules.
if grep -q -- '--plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }' $PROSODY_HOST_CONFIG ;then
sed -i 's#--plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }#plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }#g' $PROSODY_HOST_CONFIG
PROSODY_CONFIG_PRESENT="false"
fi

View File

@@ -24,11 +24,6 @@ Type: password
_Description: Jicofo user password:
The secret used to connect to xmpp server as jicofo user.
Template: jicofo/jicofosecret
Type: password
_Description: Jicofo Component secret:
The secret used to connect to xmpp server as component
Template: jitsi-meet-prosody/turn-secret
Type: string
_Description: The turn server secret

View File

@@ -45,8 +45,9 @@ case "$1" in
JVB_SERVE="false"
# this detect only old installations
RET=""
db_get jitsi-meet/jvb-serve || true
if [ -n "$RET" ] && [ "$RET" = "true" ] ; then
if [ "$RET" = "true" ] ; then
JVB_SERVE="true"
fi
@@ -68,14 +69,22 @@ case "$1" in
if [ "$APACHE_INSTALL_CHECK" = "installed" ] || [ "$APACHE_INSTALL_CHECK" = "unpacked" ] ; then
FORCE_APACHE="true"
fi
# In case user enforces apache and if apache is available, unset nginx.
RET=""
db_get jitsi-meet/enforce_apache || RET="false"
if [ "$RET" = "true" ] && [ "$FORCE_APACHE" = "true" ]; then
FORCE_NGINX="false"
fi
UPLOADED_CERT_CHOICE="I want to use my own certificate"
# if first time config ask for certs, or if we are reconfiguring
if [ -z "$JVB_HOSTNAME_OLD" ] || [ "$RECONFIGURING" = "true" ] ; then
RET=""
db_get jitsi-meet/cert-choice
CERT_CHOICE="$RET"
if [ "$CERT_CHOICE" = "$UPLOADED_CERT_CHOICE" ] ; then
RET=""
db_get jitsi-meet/cert-path-key
if [ -z "$RET" ] ; then
db_set jitsi-meet/cert-path-key "/etc/ssl/$JVB_HOSTNAME.key"
@@ -84,6 +93,7 @@ case "$1" in
db_get jitsi-meet/cert-path-key
fi
CERT_KEY="$RET"
RET=""
db_get jitsi-meet/cert-path-crt
if [ -z "$RET" ] ; then
db_set jitsi-meet/cert-path-crt "/etc/ssl/$JVB_HOSTNAME.crt"
@@ -146,12 +156,15 @@ case "$1" in
# Removing this value will force nginx or apache to be locally configured
JVB_HOSTNAME_OLD=""
RET=""
db_get jitsi-meet/cert-choice
CERT_CHOICE="$RET"
# Fix certs on upgrade from jetty
if [ "$CERT_CHOICE" = "$UPLOADED_CERT_CHOICE" ] ; then
RET=""
db_get jitsi-meet/cert-path-key
CERT_KEY="$RET"
RET=""
db_get jitsi-meet/cert-path-crt
CERT_CRT="$RET"
else
@@ -205,7 +218,7 @@ case "$1" in
# apache2 config
if [ ! -f /etc/apache2/sites-available/$JVB_HOSTNAME.conf ] ; then
# when creating new config, make sure all needed modules are enabled
a2enmod rewrite ssl headers proxy_http include
a2enmod rewrite ssl headers proxy_http proxy_wstunnel include
cp /usr/share/jitsi-meet-web-config/jitsi-meet.example-apache /etc/apache2/sites-available/$JVB_HOSTNAME.conf
a2ensite $JVB_HOSTNAME.conf
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" /etc/apache2/sites-available/$JVB_HOSTNAME.conf

View File

@@ -15,3 +15,5 @@ resources/robots.txt /usr/share/jitsi-meet/
resources/*.sh /usr/share/jitsi-meet/scripts/
pwa-worker.js /usr/share/jitsi-meet/
manifest.json /usr/share/jitsi-meet/
resources/load-test/*.html /usr/share/jitsi-meet/load-test/
resources/load-test/libs /usr/share/jitsi-meet/load-test/

View File

@@ -3,12 +3,11 @@ plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }
-- domain mapper options, must at least have domain base set to use the mapper
muc_mapper_domain_base = "jitmeet.example.com";
turncredentials_secret = "__turnSecret__";
turncredentials = {
{ type = "stun", host = "jitmeet.example.com", port = "3478" },
{ type = "turn", host = "jitmeet.example.com", port = "3478", transport = "udp" },
{ type = "turns", host = "jitmeet.example.com", port = "5349", transport = "tcp" }
external_service_secret = "__turnSecret__";
external_services = {
{ type = "stun", host = "jitmeet.example.com", port = 3478 },
{ type = "turn", host = "jitmeet.example.com", port = 3478, transport = "udp", secret = true, ttl = 86400, algorithm = "turn" },
{ type = "turns", host = "jitmeet.example.com", port = 5349, transport = "tcp", secret = true, ttl = 86400, algorithm = "turn" }
};
cross_domain_bosh = false;
@@ -44,7 +43,7 @@ VirtualHost "jitmeet.example.com"
"pubsub";
"ping"; -- Enable mod_ping
"speakerstats";
"turncredentials";
"external_services";
"conference_duration";
"muc_lobby_rooms";
}
@@ -75,7 +74,7 @@ Component "internal.auth.jitmeet.example.com" "muc"
muc_room_default_public_jids = true
VirtualHost "auth.jitmeet.example.com"
authentication = "internal_plain"
authentication = "internal_hashed"
-- Proxy to jicofo's user JID, so that it doesn't have to register as a component.
Component "focus.jitmeet.example.com" "client_proxy"

View File

@@ -1,5 +1,9 @@
server_names_hash_bucket_size 64;
types {
# nginx's default mime.types doesn't include a mapping for wasm
application/wasm wasm;
}
server {
listen 80;
listen [::]:80;
@@ -96,6 +100,15 @@ server {
tcp_nodelay on;
}
# load test minimal client, uncomment when used
#location ~ ^/_load-test/([^/?&:'"]+)$ {
# rewrite ^/_load-test/(.*)$ /load-test/index.html break;
#}
#location ~ ^/_load-test/libs/(.*)$ {
# add_header 'Access-Control-Allow-Origin' '*';
# alias /usr/share/jitsi-meet/load-test/libs/$1;
#}
location ~ ^/([^/?&:'"]+)$ {
try_files $uri @root_path;
}

View File

@@ -44,6 +44,9 @@
ProxyPreserveHost on
ProxyPass /http-bind http://localhost:5280/http-bind/
ProxyPassReverse /http-bind http://localhost:5280/http-bind/
ProxyPass /xmpp-websocket ws://localhost:5280/xmpp-websocket
ProxyPassReverse /xmpp-websocket ws://localhost:5280/xmpp-websocket
ProxyPassMatch ^/colibri-ws/default-id ws://localhost:9090
RewriteEngine on
RewriteRule ^/([a-zA-Z0-9]+)$ /index.html

View File

@@ -142,7 +142,6 @@ pidfile = "/var/run/prosody/prosody.pid"
-- server please see http://prosody.im/doc/modules/mod_auth_internal_hashed
-- for information about using the hashed backend.
-- authentication = "internal_plain"
authentication = "internal_hashed"
-- Select the storage backend to use. By default Prosody uses flat files
@@ -190,7 +189,7 @@ VirtualHost "auth.jitsi.example.com"
key = "/var/lib/prosody/auth.jitsi.example.com.key";
certificate = "/var/lib/prosody/auth.jitsi.example.com.crt";
}
authentication = "internal_plain"
authentication = "internal_hashed"
------ Components ------
-- You can specify components to add hosts that provide special services,

View File

@@ -139,7 +139,6 @@ pidfile = "/var/run/prosody/prosody.pid"
-- server please see http://prosody.im/doc/modules/mod_auth_internal_hashed
-- for information about using the hashed backend.
-- authentication = "internal_plain"
authentication = "internal_hashed"
-- Select the storage backend to use. By default Prosody uses flat files
@@ -187,7 +186,7 @@ VirtualHost "auth.jitsi.example.com"
key = "/var/lib/prosody/auth.jitsi.example.com.key";
certificate = "/var/lib/prosody/auth.jitsi.example.com.crt";
}
authentication = "internal_plain"
authentication = "internal_hashed"
------ Components ------
-- You can specify components to add hosts that provide special services,

View File

@@ -67,7 +67,7 @@ VirtualHost "auth.meet.example.com"
key = "/etc/prosody/certs/auth.meet.example.com.key";
certificate = "/etc/prosody/certs/auth.meet.example.com.crt";
}
authentication = "internal_plain"
authentication = "internal_hashed"
Component "focus.meet.example.com"
component_secret = "jicofo_secret_test"
@@ -83,5 +83,5 @@ VirtualHost "recorder.meet.example.com"
modules_enabled = {
"ping";
}
authentication = "internal_plain"
authentication = "internal_hashed"
c2s_require_encryption = false

View File

@@ -3,7 +3,7 @@
<!--#include virtual="head.html" -->
<meta charset="utf-8">
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta name="theme-color" content="#2A3A4B">
<!--#include virtual="base.html" -->

View File

@@ -292,7 +292,7 @@ PODS:
- React
- react-native-splash-screen (3.2.0):
- React
- react-native-webrtc (1.87.2):
- react-native-webrtc (1.87.3):
- React-Core
- react-native-webview (11.0.2):
- React-Core
@@ -563,7 +563,7 @@ SPEC CHECKSUMS:
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
react-native-webrtc: e6fca8432542dd1c77afa6c59629f0176ed78ee6
react-native-webrtc: dc1208bdca2c4d091f7b57859e69332bff6f1986
react-native-webview: b2542d6fd424bcc3e3b2ec5f854f0abb4ec86c87
React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6

View File

@@ -99,9 +99,30 @@
#if 0
- (void)enterPictureInPicture:(NSDictionary *)data {
[self _onJitsiMeetViewDelegateEvent:@"ENTER_PICTURE_IN_PICTURE" withData:data];
}
#endif
- (void)participantJoined:(NSDictionary *)data {
NSLog(@"%@%@", @"Participant joined: ", data[@"participantId"]);
}
- (void)participantLeft:(NSDictionary *)data {
NSLog(@"%@%@", @"Participant left: ", data[@"participantId"]);
}
- (void)audioMutedChanged:(NSDictionary *)data {
NSLog(@"%@%@", @"Audio muted changed: ", data[@"muted"]);
}
- (void)endpointTextMessageReceived:(NSDictionary *)data {
NSLog(@"%@%@", @"Endpoint text message received: ", data);
}
- (void)screenShareToggled:(NSDictionary *)data {
NSLog(@"%@%@", @"Screen share toggled: ", data);
}
#pragma mark - Helpers
- (void)terminate {

View File

@@ -41,6 +41,7 @@
C69EFA0E209A0F660027712B /* JMCallKitListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69EFA0B209A0F660027712B /* JMCallKitListener.swift */; };
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; };
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; };
C81E9AB925AC5AD800B134D9 /* ExternalAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = C81E9AB825AC5AD800B134D9 /* ExternalAPI.h */; };
C8AFD27F2462C613000293D2 /* InfoPlistUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = C8AFD27D2462C613000293D2 /* InfoPlistUtil.h */; settings = {ATTRIBUTES = (Public, ); }; };
C8AFD2802462C613000293D2 /* InfoPlistUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = C8AFD27E2462C613000293D2 /* InfoPlistUtil.m */; };
DE438CDA2350934700DD541D /* JavaScriptSandbox.m in Sources */ = {isa = PBXBuildFile; fileRef = DE438CD82350934700DD541D /* JavaScriptSandbox.m */; };
@@ -105,6 +106,7 @@
C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = "<group>"; };
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = "<group>"; };
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
C81E9AB825AC5AD800B134D9 /* ExternalAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExternalAPI.h; sourceTree = "<group>"; };
C8AFD27D2462C613000293D2 /* InfoPlistUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InfoPlistUtil.h; sourceTree = "<group>"; };
C8AFD27E2462C613000293D2 /* InfoPlistUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InfoPlistUtil.m; sourceTree = "<group>"; };
DE438CD82350934700DD541D /* JavaScriptSandbox.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JavaScriptSandbox.m; sourceTree = "<group>"; };
@@ -228,6 +230,7 @@
0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */,
C8AFD27D2462C613000293D2 /* InfoPlistUtil.h */,
C8AFD27E2462C613000293D2 /* InfoPlistUtil.m */,
C81E9AB825AC5AD800B134D9 /* ExternalAPI.h */,
);
path = src;
sourceTree = "<group>";
@@ -301,6 +304,7 @@
DE81A2D42316AC4D00AE1940 /* JitsiMeetLogger.h in Headers */,
DE65AACA2317FFCD00290BEC /* LogUtils.h in Headers */,
DEAD3226220C497000E93636 /* JitsiMeetConferenceOptions.h in Headers */,
C81E9AB925AC5AD800B134D9 /* ExternalAPI.h in Headers */,
C8AFD27F2462C613000293D2 /* InfoPlistUtil.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@@ -1,5 +1,5 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
* Copyright @ 2017-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

27
ios/sdk/src/ExternalAPI.h Normal file
View File

@@ -0,0 +1,27 @@
/* Copyright @ 2021-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface ExternalAPI : RCTEventEmitter<RCTBridgeModule>
- (void)sendHangUp;
- (void)sendSetAudioMuted: (BOOL)muted;
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message;
- (void)toggleScreenShare;
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completion;
@end

View File

@@ -1,5 +1,5 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
* Copyright @ 2017-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,17 +14,37 @@
* limitations under the License.
*/
#import <React/RCTBridgeModule.h>
#import "ExternalAPI.h"
#import "JitsiMeetView+Private.h"
@interface ExternalAPI : NSObject<RCTBridgeModule>
@end
// Events
static NSString * const hangUpAction = @"org.jitsi.meet.HANG_UP";
static NSString * const setAudioMutedAction = @"org.jitsi.meet.SET_AUDIO_MUTED";
static NSString * const sendEndpointTextMessageAction = @"org.jitsi.meet.SEND_ENDPOINT_TEXT_MESSAGE";
static NSString * const toggleScreenShareAction = @"org.jitsi.meet.TOGGLE_SCREEN_SHARE";
static NSString * const retrieveParticipantsInfoAction = @"org.jitsi.meet.RETRIEVE_PARTICIPANTS_INFO";
@implementation ExternalAPI
static NSMapTable<NSString*, void (^)(NSArray* participantsInfo)> *participantInfoCompletionHandlers;
__attribute__((constructor))
static void initializeViewsMap() {
participantInfoCompletionHandlers = [NSMapTable strongToStrongObjectsMapTable];
}
RCT_EXPORT_MODULE();
- (NSDictionary *)constantsToExport {
return @{
@"HANG_UP": hangUpAction,
@"SET_AUDIO_MUTED" : setAudioMutedAction,
@"SEND_ENDPOINT_TEXT_MESSAGE": sendEndpointTextMessageAction,
@"TOGGLE_SCREEN_SHARE": toggleScreenShareAction,
@"RETRIEVE_PARTICIPANTS_INFO": retrieveParticipantsInfoAction
};
};
/**
* Make sure all methods in this module are invoked on the main/UI thread.
*/
@@ -32,6 +52,18 @@ RCT_EXPORT_MODULE();
return dispatch_get_main_queue();
}
+ (BOOL)requiresMainQueueSetup {
return NO;
}
- (NSArray<NSString *> *)supportedEvents {
return @[ hangUpAction,
setAudioMutedAction,
sendEndpointTextMessageAction,
toggleScreenShareAction,
retrieveParticipantsInfoAction];
}
/**
* Dispatches an event that occurred on JavaScript to the view's delegate.
*
@@ -57,6 +89,11 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
if (!delegate) {
return;
}
if ([name isEqual: @"PARTICIPANTS_INFO_RETRIEVED"]) {
[self onParticipantsInfoRetrieved: data];
return;
}
SEL sel = NSSelectorFromString([self methodNameFromEventName:name]);
@@ -65,6 +102,15 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
}
}
- (void) onParticipantsInfoRetrieved:(NSDictionary *)data {
NSArray *participantsInfoArray = [data objectForKey:@"participantsInfo"];
NSString *completionHandlerId = [data objectForKey:@"requestId"];
void (^completionHandler)(NSArray*) = [participantInfoCompletionHandlers objectForKey:completionHandlerId];
completionHandler(participantsInfoArray);
[participantInfoCompletionHandlers removeObjectForKey:completionHandlerId];
}
/**
* Converts a specific event name i.e. redux action type description to a
* method name.
@@ -87,4 +133,35 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
return methodName;
}
- (void)sendHangUp {
[self sendEventWithName:hangUpAction body:nil];
}
- (void)sendSetAudioMuted:(BOOL)muted {
NSDictionary *data = @{ @"muted": [NSNumber numberWithBool:muted]};
[self sendEventWithName:setAudioMutedAction body:data];
}
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message {
NSDictionary *data = @{
@"to": to,
@"message": message
};
[self sendEventWithName:sendEndpointTextMessageAction body:data];
}
- (void)toggleScreenShare {
[self sendEventWithName:toggleScreenShareAction body:nil];
}
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completionHandler {
NSString *completionHandlerId = [[NSUUID UUID] UUIDString];
NSDictionary *data = @{ @"requestId": completionHandlerId};
[participantInfoCompletionHandlers setObject:[completionHandler copy] forKey:completionHandlerId];
[self sendEventWithName:retrieveParticipantsInfoAction body:data];
}
@end

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>3.0.0</string>
<string>3.1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>

View File

@@ -16,11 +16,13 @@
#import <React/RCTBridge.h>
#import "ExternalAPI.h"
#import "JitsiMeet.h"
@interface JitsiMeet ()
- (NSDictionary *)getDefaultProps;
- (RCTBridge *)getReactBridge;
- (ExternalAPI *)getExternalAPI;
@end

View File

@@ -213,4 +213,8 @@
return _bridgeWrapper.bridge;
}
- (ExternalAPI *)getExternalAPI {
return [_bridgeWrapper.bridge moduleForClass:ExternalAPI.class];
}
@end

View File

@@ -37,4 +37,14 @@
*/
- (void)leave;
- (void)hangUp;
- (void)setAudioMuted:(BOOL)muted;
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message;
- (void)toggleScreenShare;
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completionHandler;
@end

View File

@@ -17,6 +17,7 @@
#include <mach/mach_time.h>
#import "ExternalAPI.h"
#import "JitsiMeet+Private.h"
#import "JitsiMeetConferenceOptions+Private.h"
#import "JitsiMeetView+Private.h"
@@ -49,7 +50,6 @@ static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
* identifiers within the process).
*/
static NSMapTable<NSString *, JitsiMeetView *> *views;
/**
* This gets called automagically when the program starts.
*/
@@ -115,6 +115,31 @@ static void initializeViewsMap() {
[self setProps:@{}];
}
- (void)hangUp {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendHangUp];
}
- (void)setAudioMuted:(BOOL)muted {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendSetAudioMuted:muted];
}
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendEndpointTextMessage:to :message];
}
- (void)toggleScreenShare {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI toggleScreenShare];
}
- (void)retrieveParticipantsInfo:(void (^)(NSArray*))completionHandler {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI retrieveParticipantsInfo:completionHandler];
}
#pragma mark Private methods
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
* Copyright @ 2017-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -55,4 +55,39 @@
*/
- (void)enterPictureInPicture:(NSDictionary *)data;
/**
* Called when a participant has joined the conference.
*
* The `data` dictionary contains a `participantId` key with the id of the participant that has joined.
*/
- (void)participantJoined:(NSDictionary *)data;
/**
* Called when a participant has left the conference.
*
* The `data` dictionary contains a `participantId` key with the id of the participant that has left.
*/
- (void)participantLeft:(NSDictionary *)data;
/**
* Called when audioMuted state changed.
*
* The `data` dictionary contains a `muted` key with state of the audioMuted for the localParticipant.
*/
- (void)audioMutedChanged:(NSDictionary *)data;
/**
* Called when an endpoint text message is received.
*
* The `data` dictionary contains a `senderId` key with the participantId of the sender and a 'message' key with the content.
*/
- (void)endpointTextMessageReceived:(NSDictionary *)data;
/**
* Called when a participant toggled shared screen.
*
* The `data` dictionary contains a `participantId` key with the id of the participant and a 'sharing' key with boolean value.
*/
- (void)screenShareToggled:(NSDictionary *)data;
@end

View File

@@ -32,6 +32,7 @@
"lv": "لتونیایی",
"nl": "هلندی",
"oc": "اکسیتان(قدیمی)",
"fa": "فارسی",
"pl": "لهستانی",
"ptBR": "پرتغالی (برزیل)",
"ru": "روسی",
@@ -47,4 +48,4 @@
"vi": "ویتنامی",
"zhCN": "چینی",
"zhTW": "چینی (تایوان)"
}
}

View File

@@ -3,36 +3,48 @@
"af": "Afrikaans",
"az": "Azero",
"bg": "Bulgaro",
"ca": "Catalano",
"cs": "Ceco",
"da": "Danese",
"de": "Tedesco",
"el": "Greco",
"enGB": "Inglese (Regno Unito)",
"eo": "Esperanto",
"es": "Spagnolo",
"esUS": "Spagnolo (America Latina)",
"et": "Estone",
"eu": "Basco",
"fi": "Finlandese",
"fr": "Francese",
"frCA": "Francese (Canada)",
"he": "Ebraico",
"mr": "Marathi",
"hr": "Croato",
"hu": "Ungaro",
"hy": "Armeno",
"id": "Indonesiano",
"it": "Italiano",
"ja": "Giapponese",
"kab": "Kabyle",
"ko": "Coreano",
"nb": "Norvegese bokmal",
"lt": "Lituano",
"ml": "Malese",
"lv": "Lettone",
"nl": "Olandese",
"oc": "Occitano",
"pl": "Polacco",
"ptBR": "Portoghese (Brasile)",
"ru": "Russo",
"ru": "Russo",
"ro": "Rumeno",
"sc": "Sardo",
"sk": "Slovacco",
"sl": "Sloveno",
"sv": "Svedese",
"sl": "Sloveno",
"sr": "Serbo",
"sv": "Svedese",
"th": "Tailandese",
"tr": "Turco",
"uk": "Ucraino",
"vi": "Vietnamita",
"zhCN": "Cinese (Cina)",
"enGB": "Inglese (Regno Unito)",
"da": "Danese",
"ca": "Catalano",
"zhTW": "Cinese (Taiwan)",
"nl": "Olandese",
"hu": "Ungaro",
"hr": "Croato",
"frCA": "Francese (Canada)",
"fi": "Finlandese",
"et": "Etiope",
"esUS": "Spagnolo (USA)"
"zhTW": "Cinese (Taiwan)"
}

View File

@@ -32,6 +32,7 @@
"lv": "Latvian",
"nl": "Dutch",
"oc": "Occitan",
"fa": "Persian",
"pl": "Polish",
"ptBR": "Portuguese (Brazil)",
"ru": "Russian",

View File

@@ -8,28 +8,28 @@
"countryNotSupported": "Wir unterstützen dieses Land noch nicht.",
"countryReminder": "Telefonnummer nicht in den USA? Bitte sicherstellen, dass die Telefonnummer mit dem Ländercode beginnt.",
"defaultEmail": "Ihre Standard-E-Mail",
"disabled": "Sie können keine Teilnehmer einladen.",
"failedToAdd": "Fehler beim Hinzufügen von Teilnehmern",
"disabled": "Sie können keine Personen einladen.",
"failedToAdd": "Fehler beim Hinzufügen von Personen",
"footerText": "Abgehender Ruf ist deaktiviert.",
"googleEmail": "Google-E-Mail",
"inviteMoreHeader": "Sie sind alleine in der Sitzung",
"inviteMoreMailSubject": "An {{appName}} Meeting teilnehmen",
"inviteMorePrompt": "Mehr Leute einladen",
"linkCopied": "Link in die Zwischenablage kopiert",
"loading": "Suche nach Teilnehmern und Telefonnummern",
"loading": "Suche nach Personen und Telefonnummern",
"loadingNumber": "Telefonnummer wird überprüft",
"loadingPeople": "Suche nach einzuladenden Teilnehmern",
"loadingPeople": "Suche nach einzuladenden Personen",
"noResults": "Keine passenden Ergebnisse",
"noValidNumbers": "Telefonnummer eingeben",
"outlookEmail": "Outlook-E-Mail",
"searchNumbers": "Telefonnummern hinzufügen",
"searchPeople": "Nach Teilnehmern suchen",
"searchPeopleAndNumbers": "Nach Teilnehmen suchen oder deren Telefonnummern hinzufügen",
"searchPeople": "Nach Personen suchen",
"searchPeopleAndNumbers": "Nach Personen suchen oder deren Telefonnummern hinzufügen",
"shareInvite": "Einladung zur Versammlung teilen",
"shareLink": "Teilen Sie den Konferenzlink, um andere einzuladen",
"shareStream": "Den Livestreaminglink freigeben",
"telephone": "Telefon: {{number}}",
"title": "Teilnehmer zu dieser Konferenz einladen",
"title": "Personen zu dieser Konferenz einladen",
"yahooEmail": "Yahoo-E-Mail"
},
"audioDevices": {
@@ -105,7 +105,7 @@
"bridgeCount": "Serverzahl: ",
"codecs": "Codecs (A/V): ",
"connectedTo": "Verbunden mit:",
"e2e_rtt": "E2E RTT:",
"e2e_rtt": "Ende-zu-Ende-Paketumlaufzeit:",
"framerate": "Bildwiederholrate:",
"less": "Weniger anzeigen",
"localaddress": "Lokale Adresse:",
@@ -128,7 +128,7 @@
"remoteport_plural": "Entfernte Ports:",
"resolution": "Auflösung:",
"savelogs": "Logs speichern",
"participant_id": "Teilnehmer-ID:",
"participant_id": "Personen-ID:",
"status": "Verbindung:",
"transport": "Protokoll:",
"transport_plural": "Protokolle:",
@@ -146,7 +146,7 @@
"downloadApp": "App herunterladen",
"ifDoNotHaveApp": "Wenn Sie die App noch nicht haben:",
"ifHaveApp": "Wenn Sie die App bereits haben:",
"joinInApp": "An dem Meeting teilnehmen mit der App",
"joinInApp": "Mit der App am Meeting teilnehmen",
"launchWebButton": "Im Web öffnen",
"title": "Die Konferenz wird in {{app}} geöffnet …",
"tryAgainButton": "Erneut mit der nativen Applikation versuchen"
@@ -171,7 +171,7 @@
},
"add": "Hinzufügen",
"allow": "Erlauben",
"alreadySharedVideoMsg": "Ein anderer Teilnehmer gibt bereits ein Video weiter. Bei dieser Konferenz ist jeweils nur ein geteiltes Video möglich.",
"alreadySharedVideoMsg": "Eine andere Person gibt bereits ein Video weiter. Bei dieser Konferenz ist jeweils nur ein geteiltes Video möglich.",
"alreadySharedVideoTitle": "Nur ein geteiltes Video gleichzeitig",
"applicationWindow": "Anwendungsfenster",
"Back": "Zurück",
@@ -179,7 +179,7 @@
"cameraNotFoundError": "Kamera nicht gefunden.",
"cameraNotSendingData": "Die Kamera ist nicht verfügbar. Bitte prüfen, ob eine andere Applikation die Kamera verwendet, eine andere Kamera vom Einstellungs-Menü auswählen oder die Applikation neu laden.",
"cameraNotSendingDataTitle": "Zugriff auf Kamera nicht möglich",
"cameraPermissionDeniedError": "Die Berechtigung zur Verwendung der Kamera wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht sehen. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste, um die Berechtigungen zu erteilen.",
"cameraPermissionDeniedError": "Die Berechtigung zur Verwendung der Kamera wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Personen können Sie nicht sehen. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste, um die Berechtigungen zu erteilen.",
"cameraUnknownError": "Die Kamera kann aus einem unbekannten Grund nicht verwendet werden.",
"cameraUnsupportedResolutionError": "Die Kamera unterstützt die erforderliche Auflösung nicht.",
"Cancel": "Abbrechen",
@@ -202,21 +202,21 @@
"done": "Fertig",
"e2eeDescription": "Ende-zu-Ende-Verschlüsselung ist derzeit noch EXPERIMENTELL. Bitte beachten Sie, dass das Aktivieren der Ende-zu-Ende-Verschlüsselung diverse serverseitige Funktionen deaktiviert: Aufnahmen, Livestreaming und Telefoneinwahl. Bitte beachten Sie außerdem, dass der Konferenz dann nur noch mit Browsern beigetreten werden kann, die Insertable Streams unterstützen.",
"e2eeLabel": "Ende-zu-Ende-Verschlüsselung aktivieren",
"e2eeWarning": "WARNUNG: Nicht alle Teilnehmer dieser Konferenz scheinen Ende-zu-Ende-Verschlüsselung zu unterstützen. Wenn Sie diese aktivieren, können die entsprechenden Teilnehmer nichts mehr sehen oder hören.",
"e2eeWarning": "WARNUNG: Nicht alle Personen dieser Konferenz scheinen Ende-zu-Ende-Verschlüsselung zu unterstützen. Wenn Sie diese aktivieren, können die entsprechenden Personen nichts mehr sehen oder hören.",
"enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
"error": "Fehler",
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
"grantModeratorDialog": "Möchten Sie diesen Teilnehmer wirklich zum Moderator machen?",
"grantModeratorTitle": "Zum Moderator machen",
"IamHost": "Ich bin der Organisator",
"grantModeratorDialog": "Möchten Sie wirklich Moderationsrechte an diese Person vergeben?",
"grantModeratorTitle": "Moderationsrechte vergeben",
"IamHost": "Ich leite das Meeting",
"incorrectRoomLockPassword": "Falsches Passwort",
"incorrectPassword": "Benutzername oder Passwort ungültig",
"incorrectPassword": "Name oder Passwort ungültig",
"internalError": "Oh! Es hat etwas nicht funktioniert. Der folgende Fehler ist aufgetreten: {{error}}",
"internalErrorTitle": "Interner Fehler",
"kickMessage": "Sie können sich für mehr Details an {{participantDisplayName}} wenden.",
"kickParticipantButton": "Entfernen",
"kickParticipantDialog": "Wollen Sie diesen Teilnehmer wirklich entfernen?",
"kickParticipantTitle": "Teilnehmer entfernen?",
"kickParticipantDialog": "Wollen Sie diese Person wirklich entfernen?",
"kickParticipantTitle": "Person entfernen?",
"kickTitle": "Autsch! {{participantDisplayName}} hat Sie aus dem Meeting geworfen",
"liveStreaming": "Livestreaming",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Während einer Aufnahme nicht möglich",
@@ -227,13 +227,13 @@
"lockTitle": "Sperren fehlgeschlagen",
"logoutQuestion": "Sind Sie sicher, dass Sie sich abmelden und die Konferenz verlassen möchten?",
"logoutTitle": "Abmelden",
"maxUsersLimitReached": "Das Limit für die maximale Teilnehmerzahl ist erreicht. Die Konferenz ist voll. Bitte wenden Sie sich an den Besitzer des Meetings oder versuchen Sie es später noch einmal!",
"maxUsersLimitReachedTitle": "Maximales Teilnehmerlimit erreicht",
"maxUsersLimitReached": "Das Limit für die maximale Personenzahl ist erreicht. Die Konferenz ist voll. Bitte wenden Sie sich an die Konferenzleitung oder versuchen Sie es später noch einmal!",
"maxUsersLimitReachedTitle": "Maximale Personenzahl erreicht",
"micConstraintFailedError": "Ihr Mikrofon erfüllt die notwendigen Anforderungen nicht.",
"micNotFoundError": "Mikrofon nicht gefunden.",
"micNotSendingData": "Gehen Sie zu den Einstellungen Ihres Computers, um die Stummschaltung Ihres Mikrofons aufzuheben und seinen Pegel einzustellen",
"micNotSendingDataTitle": "Ihr Mikrofon ist durch Ihre Systemeinstellungen stumm geschaltet",
"micPermissionDeniedError": "Die Berechtigung zur Verwendung des Mikrofons wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht hören. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste, um die Berechtigungen zu erteilen.",
"micPermissionDeniedError": "Die Berechtigung zur Verwendung des Mikrofons wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Personen können Sie nicht hören. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste, um die Berechtigungen zu erteilen.",
"micUnknownError": "Das Mikrofon kann aus einem unbekannten Grund nicht verwendet werden.",
"muteEveryoneElseDialog": "Einmal stummgeschaltet, können Sie deren Stummschaltung nicht mehr beenden, aber sie können ihre Stummschaltung jederzeit selbst beenden.",
"muteEveryoneElseTitle": "Alle außer {{whom}} stummschalten?",
@@ -241,12 +241,12 @@
"muteEveryoneTitle": "Alle stummschalten?",
"muteEveryoneSelf": "sich selbst",
"muteEveryoneStartMuted": "Alle beginnen von jetzt an stummgeschaltet",
"muteParticipantBody": "Sie können die Stummschaltung anderer Teilnehmer nicht aufheben, aber ein Teilnehmer kann seine eigene Stummschaltung jederzeit beenden.",
"muteParticipantBody": "Sie können die Stummschaltung anderer Personen nicht aufheben, aber eine Person kann ihre eigene Stummschaltung jederzeit beenden.",
"muteParticipantButton": "Stummschalten",
"muteParticipantDialog": "Wollen Sie diesen Teilnehmer wirklich stummschalten? Sie können die Stummschaltung nicht wieder aufheben, der Teilnehmer kann dies aber jederzeit selbst tun.",
"muteParticipantTitle": "Teilnehmer stummschalten?",
"muteParticipantDialog": "Wollen Sie diese Person wirklich stummschalten? Sie können die Stummschaltung nicht wieder aufheben, die Person kann dies aber jederzeit selbst tun.",
"muteParticipantTitle": "Person stummschalten?",
"Ok": "OK",
"passwordLabel": "Dieses Meeting wurde von einem Teilnehmer gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um dem Meeting beizutreten.",
"passwordLabel": "Dieses Meeting wurde gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um dem Meeting beizutreten.",
"passwordNotSupported": "Das Festlegen eines Konferenzpassworts wird nicht unterstützt.",
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) nicht unterstützt",
"passwordRequired": "$t(lockRoomPasswordUppercase) erforderlich",
@@ -303,10 +303,11 @@
"tokenAuthFailedTitle": "Authentifizierung fehlgeschlagen",
"transcribing": "Wird transkribiert",
"unlockRoom": "Konferenz$t(lockRoomPassword) entfernen",
"userPassword": "Benutzerpasswort",
"WaitForHostMsg": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Wenn Sie der Organisator sind, authentifizieren Sie sich. Warten Sie andernfalls, bis der Organisator erscheint.",
"WaitForHostMsgWOk": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Wenn Sie der Organisator sind, drücken Sie zum Authentifizieren auf OK. Warten Sie andernfalls, bis der Organisator erscheint.",
"WaitingForHost": "Warten auf den Organisator ",
"user": "Anmeldename",
"userPassword": "Passwort",
"WaitForHostMsg": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
"WaitForHostMsgWOk": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
"WaitingForHost": "Warten auf den Beginn der Konferenz …",
"Yes": "Ja",
"yourEntireScreen": "Ganzer Bildschirm"
},
@@ -317,7 +318,7 @@
"title": "Freigegebenes Dokument"
},
"e2ee": {
"labelToolTip": "Audio- und Videodaten dieser Unterhaltung sind jetzt zwischen den Teilnehmern verschlüsselt"
"labelToolTip": "Audio- und Videodaten dieser Unterhaltung sind jetzt zwischen den Personen verschlüsselt"
},
"embedMeeting": {
"title": "Diese Konferenz einbetten"
@@ -369,11 +370,11 @@
"label": "Einwahlinformationen"
},
"inviteDialog": {
"alertText": "Die Einladung einiger Teilnehmer ist fehlgeschlagen.",
"alertText": "Die Einladung einiger Personen ist fehlgeschlagen.",
"header": "Einladen",
"searchCallOnlyPlaceholder": "Telefonnummer eingeben",
"searchPeopleOnlyPlaceholder": "Nach Teilnehmern suchen",
"searchPlaceholder": "Teilnehmer oder Telefonnummer",
"searchPeopleOnlyPlaceholder": "Nach Personen suchen",
"searchPlaceholder": "Personen oder Telefonnummer",
"send": "Senden"
},
"inlineDialogFailure": {
@@ -384,14 +385,14 @@
},
"keyboardShortcuts": {
"focusLocal": "Lokales Video fokussieren",
"focusRemote": "Auf das Video eines anderen Teilnehmers fokussieren",
"focusRemote": "Auf das Video einer anderen Person fokussieren",
"fullScreen": "Vollbildmodus aktivieren oder deaktivieren",
"keyboardShortcuts": "Tastenkürzel",
"localRecording": "Lokale Aufzeichnungssteuerelemente ein- oder ausblenden",
"mute": "Stummschaltung aktivieren oder deaktivieren",
"pushToTalk": "Push-to-Talk (Sprechtaste)",
"raiseHand": "Hand erheben",
"showSpeakerStats": "Sprecherstatistik anzeigen",
"raiseHand": "Hand heben",
"showSpeakerStats": "Sprechstatistik anzeigen",
"toggleChat": "Chat öffnen oder schließen",
"toggleFilmstrip": "Video-Miniaturansichten ein- oder ausblenden",
"toggleScreensharing": "Zwischen Kamera und Bildschirmfreigabe wechseln",
@@ -400,8 +401,8 @@
"videoQuality": "Anrufqualität verwalten"
},
"liveStreaming": {
"limitNotificationDescriptionWeb": "Wegen hoher Nachfrage ist Ihr Stream auf {{limit}} min. begrenzt. Für unlimitiertes Streaming nutzen Sie bitte <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"limitNotificationDescriptionNative": "Ihr Stream ist begrenzt auf {{limit}} min. Für unlimitiertes Streaming, nutzen Sie bitte {{app}}.",
"limitNotificationDescriptionWeb": "Wegen hoher Nachfrage ist Ihr Stream auf {{limit}} Min. begrenzt. Für unlimitiertes Streaming nutzen Sie bitte <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"limitNotificationDescriptionNative": "Ihr Stream ist begrenzt auf {{limit}} Min. Für unlimitiertes Streaming, nutzen Sie bitte {{app}}.",
"busy": "Es werden Ressourcen zum Streamen bereitgestellt. Bitte in ein paar Minuten erneut versuchen.",
"busyTitle": "Alle Streaming-Instanzen sind in Gebrauch",
"changeSignIn": "Konten wechseln.",
@@ -449,14 +450,14 @@
"me": "Ich",
"messages": {
"engaged": "Lokale Aufzeichnung ist aktiviert.",
"finished": "Aufzeichnung der Sitzung {{token}} ist beendet. Senden Sie die aufgezeichnete Datei an den Moderator.",
"finishedModerator": "Aufzeichnung der Sitzung {{token}} ist beendet. Die Aufzeichnung des lokalen Verlaufs wurde gespeichert. Bitten Sie die anderen Teilnehmer, ihre Aufzeichnungen zu übermitteln.",
"notModerator": "Sie sind nicht der Moderator. Sie können die lokale Aufzeichnung nicht starten oder stoppen."
"finished": "Aufzeichnung der Sitzung {{token}} ist beendet. Senden Sie die aufgezeichnete Datei an die Moderation.",
"finishedModerator": "Aufzeichnung der Sitzung {{token}} ist beendet. Die Aufzeichnung des lokalen Verlaufs wurde gespeichert. Bitten Sie die anderen Personen, ihre Aufzeichnungen zu übermitteln.",
"notModerator": "Sie moderieren nicht. Sie können die lokale Aufzeichnung nicht starten oder stoppen."
},
"moderator": "Moderator",
"moderator": "Moderation",
"no": "Nein",
"participant": "Teilnehmer",
"participantStats": "Teilnehmerstatistik",
"participant": "Person",
"participantStats": "Personenstatistik",
"sessionToken": "Sitzungs-Token",
"start": "Aufnahme starten",
"stop": "Aufnahme stoppen",
@@ -470,21 +471,21 @@
"connectedThreePlusMembers": "{{name}} und {{count}} andere Personen nehmen am Meeting teil",
"connectedTwoMembers": "{{first}} und {{second}} nehmen am Meeting teil",
"disconnected": "getrennt",
"focus": "Konferenz-Organisator",
"focusFail": "{{component}} ist im Moment nicht verfügbar - wiederholen in {{ms}} Sekunden",
"grantedTo": "Moderatorenrechte an {{to}} vergeben!",
"focus": "Konferenzleitung",
"focusFail": "{{component}} ist im Moment nicht verfügbar wiederholen in {{ms}} Sekunden",
"grantedTo": "Moderationsrechte an {{to}} vergeben!",
"invitedOneMember": "{{name}} wurde eingeladen",
"invitedThreePlusMembers": "{{name}} und {{count}} andere wurden eingeladen",
"invitedTwoMembers": "{{first}} und {{second}} wurden eingeladen",
"kickParticipant": "{{kicked}} wurde von {{kicker}} ausgewiesen",
"me": "Ich",
"moderator": "Moderatorenrechte vergeben!",
"moderator": "Moderationsrechte vergeben!",
"muted": "Der Konferenz wurde stumm beigetreten.",
"mutedTitle": "Stummschaltung aktiv!",
"mutedRemotelyTitle": "Sie wurden von {{participantDisplayName}} stummgeschaltet!",
"mutedRemotelyDescription": "Sie können jederzeit die Stummschaltung aufheben, wenn Sie bereit sind zu sprechen. Wenn Sie fertig sind, können Sie sich wieder stummschalten, um Geräusche vom Meeting fernzuhalten.",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) von einem anderen Teilnehmer entfernt",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einem anderen Teilnehmer gesetzt",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person entfernt",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person gesetzt",
"raisedHand": "{{name}} möchte sprechen.",
"somebody": "Jemand",
"startSilentTitle": "Sie sind ohne Audioausgabe beigetreten!",
@@ -500,7 +501,7 @@
"oldElectronClientDescription2": "aktuelle Version",
"oldElectronClientDescription3": "!"
},
"passwordSetRemotely": "von einem anderen Teilnehmer gesetzt",
"passwordSetRemotely": "von einer anderen Person gesetzt",
"passwordDigitsOnly": "Bis zu {{number}} Ziffern",
"poweredby": "Betrieben von",
"prejoin": {
@@ -525,7 +526,7 @@
"goodQuality": "Großartig! Ihre Bild- und Tonqualität sollte super sein.",
"noMediaConnectivity": "Es konnte für diesen Test keine Medienverbindung hergestellt werden. Das wird gewöhnlich durch eine Firewall oder ein NAT ausgelöst.",
"noVideo": "Ihr Bild wird wahrscheinlich eine schlechte Qualität haben.",
"undetectable": "Wenn Sie mit Ihrem Browser weiterhin Probleme in Konferenzen haben, sollten Sie die Verbindung und Funktion Ihrer Lautsprecher, Ihres Mikrofons und Ihrer Kamera überprüfen. Stellen Sie außerdem sicher, dass Ihr Browser die erforderlichen Rechte hat, auf das Mikrofon und die Kamera zuzugreifen, und dass Sie die neuste Browserversion installiert haben. Sollten Sie immer noch Probleme haben, kontaktieren Sie bitte den Entwickler der Webanwendung.",
"undetectable": "Wenn Sie mit Ihrem Browser weiterhin Probleme in Konferenzen haben, sollten Sie die Verbindung und Funktion Ihrer Lautsprecher, Ihres Mikrofons und Ihrer Kamera überprüfen. Stellen Sie außerdem sicher, dass Ihr Browser die erforderlichen Rechte hat, auf das Mikrofon und die Kamera zuzugreifen, und dass Sie die neuste Browserversion installiert haben. Sollten Sie immer noch Probleme haben, kontaktieren Sie bitte den Support der Webanwendung.",
"veryPoorConnection": "Ihre Konferenzqualität wird wahrscheinlich sehr schlecht sein.",
"videoFreezing": "Ihr Bild wird wahrscheinlich einfrieren, schwarz werden und eine geringe Auflösung haben.",
"videoHighQuality": "Ihr Bild sollte sehr gut aussehen.",
@@ -582,8 +583,8 @@
},
"raisedHand": "Ich möchte sprechen",
"recording": {
"limitNotificationDescriptionWeb": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} min. begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"limitNotificationDescriptionNative": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} min begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <3>{{app}}</3>.",
"limitNotificationDescriptionWeb": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} Min. begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"limitNotificationDescriptionNative": "Wegen hoher Nachfrage ist Ihre Aufnahme auf {{limit}} Min. begrenzt. Für unlimitierte Aufnahmen nutzen Sie bitte <3>{{app}}</3>.",
"authDropboxText": "In Dropbox hochladen",
"availableSpace": "Verfügbarer Speicherplatz: {{spaceLeft}} MB (ca. {{duration}} Minuten Aufzeichnung)",
"beta": "BETA",
@@ -594,7 +595,7 @@
"expandedOn": "Das Meeting wird momentan aufgezeichnet.",
"expandedPending": "Aufzeichnung wird gestartet…",
"failedToStart": "Die Aufnahme konnte nicht gestartet werden",
"fileSharingdescription": "Aufzeichnung mit Konferenzteilnehmer teilen",
"fileSharingdescription": "Aufzeichnung mit den Personen der Konferenz teilen",
"live": "LIVE",
"loggedIn": "Als {{userName}} angemeldet",
"off": "Aufnahme gestoppt",
@@ -614,9 +615,9 @@
"pullToRefresh": "Ziehen, um zu aktualisieren"
},
"security": {
"about": "Sie können Ihre Konferenz mit einem Passwort sichern. Teilnehmer müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
"aboutReadOnly": "Moderatoren können die Konferenz mit einem Passwort sichern. Teilnehmer müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
"insecureRoomNameWarning": "Der Raumname ist unsicher. Unerwünschte Teilnehmer könnten Ihrer Konferenz beitreten",
"about": "Sie können Ihre Konferenz mit einem Passwort sichern. Personen müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
"aboutReadOnly": "Mit Moderationsrechten kann die Konferenz mit einem Passwort gesichert werden. Personen müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
"insecureRoomNameWarning": "Der Raumname ist unsicher. Unerwünschte Personen könnten Ihrer Konferenz beitreten",
"securityOptions": "Sicherheitsoptionen"
},
"settings": {
@@ -628,11 +629,11 @@
"title": "Kalender"
},
"devices": "Geräte",
"followMe": "Follow-me für alle Teilnehmer",
"followMe": "Follow-me für alle Personen",
"language": "Sprache",
"loggedIn": "Als {{name}} angemeldet",
"microphones": "Mikrofon",
"moderator": "Moderator",
"moderator": "Moderation",
"more": "Mehr",
"name": "Name",
"noDevice": "Kein",
@@ -640,8 +641,8 @@
"selectCamera": "Kamera",
"selectMic": "Mikrofon",
"speakers": "Lautsprecher",
"startAudioMuted": "Alle Teilnehmer treten stumm geschaltet bei",
"startVideoMuted": "Alle Teilnehmer treten ohne Video bei",
"startAudioMuted": "Alle Personen treten stumm geschaltet bei",
"startVideoMuted": "Alle Personen treten ohne Video bei",
"title": "Einstellungen"
},
"settingsView": {
@@ -670,14 +671,14 @@
"dialInfoText": "\n\n=====\n\nWollen Sie sich nur auf Ihrem Telefon einwählen?\n\n{{defaultDialInNumber}}Klicken Sie auf diesen Link, um die eingewählten Telefonnummern für dieses Meeting zu sehen\n{{dialInfoPageUrl}}",
"mainText": "Klicken Sie auf den folgenden Link, um dem Meeting beizutreten:\n{{roomUrl}}"
},
"speaker": "Sprecher",
"speaker": "Sprecher/-in",
"speakerStats": {
"hours": "{{count}}h",
"minutes": "{{count}}m",
"hours": "{{count}} Std.",
"minutes": "{{count}} Min.",
"name": "Name",
"seconds": "{{count}}s",
"speakerStats": "Sprecherstatistik",
"speakerTime": "Sprecherzeit"
"seconds": "{{count}} Sek.",
"speakerStats": "Sprechstatistik",
"speakerTime": "Sprechzeit"
},
"startupoverlay": {
"policyText": " ",
@@ -700,11 +701,11 @@
"embedMeeting": "Konferenz einbetten",
"feedback": "Feedback hinterlassen",
"fullScreen": "Vollbildmodus ein-/ausschalten",
"grantModerator": "Zum Moderator machen",
"grantModerator": "Moderationsrechte vergeben",
"hangup": "Anruf beenden",
"help": "Hilfe",
"invite": "Teilnehmer einladen",
"kick": "Teilnehmer entfernen",
"invite": "Person einladen",
"kick": "Person entfernen",
"lobbyButton": "Lobbymodus ein-/ausschalten",
"localRecording": "Lokale Aufzeichnungssteuerelemente ein-/ausschalten",
"lockRoom": "Konferenzpasswort ein-/ausschalten",
@@ -718,7 +719,7 @@
"profile": "Profil bearbeiten",
"raiseHand": "„Melden“ ein-/ausschalten",
"recording": "Aufzeichnung ein-/ausschalten",
"remoteMute": "Teilnehmer stummschalten",
"remoteMute": "Personen stummschalten",
"security": "Sicherheitsoptionen",
"Settings": "Einstellungen ein-/ausschalten",
"sharedvideo": "YouTube-Videofreigabe ein-/ausschalten",
@@ -726,14 +727,14 @@
"shareYourScreen": "Bildschirmfreigabe ein-/ausschalten",
"shortcuts": "Tastenkombinationen ein-/ausblenden",
"show": "Im Vordergrund anzeigen",
"speakerStats": "Sprecherstatistik ein-/ausblenden",
"speakerStats": "Sprechstatistik ein-/ausblenden",
"tileView": "Kachelansicht ein-/ausschalten",
"toggleCamera": "Kamera wechseln",
"toggleFilmstrip": "Miniaturansichten ein-/ausschalten",
"videomute": "„Video stummschalten“ ein-/ausschalten",
"videoblur": "Video-Unschärfe ein-/ausschalten"
},
"addPeople": "Teilnehmer zur Konferenz hinzufügen",
"addPeople": "Personen zur Konferenz hinzufügen",
"audioOnlyOff": "Modus „Nur Audio“ deaktivieren",
"audioOnlyOn": "Modus „Nur Audio“ aktivieren",
"audioRoute": "Audiogerät auswählen",
@@ -753,7 +754,7 @@
"feedback": "Feedback hinterlassen",
"hangup": "Verlassen",
"help": "Hilfe",
"invite": "Teilnehmer einladen",
"invite": "Personen einladen",
"lobbyButtonDisable": "Lobbymodus deaktivieren",
"lobbyButtonEnable": "Lobbymodus aktivieren",
"login": "Anmelden",
@@ -781,7 +782,7 @@
"sharedvideo": "YouTube-Video teilen",
"shareRoom": "Person einladen",
"shortcuts": "Tastenkürzel anzeigen",
"speakerStats": "Sprecherstatistik",
"speakerStats": "Sprechstatistik",
"startScreenSharing": "Bildschirmfreigabe starten",
"startSubtitles": "Untertitel einschalten",
"stopScreenSharing": "Bildschirmfreigabe stoppen",
@@ -850,14 +851,14 @@
"domute": "Stummschalten",
"domuteOthers": "Alle anderen stummschalten",
"flip": "Spiegeln",
"grantModerator": "Zum Moderator machen",
"grantModerator": "Moderationsrechte vergeben",
"kick": "Hinauswerfen",
"moderator": "Moderator",
"mute": "Teilnehmer ist stumm geschaltet",
"moderator": "Moderation",
"mute": "Person ist stumm geschaltet",
"muted": "Stummgeschaltet",
"remoteControl": "Fernsteuerung",
"show": "Im Vordergrund anzeigen",
"videomute": "Teilnehmer hat die Kamera angehalten"
"videomute": "Person hat die Kamera angehalten"
},
"welcomepage": {
"accessibilityLabel": {
@@ -876,9 +877,11 @@
"getHelp": "Hilfe",
"go": "Los",
"goSmall": "Los",
"headerTitle": "Jitsi Meet",
"headerSubtitle": "Sichere und hochqualitative Meetings",
"info": "Einwahlinformationen",
"join": "ERSTELLEN / BEITRETEN",
"moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, unter der Sie der einzige Moderator sind.",
"moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, die nur Sie moderieren.",
"privacy": "Datenschutz",
"recentList": "Verlauf",
"recentListDelete": "Eintrag löschen",
@@ -886,35 +889,36 @@
"reducedUIText": "Willkommen bei {{app}}!",
"roomNameAllowedChars": "Der Konferenzname sollte keines der folgenden Zeichen enthalten: ?, &, :, ', \", %, #.",
"roomname": "Konferenzname eingeben",
"roomnameHint": "Name oder URL der Konferenz, der Sie beitreten möchten. Sie können einen Namen erfinden, er muss nur den anderen Teilnehmern übermittelt werden, damit diese der gleichen Konferenz beitreten.",
"roomnameHint": "Name oder URL der Konferenz, der Sie beitreten möchten. Sie können einen Namen erfinden, er muss nur den anderen Personen übermittelt werden, damit diese der gleichen Konferenz beitreten.",
"sendFeedback": "Feedback senden",
"startMeeting": "Meeting starten",
"terms": "AGB",
"title": "Sichere, voll funktionale und komplett kostenlose Videokonferenzen"
},
"lonelyMeetingExperience": {
"button": "Andere einladen",
"youAreAlone": "Sie sind alleine in dieser Konferenz"
"youAreAlone": "Nur Sie sind in dieser Konferenz"
},
"helpView": {
"header": "Hilfecenter"
},
"lobby": {
"knockingParticipantList": "Liste anklopfender Teilnehmer",
"knockingParticipantList": "Liste anklopfender Personen",
"allow": "Annehmen",
"backToKnockModeButton": "Kein Passwort, stattdessen Beitritt anfragen",
"dialogTitle": "Lobbymodus",
"disableDialogContent": "Lobbymodus derzeit aktiviert. Diese Funktion stellt sicher, dass unerwünschte Personen Ihrer Konferenz nicht beitreten können. Funktion deaktivieren?",
"disableDialogContent": "Der Lobbymodus ist derzeit aktiviert. Diese Funktion stellt sicher, dass unerwünschte Personen Ihrer Konferenz nicht beitreten können. Funktion deaktivieren?",
"disableDialogSubmit": "Deaktivieren",
"emailField": "E-Mail-Adresse eingeben",
"enableDialogPasswordField": "Passwort setzen (optional)",
"enableDialogSubmit": "Aktivieren",
"enableDialogText": "Mit dem Lobbymodus schützen Sie Ihre Konferenz, da nur von einem Moderator angenommene Teilnehmer beitreten können.",
"enableDialogText": "Mit dem Lobbymodus schützen Sie Ihre Konferenz, damit der Beitritt von Ihnen moderiert werden kann.",
"enterPasswordButton": "Konferenzpasswort eingeben",
"enterPasswordTitle": "Passwort zum Beitreten benutzen",
"invalidPassword": "Ungültiges Passwort",
"joiningMessage": "Sie treten der Konferenz bei, sobald jemand Ihre Anfrage annimmt.",
"joinWithPasswordMessage": "Beitrittsversuch mit Passwort, bitte warten …",
"joinRejectedMessage": "Ihr Beitrittsanfrage wurde von einem Moderator abgelehnt.",
"joinRejectedMessage": "Ihre Beitrittsanfrage wurde von der Moderation abgelehnt.",
"joinTitle": "Konferenz beitreten",
"joiningTitle": "Beitritt anfragen …",
"joiningWithPasswordTitle": "Mit Passwort beitreten …",

View File

@@ -200,7 +200,7 @@
"dismiss": "رد کردن",
"displayNameRequired": "نام خود را وارد نمایید",
"done": "تایید",
"e2eeDescription": "<p>رمزگذاری دو طرفه در حال حاضر به صورت <strong>آزمایشی</strong> استفاده می‌شود؛ برای اطلاعات بیشتر می‌توانید <a href='https://blog.bebbin.ir/e2ee/' target='_blank'>این مقاله</a> را ببینید.</p><br/><p>در نظر داشته باشید با فعال کردن رمز گذاری دو طرفه قابلیت‌های سمت سرو غیرفعال خواهند شد، قابلیت‌هایی از قبیل:ضبط، پخش زنده و مشارکت تلفنی در جلسات؛ همچنین افرادی می‌توانند به این جلسه بپیوندند که مرورگر آن‌ها از قابلیت پخش درون مرورگری پشتیبانی کند،</p>",
"e2eeDescription": "<p>رمزگذاری دو طرفه در حال حاضر به صورت <strong>آزمایشی</strong> استفاده می‌شود؛ برای اطلاعات بیشتر می‌توانید <a href='https://jitsi.org/blog/e2ee/' target='_blank'>این مقاله</a> را ببینید.</p><br/><p>در نظر داشته باشید با فعال کردن رمز گذاری دو طرفه قابلیت‌های سمت سرو غیرفعال خواهند شد، قابلیت‌هایی از قبیل:ضبط، پخش زنده و مشارکت تلفنی در جلسات؛ همچنین افرادی می‌توانند به این جلسه بپیوندند که مرورگر آن‌ها از قابلیت پخش درون مرورگری پشتیبانی کند،</p>",
"e2eeLabel": "کلید E2EE",
"e2eeWarning": "<br /><p><strong>هشدار:</strong> همه‌ی مشارکت کنندگان رمزنگاری دو طرفه را پشتیبانی نمی‌کنند؛ اگر این قابلیت را فعال کنید آن‌ها صدای جلسه را نمی‌شنوند و تصویر را مشاهده نخواهند کرد</p>",
"enterDisplayName": "لطفا نام نمایشی خود را وارد نمایید",

View File

@@ -602,6 +602,7 @@
"rec": "REC",
"serviceDescription": "La tua registrazione verrà salvata dal servizio di registrazione che hai scelto",
"serviceName": "Servizio di registrazione",
"serviceDescriptionCloud": "Registrazione in rete",
"signIn": "Entra",
"signOut": "Esci",
"unavailable": "Ops! Il {{serviceName}} non è al momento disponibile. Stiamo lavorando per risolvere il problema. Riprova più tardi.",
@@ -628,6 +629,7 @@
"followMe": "Tutti mi seguono",
"language": "Lingua",
"loggedIn": "Connesso come {{name}}",
"microphones": "Microfoni",
"moderator": "Moderatore",
"more": "Altro",
"name": "Nome",
@@ -635,6 +637,7 @@
"selectAudioOutput": "Uscita audio",
"selectCamera": "Videocamera",
"selectMic": "Microfono",
"speakers": "Altoparlanti",
"startAudioMuted": "Tutti cominciano con il microfono disattivato",
"startVideoMuted": "Tutti cominciano con il video disattivato",
"title": "Impostazioni"
@@ -841,6 +844,7 @@
"standardDefinition": "Definizione standard"
},
"videothumbnail": {
"connectionInfo": "Info connessione",
"domute": "Disattiva audio",
"domuteOthers": "Zittisci tutti gli altri",
"flip": "Rifletti",

View File

@@ -489,7 +489,7 @@
"suboptimalBrowserWarning": "あなたのミーティングの体験が、ここではあまり良好ではないのではと心配しています。これを改善する方法を模索していますが、それまでは<a href='{{recommendedBrowserPageLink}}' target='_blank'> フルサポートしているブラウザー</a>のいずれかを利用してください。",
"suboptimalExperienceTitle": "ブラウザーの警告",
"unmute": "ミュート解除",
"newDeviceCameraTitle": "新しいカメラを出しました",
"newDeviceCameraTitle": "新しいカメラを出しました",
"newDeviceAudioTitle": "新しいオーディオデバイスを検出しました",
"newDeviceAction": "使用する",
"OldElectronAPPTitle": "セキュリティ上の脆弱性があります!",

View File

@@ -233,9 +233,9 @@
"passwordRequired": "비밀번호 필수",
"popupError": "브라우저가이 사이트의 팝업 창을 차단하고 있습니다. 브라우저의 보안 설정에서 팝업을 활성화하고 다시 시도하십시오.",
"popupErrorTitle": "팝업 차단됨",
"recording": "레코딩",
"recordingDisabledForGuestTooltip": "게스트는 녹음을 시작할 수 없습니다.",
"recordingDisabledTooltip": "녹화 비활성화 되었습니다.",
"recording": "녹화",
"recordingDisabledForGuestTooltip": "게스트는 녹화를 시작할 수 없습니다.",
"recordingDisabledTooltip": "녹화 비활성화 되었습니다.",
"rejoinNow": "지금 재가입",
"remoteControlAllowedMessage": "{{user}}이(가) 원격 제어 요청을 수락했습니다",
"remoteControlDeniedMessage": "{{user}}이(가) 원격 제어 요청을 거부했습니다",
@@ -270,11 +270,11 @@
"shareYourScreenDisabled": "화면 공유가 비활성화 되었습니다.",
"shareYourScreenDisabledForGuest": "게스트는 화면을 공유 할 수 없습니다.",
"startLiveStreaming": "라이브 스트리밍 시작",
"startRecording": "레코딩 시작",
"startRecording": "녹화 시작",
"startRemoteControlErrorMessage": "원격 제어 세션을 시작하는 동안 오류가 발생했습니다",
"stopLiveStreaming": "라이브 스트리밍 종료",
"stopRecording": "레코딩 종료",
"stopRecordingWarning": "레코딩을 중단하고 싶으십니까?",
"stopRecording": "녹화 종료",
"stopRecordingWarning": "녹화를 중단하고 싶으십니까?",
"stopStreamingWarning": "라이브 스트리밍을 중단하고 싶으십니까?",
"streamKey": "라이브 스트리밍 키",
"Submit": "제출",
@@ -362,7 +362,7 @@
"focusRemote": "다른 발신자의 동영상에 포커스",
"fullScreen": "전체화면 표시 또는 종료",
"keyboardShortcuts": "키보드 단축키",
"localRecording": "로컬 녹 컨트롤 표시 또는 숨기기",
"localRecording": "로컬 녹 컨트롤 표시 또는 숨기기",
"mute": "마이크 음소거 또는 음소거 해제",
"pushToTalk": "대화 요청",
"raiseHand": "말하기 요청/해제",
@@ -429,8 +429,8 @@
"participant": "",
"participantStats": "",
"sessionToken": "",
"start": "레코딩 시작",
"stop": "레코딩 종료",
"start": "녹화 시작",
"stop": "녹화 종료",
"yes": "예"
},
"lockRoomPassword": "비밀번호",
@@ -494,26 +494,26 @@
"authDropboxText": "Dropbox에 업로드",
"availableSpace": "사용 가능한 공간 : {{spaceLeft}}MB (약 {{duration}}분 녹화)",
"beta": "베타",
"busy": "레코딩 자원을 확보하고 있습니다. 몇 분 후에 다시 시도하십시오.",
"busyTitle": "모든 레코더가 현재 사용중입니다",
"error": "레코딩이 실패했습니다. 다시 시도하십시오.",
"expandedOff": "레코딩이 중지됨",
"expandedOn": "회의가 현재 녹화중입니다.",
"busy": "녹화 자원을 확보하고 있습니다. 몇 분 후에 다시 시도하십시오.",
"busyTitle": "모든 레코더가 현재 사용 중입니다",
"error": "녹화가 실패했습니다. 다시 시도하십시오.",
"expandedOff": "녹화가 중지됨",
"expandedOn": "회의가 현재 녹화 중입니다.",
"expandedPending": "녹화가 시작됩니다 ...",
"failedToStart": "레코딩을 시작하지 못했습니다",
"fileSharingdescription": "회의 참가자와 녹 공유",
"failedToStart": "녹화를 시작하지 못했습니다",
"fileSharingdescription": "회의 참가자와 녹 공유",
"live": "라이브",
"loggedIn": "{{userName}}으로 로그인했습니다.",
"off": "레코딩이 중지됨",
"on": "레코딩",
"off": "녹화가 중지됨",
"on": "녹화",
"pending": "참석할 멤버를 기다리는 중입니다…",
"rec": "녹",
"serviceDescription": "녹음은 서비스에 의해 저장됩니다.",
"serviceName": "레코딩 서비스",
"rec": "녹",
"serviceDescription": "녹화는 서비스에 의해 저장됩니다.",
"serviceName": "녹화 서비스",
"signIn": "로그인",
"signOut": "로그아웃",
"unavailable": "죄송합니다. {{serviceName}}은 현재 사용할 수 없습니다. 저희는 문제를 해결하기 위해 노력하고 있습니다. 나중에 다시 시도 해주십시오.",
"unavailableTitle": "레코딩을 사용할 수 없습니다"
"unavailableTitle": "녹화를 사용할 수 없습니다"
},
"sectionList": {
"pullToRefresh": "당겨서 새로고침"
@@ -665,8 +665,8 @@
},
"transcribing": {
"ccButtonTooltip": "자막 시작/종료",
"error": "레코딩이 실패했습니다. 다시 시도하십시오.",
"expandedLabel": "현재 스크립트 작성중",
"error": "녹화가 실패했습니다. 다시 시도하십시오.",
"expandedLabel": "현재 스크립트 작성 중",
"failedToStart": "스크립트 작성을 시작하지 못했습니다.",
"labelToolTip": "회의가 기록되고 있습니다.",
"off": "스크립트 작성이 중지되었습니다.",

View File

@@ -203,6 +203,8 @@
"enterDisplayName": "Voer hier uw naam in",
"error": "Fout",
"gracefulShutdown": "Onze service is momenteel niet beschikbaar vanwege onderhoud. Probeer het later opnieuw.",
"grantModeratorDialog": "Weet u zeker dat u de moderatorrechten wilt verlenen aan deze deelnemer?",
"grantModeratorTitle": "Moderatorrechten verlenen",
"IamHost": "Ik ben de host",
"incorrectRoomLockPassword": "Onjuist wachtwoord",
"incorrectPassword": "Onjuiste gebruikersnaam of wachtwoord",
@@ -669,6 +671,7 @@
"e2ee": "Eind-tot-eind-versleuteling",
"feedback": "Feedback achterlaten",
"fullScreen": "Volledig scherm in- of uitschakelen",
"grantModerator": "Moderatorrechten verlenen",
"hangup": "Het gesprek verlaten",
"help": "Hulp",
"invite": "Personen uitnodigen",
@@ -696,7 +699,7 @@
"show": "Op podium weergeven",
"speakerStats": "Sprekerstatistieken in- of uitschakelen",
"tileView": "Tegelweergave in- of uitschakelen",
"toggleCamera": "Camera in- of uitschakelen",
"toggleCamera": "Camera wisselen",
"toggleFilmstrip": "Filmstrip in- of uitschakelen",
"videomute": "Video dempen in- of uitschakelen",
"videoblur": "Video vervagen in- of uitschakelen"
@@ -817,6 +820,7 @@
"domute": "Dempen",
"domuteOthers": "Alle anderen dempen",
"flip": "Omdraaien",
"grantModerator": "Moderatorrechten verlenen",
"kick": "Verwijderen",
"moderator": "Moderator",
"mute": "Deelnemer is gedempt",

View File

@@ -546,8 +546,8 @@
"errorValidation": "Validação do número falhou",
"iWantToDialIn": "Eu quere discar",
"joinAudioByPhone": "Participar com o áudio via ligação",
"joinMeeting": "Particar da reunião",
"joinWithoutAudio": "Particar sem áudio",
"joinMeeting": "Participar da reunião",
"joinWithoutAudio": "Participar sem áudio",
"initiated": "Chamada iniciada",
"linkCopied": "Link copiado para a área de transferência",
"lookGood": "Seu microfone está funcionando corretamente",

View File

@@ -908,6 +908,8 @@
"getHelp": "Справка",
"go": "ОК",
"goSmall": "ОК",
"headerTitle":"Сервер видеоконференцсвязи Jitsi Meet",
"headerSubtitle":"Защищенная высококачественная видеосвязь",
"info": "Инфо",
"join": "СОЗДАТЬ / ПРИСОЕДИНИТЬСЯ",
"moderatedMessage": "Или заранее <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">зарезервируйте URL-адрес встречи</a>, где вы будете единственным модератором.",
@@ -920,6 +922,7 @@
"roomname": "Укажите название комнаты",
"roomnameHint": "Укажите название комнаты или ее адрес. Можете сами создать название и передать его будущим участникам встречи, чтобы они использовали именно его.",
"sendFeedback": "Обратная связь",
"startMeeting": "Создать конференцию",
"terms": "Условия",
"title": "Защищенная, полнофункциональная и совершенно бесплатная система видеоконференций"
}

View File

@@ -60,7 +60,7 @@
"today": "Bugün"
},
"chat": {
"error": "Hata: mesajınız gönderilmedi. Gerekçe: {{error}}",
"error": "Hata: Mesajınız gönderilmedi. Neden: {{error}}",
"fieldPlaceHolder": "Mesajınızı buraya yazın",
"messagebox": "Bir mesaj yazın",
"messageTo": "{{recipient}} adlı kişiye özel mesaj",
@@ -730,7 +730,7 @@
"noisyAudioInputDesc": "Mikrofonunuz gürültü yapıyor gibi görünüyor, lütfen cihazı kapatmayı veya değiştirmeyi düşünün.",
"openChat": "Mesajlaşmayı aç",
"pip": "Resim içinde Resim moduna gir",
"privateMessage": "Özel mesajgönder",
"privateMessage": "Özel mesaj gönder",
"profile": "Profilinizi düzenleyin",
"raiseHand": "Elinizi kaldırın/indirin",
"raiseYourHand": "Elinizi kaldırın",
@@ -806,7 +806,7 @@
"domute": "Sustur",
"domuteOthers": "Diğer herkesi sustur",
"flip": "Döndür",
"kick": ıkarıldı",
"kick": ıkar",
"moderator": "Yönetici",
"mute": "Katılımcı sessiz",
"muted": "Sessiz",

View File

@@ -180,6 +180,7 @@
"cameraNotSendingData": "We are unable to access your camera. Please check if another application is using this device, select another device from the settings menu or try to reload the application.",
"cameraNotSendingDataTitle": "Unable to access camera",
"cameraPermissionDeniedError": "You have not granted permission to use your camera. You can still join the conference but others won't see you. Use the camera button in the address bar to fix this.",
"cameraTimeoutError": "Could not start video source. Timeout occured!",
"cameraUnknownError": "Cannot use camera for an unknown reason.",
"cameraUnsupportedResolutionError": "Your camera does not support required video resolution.",
"Cancel": "Cancel",
@@ -233,6 +234,7 @@
"micNotSendingData": "Go to your computer's settings to unmute your mic and adjust its level",
"micNotSendingDataTitle": "Your mic is muted by your system settings",
"micPermissionDeniedError": "You have not granted permission to use your microphone. You can still join the conference but others won't hear you. Use the camera button in the address bar to fix this.",
"micTimeoutError": "Could not start audio source. Timeout occured!",
"micUnknownError": "Cannot use microphone for an unknown reason.",
"muteEveryoneElseDialog": "Once muted, you won't be able to unmute them, but they can unmute themselves at any time.",
"muteEveryoneElseTitle": "Mute everyone except {{whom}}?",
@@ -280,6 +282,7 @@
"sendPrivateMessageTitle": "Send privately?",
"serviceUnavailable": "Service unavailable",
"sessTerminated": "Call terminated",
"sessionRestarted": "Call restarted by the bridge",
"Share": "Share",
"shareVideoLinkError": "Please provide a correct youtube link.",
"shareVideoTitle": "Share a video",
@@ -809,7 +812,7 @@
"androidGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"edgeGrantPermissions": "Select <b><i>Yes</i></b> when your browser asks for permissions.",
"electronGrantPermissions": "Please grant permissions to use your camera and microphone",
"electronGrantPermissions": "Trying to access your camera and microphone",
"firefoxGrantPermissions": "Select <b><i>Share Selected Device</i></b> when your browser asks for permissions.",
"iexplorerGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions.",
"nwjsGrantPermissions": "Please grant permissions to use your camera and microphone",

View File

@@ -432,6 +432,32 @@ function initCommands() {
case 'is-sharing-screen':
callback(Boolean(APP.conference.isSharingScreen));
break;
case 'get-content-sharing-participants': {
const tracks = getState()['features/base/tracks'];
const sharingParticipantIds = tracks.filter(tr => tr.videoType === 'desktop').map(t => t.participantId);
callback({
sharingParticipantIds
});
break;
}
case 'get-livestream-url': {
const state = APP.store.getState();
const conference = getCurrentConference(state);
let livestreamUrl;
if (conference) {
const activeSession = getActiveSession(state, JitsiRecordingConstants.mode.STREAM);
livestreamUrl = activeSession?.liveStreamViewURL;
} else {
logger.error('Conference is not defined');
}
callback({
livestreamUrl
});
break;
}
default:
return false;
}
@@ -547,6 +573,21 @@ class API {
}
}
/**
* Notify external application (if API is enabled) that the chat state has been updated.
*
* @param {number} unreadCount - The unread messages counter.
* @param {boolean} isOpen - True if the chat panel is open.
* @returns {void}
*/
notifyChatUpdated(unreadCount: number, isOpen: boolean) {
this._sendEvent({
name: 'chat-updated',
unreadCount,
isOpen
});
}
/**
* Notify external application (if API is enabled) that message was sent.
*
@@ -583,8 +624,8 @@ class API {
* @returns {void}
*/
notifyReceivedChatMessage(
{ body, id, nick, ts }: {
body: *, id: string, nick: string, ts: *
{ body, id, nick, privateMessage, ts }: {
body: *, id: string, nick: string, privateMessage: boolean, ts: *
} = {}) {
if (APP.conference.isLocalId(id)) {
return;
@@ -595,6 +636,7 @@ class API {
from: id,
message: body,
nick,
privateMessage,
stamp: ts
});
}
@@ -675,6 +717,19 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that the list of sharing participants changed.
*
* @param {Object} data - The event data.
* @returns {void}
*/
notifySharingParticipantsChanged(data: Object) {
this._sendEvent({
name: 'content-sharing-participants-changed',
data
});
}
/**
* Notify external application (if API is enabled) that the device list has
* changed.

View File

@@ -64,6 +64,8 @@ const events = {
'audio-availability-changed': 'audioAvailabilityChanged',
'audio-mute-status-changed': 'audioMuteStatusChanged',
'camera-error': 'cameraError',
'chat-updated': 'chatUpdated',
'content-sharing-participants-changed': 'contentSharingParticipantsChanged',
'device-list-changed': 'deviceListChanged',
'display-name-change': 'displayNameChange',
'email-change': 'emailChange',
@@ -317,7 +319,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
const frameName = `jitsiConferenceFrame${id}`;
this._frame = document.createElement('iframe');
this._frame.allow = 'camera; microphone; display-capture';
this._frame.allow = 'camera; microphone; display-capture; autoplay;';
this._frame.src = this._url;
this._frame.name = frameName;
this._frame.id = frameName;
@@ -571,6 +573,12 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* logLevel: the message log level
* arguments: an array of strings that compose the actual log message
* }}
* {@code chatUpdated} - receives event notifications about chat state being
* updated. The listener will receive object with the following structure:
* {{
* 'unreadCount': unreadCounter, // the unread message(s) counter,
* 'isOpen': isOpen, // whether the chat panel is open or not
* }}
* {@code incomingMessage} - receives event notifications about incoming
* messages. The listener will receive object with the following structure:
* {{
@@ -725,6 +733,17 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
return getAvailableDevices(this._transport);
}
/**
* Gets a list of the currently sharing participant id's.
*
* @returns {Promise} - Resolves with the list of participant id's currently sharing.
*/
getContentSharingParticipants() {
return this._transport.sendRequest({
name: 'get-content-sharing-participants'
});
}
/**
* Returns Promise that resolves with current selected devices.
*
@@ -734,6 +753,18 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
return getCurrentDevices(this._transport);
}
/**
* Returns the current livestream url.
*
* @returns {Promise} - Resolves with the current livestream URL if exists, with
* undefined if not and rejects on failure.
*/
getLivestreamUrl() {
return this._transport.sendRequest({
name: 'get-livestream-url'
});
}
/**
* Returns the conference participants information.
*

View File

@@ -7,7 +7,6 @@ import EventEmitter from 'events';
import Logger from 'jitsi-meet-logger';
import { isMobileBrowser } from '../../react/features/base/environment/utils';
import { getLocalParticipant } from '../../react/features/base/participants';
import { toggleChat } from '../../react/features/chat';
import { setDocumentUrl } from '../../react/features/etherpad';
import { setFilmstripVisible } from '../../react/features/filmstrip';
@@ -91,29 +90,11 @@ UI.notifyReservationError = function(code, msg) {
});
};
/**
* Change nickname for the user.
* @param {string} id user id
* @param {string} displayName new nickname
*/
UI.changeDisplayName = function(id, displayName) {
VideoLayout.onDisplayNameChanged(id, displayName);
};
/**
* Initialize conference UI.
*/
UI.initConference = function() {
const { getState } = APP.store;
const { id, name } = getLocalParticipant(getState);
UI.showToolbar();
const displayName = config.displayJids ? id : name;
if (displayName) {
UI.changeDisplayName('localVideoContainer', displayName);
}
};
/**
@@ -238,19 +219,12 @@ UI.getSharedDocumentManager = () => etherpadManager;
* @param {JitsiParticipant} user
*/
UI.addUser = function(user) {
const id = user.getId();
const displayName = user.getDisplayName();
const status = user.getStatus();
if (status) {
// FIXME: move updateUserStatus in participantPresenceChanged action
UI.updateUserStatus(user, status);
}
// set initial display name
if (displayName) {
UI.changeDisplayName(id, displayName);
}
};
/**
@@ -442,14 +416,6 @@ UI.handleLastNEndpoints = function(leavingIds, enteringIds) {
*/
UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
/**
* Hide connection quality statistics from UI.
*/
UI.hideStats = function() {
VideoLayout.hideStats();
};
UI.notifyTokenAuthFailed = function() {
messageHandler.showError({
descriptionKey: 'dialog.tokenAuthFailed',

View File

@@ -1,10 +1,15 @@
/* global $ */
/* global $, APP */
import Logger from 'jitsi-meet-logger';
/* eslint-disable no-unused-vars */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { i18next } from '../../../react/features/base/i18n';
import { Thumbnail } from '../../../react/features/filmstrip';
import SmallVideo from '../videolayout/SmallVideo';
const logger = Logger.getLogger(__filename);
/* eslint-enable no-unused-vars */
/**
*
@@ -13,28 +18,21 @@ export default class SharedVideoThumb extends SmallVideo {
/**
*
* @param {*} participant
* @param {*} videoType
* @param {*} VideoLayout
*/
constructor(participant, videoType, VideoLayout) {
super(VideoLayout);
constructor(participant) {
super();
this.id = participant.id;
this.isLocal = false;
this.url = participant.id;
this.videoSpanId = 'sharedVideoContainer';
this.container = this.createContainer(this.videoSpanId);
this.$container = $(this.container);
this.renderThumbnail();
this._setThumbnailSize();
this.bindHoverHandler();
this.updateDisplayName();
this.container.onclick = this._onContainerClick;
}
/**
*
*/
initializeAvatar() {} // eslint-disable-line no-empty-function
/**
*
* @param {*} spanId
@@ -45,18 +43,6 @@ export default class SharedVideoThumb extends SmallVideo {
container.id = spanId;
container.className = 'videocontainer';
// add the avatar
const avatar = document.createElement('img');
avatar.className = 'sharedVideoAvatar';
avatar.src = `https://img.youtube.com/vi/${this.url}/0.jpg`;
container.appendChild(avatar);
const displayNameContainer = document.createElement('div');
displayNameContainer.className = 'displayNameContainer';
container.appendChild(displayNameContainer);
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
@@ -68,21 +54,14 @@ export default class SharedVideoThumb extends SmallVideo {
}
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
* Renders the thumbnail.
*/
updateDisplayName() {
if (!this.container) {
logger.warn(`Unable to set displayName - ${this.videoSpanId
} does not exist`);
return;
}
this._renderDisplayName({
elementID: `${this.videoSpanId}_name`,
participantID: this.id
});
renderThumbnail(isHovered = false) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
</I18nextProvider>
</Provider>, this.container);
}
}

View File

@@ -37,7 +37,6 @@ const Filmstrip = {
*/
resizeThumbnailsForTileView(width, height, forceUpdate = false) {
const thumbs = this._getThumbs(!forceUpdate);
const avatarSize = height / 2;
if (thumbs.localThumb) {
thumbs.localThumb.css({
@@ -58,11 +57,6 @@ const Filmstrip = {
width: `${width}px`
});
}
$('.avatar-container').css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
},
/**
@@ -77,7 +71,6 @@ const Filmstrip = {
if (thumbs.localThumb) {
const { height, width } = local;
const avatarSize = height / 2;
thumbs.localThumb.css({
height: `${height}px`,
@@ -85,15 +78,10 @@ const Filmstrip = {
'min-width': `${width}px`,
width: `${width}px`
});
$('#localVideoContainer > .avatar-container').css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
if (thumbs.remoteThumbs) {
const { height, width } = remote;
const avatarSize = height / 2;
thumbs.remoteThumbs.css({
height: `${height}px`,
@@ -101,10 +89,6 @@ const Filmstrip = {
'min-width': `${width}px`,
width: `${width}px`
});
$('#filmstripRemoteVideosContainer > span > .avatar-container').css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
},
@@ -126,10 +110,6 @@ const Filmstrip = {
'min-width': '',
'min-height': ''
});
$('#localVideoContainer > .avatar-container').css({
height: '50%',
width: `${heightToWidthPercent / 2}%`
});
}
if (thumbs.remoteThumbs) {
@@ -142,10 +122,6 @@ const Filmstrip = {
'min-width': '',
'min-height': ''
});
$('#filmstripRemoteVideosContainer > span > .avatar-container').css({
height: '50%',
width: `${heightToWidthPercent / 2}%`
});
}
},

View File

@@ -372,7 +372,7 @@ export default class LargeVideoManager {
let widthToUse = this.preferredWidth || window.innerWidth;
const { isOpen } = APP.store.getState()['features/chat'];
if (isOpen) {
if (isOpen && window.innerWidth > 580) {
/**
* If chat state is open, we re-compute the container width
* by subtracting the default width of the chat.

View File

@@ -1,35 +1,34 @@
/* global $, config, interfaceConfig, APP */
/* global $, config, APP */
import Logger from 'jitsi-meet-logger';
/* eslint-disable no-unused-vars */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { i18next } from '../../../react/features/base/i18n';
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
import { VideoTrack } from '../../../react/features/base/media';
import { updateSettings } from '../../../react/features/base/settings';
import { getLocalVideoTrack } from '../../../react/features/base/tracks';
import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
import { shouldDisplayTileView } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
import UIEvents from '../../../service/UI/UIEvents';
import SmallVideo from './SmallVideo';
const logger = Logger.getLogger(__filename);
/**
*
*/
export default class LocalVideo extends SmallVideo {
/**
*
* @param {*} VideoLayout
* @param {*} emitter
* @param {*} streamEndedCallback
*/
constructor(VideoLayout, emitter, streamEndedCallback) {
super(VideoLayout);
constructor(emitter, streamEndedCallback) {
super();
this.videoSpanId = 'localVideoContainer';
this.streamEndedCallback = streamEndedCallback;
this.container = this.createContainer();
@@ -37,6 +36,7 @@ export default class LocalVideo extends SmallVideo {
this.isLocal = true;
this._setThumbnailSize();
this.updateDOMLocation();
this.renderThumbnail();
this.localVideoId = null;
this.bindHoverHandler();
@@ -44,7 +44,6 @@ export default class LocalVideo extends SmallVideo {
this._buildContextMenu();
}
this.emitter = emitter;
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left top' : 'top center';
Object.defineProperty(this, 'id', {
get() {
@@ -53,18 +52,6 @@ export default class LocalVideo extends SmallVideo {
});
this.initBrowserSpecificProperties();
// Set default display name.
this.updateDisplayName();
// Initialize the avatar display with an avatar url selected from the redux
// state. Redux stores the local user with a hardcoded participant id of
// 'local' if no id has been assigned yet.
this.initializeAvatar();
this.addAudioLevelIndicator();
this.updateIndicators();
this.updateStatusBar();
this.container.onclick = this._onContainerClick;
}
@@ -77,38 +64,19 @@ export default class LocalVideo extends SmallVideo {
containerSpan.classList.add('videocontainer');
containerSpan.id = this.videoSpanId;
containerSpan.innerHTML = `
<div class = 'videocontainer__background'></div>
<span id = 'localVideoWrapper'></span>
<div class = 'videocontainer__toolbar'></div>
<div class = 'videocontainer__toptoolbar'></div>
<div class = 'videocontainer__hoverOverlay'></div>
<div class = 'displayNameContainer'></div>
<div class = 'avatar-container'></div>`;
return containerSpan;
}
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
* Renders the thumbnail.
*/
updateDisplayName() {
if (!this.container) {
logger.warn(
`Unable to set displayName - ${this.videoSpanId
} does not exist`);
return;
}
this._renderDisplayName({
allowEditing: !config.disableProfile,
displayNameSuffix: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
elementID: 'localDisplayName',
participantID: this.id
});
renderThumbnail(isHovered = false) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
</I18nextProvider>
</Provider>, this.container);
}
/**
@@ -116,9 +84,7 @@ export default class LocalVideo extends SmallVideo {
* @param {*} stream
*/
changeVideo(stream) {
this.videoStream = stream;
this.localVideoId = `localVideo_${stream.getId()}`;
this._updateVideoElement();
// eslint-disable-next-line eqeqeq
const isVideo = stream.videoType != 'desktop';
@@ -128,17 +94,6 @@ export default class LocalVideo extends SmallVideo {
this.setFlipX(isVideo ? settings.localFlipX : false);
const endedHandler = () => {
const localVideoContainer
= document.getElementById('localVideoWrapper');
// Only remove if there is no video and not a transition state.
// Previous non-react logic created a new video element with each track
// removal whereas react reuses the video component so it could be the
// stream ended but a new one is being used.
if (localVideoContainer && this.videoStream.isEnded()) {
ReactDOM.unmountComponentAtNode(localVideoContainer);
}
this._notifyOfStreamEnded();
stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
};
@@ -254,35 +209,5 @@ export default class LocalVideo extends SmallVideo {
: document.getElementById('filmstripLocalVideoThumbnail');
appendTarget && appendTarget.appendChild(this.container);
this._updateVideoElement();
}
/**
* Renders the React Element for displaying video in {@code LocalVideo}.
*
*/
_updateVideoElement() {
const localVideoContainer = document.getElementById('localVideoWrapper');
const videoTrack
= getLocalVideoTrack(APP.store.getState()['features/base/tracks']);
ReactDOM.render(
<Provider store = { APP.store }>
<VideoTrack
id = 'localVideo_container'
videoTrack = { videoTrack } />
</Provider>,
localVideoContainer
);
// Ensure the video gets play() called on it. This may be necessary in the
// case where the local video container was moved and re-attached, in which
// case video does not autoplay. Also, set the playsinline attribute on the
// video element so that local video doesn't open in full screen by default
// in Safari browser on iOS.
const video = this.container.querySelector('video');
video && video.setAttribute('playsinline', 'true');
video && !config.testing?.noAutoPlayVideo && video.play();
}
}

View File

@@ -1,4 +1,4 @@
/* global $, APP, interfaceConfig */
/* global $, APP, config */
/* eslint-disable no-unused-vars */
import { AtlasKitThemeProvider } from '@atlaskit/theme';
@@ -15,6 +15,7 @@ import {
import { getParticipantById } from '../../../react/features/base/participants';
import { isTestModeEnabled } from '../../../react/features/base/testing';
import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
import { Thumbnail, isVideoPlayable } from '../../../react/features/filmstrip';
import { PresenceLabel } from '../../../react/features/presence-status';
import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
@@ -44,16 +45,6 @@ function createContainer(spanId) {
container.id = spanId;
container.className = 'videocontainer';
container.innerHTML = `
<div class = 'videocontainer__background'></div>
<div class = 'videocontainer__toptoolbar'></div>
<div class = 'videocontainer__toolbar'></div>
<div class = 'videocontainer__hoverOverlay'></div>
<div class = 'displayNameContainer'></div>
<div class = 'avatar-container'></div>
<div class ='presence-label-container'></div>
<span class = 'remotevideomenu'></span>`;
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
@@ -72,21 +63,16 @@ export default class RemoteVideo extends SmallVideo {
* Creates new instance of the <tt>RemoteVideo</tt>.
* @param user {JitsiParticipant} the user for whom remote video instance will
* be created.
* @param {VideoLayout} VideoLayout the video layout instance.
* @constructor
*/
constructor(user, VideoLayout) {
super(VideoLayout);
constructor(user) {
super();
this.user = user;
this.id = user.getId();
this.videoSpanId = `participant_${this.id}`;
this._audioStreamElement = null;
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center';
this.addRemoteVideoContainer();
this.updateIndicators();
this.updateDisplayName();
this.bindHoverHandler();
this.flipX = false;
this.isLocal = false;
@@ -100,11 +86,6 @@ export default class RemoteVideo extends SmallVideo {
*/
this._canPlayEventReceived = false;
// Bind event handlers so they are only bound once for every instance.
// TODO The event handlers should be turned into actions so changes can be
// handled through reducers and middleware.
this._setAudioVolume = this._setAudioVolume.bind(this);
this.container.onclick = this._onContainerClick;
}
@@ -114,76 +95,23 @@ export default class RemoteVideo extends SmallVideo {
addRemoteVideoContainer() {
this.container = createContainer(this.videoSpanId);
this.$container = $(this.container);
this.initializeAvatar();
this.renderThumbnail();
this._setThumbnailSize();
this.initBrowserSpecificProperties();
this.updateRemoteVideoMenu();
this.updateStatusBar();
this.addAudioLevelIndicator();
this.addPresenceLabel();
return this.container;
}
/**
* Generates the popup menu content.
*
* @returns {Element|*} the constructed element, containing popup menu items
* @private
* Renders the thumbnail.
*/
_generatePopupContent() {
const remoteVideoMenuContainer
= this.container.querySelector('.remotevideomenu');
if (!remoteVideoMenuContainer) {
return;
}
const initialVolumeValue = this._audioStreamElement && this._audioStreamElement.volume;
// hide volume when in silent mode
const onVolumeChange
= APP.store.getState()['features/base/config'].startSilent ? undefined : this._setAudioVolume;
renderThumbnail(isHovered = false) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<AtlasKitThemeProvider mode = 'dark'>
<RemoteVideoMenuTriggerButton
initialVolumeValue = { initialVolumeValue }
onMenuDisplay
= {this._onRemoteVideoMenuDisplay.bind(this)}
onVolumeChange = { onVolumeChange }
participantID = { this.id } />
</AtlasKitThemeProvider>
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
</I18nextProvider>
</Provider>,
remoteVideoMenuContainer);
}
/**
*
*/
_onRemoteVideoMenuDisplay() {
this.updateRemoteVideoMenu();
}
/**
* Change the remote participant's volume level.
*
* @param {int} newVal - The value to set the slider to.
*/
_setAudioVolume(newVal) {
if (this._audioStreamElement) {
this._audioStreamElement.volume = newVal;
}
}
/**
* Updates the remote video menu.
*/
updateRemoteVideoMenu() {
this._generatePopupContent();
</Provider>, this.container);
}
/**
@@ -199,7 +127,7 @@ export default class RemoteVideo extends SmallVideo {
}
const isVideo = stream.isVideoTrack();
const elementID = SmallVideo.getStreamElementID(stream);
const elementID = `remoteVideo_${stream.getId()}`;
const select = $(`#${elementID}`);
select.remove();
@@ -207,11 +135,7 @@ export default class RemoteVideo extends SmallVideo {
this._canPlayEventReceived = false;
}
logger.info(`${isVideo ? 'Video' : 'Audio'} removed ${this.id}`, select);
if (stream === this.videoStream) {
this.videoStream = null;
}
logger.info(`Video removed ${this.id}`, select);
this.updateView();
}
@@ -223,14 +147,7 @@ export default class RemoteVideo extends SmallVideo {
* @override
*/
isVideoPlayable() {
const participant = getParticipantById(APP.store.getState(), this.id);
const { connectionStatus } = participant || {};
return (
super.isVideoPlayable()
&& this._canPlayEventReceived
&& connectionStatus === JitsiParticipantConnectionStatus.ACTIVE
);
return isVideoPlayable(APP.store.getState(), this.id) && this._canPlayEventReceived;
}
/**
@@ -245,9 +162,8 @@ export default class RemoteVideo extends SmallVideo {
* Removes RemoteVideo from the page.
*/
remove() {
ReactDOM.unmountComponentAtNode(this.container);
super.remove();
this.removePresenceLabel();
this.removeRemoteVideoMenu();
}
/**
@@ -295,19 +211,16 @@ export default class RemoteVideo extends SmallVideo {
const isVideo = stream.isVideoTrack();
if (isVideo) {
this.videoStream = stream;
} else {
this.audioStream = stream;
}
if (!stream.getOriginalStream()) {
logger.debug('Remote video stream has no original stream');
return;
}
let streamElement = SmallVideo.createStreamElement(stream);
let streamElement = document.createElement('video');
streamElement.autoplay = !config.testing?.noAutoPlayVideo;
streamElement.id = `remoteVideo_${stream.getId()}`;
// Put new stream element always in front
streamElement = UIUtils.prependChild(this.container, streamElement);
@@ -315,14 +228,7 @@ export default class RemoteVideo extends SmallVideo {
this.waitForPlayback(streamElement, stream);
stream.attach(streamElement);
if (!isVideo) {
this._audioStreamElement = streamElement;
// If the remote video menu was created before the audio stream was
// attached we need to update the menu in order to show the volume
// slider.
this.updateRemoteVideoMenu();
} else if (isTestModeEnabled(APP.store.getState())) {
if (isVideo && isTestModeEnabled(APP.store.getState())) {
const cb = name => APP.store.dispatch(updateLastTrackVideoMediaEvent(stream, name));
@@ -331,72 +237,4 @@ export default class RemoteVideo extends SmallVideo {
});
}
}
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
*/
updateDisplayName() {
if (!this.container) {
logger.warn(`Unable to set displayName - ${this.videoSpanId} does not exist`);
return;
}
this._renderDisplayName({
elementID: `${this.videoSpanId}_name`,
participantID: this.id
});
}
/**
* Removes remote video menu element from video element identified by
* given <tt>videoElementId</tt>.
*
* @param videoElementId the id of local or remote video element.
*/
removeRemoteVideoMenu() {
const menuSpan = this.$container.find('.remotevideomenu');
if (menuSpan.length) {
ReactDOM.unmountComponentAtNode(menuSpan.get(0));
menuSpan.remove();
}
}
/**
* Mounts the {@code PresenceLabel} for displaying the participant's current
* presence status.
*
* @return {void}
*/
addPresenceLabel() {
const presenceLabelContainer = this.container.querySelector('.presence-label-container');
if (presenceLabelContainer) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<PresenceLabel
participantID = { this.id }
className = 'presence-label' />
</I18nextProvider>
</Provider>,
presenceLabelContainer);
}
}
/**
* Unmounts the {@code PresenceLabel} component.
*
* @return {void}
*/
removePresenceLabel() {
const presenceLabelContainer = this.container.querySelector('.presence-label-container');
if (presenceLabelContainer) {
ReactDOM.unmountComponentAtNode(presenceLabelContainer);
}
}
}

View File

@@ -1,4 +1,4 @@
/* global $, APP, config, interfaceConfig */
/* global $, APP, interfaceConfig */
/* eslint-disable no-unused-vars */
import { AtlasKitThemeProvider } from '@atlaskit/theme';
@@ -21,6 +21,7 @@ import {
pinParticipant
} from '../../../react/features/base/participants';
import {
getLocalVideoTrack,
getTrackByMediaTypeAndParticipant,
isLocalTrackMuted,
isRemoteTrackMuted
@@ -30,7 +31,8 @@ import { DisplayName } from '../../../react/features/display-name';
import {
DominantSpeakerIndicator,
RaisedHandIndicator,
StatusIndicators
StatusIndicators,
isVideoPlayable
} from '../../../react/features/filmstrip';
import {
LAYOUTS,
@@ -89,37 +91,10 @@ export default class SmallVideo {
/**
* Constructor.
*/
constructor(VideoLayout) {
this.videoStream = null;
this.audioStream = null;
this.VideoLayout = VideoLayout;
constructor() {
this.videoIsHovered = false;
this.videoType = undefined;
/**
* Whether or not the connection indicator should be displayed.
*
* @private
* @type {boolean}
*/
this._showConnectionIndicator = !interfaceConfig.CONNECTION_INDICATOR_DISABLED;
/**
* Whether or not the dominant speaker indicator should be displayed.
*
* @private
* @type {boolean}
*/
this._showDominantSpeaker = false;
/**
* Whether or not the raised hand indicator should be displayed.
*
* @private
* @type {boolean}
*/
this._showRaisedHand = false;
// Bind event handlers so they are only bound once for every instance.
this.updateView = this.updateView.bind(this);
@@ -145,33 +120,6 @@ export default class SmallVideo {
return this.$container.is(':visible');
}
/**
* Creates an audio or video element for a particular MediaStream.
*/
static createStreamElement(stream) {
const isVideo = stream.isVideoTrack();
const element = isVideo ? document.createElement('video') : document.createElement('audio');
if (isVideo) {
element.setAttribute('muted', 'true');
element.setAttribute('playsInline', 'true'); /* for Safari on iOS to work */
} else if (config.startSilent) {
element.muted = true;
}
element.autoplay = !config.testing?.noAutoPlayVideo;
element.id = SmallVideo.getStreamElementID(stream);
return element;
}
/**
* Returns the element id for a particular MediaStream.
*/
static getStreamElementID(stream) {
return (stream.isVideoTrack() ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
}
/**
* Configures hoverIn/hoverOut handlers. Depends on connection indicator.
*/
@@ -180,103 +128,22 @@ export default class SmallVideo {
this.$container.hover(
() => {
this.videoIsHovered = true;
this.renderThumbnail(true);
this.updateView();
this.updateIndicators();
},
() => {
this.videoIsHovered = false;
this.renderThumbnail(false);
this.updateView();
this.updateIndicators();
}
);
}
/**
* Unmounts the ConnectionIndicator component.
* @returns {void}
*/
removeConnectionIndicator() {
this._showConnectionIndicator = false;
this.updateIndicators();
}
/**
* Create or updates the ReactElement for displaying status indicators about
* audio mute, video mute, and moderator status.
*
* @returns {void}
* Renders the thumbnail.
*/
updateStatusBar() {
const statusBarContainer = this.container.querySelector('.videocontainer__toolbar');
if (!statusBarContainer) {
return;
}
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<StatusIndicators
participantID = { this.id } />
</I18nextProvider>
</Provider>,
statusBarContainer);
}
/**
* Adds the element indicating the audio level of the participant.
*
* @returns {void}
*/
addAudioLevelIndicator() {
let audioLevelContainer = this._getAudioLevelContainer();
if (audioLevelContainer) {
return;
}
audioLevelContainer = document.createElement('span');
audioLevelContainer.className = 'audioindicator-container';
this.container.appendChild(audioLevelContainer);
this.updateAudioLevelIndicator();
}
/**
* Removes the element indicating the audio level of the participant.
*
* @returns {void}
*/
removeAudioLevelIndicator() {
const audioLevelContainer = this._getAudioLevelContainer();
if (audioLevelContainer) {
ReactDOM.unmountComponentAtNode(audioLevelContainer);
}
}
/**
* Updates the audio level for this small video.
*
* @param lvl the new audio level to set
* @returns {void}
*/
updateAudioLevelIndicator(lvl = 0) {
const audioLevelContainer = this._getAudioLevelContainer();
if (audioLevelContainer) {
ReactDOM.render(<AudioLevelIndicator audioLevel = { lvl }/>, audioLevelContainer);
}
}
/**
* Queries the component's DOM for the element that should be the parent to the
* AudioLevelIndicator.
*
* @returns {HTMLElement} The DOM element that holds the AudioLevelIndicator.
*/
_getAudioLevelContainer() {
return this.container.querySelector('.audioindicator-container');
renderThumbnail() {
// Should be implemented by in subclasses.
}
/**
@@ -293,62 +160,6 @@ export default class SmallVideo {
return $($(this.container).find('video')[0]);
}
/**
* Selects the HTML image element which displays user's avatar.
*
* @return {jQuery|HTMLElement} a jQuery selector pointing to the HTML image
* element which displays the user's avatar.
*/
$avatar() {
return this.$container.find('.avatar-container');
}
/**
* Returns the display name element, which appears on the video thumbnail.
*
* @return {jQuery} a jQuery selector pointing to the display name element of
* the video thumbnail
*/
$displayName() {
return this.$container.find('.displayNameContainer');
}
/**
* Creates or updates the participant's display name that is shown over the
* video preview.
*
* @param {Object} props - The React {@code Component} props to pass into the
* {@code DisplayName} component.
* @returns {void}
*/
_renderDisplayName(props) {
const displayNameContainer = this.container.querySelector('.displayNameContainer');
if (displayNameContainer) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<DisplayName { ...props } />
</I18nextProvider>
</Provider>,
displayNameContainer);
}
}
/**
* Removes the component responsible for showing the participant's display name,
* if its container is present.
*
* @returns {void}
*/
removeDisplayName() {
const displayNameContainer = this.container.querySelector('.displayNameContainer');
if (displayNameContainer) {
ReactDOM.unmountComponentAtNode(displayNameContainer);
}
}
/**
* Enables / disables the css responsible for focusing/pinning a video
* thumbnail.
@@ -392,18 +203,7 @@ export default class SmallVideo {
* or <tt>false</tt> otherwise.
*/
isVideoPlayable() {
const state = APP.store.getState();
const tracks = state['features/base/tracks'];
const participant = this.id ? getParticipantById(state, this.id) : getLocalParticipant(state);
let isVideoMuted = true;
if (participant?.local) {
isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO);
} else if (!participant?.isFakeParticipant) { // remote participants excluding shared video
isVideoMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, this.id);
}
return this.videoStream && !isVideoMuted && !APP.conference.isAudioOnly();
return isVideoPlayable(APP.store.getState(), this.id);
}
/**
@@ -436,13 +236,15 @@ export default class SmallVideo {
let isScreenSharing = false;
let connectionStatus;
const state = APP.store.getState();
const participant = getParticipantById(state, this.id);
const id = this.id;
const participant = getParticipantById(state, id);
const isLocal = participant?.local ?? true;
const tracks = state['features/base/tracks'];
const videoTrack
= isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
if (typeof participant !== 'undefined' && !participant.isFakeParticipant && !participant.local) {
const tracks = state['features/base/tracks'];
const track = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, this.id);
isScreenSharing = typeof track !== 'undefined' && track.videoType === 'desktop';
isScreenSharing = videoTrack?.videoType === 'desktop';
connectionStatus = participant.connectionStatus;
}
@@ -455,9 +257,9 @@ export default class SmallVideo {
hasVideo: Boolean(this.selectVideoElement().length),
connectionStatus,
canPlayEventReceived: this._canPlayEventReceived,
videoStream: Boolean(this.videoStream),
videoStream: Boolean(videoTrack),
isScreenSharing,
videoStreamMuted: this.videoStream ? this.videoStream.isMuted() : 'no stream'
videoStreamMuted: videoTrack ? videoTrack.muted : 'no stream'
};
}
@@ -527,43 +329,6 @@ export default class SmallVideo {
}
}
/**
* Updates the react component displaying the avatar with the passed in avatar
* url.
*
* @returns {void}
*/
initializeAvatar() {
const thumbnail = this.$avatar().get(0);
if (thumbnail) {
// Maybe add a special case for local participant, as on init of
// LocalVideo.js the id is set to "local" but will get updated later.
ReactDOM.render(
<Provider store = { APP.store }>
<AvatarDisplay
className = 'userAvatar'
participantId = { this.id } />
</Provider>,
thumbnail
);
}
}
/**
* Unmounts any attached react components (particular the avatar image) from
* the avatar container.
*
* @returns {void}
*/
removeAvatar() {
const thumbnail = this.$avatar().get(0);
if (thumbnail) {
ReactDOM.unmountComponentAtNode(thumbnail);
}
}
/**
* Shows or hides the dominant speaker indicator.
* @param show whether to show or hide.
@@ -580,30 +345,8 @@ export default class SmallVideo {
return;
}
if (this._showDominantSpeaker === show) {
return;
}
this._showDominantSpeaker = show;
this.$container.toggleClass('active-speaker', this._showDominantSpeaker);
this.updateIndicators();
this.updateView();
}
/**
* Shows or hides the raised hand indicator.
* @param show whether to show or hide.
*/
showRaisedHandIndicator(show) {
if (!this.container) {
logger.warn(`Unable to raised hand indication - ${
this.videoSpanId} does not exist`);
return;
}
this._showRaisedHand = show;
this.updateIndicators();
this.$container.toggleClass('active-speaker', show);
}
/**
@@ -634,19 +377,7 @@ export default class SmallVideo {
*/
remove() {
logger.log('Remove thumbnail', this.id);
this.removeAudioLevelIndicator();
const toolbarContainer
= this.container.querySelector('.videocontainer__toolbar');
if (toolbarContainer) {
ReactDOM.unmountComponentAtNode(toolbarContainer);
}
this.removeConnectionIndicator();
this.removeDisplayName();
this.removeAvatar();
this._unmountIndicators();
this._unmountThumbnail();
// Remove whole container
if (this.container.parentNode) {
@@ -661,76 +392,9 @@ export default class SmallVideo {
* @returns {void}
*/
rerender() {
this.updateIndicators();
this.updateStatusBar();
this.updateView();
}
/**
* Updates the React element responsible for showing connection status, dominant
* speaker, and raised hand icons. Uses instance variables to get the necessary
* state to display. Will create the React element if not already created.
*
* @private
* @returns {void}
*/
updateIndicators() {
const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
if (!indicatorToolbar) {
return;
}
const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
const iconSize = NORMAL;
const showConnectionIndicator = this.videoIsHovered || !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
const state = APP.store.getState();
const currentLayout = getCurrentLayout(state);
const participantCount = getParticipantCount(state);
let statsPopoverPosition, tooltipPosition;
if (currentLayout === LAYOUTS.TILE_VIEW) {
statsPopoverPosition = 'right top';
tooltipPosition = 'right';
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
statsPopoverPosition = this.statsPopoverLocation;
tooltipPosition = 'left';
} else {
statsPopoverPosition = this.statsPopoverLocation;
tooltipPosition = 'top';
}
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<div>
<AtlasKitThemeProvider mode = 'dark'>
{ this._showConnectionIndicator
? <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
iconSize = { iconSize }
isLocalVideo = { this.isLocal }
enableStatsDisplay = { true }
participantId = { this.id }
statsPopoverPosition = { statsPopoverPosition } />
: null }
<RaisedHandIndicator
iconSize = { iconSize }
participantId = { this.id }
tooltipPosition = { tooltipPosition } />
{ this._showDominantSpeaker && participantCount > 2
? <DominantSpeakerIndicator
iconSize = { iconSize }
tooltipPosition = { tooltipPosition } />
: null }
</AtlasKitThemeProvider>
</div>
</I18nextProvider>
</Provider>,
indicatorToolbar
);
}
/**
* Callback invoked when the thumbnail is clicked and potentially trigger
* pinning of the participant.
@@ -788,18 +452,10 @@ export default class SmallVideo {
}
/**
* Removes the React element responsible for showing connection status, dominant
* speaker, and raised hand icons.
*
* @private
* @returns {void}
* Unmounts the thumbnail.
*/
_unmountIndicators() {
const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
if (indicatorToolbar) {
ReactDOM.unmountComponentAtNode(indicatorToolbar);
}
_unmountThumbnail() {
ReactDOM.unmountComponentAtNode(this.container);
}
/**
@@ -813,10 +469,6 @@ export default class SmallVideo {
switch (layout) {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
this.$container.css('padding-top', `${heightToWidthPercent}%`);
this.$avatar().css({
height: '50%',
width: `${heightToWidthPercent / 2}%`
});
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
@@ -826,7 +478,6 @@ export default class SmallVideo {
if (typeof size !== 'undefined') {
const { height, width } = size;
const avatarSize = height / 2;
this.$container.css({
height: `${height}px`,
@@ -834,10 +485,6 @@ export default class SmallVideo {
'min-width': `${width}px`,
width: `${width}px`
});
this.$avatar().css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
break;
}
@@ -847,7 +494,6 @@ export default class SmallVideo {
if (typeof thumbnailSize !== 'undefined') {
const { height, width } = thumbnailSize;
const avatarSize = height / 2;
this.$container.css({
height: `${height}px`,
@@ -855,10 +501,6 @@ export default class SmallVideo {
'min-width': `${width}px`,
width: `${width}px`
});
this.$avatar().css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
break;
}

View File

@@ -72,7 +72,6 @@ const VideoLayout = {
eventEmitter = emitter;
localVideoThumbnail = new LocalVideo(
VideoLayout,
emitter,
this._updateLargeVideoIfDisplayed.bind(this));
@@ -116,12 +115,6 @@ const VideoLayout = {
* @param lvl the new audio level to update to
*/
setAudioLevel(id, lvl) {
const smallVideo = this.getSmallVideo(id);
if (smallVideo) {
smallVideo.updateAudioLevelIndicator(lvl);
}
if (largeVideo && id === largeVideo.id) {
largeVideo.updateLargeVideoAudioLevel(lvl);
}
@@ -137,19 +130,6 @@ const VideoLayout = {
this._updateLargeVideoIfDisplayed(localId);
},
/**
* Get's the localID of the conference and set it to the local video
* (small one). This needs to be called as early as possible, when muc is
* actually joined. Otherwise events can come with information like email
* and setting them assume the id is already set.
*/
mucJoined() {
// FIXME: replace this call with a generic update call once SmallVideo
// only contains a ReactElement. Then remove this call once the
// Filmstrip is fully in React.
localVideoThumbnail.updateIndicators();
},
/**
* Shows/hides local video.
* @param {boolean} true to make the local video visible, false - otherwise
@@ -172,11 +152,8 @@ const VideoLayout = {
remoteVideo.addRemoteStreamElement(stream);
// Make sure track's muted state is reflected
if (stream.getType() !== 'audio') {
this.onVideoMute(id);
remoteVideo.updateView();
}
this.onVideoMute(id);
remoteVideo.updateView();
},
onRemoteStreamRemoved(stream) {
@@ -184,13 +161,12 @@ const VideoLayout = {
const remoteVideo = remoteVideos[id];
// Remote stream may be removed after participant left the conference.
if (remoteVideo) {
remoteVideo.removeRemoteStreamElement(stream);
remoteVideo.updateView();
}
this.updateMutedForNoTracks(id, stream.getType());
this.updateVideoMutedForNoTracks(id);
},
/**
@@ -199,19 +175,12 @@ const VideoLayout = {
*
* If participant has no tracks will make the UI display muted status.
* @param {string} participantId
* @param {string} mediaType 'audio' or 'video'
*/
updateMutedForNoTracks(participantId, mediaType) {
updateVideoMutedForNoTracks(participantId) {
const participant = APP.conference.getParticipantById(participantId);
if (participant && !participant.getTracksByMediaType(mediaType).length) {
if (mediaType === 'audio') {
APP.UI.setAudioMuted(participantId, true);
} else if (mediaType === 'video') {
APP.UI.setVideoMuted(participantId);
} else {
logger.error(`Unsupported media type: ${mediaType}`);
}
if (participant && !participant.getTracksByMediaType('video').length) {
APP.UI.setVideoMuted(participantId);
}
},
@@ -279,10 +248,7 @@ const VideoLayout = {
if (!participant || participant.local) {
return;
} else if (participant.isFakeParticipant) {
const sharedVideoThumb = new SharedVideoThumb(
participant,
SHARED_VIDEO_CONTAINER_TYPE,
VideoLayout);
const sharedVideoThumb = new SharedVideoThumb(participant);
this.addRemoteVideoContainer(participant.id, sharedVideoThumb);
@@ -291,12 +257,10 @@ const VideoLayout = {
const id = participant.id;
const jitsiParticipant = APP.conference.getParticipantById(id);
const remoteVideo = new RemoteVideo(jitsiParticipant, VideoLayout);
const remoteVideo = new RemoteVideo(jitsiParticipant);
this.addRemoteVideoContainer(id, remoteVideo);
this.updateMutedForNoTracks(id, 'audio');
this.updateMutedForNoTracks(id, 'video');
this.updateVideoMutedForNoTracks(id);
},
/**
@@ -331,22 +295,6 @@ const VideoLayout = {
this._updateLargeVideoIfDisplayed(id, true);
},
/**
* Display name changed.
*/
onDisplayNameChanged(id) {
if (id === 'localVideoContainer'
|| APP.conference.isLocalId(id)) {
localVideoThumbnail.updateDisplayName();
} else {
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
remoteVideo.updateDisplayName();
}
}
},
/**
* On dominant speaker changed event.
*
@@ -413,20 +361,6 @@ const VideoLayout = {
}
},
/**
* Hides all the indicators
*/
hideStats() {
for (const video in remoteVideos) { // eslint-disable-line guard-for-in
const remoteVideo = remoteVideos[video];
if (remoteVideo) {
remoteVideo.removeConnectionIndicator();
}
}
localVideoThumbnail.removeConnectionIndicator();
},
removeParticipantContainer(id) {
// Unlock large video
if (this.getPinnedId() === id) {
@@ -477,15 +411,6 @@ const VideoLayout = {
},
changeUserAvatar(id, avatarUrl) {
const smallVideo = VideoLayout.getSmallVideo(id);
if (smallVideo) {
smallVideo.initializeAvatar();
} else {
logger.warn(
`Missed avatar update - no small video yet for ${id}`
);
}
if (this.isCurrentlyOnLarge(id)) {
largeVideo.updateAvatar(avatarUrl);
}

3454
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,24 +15,24 @@
"author": "",
"readmeFilename": "README.md",
"dependencies": {
"@atlaskit/button": "10.1.1",
"@atlaskit/checkbox": "5.0.10",
"@atlaskit/dropdown-menu": "6.1.25",
"@atlaskit/field-text": "7.0.19",
"@atlaskit/field-text-area": "4.0.15",
"@atlaskit/flag": "13.0.0",
"@atlaskit/icon": "15.0.3",
"@atlaskit/inline-dialog": "5.3.0",
"@atlaskit/inline-message": "7.0.10",
"@atlaskit/lozenge": "6.2.4",
"@atlaskit/modal-dialog": "8.0.1",
"@atlaskit/multi-select": "11.0.13",
"@atlaskit/spinner": "9.0.13",
"@atlaskit/tabs": "8.0.11",
"@atlaskit/theme": "7.0.2",
"@atlaskit/toggle": "5.0.14",
"@atlaskit/tooltip": "12.1.13",
"@jitsi/js-utils": "1.0.3",
"@atlaskit/button": "15.1.4",
"@atlaskit/checkbox": "12.0.0",
"@atlaskit/dropdown-menu": "10.1.2",
"@atlaskit/field-text": "11.0.4",
"@atlaskit/field-text-area": "8.0.4",
"@atlaskit/flag": "14.1.0",
"@atlaskit/icon": "21.2.0",
"@atlaskit/inline-dialog": "13.0.9",
"@atlaskit/inline-message": "11.0.8",
"@atlaskit/lozenge": "10.1.1",
"@atlaskit/modal-dialog": "11.2.4",
"@atlaskit/multi-select": "15.0.5",
"@atlaskit/spinner": "15.0.6",
"@atlaskit/tabs": "12.1.2",
"@atlaskit/theme": "11.0.2",
"@atlaskit/toggle": "12.0.3",
"@atlaskit/tooltip": "17.1.2",
"@jitsi/js-utils": "1.0.5",
"@microsoft/microsoft-graph-client": "1.1.0",
"@react-native-async-storage/async-storage": "1.13.2",
"@react-native-community/google-signin": "3.0.1",
@@ -56,7 +56,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#87c6e374755718fdb0804c2c798ea4bc832f4fca",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#84357ce1a8d06c67a502709877130520aaf71592",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19",
"moment": "2.19.4",
@@ -64,15 +64,15 @@
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"pixelmatch": "5.1.0",
"punycode": "2.1.1",
"react": "16.9",
"react-dom": "16.9",
"react": "16.12",
"react-dom": "16.12",
"react-emoji-render": "1.2.4",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"react-native": "github:jitsi/react-native#891986ec5ecaef65d1c8a7fe472f86cf84fe7551",
"react-native-background-timer": "2.4.0",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#df48ecdc4e1e90c5352f803ddbab1fa7269b74a7",
"react-native-callstats": "3.61.0",
"react-native-callstats": "3.70.1",
"react-native-collapsible": "1.5.1",
"react-native-default-preference": "1.4.2",
"react-native-device-info": "8.0.0",
@@ -85,7 +85,7 @@
"react-native-svg-transformer": "0.14.3",
"react-native-url-polyfill": "1.2.0",
"react-native-watch-connectivity": "0.4.3",
"react-native-webrtc": "1.87.2",
"react-native-webrtc": "1.87.3",
"react-native-webview": "11.0.2",
"react-native-youtube-iframe": "1.2.3",
"react-redux": "7.1.0",

View File

@@ -29,7 +29,6 @@ import '../blur/reducer';
import '../calendar-sync/reducer';
import '../chat/reducer';
import '../deep-linking/reducer';
import '../device-selection/reducer';
import '../dropbox/reducer';
import '../dynamic-branding/reducer';
import '../etherpad/reducer';

View File

@@ -4,6 +4,7 @@ import '../authentication/reducer';
import '../mobile/audio-mode/reducer';
import '../mobile/background/reducer';
import '../mobile/call-integration/reducer';
import '../mobile/external-api/reducer';
import '../mobile/full-screen/reducer';
import '../mobile/incoming-call/reducer';
import '../mobile/watchos/reducer';

View File

@@ -39,8 +39,11 @@ class AudioLevelIndicator extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { audioLevel: passedAudioLevel } = this.props;
// First make sure we are sensitive enough.
const audioLevel = Math.min(this.props.audioLevel * 1.2, 1);
const audioLevel = typeof passedAudioLevel === 'number' && !isNaN(passedAudioLevel)
? Math.min(passedAudioLevel * 1.2, 1) : 0;
// Let's now stretch the audio level over the number of dots we have.
const stretchedAudioLevel = AUDIO_LEVEL_DOTS * audioLevel;

View File

@@ -62,6 +62,16 @@ export const CONFERENCE_SUBJECT_CHANGED = 'CONFERENCE_SUBJECT_CHANGED';
*/
export const CONFERENCE_TIMESTAMP_CHANGED = 'CONFERENCE_TIMESTAMP_CHANGED';
/**
* The type of (redux) action which signals that an uuid for a conference has been set.
*
* {
* type: CONFERENCE_UNIQUE_ID_SET,
* conference: JitsiConference
* }
*/
export const CONFERENCE_UNIQUE_ID_SET = 'CONFERENCE_UNIQUE_ID_SET';
/**
* The type of (redux) action which signals that a specific conference will be
* joined.

View File

@@ -36,6 +36,7 @@ import {
CONFERENCE_LEFT,
CONFERENCE_SUBJECT_CHANGED,
CONFERENCE_TIMESTAMP_CHANGED,
CONFERENCE_UNIQUE_ID_SET,
CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE,
DATA_CHANNEL_OPENED,
@@ -327,6 +328,22 @@ export function conferenceTimestampChanged(conferenceTimestamp: number) {
};
}
/**
* Signals that the unique identifier for conference has been set.
*
* @param {JitsiConference} conference - The JitsiConference instance, where the uuid has been set.
* @returns {{
* type: CONFERENCE_UNIQUE_ID_SET,
* conference: JitsiConference,
* }}
*/
export function conferenceUniqueIdSet(conference: Object) {
return {
type: CONFERENCE_UNIQUE_ID_SET,
conference
};
}
/**
* Adds any existing local tracks to a specific conference before the conference
* is joined. Then signals the intention of the application to have the local

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