mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-23 01:07:46 +00:00
Compare commits
20 Commits
7182
...
jitsi-meet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
732754c566 | ||
|
|
b360a9e572 | ||
|
|
f88fa81616 | ||
|
|
e0e66119f5 | ||
|
|
ba4784f149 | ||
|
|
7fb7c3de9c | ||
|
|
3d2d449d31 | ||
|
|
ca60c33dda | ||
|
|
162512496a | ||
|
|
7819c97839 | ||
|
|
e9c8603c3c | ||
|
|
9e165c337a | ||
|
|
58af1b98c0 | ||
|
|
6a077333c6 | ||
|
|
cb234e6b1b | ||
|
|
67a9f35176 | ||
|
|
9396e8b0c0 | ||
|
|
200d857012 | ||
|
|
1a22b7d0dd | ||
|
|
035cccb97b |
3
.npmrc
3
.npmrc
@@ -1,3 +1,6 @@
|
||||
package-lock=true
|
||||
; FIXME Set legacy-peer-deps=false when we upgrade RN.
|
||||
legacy-peer-deps=true
|
||||
; Omit optional dependencies
|
||||
omit=optional
|
||||
|
||||
|
||||
5
Makefile
5
Makefile
@@ -63,10 +63,7 @@ deploy-appbundle:
|
||||
|
||||
deploy-lib-jitsi-meet:
|
||||
cp \
|
||||
$(LIBJITSIMEET_DIR)/dist/umd/lib-jitsi-meet.min.js \
|
||||
$(LIBJITSIMEET_DIR)/dist/umd/lib-jitsi-meet.min.map \
|
||||
$(LIBJITSIMEET_DIR)/dist/umd/lib-jitsi-meet.e2ee-worker.js \
|
||||
$(LIBJITSIMEET_DIR)/modules/browser/capabilities.json \
|
||||
$(LIBJITSIMEET_DIR)/dist/umd/lib-jitsi-meet.* \
|
||||
$(DEPLOY_DIR)
|
||||
|
||||
deploy-olm:
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 The WebRTC project authors. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import org.webrtc.VideoCodecInfo;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
|
||||
/** Container for static helper functions related to dealing with H264 codecs. */
|
||||
class H264Utils {
|
||||
public static final String H264_FMTP_PROFILE_LEVEL_ID = "profile-level-id";
|
||||
public static final String H264_FMTP_LEVEL_ASYMMETRY_ALLOWED = "level-asymmetry-allowed";
|
||||
public static final String H264_FMTP_PACKETIZATION_MODE = "packetization-mode";
|
||||
|
||||
public static final String H264_PROFILE_CONSTRAINED_BASELINE = "42e0";
|
||||
public static final String H264_PROFILE_CONSTRAINED_HIGH = "640c";
|
||||
public static final String H264_LEVEL_3_1 = "1f"; // 31 in hex.
|
||||
public static final String H264_CONSTRAINED_HIGH_3_1 =
|
||||
H264_PROFILE_CONSTRAINED_HIGH + H264_LEVEL_3_1;
|
||||
public static final String H264_CONSTRAINED_BASELINE_3_1 =
|
||||
H264_PROFILE_CONSTRAINED_BASELINE + H264_LEVEL_3_1;
|
||||
|
||||
public static Map<String, String> getDefaultH264Params(boolean isHighProfile) {
|
||||
final Map<String, String> params = new HashMap<>();
|
||||
params.put(VideoCodecInfo.H264_FMTP_LEVEL_ASYMMETRY_ALLOWED, "1");
|
||||
params.put(VideoCodecInfo.H264_FMTP_PACKETIZATION_MODE, "1");
|
||||
params.put(VideoCodecInfo.H264_FMTP_PROFILE_LEVEL_ID,
|
||||
isHighProfile ? VideoCodecInfo.H264_CONSTRAINED_HIGH_3_1
|
||||
: VideoCodecInfo.H264_CONSTRAINED_BASELINE_3_1);
|
||||
return params;
|
||||
}
|
||||
|
||||
public static VideoCodecInfo DEFAULT_H264_BASELINE_PROFILE_CODEC =
|
||||
new VideoCodecInfo("H264", getDefaultH264Params(/* isHighProfile= */ false));
|
||||
public static VideoCodecInfo DEFAULT_H264_HIGH_PROFILE_CODEC =
|
||||
new VideoCodecInfo("H264", getDefaultH264Params(/* isHighProfile= */ true));
|
||||
}
|
||||
@@ -32,17 +32,17 @@ import com.facebook.react.jscexecutor.JSCExecutorFactory;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.oney.WebRTCModule.EglUtils;
|
||||
import com.oney.WebRTCModule.RTCVideoViewManager;
|
||||
import com.oney.WebRTCModule.WebRTCModule;
|
||||
import com.oney.WebRTCModule.WebRTCModuleOptions;
|
||||
import com.oney.WebRTCModule.webrtcutils.H264AndSoftwareVideoDecoderFactory;
|
||||
import com.oney.WebRTCModule.webrtcutils.H264AndSoftwareVideoEncoderFactory;
|
||||
|
||||
import org.devio.rn.splashscreen.SplashScreenModule;
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.audio.AudioDeviceModule;
|
||||
import org.webrtc.audio.JavaAudioDeviceModule;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
class ReactInstanceManagerHolder {
|
||||
@@ -79,30 +79,11 @@ class ReactInstanceManagerHolder {
|
||||
nativeModules.add(new RNConnectionService(reactContext));
|
||||
}
|
||||
|
||||
// Initialize the WebRTC module by hand, since we want to override some
|
||||
// initialization options.
|
||||
WebRTCModule.Options options = new WebRTCModule.Options();
|
||||
|
||||
AudioDeviceModule adm = JavaAudioDeviceModule.builder(reactContext)
|
||||
.setEnableVolumeLogger(false)
|
||||
.createAudioDeviceModule();
|
||||
options.setAudioDeviceModule(adm);
|
||||
|
||||
EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
|
||||
|
||||
options.setVideoDecoderFactory(new WebRTCVideoDecoderFactory(eglContext));
|
||||
options.setVideoEncoderFactory(new WebRTCVideoEncoderFactory(eglContext));
|
||||
|
||||
nativeModules.add(new WebRTCModule(reactContext, options));
|
||||
|
||||
return nativeModules;
|
||||
}
|
||||
|
||||
private static List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Arrays.<ViewManager>asList(
|
||||
// WebRTC, see createNativeModules for details.
|
||||
new RTCVideoViewManager()
|
||||
);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
static List<ReactPackage> getReactNativePackages() {
|
||||
@@ -122,6 +103,7 @@ class ReactInstanceManagerHolder {
|
||||
new com.reactnativecommunity.webview.RNCWebViewPackage(),
|
||||
new com.kevinresol.react_native_default_preference.RNDefaultPreferencePackage(),
|
||||
new com.learnium.RNDeviceInfo.RNDeviceInfo(),
|
||||
new com.oney.WebRTCModule.WebRTCModulePackage(),
|
||||
new com.swmansion.gesturehandler.RNGestureHandlerPackage(),
|
||||
new org.linusu.RNGetRandomValuesPackage(),
|
||||
new com.rnimmersive.RNImmersivePackage(),
|
||||
@@ -254,6 +236,14 @@ class ReactInstanceManagerHolder {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize the WebRTC module options.
|
||||
WebRTCModuleOptions options = WebRTCModuleOptions.getInstance();
|
||||
|
||||
EglBase.Context eglContext = EglUtils.getRootEglBaseContext();
|
||||
|
||||
options.videoDecoderFactory = new H264AndSoftwareVideoDecoderFactory(eglContext);
|
||||
options.videoEncoderFactory = new H264AndSoftwareVideoEncoderFactory(eglContext);
|
||||
|
||||
Log.d(TAG, "initializing RN with Application");
|
||||
|
||||
reactInstanceManager
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
/** Enumeration of supported video codec types. */
|
||||
public enum VideoCodecMimeType {
|
||||
VP8("video/x-vnd.on2.vp8"),
|
||||
VP9("video/x-vnd.on2.vp9"),
|
||||
H264("video/avc"),
|
||||
AV1("video/av01");
|
||||
|
||||
private final String mimeType;
|
||||
|
||||
private VideoCodecMimeType(String mimeType) {
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
String mimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.HardwareVideoDecoderFactory;
|
||||
import org.webrtc.SoftwareVideoDecoderFactory;
|
||||
import org.webrtc.VideoCodecInfo;
|
||||
import org.webrtc.VideoDecoder;
|
||||
import org.webrtc.VideoDecoderFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This is a custom video decoder factory for WebRTC which behaves similarly
|
||||
* to the default one in iOS. It supports the following codecs:
|
||||
*
|
||||
* - In hardware: H.264 (baseline)
|
||||
* - In software: VP8, VP9, AV1
|
||||
*/
|
||||
public class WebRTCVideoDecoderFactory implements VideoDecoderFactory {
|
||||
private final VideoDecoderFactory hardwareVideoDecoderFactory;
|
||||
private final VideoDecoderFactory softwareVideoDecoderFactory = new SoftwareVideoDecoderFactory();
|
||||
|
||||
public WebRTCVideoDecoderFactory(@Nullable EglBase.Context eglContext) {
|
||||
this.hardwareVideoDecoderFactory = new HardwareVideoDecoderFactory(eglContext);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public VideoDecoder createDecoder(VideoCodecInfo codecInfo) {
|
||||
if (codecInfo.name.equalsIgnoreCase(VideoCodecMimeType.H264.name())) {
|
||||
return this.hardwareVideoDecoderFactory.createDecoder(codecInfo);
|
||||
}
|
||||
|
||||
return this.softwareVideoDecoderFactory.createDecoder(codecInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoCodecInfo[] getSupportedCodecs() {
|
||||
List<VideoCodecInfo> codecs = new ArrayList<>();
|
||||
|
||||
codecs.add(H264Utils.DEFAULT_H264_BASELINE_PROFILE_CODEC);
|
||||
codecs.add(new VideoCodecInfo(VideoCodecMimeType.VP8.name(), new HashMap<>()));
|
||||
codecs.add(new VideoCodecInfo(VideoCodecMimeType.VP9.name(), new HashMap<>()));
|
||||
codecs.add(new VideoCodecInfo(VideoCodecMimeType.AV1.name(), new HashMap<>()));
|
||||
|
||||
return codecs.toArray(new VideoCodecInfo[codecs.size()]);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.HardwareVideoEncoderFactory;
|
||||
import org.webrtc.SoftwareVideoEncoderFactory;
|
||||
import org.webrtc.VideoCodecInfo;
|
||||
import org.webrtc.VideoEncoder;
|
||||
import org.webrtc.VideoEncoderFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This is a custom video encoder factory for WebRTC which behaves similarly
|
||||
* to the default one in iOS. It supports the following codecs:
|
||||
*
|
||||
* - In hardware: H.264 (baseline)
|
||||
* - In software: VP8, VP9, AV1
|
||||
*/
|
||||
public class WebRTCVideoEncoderFactory implements VideoEncoderFactory {
|
||||
private final VideoEncoderFactory hardwareVideoEncoderFactory;
|
||||
private final VideoEncoderFactory softwareVideoEncoderFactory = new SoftwareVideoEncoderFactory();
|
||||
|
||||
public WebRTCVideoEncoderFactory(@Nullable EglBase.Context eglContext) {
|
||||
this.hardwareVideoEncoderFactory =
|
||||
new HardwareVideoEncoderFactory(eglContext, false, false);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public VideoEncoder createEncoder(VideoCodecInfo codecInfo) {
|
||||
if (codecInfo.name.equalsIgnoreCase(VideoCodecMimeType.H264.name())) {
|
||||
return this.hardwareVideoEncoderFactory.createEncoder(codecInfo);
|
||||
}
|
||||
|
||||
return this.softwareVideoEncoderFactory.createEncoder(codecInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VideoCodecInfo[] getSupportedCodecs() {
|
||||
List<VideoCodecInfo> codecs = new ArrayList<>();
|
||||
|
||||
codecs.add(H264Utils.DEFAULT_H264_BASELINE_PROFILE_CODEC);
|
||||
codecs.add(new VideoCodecInfo(VideoCodecMimeType.VP8.name(), new HashMap<>()));
|
||||
codecs.add(new VideoCodecInfo(VideoCodecMimeType.VP9.name(), new HashMap<>()));
|
||||
codecs.add(new VideoCodecInfo(VideoCodecMimeType.AV1.name(), new HashMap<>()));
|
||||
|
||||
return codecs.toArray(new VideoCodecInfo[codecs.size()]);
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,7 @@ Component "conference.jitmeet.example.com" "muc"
|
||||
restrict_room_creation = true
|
||||
storage = "memory"
|
||||
modules_enabled = {
|
||||
"muc_hide_all";
|
||||
"muc_meeting_id";
|
||||
"muc_domain_mapper";
|
||||
"polls";
|
||||
@@ -92,6 +93,7 @@ Component "breakout.jitmeet.example.com" "muc"
|
||||
restrict_room_creation = true
|
||||
storage = "memory"
|
||||
modules_enabled = {
|
||||
"muc_hide_all";
|
||||
"muc_meeting_id";
|
||||
"muc_domain_mapper";
|
||||
"muc_rate_limit";
|
||||
@@ -105,6 +107,7 @@ Component "breakout.jitmeet.example.com" "muc"
|
||||
Component "internal.auth.jitmeet.example.com" "muc"
|
||||
storage = "memory"
|
||||
modules_enabled = {
|
||||
"muc_hide_all";
|
||||
"ping";
|
||||
}
|
||||
admins = { "focusUser@auth.jitmeet.example.com", "jvb@auth.jitmeet.example.com" }
|
||||
@@ -139,6 +142,7 @@ Component "lobby.jitmeet.example.com" "muc"
|
||||
muc_room_locking = false
|
||||
muc_room_default_public_jids = true
|
||||
modules_enabled = {
|
||||
"muc_hide_all";
|
||||
"muc_rate_limit";
|
||||
"polls";
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ target 'JitsiMeetSDK' do
|
||||
|
||||
pod 'CocoaLumberjack', '3.7.2'
|
||||
pod 'ObjectiveDropboxOfficial', '6.2.3'
|
||||
pod 'JitsiWebRTC', '~> 106.0.0'
|
||||
pod 'JitsiWebRTC', '~> 111.0.0'
|
||||
end
|
||||
|
||||
target 'JitsiMeetSDKLite' do
|
||||
|
||||
103
ios/Podfile.lock
103
ios/Podfile.lock
@@ -3,11 +3,12 @@ PODS:
|
||||
- amplitude-react-native (2.7.0):
|
||||
- Amplitude (= 8.7.1)
|
||||
- React-Core
|
||||
- AppAuth (1.4.0):
|
||||
- AppAuth/Core (= 1.4.0)
|
||||
- AppAuth/ExternalUserAgent (= 1.4.0)
|
||||
- AppAuth/Core (1.4.0)
|
||||
- AppAuth/ExternalUserAgent (1.4.0)
|
||||
- AppAuth (1.6.1):
|
||||
- AppAuth/Core (= 1.6.1)
|
||||
- AppAuth/ExternalUserAgent (= 1.6.1)
|
||||
- AppAuth/Core (1.6.1)
|
||||
- AppAuth/ExternalUserAgent (1.6.1):
|
||||
- AppAuth/Core
|
||||
- boost (1.76.0)
|
||||
- CocoaLumberjack (3.7.2):
|
||||
- CocoaLumberjack/Core (= 3.7.2)
|
||||
@@ -102,56 +103,56 @@ PODS:
|
||||
- GoogleUtilities/Network (~> 7.7)
|
||||
- "GoogleUtilities/NSData+zlib (~> 7.7)"
|
||||
- nanopb (~> 2.30908.0)
|
||||
- GoogleDataTransport (9.1.4):
|
||||
- GoogleDataTransport (9.2.2):
|
||||
- GoogleUtilities/Environment (~> 7.7)
|
||||
- nanopb (< 2.30910.0, >= 2.30908.0)
|
||||
- PromisesObjC (< 3.0, >= 1.2)
|
||||
- GoogleSignIn (6.0.2):
|
||||
- AppAuth (~> 1.4)
|
||||
- GTMAppAuth (~> 1.0)
|
||||
- GTMSessionFetcher/Core (~> 1.1)
|
||||
- GoogleUtilities/AppDelegateSwizzler (7.7.0):
|
||||
- GoogleSignIn (6.2.4):
|
||||
- AppAuth (~> 1.5)
|
||||
- GTMAppAuth (~> 1.3)
|
||||
- GTMSessionFetcher/Core (< 3.0, >= 1.1)
|
||||
- GoogleUtilities/AppDelegateSwizzler (7.11.1):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Network
|
||||
- GoogleUtilities/Environment (7.7.0):
|
||||
- GoogleUtilities/Environment (7.11.1):
|
||||
- PromisesObjC (< 3.0, >= 1.2)
|
||||
- GoogleUtilities/Logger (7.7.0):
|
||||
- GoogleUtilities/Logger (7.11.1):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/MethodSwizzler (7.7.0):
|
||||
- GoogleUtilities/MethodSwizzler (7.11.1):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Network (7.7.0):
|
||||
- GoogleUtilities/Network (7.11.1):
|
||||
- GoogleUtilities/Logger
|
||||
- "GoogleUtilities/NSData+zlib"
|
||||
- GoogleUtilities/Reachability
|
||||
- "GoogleUtilities/NSData+zlib (7.7.0)"
|
||||
- GoogleUtilities/Reachability (7.7.0):
|
||||
- "GoogleUtilities/NSData+zlib (7.11.1)"
|
||||
- GoogleUtilities/Reachability (7.11.1):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/UserDefaults (7.7.0):
|
||||
- GoogleUtilities/UserDefaults (7.11.1):
|
||||
- GoogleUtilities/Logger
|
||||
- GTMAppAuth (1.2.2):
|
||||
- AppAuth/Core (~> 1.4)
|
||||
- GTMSessionFetcher/Core (~> 1.5)
|
||||
- GTMSessionFetcher/Core (1.7.0)
|
||||
- JitsiWebRTC (106.0.0)
|
||||
- libwebp (1.2.1):
|
||||
- libwebp/demux (= 1.2.1)
|
||||
- libwebp/mux (= 1.2.1)
|
||||
- libwebp/webp (= 1.2.1)
|
||||
- libwebp/demux (1.2.1):
|
||||
- GTMAppAuth (1.3.1):
|
||||
- AppAuth/Core (~> 1.6)
|
||||
- GTMSessionFetcher/Core (< 3.0, >= 1.5)
|
||||
- GTMSessionFetcher/Core (2.3.0)
|
||||
- JitsiWebRTC (111.0.1)
|
||||
- libwebp (1.2.4):
|
||||
- libwebp/demux (= 1.2.4)
|
||||
- libwebp/mux (= 1.2.4)
|
||||
- libwebp/webp (= 1.2.4)
|
||||
- libwebp/demux (1.2.4):
|
||||
- libwebp/webp
|
||||
- libwebp/mux (1.2.1):
|
||||
- libwebp/mux (1.2.4):
|
||||
- libwebp/demux
|
||||
- libwebp/webp (1.2.1)
|
||||
- libwebp/webp (1.2.4)
|
||||
- nanopb (2.30908.0):
|
||||
- nanopb/decode (= 2.30908.0)
|
||||
- nanopb/encode (= 2.30908.0)
|
||||
- nanopb/decode (2.30908.0)
|
||||
- nanopb/encode (2.30908.0)
|
||||
- ObjectiveDropboxOfficial (6.2.3)
|
||||
- PromisesObjC (2.1.1)
|
||||
- PromisesSwift (2.1.1):
|
||||
- PromisesObjC (= 2.1.1)
|
||||
- PromisesObjC (2.2.0)
|
||||
- PromisesSwift (2.2.0):
|
||||
- PromisesObjC (= 2.2.0)
|
||||
- RCT-Folly (2021.06.28.00-v2):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
@@ -389,8 +390,8 @@ PODS:
|
||||
- react-native-video/Video (6.0.0-alpha.1):
|
||||
- PromisesSwift
|
||||
- React-Core
|
||||
- react-native-webrtc (106.0.7):
|
||||
- JitsiWebRTC (~> 106.0.0)
|
||||
- react-native-webrtc (111.0.0):
|
||||
- JitsiWebRTC (~> 111.0.0)
|
||||
- React-Core
|
||||
- react-native-webview (11.15.1):
|
||||
- React-Core
|
||||
@@ -471,8 +472,8 @@ PODS:
|
||||
- React-Core
|
||||
- RNGestureHandler (2.9.0):
|
||||
- React-Core
|
||||
- RNGoogleSignin (7.0.4):
|
||||
- GoogleSignIn (~> 6.0.0)
|
||||
- RNGoogleSignin (9.0.2):
|
||||
- GoogleSignIn (~> 6.2)
|
||||
- React-Core
|
||||
- RNScreens (3.13.1):
|
||||
- React-Core
|
||||
@@ -500,7 +501,7 @@ DEPENDENCIES:
|
||||
- Firebase/DynamicLinks (~> 8.0)
|
||||
- "giphy-react-native-sdk (from `../node_modules/@giphy/react-native-sdk`)"
|
||||
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||
- JitsiWebRTC (~> 106.0.0)
|
||||
- JitsiWebRTC (~> 111.0.0)
|
||||
- ObjectiveDropboxOfficial (= 6.2.3)
|
||||
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
|
||||
@@ -701,7 +702,7 @@ EXTERNAL SOURCES:
|
||||
SPEC CHECKSUMS:
|
||||
Amplitude: 834c7332dfb9640a751e21c13efb22a07c0c12d4
|
||||
amplitude-react-native: 0ed8cab759aafaa94961b82122bf56297da607ad
|
||||
AppAuth: 31bcec809a638d7bd2f86ea8a52bd45f6e81e7c7
|
||||
AppAuth: e48b432bb4ba88b10cb2bcc50d7f3af21e78b9c2
|
||||
boost: a7c83b31436843459a1961bfd74b96033dc77234
|
||||
CocoaLumberjack: b7e05132ff94f6ae4dfa9d5bce9141893a21d9da
|
||||
DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662
|
||||
@@ -719,17 +720,17 @@ SPEC CHECKSUMS:
|
||||
giphy-react-native-sdk: 7abccf2b52123a0f30ce99da895ab6288023680c
|
||||
glog: 476ee3e89abb49e07f822b48323c51c57124b572
|
||||
GoogleAppMeasurement: 4c19f031220c72464d460c9daa1fb5d1acce958e
|
||||
GoogleDataTransport: 5fffe35792f8b96ec8d6775f5eccd83c998d5a3b
|
||||
GoogleSignIn: fd381840dbe7c1137aa6dc30849a5c3e070c034a
|
||||
GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1
|
||||
GTMAppAuth: ad5c2b70b9a8689e1a04033c9369c4915bfcbe89
|
||||
GTMSessionFetcher: 43748f93435c2aa068b1cbe39655aaf600652e91
|
||||
JitsiWebRTC: f441eb0e2d67f0588bf24e21c5162e97342714fb
|
||||
libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
|
||||
GoogleDataTransport: 8378d1fa8ac49753ea6ce70d65a7cb70ce5f66e6
|
||||
GoogleSignIn: 5651ce3a61e56ca864160e79b484cd9ed3f49b7a
|
||||
GoogleUtilities: 9aa0ad5a7bc171f8bae016300bfcfa3fb8425749
|
||||
GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd
|
||||
GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2
|
||||
JitsiWebRTC: 9619c1f71cc16eeca76df68aa2d213c6d63274a8
|
||||
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
|
||||
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
|
||||
ObjectiveDropboxOfficial: fe206ce8c0bc49976c249d472db7fdbc53ebbd53
|
||||
PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb
|
||||
PromisesSwift: 99fddfe4a0ec88a56486644c0da106694c92a604
|
||||
PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef
|
||||
PromisesSwift: cf9eb58666a43bbe007302226e510b16c1e10959
|
||||
RCT-Folly: 4d8508a426467c48885f1151029bc15fa5d7b3b8
|
||||
RCTRequired: 92cbd71369a2de6add25fd2403ac39838f1b694f
|
||||
RCTTypeSafety: 494e8af41d7410ed0b877210859ee3984f37e6b4
|
||||
@@ -754,7 +755,7 @@ SPEC CHECKSUMS:
|
||||
react-native-slider: 6e9b86e76cce4b9e35b3403193a6432ed07e0c81
|
||||
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
|
||||
react-native-video: bb6f12a7198db53b261fefb5d609dc77417acc8b
|
||||
react-native-webrtc: 0df36747802476e758af6b6dceccdeaed8c826c2
|
||||
react-native-webrtc: a9d4d8ef61adb634e006ffd956c494ad8318d95c
|
||||
react-native-webview: ea4899a1056c782afa96dd082179a66cbebf5504
|
||||
React-perflogger: 46620fc6d1c3157b60ed28434e08f7fd7f3f3353
|
||||
React-RCTActionSheet: b1f7e72a0ba760ec684df335c61f730b5179f5ff
|
||||
@@ -774,13 +775,13 @@ SPEC CHECKSUMS:
|
||||
RNDefaultPreference: 08bdb06cfa9188d5da97d4642dac745218d7fb31
|
||||
RNDeviceInfo: 0400a6d0c94186d1120c3cbd97b23abc022187a9
|
||||
RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39
|
||||
RNGoogleSignin: c4381751eefd73c552b923ba347a9bfc6f18771c
|
||||
RNGoogleSignin: 22e468a9474dbcb8618d8847205ad4f0b2575d13
|
||||
RNScreens: 40a2cb40a02a609938137a1e0acfbf8fc9eebf19
|
||||
RNSound: 27e8268bdb0a1f191f219a33267f7e0445e8d62f
|
||||
RNSVG: f3b60aeeaa81960e2e0536c3a9eef50b667ef3a9
|
||||
RNWatch: dae6c858a2051dbdcfb00b9a86cf4d90400263b4
|
||||
Yoga: 7929b92b1828675c1bebeb114dae8cb8fa7ef6a3
|
||||
|
||||
PODFILE CHECKSUM: e671cdcdb80fab67e305861c36bfae8ed5a5b0ef
|
||||
PODFILE CHECKSUM: d9116cb59cd7e921956e45de7cbbd75bef3862c1
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
|
||||
@@ -39,6 +39,11 @@
|
||||
[builder setFeatureFlag:@"ios.screensharing.enabled" withBoolean:YES];
|
||||
[builder setFeatureFlag:@"ios.recording.enabled" withBoolean:YES];
|
||||
builder.serverURL = [NSURL URLWithString:@"https://meet.jit.si"];
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
// CallKit has started to create problems starting with the iOS 16 simulator.
|
||||
// Disable it since it never worked in the simulator anyway.
|
||||
[builder setFeatureFlag:@"call-integration.enabled" withBoolean:NO];
|
||||
#endif
|
||||
}];
|
||||
|
||||
[jitsiMeet application:application didFinishLaunchingWithOptions:launchOptions];
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
#import <Intents/Intents.h>
|
||||
#import <WebRTC/RTCLogging.h>
|
||||
|
||||
#import "Orientation.h"
|
||||
|
||||
#import "JitsiMeet+Private.h"
|
||||
@@ -26,6 +26,8 @@
|
||||
#import "RNSplashScreen.h"
|
||||
#import "ScheenshareEventEmiter.h"
|
||||
|
||||
#import <react-native-webrtc/WebRTCModuleOptions.h>
|
||||
|
||||
#if !defined(JITSI_MEET_SDK_LITE)
|
||||
#import <RNGoogleSignin/RNGoogleSignin.h>
|
||||
#import "Dropbox.h"
|
||||
@@ -52,6 +54,12 @@
|
||||
|
||||
- (instancetype)init {
|
||||
if (self = [super init]) {
|
||||
#if 0
|
||||
// Initialize WebRTC options.
|
||||
WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance];
|
||||
options.loggingSeverity = RTCLoggingSeverityInfo;
|
||||
#endif
|
||||
|
||||
// Initialize the one and only bridge for interfacing with React Native.
|
||||
_bridgeWrapper = [[RCTBridgeWrapper alloc] init];
|
||||
|
||||
@@ -63,11 +71,6 @@
|
||||
|
||||
// Register a log handler for React.
|
||||
registerReactLogHandler();
|
||||
|
||||
#if 0
|
||||
// Enable WebRTC logs
|
||||
RTCSetMinDebugLogLevel(RTCLoggingSeverityInfo);
|
||||
#endif
|
||||
}
|
||||
|
||||
return self;
|
||||
|
||||
141
package-lock.json
generated
141
package-lock.json
generated
@@ -34,7 +34,7 @@
|
||||
"@react-native-community/clipboard": "1.5.1",
|
||||
"@react-native-community/netinfo": "7.1.7",
|
||||
"@react-native-community/slider": "4.1.12",
|
||||
"@react-native-google-signin/google-signin": "7.0.4",
|
||||
"@react-native-google-signin/google-signin": "9.0.2",
|
||||
"@react-navigation/bottom-tabs": "6.5.3",
|
||||
"@react-navigation/elements": "1.3.13",
|
||||
"@react-navigation/material-top-tabs": "6.5.2",
|
||||
@@ -49,7 +49,7 @@
|
||||
"@types/w3c-web-hid": "1.0.3",
|
||||
"@vladmandic/human": "2.6.5",
|
||||
"@vladmandic/human-models": "2.5.9",
|
||||
"@xmldom/xmldom": "0.7.9",
|
||||
"@xmldom/xmldom": "0.8.7",
|
||||
"amplitude-js": "8.2.1",
|
||||
"base64-js": "1.3.1",
|
||||
"bc-css-flags": "3.0.0",
|
||||
@@ -68,7 +68,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1615.0.0+a23a8c7c/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1617.0.0+faeff49a/lib-jitsi-meet.tgz",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
@@ -107,7 +107,7 @@
|
||||
"react-native-url-polyfill": "1.3.0",
|
||||
"react-native-video": "https://git@github.com/react-native-video/react-native-video#7c48ae7c8544b2b537fb60194e9620b9fcceae52",
|
||||
"react-native-watch-connectivity": "1.0.11",
|
||||
"react-native-webrtc": "106.0.7",
|
||||
"react-native-webrtc": "111.0.0",
|
||||
"react-native-webview": "11.15.1",
|
||||
"react-native-youtube-iframe": "2.2.1",
|
||||
"react-redux": "7.1.0",
|
||||
@@ -171,7 +171,7 @@
|
||||
"ts-loader": "9.4.1",
|
||||
"typescript": "4.7.4",
|
||||
"unorm": "1.6.0",
|
||||
"webpack": "5.57.1",
|
||||
"webpack": "5.76.0",
|
||||
"webpack-bundle-analyzer": "4.4.2",
|
||||
"webpack-cli": "4.9.0",
|
||||
"webpack-dev-server": "4.7.3"
|
||||
@@ -5245,12 +5245,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-google-signin/google-signin": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-google-signin/google-signin/-/google-signin-7.0.4.tgz",
|
||||
"integrity": "sha512-N5uVDlTp/mgpa5gFr6VIr8pldt68jlmHOlqcTSnSwBuZXusXMiK53DCIOWuYk4OJ1rlb8Esa9J4FJwUB0psU9Q==",
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-google-signin/google-signin/-/google-signin-9.0.2.tgz",
|
||||
"integrity": "sha512-oTD0ZT6ZSH7CZjM1i6hR/gFAXv9As7nxhN99XGliNxf6SChZcQRwGeaz1DjoYrWExWDVX00EH1Per3dMjhAWkQ==",
|
||||
"peerDependencies": {
|
||||
"expo": ">=47.0.0",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"expo": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/assets": {
|
||||
@@ -6173,9 +6179,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "0.0.50",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz",
|
||||
"integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==",
|
||||
"version": "0.0.51",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz",
|
||||
"integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
@@ -7229,9 +7235,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.9.tgz",
|
||||
"integrity": "sha512-yceMpm/xd4W2a85iqZyO09gTnHvXF6pyiWjD2jcOJs7hRoZtNNOO1eJlhHj1ixA+xip2hOyGn+LgcvLCMo5zXA==",
|
||||
"version": "0.8.7",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.7.tgz",
|
||||
"integrity": "sha512-sI1Ly2cODlWStkINzqGrZ8K6n+MTSbAeQnAipGyL+KZCXuHaRlj2gyyy8B/9MvsFFqN7XHryQnB2QwhzvJXovg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
@@ -9688,9 +9694,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.9.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.0.tgz",
|
||||
"integrity": "sha512-weDYmzbBygL7HzGGS26M3hGQx68vehdEg6VUmqSOaFzXExFqlnKuSvsEJCVGQHScS8CQMbrAqftT+AzzHNt/YA==",
|
||||
"version": "5.12.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
|
||||
"integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
@@ -13382,8 +13388,8 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1615.0.0+a23a8c7c/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-OfmtbNqL4FoUHK/RA4VxkYVMT8kbPPQA2au5hmVeadB+PVhqJqlHUgbsV+H4yGJjP9AEcY7UejXtVVkPSG4aHA==",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1617.0.0+faeff49a/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-axTNZ1eYEq4h1ihK7YSmNXknkXIRI+xdVTAf+jqzmRU92MVdjr+dA9aw9i7GRaQzCdFaJiY8QudCliNTWNbv1Q==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -16567,10 +16573,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-webrtc": {
|
||||
"version": "106.0.7",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-106.0.7.tgz",
|
||||
"integrity": "sha512-Ro/NoxN/9/b/bcZErM+oVbWC8ZCplp00sHWDOOh289KDkAkFYZdyFZAKsgJQoMj5rOJZVzIIxuJW91ox+0k92w==",
|
||||
"hasInstallScript": true,
|
||||
"version": "111.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-111.0.0.tgz",
|
||||
"integrity": "sha512-eCvjPiU28cT85K8TBV7geAA92+uCldhmJ6F+3CMtAAG7U4AOQzxYOI5HC3h9Lt2zfTwsUzg3tyamqWf00PWndA==",
|
||||
"dependencies": {
|
||||
"adm-zip": "0.5.9",
|
||||
"base64-js": "1.5.1",
|
||||
@@ -19573,9 +19578,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
|
||||
"integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
@@ -19642,35 +19647,35 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.57.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.57.1.tgz",
|
||||
"integrity": "sha512-kHszukYjTPVfCOEyrUthA3jqJwduY/P3eO8I0gMNOZGIQWKAwZftxmp5hq6paophvwo9NoUrcZOecs9ulOyyTg==",
|
||||
"version": "5.76.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz",
|
||||
"integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.0",
|
||||
"@types/estree": "^0.0.50",
|
||||
"@types/eslint-scope": "^3.7.3",
|
||||
"@types/estree": "^0.0.51",
|
||||
"@webassemblyjs/ast": "1.11.1",
|
||||
"@webassemblyjs/wasm-edit": "1.11.1",
|
||||
"@webassemblyjs/wasm-parser": "1.11.1",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn": "^8.7.1",
|
||||
"acorn-import-assertions": "^1.7.6",
|
||||
"browserslist": "^4.14.5",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
"enhanced-resolve": "^5.8.3",
|
||||
"enhanced-resolve": "^5.10.0",
|
||||
"es-module-lexer": "^0.9.0",
|
||||
"eslint-scope": "5.1.1",
|
||||
"events": "^3.2.0",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"graceful-fs": "^4.2.4",
|
||||
"json-parse-better-errors": "^1.0.2",
|
||||
"graceful-fs": "^4.2.9",
|
||||
"json-parse-even-better-errors": "^2.3.1",
|
||||
"loader-runner": "^4.2.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"neo-async": "^2.6.2",
|
||||
"schema-utils": "^3.1.0",
|
||||
"tapable": "^2.1.1",
|
||||
"terser-webpack-plugin": "^5.1.3",
|
||||
"watchpack": "^2.2.0",
|
||||
"webpack-sources": "^3.2.0"
|
||||
"watchpack": "^2.4.0",
|
||||
"webpack-sources": "^3.2.3"
|
||||
},
|
||||
"bin": {
|
||||
"webpack": "bin/webpack.js"
|
||||
@@ -24266,9 +24271,9 @@
|
||||
"integrity": "sha512-CiuLZ2orueBiWHYxfaJF57jQY6HY2Q3z5pdAE4MKH8EqIImr/jgDJrJ/UxOVZHK1Ng9P+XlGIKfVIcuWZ6guuA=="
|
||||
},
|
||||
"@react-native-google-signin/google-signin": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-google-signin/google-signin/-/google-signin-7.0.4.tgz",
|
||||
"integrity": "sha512-N5uVDlTp/mgpa5gFr6VIr8pldt68jlmHOlqcTSnSwBuZXusXMiK53DCIOWuYk4OJ1rlb8Esa9J4FJwUB0psU9Q=="
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-google-signin/google-signin/-/google-signin-9.0.2.tgz",
|
||||
"integrity": "sha512-oTD0ZT6ZSH7CZjM1i6hR/gFAXv9As7nxhN99XGliNxf6SChZcQRwGeaz1DjoYrWExWDVX00EH1Per3dMjhAWkQ=="
|
||||
},
|
||||
"@react-native/assets": {
|
||||
"version": "1.0.0",
|
||||
@@ -24930,9 +24935,9 @@
|
||||
}
|
||||
},
|
||||
"@types/estree": {
|
||||
"version": "0.0.50",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz",
|
||||
"integrity": "sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==",
|
||||
"version": "0.0.51",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz",
|
||||
"integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/express": {
|
||||
@@ -25746,9 +25751,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@xmldom/xmldom": {
|
||||
"version": "0.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.9.tgz",
|
||||
"integrity": "sha512-yceMpm/xd4W2a85iqZyO09gTnHvXF6pyiWjD2jcOJs7hRoZtNNOO1eJlhHj1ixA+xip2hOyGn+LgcvLCMo5zXA=="
|
||||
"version": "0.8.7",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.7.tgz",
|
||||
"integrity": "sha512-sI1Ly2cODlWStkINzqGrZ8K6n+MTSbAeQnAipGyL+KZCXuHaRlj2gyyy8B/9MvsFFqN7XHryQnB2QwhzvJXovg=="
|
||||
},
|
||||
"@xobotyi/scrollbar-width": {
|
||||
"version": "1.9.5",
|
||||
@@ -27647,9 +27652,9 @@
|
||||
}
|
||||
},
|
||||
"enhanced-resolve": {
|
||||
"version": "5.9.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.0.tgz",
|
||||
"integrity": "sha512-weDYmzbBygL7HzGGS26M3hGQx68vehdEg6VUmqSOaFzXExFqlnKuSvsEJCVGQHScS8CQMbrAqftT+AzzHNt/YA==",
|
||||
"version": "5.12.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
|
||||
"integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
@@ -30458,8 +30463,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1615.0.0+a23a8c7c/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-OfmtbNqL4FoUHK/RA4VxkYVMT8kbPPQA2au5hmVeadB+PVhqJqlHUgbsV+H4yGJjP9AEcY7UejXtVVkPSG4aHA==",
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1617.0.0+faeff49a/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-axTNZ1eYEq4h1ihK7YSmNXknkXIRI+xdVTAf+jqzmRU92MVdjr+dA9aw9i7GRaQzCdFaJiY8QudCliNTWNbv1Q==",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.0.0",
|
||||
"@jitsi/logger": "2.0.0",
|
||||
@@ -32877,9 +32882,9 @@
|
||||
}
|
||||
},
|
||||
"react-native-webrtc": {
|
||||
"version": "106.0.7",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-106.0.7.tgz",
|
||||
"integrity": "sha512-Ro/NoxN/9/b/bcZErM+oVbWC8ZCplp00sHWDOOh289KDkAkFYZdyFZAKsgJQoMj5rOJZVzIIxuJW91ox+0k92w==",
|
||||
"version": "111.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-111.0.0.tgz",
|
||||
"integrity": "sha512-eCvjPiU28cT85K8TBV7geAA92+uCldhmJ6F+3CMtAAG7U4AOQzxYOI5HC3h9Lt2zfTwsUzg3tyamqWf00PWndA==",
|
||||
"requires": {
|
||||
"adm-zip": "0.5.9",
|
||||
"base64-js": "1.5.1",
|
||||
@@ -35130,9 +35135,9 @@
|
||||
"integrity": "sha512-5otny2JrfRNKIc+zi1YSOrNxXe47trEQbpY6g/MtHrFwLumKSJyAIobGXH1tlEBezE95eIsmDokBbUZtIZTvvA=="
|
||||
},
|
||||
"watchpack": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
|
||||
"integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
@@ -35184,35 +35189,35 @@
|
||||
}
|
||||
},
|
||||
"webpack": {
|
||||
"version": "5.57.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.57.1.tgz",
|
||||
"integrity": "sha512-kHszukYjTPVfCOEyrUthA3jqJwduY/P3eO8I0gMNOZGIQWKAwZftxmp5hq6paophvwo9NoUrcZOecs9ulOyyTg==",
|
||||
"version": "5.76.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz",
|
||||
"integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/eslint-scope": "^3.7.0",
|
||||
"@types/estree": "^0.0.50",
|
||||
"@types/eslint-scope": "^3.7.3",
|
||||
"@types/estree": "^0.0.51",
|
||||
"@webassemblyjs/ast": "1.11.1",
|
||||
"@webassemblyjs/wasm-edit": "1.11.1",
|
||||
"@webassemblyjs/wasm-parser": "1.11.1",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn": "^8.7.1",
|
||||
"acorn-import-assertions": "^1.7.6",
|
||||
"browserslist": "^4.14.5",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
"enhanced-resolve": "^5.8.3",
|
||||
"enhanced-resolve": "^5.10.0",
|
||||
"es-module-lexer": "^0.9.0",
|
||||
"eslint-scope": "5.1.1",
|
||||
"events": "^3.2.0",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"graceful-fs": "^4.2.4",
|
||||
"json-parse-better-errors": "^1.0.2",
|
||||
"graceful-fs": "^4.2.9",
|
||||
"json-parse-even-better-errors": "^2.3.1",
|
||||
"loader-runner": "^4.2.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"neo-async": "^2.6.2",
|
||||
"schema-utils": "^3.1.0",
|
||||
"tapable": "^2.1.1",
|
||||
"terser-webpack-plugin": "^5.1.3",
|
||||
"watchpack": "^2.2.0",
|
||||
"webpack-sources": "^3.2.0"
|
||||
"watchpack": "^2.4.0",
|
||||
"webpack-sources": "^3.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"schema-utils": {
|
||||
|
||||
10
package.json
10
package.json
@@ -39,7 +39,7 @@
|
||||
"@react-native-community/clipboard": "1.5.1",
|
||||
"@react-native-community/netinfo": "7.1.7",
|
||||
"@react-native-community/slider": "4.1.12",
|
||||
"@react-native-google-signin/google-signin": "7.0.4",
|
||||
"@react-native-google-signin/google-signin": "9.0.2",
|
||||
"@react-navigation/bottom-tabs": "6.5.3",
|
||||
"@react-navigation/elements": "1.3.13",
|
||||
"@react-navigation/material-top-tabs": "6.5.2",
|
||||
@@ -54,7 +54,7 @@
|
||||
"@types/w3c-web-hid": "1.0.3",
|
||||
"@vladmandic/human": "2.6.5",
|
||||
"@vladmandic/human-models": "2.5.9",
|
||||
"@xmldom/xmldom": "0.7.9",
|
||||
"@xmldom/xmldom": "0.8.7",
|
||||
"amplitude-js": "8.2.1",
|
||||
"base64-js": "1.3.1",
|
||||
"bc-css-flags": "3.0.0",
|
||||
@@ -73,7 +73,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1615.0.0+a23a8c7c/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1617.0.0+faeff49a/lib-jitsi-meet.tgz",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
@@ -112,7 +112,7 @@
|
||||
"react-native-url-polyfill": "1.3.0",
|
||||
"react-native-video": "https://git@github.com/react-native-video/react-native-video#7c48ae7c8544b2b537fb60194e9620b9fcceae52",
|
||||
"react-native-watch-connectivity": "1.0.11",
|
||||
"react-native-webrtc": "106.0.7",
|
||||
"react-native-webrtc": "111.0.0",
|
||||
"react-native-webview": "11.15.1",
|
||||
"react-native-youtube-iframe": "2.2.1",
|
||||
"react-redux": "7.1.0",
|
||||
@@ -176,7 +176,7 @@
|
||||
"ts-loader": "9.4.1",
|
||||
"typescript": "4.7.4",
|
||||
"unorm": "1.6.0",
|
||||
"webpack": "5.57.1",
|
||||
"webpack": "5.76.0",
|
||||
"webpack-bundle-analyzer": "4.4.2",
|
||||
"webpack-cli": "4.9.0",
|
||||
"webpack-dev-server": "4.7.3"
|
||||
|
||||
@@ -38,7 +38,7 @@ import { navigate }
|
||||
import { shouldEnableAutoKnock } from '../../../mobile/navigation/functions';
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
import { setPictureInPictureEnabled } from '../../../mobile/picture-in-picture/functions';
|
||||
import Captions from '../../../subtitles/components/Captions.native';
|
||||
import Captions from '../../../subtitles/components/native/Captions';
|
||||
import { setToolboxVisible } from '../../../toolbox/actions';
|
||||
import Toolbox from '../../../toolbox/components/native/Toolbox';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions';
|
||||
|
||||
@@ -358,7 +358,7 @@ class DesktopPicker extends PureComponent<IProps, IState> {
|
||||
type => {
|
||||
return {
|
||||
accessibilityLabel: t(TAB_LABELS[type as keyof typeof TAB_LABELS]),
|
||||
id: `${type}-tab`,
|
||||
id: `${type}`,
|
||||
controlsId: `${type}-panel`,
|
||||
label: t(TAB_LABELS[type as keyof typeof TAB_LABELS])
|
||||
};
|
||||
@@ -369,7 +369,7 @@ class DesktopPicker extends PureComponent<IProps, IState> {
|
||||
accessibilityLabel = { t('dialog.sharingTabs') }
|
||||
className = 'desktop-picker-tabs-container'
|
||||
onChange = { this._onTabSelected }
|
||||
selected = { `${this.state.selectedTab}-tab` }
|
||||
selected = { `${this.state.selectedTab}` }
|
||||
tabs = { tabs } />);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export default createStyleSheet({
|
||||
*/
|
||||
signOutButton: {
|
||||
alignSelf: 'center',
|
||||
maxWidth: 104,
|
||||
maxWidth: 120,
|
||||
width: 'auto'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ import StageParticipantNameLabel from '../../display-name/components/web/StagePa
|
||||
import { FILMSTRIP_BREAKPOINT } from '../../filmstrip/constants';
|
||||
import { getVerticalViewMaxWidth, isFilmstripResizable } from '../../filmstrip/functions.web';
|
||||
import SharedVideo from '../../shared-video/components/web/SharedVideo';
|
||||
import Captions from '../../subtitles/components/Captions.web';
|
||||
import Captions from '../../subtitles/components/web/Captions';
|
||||
import { setTileView } from '../../video-layout/actions.web';
|
||||
import Whiteboard from '../../whiteboard/components/web/Whiteboard';
|
||||
import { isWhiteboardEnabled } from '../../whiteboard/functions';
|
||||
|
||||
@@ -37,6 +37,9 @@ import SecurityDialog
|
||||
import SpeakerStats
|
||||
// @ts-ignore
|
||||
from '../../../../../speaker-stats/components/native/SpeakerStats';
|
||||
import LanguageSelectorDialog
|
||||
// @ts-ignore
|
||||
from '../../../../../subtitles/components/native/LanguageSelectorDialog';
|
||||
// @ts-ignore
|
||||
import { screen } from '../../../routes';
|
||||
import {
|
||||
@@ -54,7 +57,8 @@ import {
|
||||
securityScreenOptions,
|
||||
settingsNavigationContainerScreenOptions,
|
||||
sharedDocumentScreenOptions,
|
||||
speakerStatsScreenOptions
|
||||
speakerStatsScreenOptions,
|
||||
subtitlesScreenOptions
|
||||
// @ts-ignore
|
||||
} from '../../../screenOptions';
|
||||
// @ts-ignore
|
||||
@@ -191,6 +195,13 @@ const ConferenceNavigationContainer = () => {
|
||||
...carmodeScreenOptions,
|
||||
title: t('carmode.labels.title')
|
||||
}} />
|
||||
<ConferenceStack.Screen
|
||||
component = { LanguageSelectorDialog }
|
||||
name = { screen.conference.subtitles }
|
||||
options = {{
|
||||
...subtitlesScreenOptions,
|
||||
title: t('transcribing.subtitles')
|
||||
}} />
|
||||
</ConferenceStack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
@@ -30,7 +30,8 @@ export const screen = {
|
||||
participants: 'Participants',
|
||||
gifsMenu: 'GIPHY',
|
||||
invite: 'Invite',
|
||||
sharedDocument: 'Shared document'
|
||||
sharedDocument: 'Shared document',
|
||||
subtitles: 'Subtitles'
|
||||
},
|
||||
lobby: {
|
||||
root: 'Lobby root',
|
||||
|
||||
@@ -134,6 +134,11 @@ export const recordingScreenOptions = presentationScreenOptions;
|
||||
*/
|
||||
export const liveStreamScreenOptions = presentationScreenOptions;
|
||||
|
||||
/**
|
||||
* Screen options for subtitles modal.
|
||||
*/
|
||||
export const subtitlesScreenOptions = presentationScreenOptions;
|
||||
|
||||
/**
|
||||
* Screen options for lobby modal.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { toggleDialog } from '../base/dialog/actions';
|
||||
|
||||
import LanguageSelectorDialogWeb from './components/LanguageSelectorDialog.web';
|
||||
import LanguageSelectorDialog from './components/web/LanguageSelectorDialog';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
@@ -14,6 +14,6 @@ export * from './actions.any';
|
||||
*/
|
||||
export function toggleLanguageSelectorDialog() {
|
||||
return function(dispatch: IStore['dispatch']) {
|
||||
dispatch(toggleDialog(LanguageSelectorDialogWeb));
|
||||
dispatch(toggleDialog(LanguageSelectorDialog));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, ReactElement } from 'react';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
|
||||
|
||||
/**
|
||||
* {@code AbstractCaptions} Properties.
|
||||
*/
|
||||
export type AbstractCaptionsProps = {
|
||||
export interface IAbstractCaptionsProps {
|
||||
|
||||
/**
|
||||
* Whether local participant is requesting to see subtitles.
|
||||
@@ -17,23 +18,22 @@ export type AbstractCaptionsProps = {
|
||||
* Mapped by id just to have the keys for convenience during the rendering
|
||||
* process.
|
||||
*/
|
||||
_transcripts?: Map<string, string>;
|
||||
};
|
||||
_transcripts: Map<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract React {@code Component} which can display speech-to-text results
|
||||
* from Jigasi as subtitles.
|
||||
*/
|
||||
export class AbstractCaptions<P extends AbstractCaptionsProps>
|
||||
extends Component<P> {
|
||||
export class AbstractCaptions<P extends IAbstractCaptionsProps> extends Component<P> {
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {React$Element}
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
render(): any {
|
||||
const { _requestingSubtitles, _transcripts } = this.props;
|
||||
|
||||
if (!_requestingSubtitles || !_transcripts || !_transcripts.size) {
|
||||
@@ -42,10 +42,12 @@ export class AbstractCaptions<P extends AbstractCaptionsProps>
|
||||
|
||||
const paragraphs = [];
|
||||
|
||||
// @ts-ignore
|
||||
for (const [ id, text ] of _transcripts ?? []) {
|
||||
paragraphs.push(this._renderParagraph(id, text));
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return this._renderSubtitlesContainer(paragraphs);
|
||||
}
|
||||
|
||||
@@ -58,7 +60,7 @@ export class AbstractCaptions<P extends AbstractCaptionsProps>
|
||||
* @param {string} _text - Subtitles text formatted with the participant's
|
||||
* name.
|
||||
* @protected
|
||||
* @returns {React$Element} - The React element which displays the text.
|
||||
* @returns {ReactElement} - The React element which displays the text.
|
||||
*/
|
||||
_renderParagraph(_id: string, _text: string) {
|
||||
return <></>;
|
||||
@@ -68,12 +70,12 @@ export class AbstractCaptions<P extends AbstractCaptionsProps>
|
||||
* Renders the subtitles container.
|
||||
*
|
||||
* @abstract
|
||||
* @param {Array<React$Element>} _el - An array of elements created
|
||||
* @param {Array<ReactElement>} _el - An array of elements created
|
||||
* for each subtitle using the {@link _renderParagraph} method.
|
||||
* @protected
|
||||
* @returns {React$Element} - The subtitles container.
|
||||
* @returns {ReactElement} - The subtitles container.
|
||||
*/
|
||||
_renderSubtitlesContainer(_el: Array<React.ReactElement>) {
|
||||
_renderSubtitlesContainer(_el: Array<ReactElement>) {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
@@ -129,7 +131,7 @@ export function _abstractMapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_requestingSubtitles,
|
||||
|
||||
// avoid rerenders by setting to props new empty Map instances.
|
||||
// avoid re-renders by setting to prop new empty Map instances.
|
||||
_transcripts: transcripts.size === 0 ? undefined : transcripts
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
|
||||
import { createToolbarEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { MEET_FEATURES } from '../../base/jwt/constants';
|
||||
import { isLocalParticipantModerator } from '../../base/participants/functions';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../base/toolbox/components/AbstractButton';
|
||||
// @ts-ignore
|
||||
import { maybeShowPremiumFeatureDialog } from '../../jaas/actions';
|
||||
|
||||
export interface IAbstractProps extends AbstractButtonProps {
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { ComponentType, useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import {
|
||||
TRANSLATION_LANGUAGES,
|
||||
TRANSLATION_LANGUAGES_HEAD
|
||||
} from '../../base/i18n/i18next';
|
||||
import { setRequestingSubtitles, updateTranslationLanguage } from '../actions.any';
|
||||
|
||||
|
||||
export interface IAbstractLanguageSelectorDialogProps {
|
||||
dispatch: Function;
|
||||
language: string;
|
||||
listItems: Array<any>;
|
||||
onLanguageSelected: (e: string) => void;
|
||||
subtitles: string;
|
||||
t: Function;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Higher Order Component taking in a concrete LanguageSelector component and
|
||||
* augmenting it with state/behavior common to both web and native implementations.
|
||||
*
|
||||
* @param {React.Component} Component - The concrete component.
|
||||
* @returns {React.Component}
|
||||
*/
|
||||
const AbstractLanguageSelectorDialog = (Component: ComponentType<IAbstractLanguageSelectorDialogProps>) => () => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const off = 'transcribing.subtitlesOff';
|
||||
|
||||
const [ subtitles, setSubtiles ] = useState(off);
|
||||
const language = useSelector((state: IReduxState) => state['features/subtitles']._language);
|
||||
|
||||
const transcription = useSelector((state: IReduxState) => state['features/base/config'].transcription);
|
||||
const translationLanguagesHead = transcription?.translationLanguagesHead ?? TRANSLATION_LANGUAGES_HEAD;
|
||||
const languagesHead = translationLanguagesHead?.map((lang: string) => `translation-languages:${lang}`);
|
||||
|
||||
// The off and the head languages are always on the top of the list. But once you are selecting
|
||||
// a language from the translationLanguages, that language is moved under the fixedItems list,
|
||||
// until a new languages is selected. FixedItems keep their positions.
|
||||
const fixedItems = [ off, ...languagesHead ];
|
||||
const translationLanguages = transcription?.translationLanguages ?? TRANSLATION_LANGUAGES;
|
||||
const languages = translationLanguages
|
||||
.map((lang: string) => `translation-languages:${lang}`)
|
||||
.filter((lang: string) => !(lang === subtitles || languagesHead?.includes(lang)));
|
||||
const listItems = (fixedItems?.includes(subtitles)
|
||||
? [ ...fixedItems, ...languages ]
|
||||
: [ ...fixedItems, subtitles, ...languages ])
|
||||
.map((lang, index) => {
|
||||
return {
|
||||
id: lang + index,
|
||||
lang,
|
||||
selected: lang === subtitles
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
language ? setSubtiles(language) : setSubtiles(off);
|
||||
}, []);
|
||||
|
||||
const onLanguageSelected = useCallback((e: string) => {
|
||||
setSubtiles(e);
|
||||
dispatch(updateTranslationLanguage(e));
|
||||
dispatch(setRequestingSubtitles(e !== off));
|
||||
}, [ language ]);
|
||||
|
||||
return (
|
||||
<Component
|
||||
dispatch = { dispatch }
|
||||
language = { language }
|
||||
listItems = { listItems }
|
||||
onLanguageSelected = { onLanguageSelected }
|
||||
subtitles = { subtitles }
|
||||
t = { t } />
|
||||
);
|
||||
};
|
||||
|
||||
export default AbstractLanguageSelectorDialog;
|
||||
@@ -1,45 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { CLOSE_CAPTIONS_ENABLED } from '../../base/flags/constants';
|
||||
import { getFeatureFlag } from '../../base/flags/functions';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { IconSubtitles } from '../../base/icons/svg';
|
||||
|
||||
import {
|
||||
AbstractClosedCaptionButton,
|
||||
_abstractMapStateToProps
|
||||
} from './AbstractClosedCaptionButton';
|
||||
|
||||
/**
|
||||
* A button which starts/stops the transcriptions.
|
||||
*/
|
||||
class ClosedCaptionButton
|
||||
extends AbstractClosedCaptionButton {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.cc';
|
||||
icon = IconSubtitles;
|
||||
label = 'transcribing.start';
|
||||
toggledLabel = 'transcribing.stop';
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for this component.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {Object} ownProps - The properties explicitly passed to the component
|
||||
* instance.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
export function mapStateToProps(state: Object, ownProps: Object) {
|
||||
const enabled = getFeatureFlag(state, CLOSE_CAPTIONS_ENABLED, true);
|
||||
const abstractProps = _abstractMapStateToProps(state, ownProps);
|
||||
|
||||
return {
|
||||
...abstractProps,
|
||||
visible: abstractProps.visible && enabled
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(ClosedCaptionButton));
|
||||
@@ -1,141 +0,0 @@
|
||||
import i18next from 'i18next';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { translate, translateToHTML } from '../../base/i18n/functions';
|
||||
import { TRANSLATION_LANGUAGES, TRANSLATION_LANGUAGES_HEAD } from '../../base/i18n/i18next';
|
||||
import Dialog from '../../base/ui/components/web/Dialog';
|
||||
import { openSettingsDialog } from '../../settings/actions';
|
||||
import { SETTINGS_TABS } from '../../settings/constants';
|
||||
import { setRequestingSubtitles, toggleLanguageSelectorDialog, updateTranslationLanguage } from '../actions';
|
||||
|
||||
import LanguageList from './LanguageList.web';
|
||||
|
||||
|
||||
interface ILanguageSelectorDialogProps extends WithTranslation {
|
||||
_language: string;
|
||||
_translationLanguages: Array<string>;
|
||||
_translationLanguagesHead: Array<string>;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
paragraphWrapper: {
|
||||
fontSize: 14,
|
||||
margin: '10px 0px',
|
||||
color: theme.palette.text01
|
||||
},
|
||||
spanWrapper: {
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
color: theme.palette.link01,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.ui04,
|
||||
color: theme.palette.link01Hover
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Component that renders the subtitle language selector dialog.
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
const LanguageSelectorDialog = ({
|
||||
t,
|
||||
_language,
|
||||
_translationLanguages,
|
||||
_translationLanguagesHead
|
||||
}: ILanguageSelectorDialogProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const off = 'transcribing.subtitlesOff';
|
||||
const [ language, setLanguage ] = useState(off);
|
||||
|
||||
const languagesHead = _translationLanguagesHead.map((lang: string) => `translation-languages:${lang}`);
|
||||
|
||||
// The off and the head languages are always on the top of the list. But once you are selecting
|
||||
// a language from the translationLanguages, that language is moved under the fixedItems list,
|
||||
// until a new languages is selected. FixedItems keep their positions.
|
||||
const fixedItems = [ off, ...languagesHead ];
|
||||
const languages = _translationLanguages
|
||||
.map((lang: string) => `translation-languages:${lang}`)
|
||||
.filter((lang: string) => !(lang === language || languagesHead.includes(lang)));
|
||||
|
||||
const listItems = (fixedItems.includes(language)
|
||||
? [ ...fixedItems, ...languages ]
|
||||
: [ ...fixedItems, language, ...languages ])
|
||||
.map((lang, index) => {
|
||||
return {
|
||||
id: lang + index,
|
||||
lang,
|
||||
selected: lang === language
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
_language ? setLanguage(_language) : setLanguage(off);
|
||||
}, []);
|
||||
|
||||
const onLanguageSelected = useCallback((e: string) => {
|
||||
setLanguage(e);
|
||||
dispatch(updateTranslationLanguage(e));
|
||||
dispatch(setRequestingSubtitles(e !== off));
|
||||
dispatch(toggleLanguageSelectorDialog());
|
||||
}, [ _language ]);
|
||||
|
||||
const onSourceLanguageClick = useCallback(() => {
|
||||
dispatch(openSettingsDialog(SETTINGS_TABS.PROFILE, false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancel = {{ hidden: true }}
|
||||
ok = {{ hidden: true }}
|
||||
titleKey = 'transcribing.subtitles'>
|
||||
<p className = { styles.paragraphWrapper } >
|
||||
{
|
||||
translateToHTML(t, 'transcribing.sourceLanguageDesc', {
|
||||
'sourceLanguage': t(`languages:${i18next.language}`).toLowerCase()
|
||||
})
|
||||
}<span
|
||||
className = { styles.spanWrapper }
|
||||
onClick = { onSourceLanguageClick }>{t('transcribing.sourceLanguageHere')}.</span>
|
||||
</p>
|
||||
<LanguageList
|
||||
items = { listItems }
|
||||
onLanguageSelected = { onLanguageSelected }
|
||||
selectedLanguage = { language } />
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code LanguageSelectorDialog} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
const { conference } = state['features/base/conference'];
|
||||
const { _language } = state['features/subtitles'];
|
||||
const { transcription } = state['features/base/config'];
|
||||
|
||||
const languages = transcription?.translationLanguages ?? TRANSLATION_LANGUAGES;
|
||||
const languagesHead = transcription?.translationLanguagesHead ?? TRANSLATION_LANGUAGES_HEAD;
|
||||
|
||||
return {
|
||||
_conference: conference,
|
||||
_language,
|
||||
_translationLanguages: languages,
|
||||
_translationLanguagesHead: languagesHead
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(LanguageSelectorDialog));
|
||||
@@ -1,32 +1,35 @@
|
||||
// @flow
|
||||
/* eslint-disable lines-around-comment */
|
||||
|
||||
import React from 'react';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { GestureResponderEvent, StyleProp } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Container from '../../base/react/components/native/Container';
|
||||
import Text from '../../base/react/components/native/Text';
|
||||
|
||||
// @ts-ignore
|
||||
import Container from '../../../base/react/components/native/Container';
|
||||
// @ts-ignore
|
||||
import Text from '../../../base/react/components/native/Text';
|
||||
import {
|
||||
AbstractCaptions,
|
||||
type AbstractCaptionsProps,
|
||||
type IAbstractCaptionsProps,
|
||||
_abstractMapStateToProps
|
||||
} from './AbstractCaptions';
|
||||
} from '../AbstractCaptions';
|
||||
|
||||
// @ts-ignore
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link Captions}.
|
||||
*/
|
||||
type Props = AbstractCaptionsProps & {
|
||||
onPress: Function
|
||||
};
|
||||
interface IProps extends IAbstractCaptionsProps {
|
||||
onPress: (event: GestureResponderEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* React {@code Component} which can display speech-to-text results from
|
||||
* Jigasi as subtitles.
|
||||
*/
|
||||
class Captions
|
||||
extends AbstractCaptions<Props> {
|
||||
|
||||
class Captions extends AbstractCaptions<IProps> {
|
||||
/**
|
||||
* Renders the transcription text.
|
||||
*
|
||||
@@ -35,14 +38,14 @@ class Captions
|
||||
* @param {string} text - Subtitles text formatted with the participant's
|
||||
* name.
|
||||
* @protected
|
||||
* @returns {React$Element} - The React element which displays the text.
|
||||
* @returns {ReactElement} - The React element which displays the text.
|
||||
*/
|
||||
_renderParagraph(id: string, text: string): React$Element<*> {
|
||||
_renderParagraph(id: string, text: string): ReactElement {
|
||||
return (
|
||||
<Text
|
||||
key = { id }
|
||||
onPress = { this.props.onPress }
|
||||
style = { styles.subtitle } >
|
||||
style = { styles.captionsSubtitles as StyleProp<Object> } >
|
||||
{ text }
|
||||
</Text>
|
||||
);
|
||||
@@ -51,19 +54,19 @@ class Captions
|
||||
/**
|
||||
* Renders the subtitles container.
|
||||
*
|
||||
* @param {Array<React$Element>} paragraphs - An array of elements created
|
||||
* @param {Array<ReactElement>} paragraphs - An array of elements created
|
||||
* for each subtitle using the {@link _renderParagraph} method.
|
||||
* @protected
|
||||
* @returns {React$Element} - The subtitles container.
|
||||
* @returns {ReactElement} - The subtitles container.
|
||||
*/
|
||||
_renderSubtitlesContainer(
|
||||
paragraphs: Array<React$Element<*>>): React$Element<*> {
|
||||
_renderSubtitlesContainer(paragraphs: Array<ReactElement>): ReactElement {
|
||||
return (
|
||||
<Container style = { styles.subtitlesContainer } >
|
||||
<Container style = { styles.captionsSubtitlesContainer } >
|
||||
{ paragraphs }
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
export default connect(_abstractMapStateToProps)(Captions);
|
||||
@@ -0,0 +1,65 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { CLOSE_CAPTIONS_ENABLED } from '../../../base/flags/constants';
|
||||
import { getFeatureFlag } from '../../../base/flags/functions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconSubtitles } from '../../../base/icons/svg';
|
||||
import { navigate }
|
||||
// @ts-ignore
|
||||
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
// @ts-ignore
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
import {
|
||||
AbstractClosedCaptionButton,
|
||||
IAbstractProps,
|
||||
_abstractMapStateToProps
|
||||
} from '../AbstractClosedCaptionButton';
|
||||
|
||||
|
||||
/**
|
||||
* A button which starts/stops the transcriptions.
|
||||
*/
|
||||
class ClosedCaptionButton
|
||||
extends AbstractClosedCaptionButton {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.cc';
|
||||
icon = IconSubtitles;
|
||||
label = 'toolbar.startSubtitles';
|
||||
labelProps = {
|
||||
language: this.props.t(this.props._language),
|
||||
languages: this.props.t(this.props.languages ?? ''),
|
||||
languagesHead: this.props.t(this.props.languagesHead ?? '')
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle language selection dialog.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClickOpenLanguageSelector() {
|
||||
navigate(screen.conference.subtitles);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for this component.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {Object} ownProps - The properties explicitly passed to the component
|
||||
* instance.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
export function mapStateToProps(state: IReduxState, ownProps: IAbstractProps) {
|
||||
const enabled = getFeatureFlag(state, CLOSE_CAPTIONS_ENABLED, true);
|
||||
const abstractProps = _abstractMapStateToProps(state, ownProps);
|
||||
|
||||
return {
|
||||
...abstractProps,
|
||||
visible: abstractProps.visible && enabled
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(ClosedCaptionButton));
|
||||
48
react/features/subtitles/components/native/LanguageList.tsx
Normal file
48
react/features/subtitles/components/native/LanguageList.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
|
||||
import React from 'react';
|
||||
import { ScrollView } from 'react-native';
|
||||
|
||||
import LanguageListItem from './LanguageListItem';
|
||||
// @ts-ignore
|
||||
import styles from './styles';
|
||||
|
||||
interface ILanguageListProps {
|
||||
items: Array<ILanguageItem>;
|
||||
onLanguageSelected: (lang: string) => void;
|
||||
selectedLanguage: string;
|
||||
}
|
||||
|
||||
|
||||
interface ILanguageItem {
|
||||
id: string;
|
||||
lang: string;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders the security options dialog.
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
const LanguageList = ({ items, onLanguageSelected }: ILanguageListProps) => {
|
||||
|
||||
const listItems = items?.map(item => (
|
||||
<LanguageListItem
|
||||
key = { item.id }
|
||||
// @ts-ignore
|
||||
lang = { item.lang }
|
||||
onLanguageSelected = { onLanguageSelected }
|
||||
selected = { item.selected } />
|
||||
));
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
bounces = { false }
|
||||
style = { styles.itemsContainer }>
|
||||
{ listItems }
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageList;
|
||||
@@ -0,0 +1,70 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { StyleProp, TouchableHighlight, View, ViewStyle } from 'react-native';
|
||||
import { Text } from 'react-native-paper';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconCheck } from '../../../base/icons/svg';
|
||||
|
||||
// @ts-ignore
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
interface ILanguageListItemProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Language string.
|
||||
*/
|
||||
lang: string;
|
||||
|
||||
/**
|
||||
* Callback for language selection.
|
||||
*/
|
||||
onLanguageSelected: (lang: string) => void;
|
||||
|
||||
/**
|
||||
* If language item is selected or not.
|
||||
*/
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders the language list item.
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
|
||||
const LanguageListItem = ({ t, lang, selected, onLanguageSelected
|
||||
}: ILanguageListItemProps) => {
|
||||
|
||||
const onLanguageSelectedWrapper
|
||||
= useCallback(() => onLanguageSelected(lang), [ lang ]);
|
||||
|
||||
return (
|
||||
<View style = { styles.languageItemWrapper as StyleProp<ViewStyle> }>
|
||||
<View style = { styles.iconWrapper }>
|
||||
{
|
||||
selected
|
||||
&& <Icon
|
||||
size = { 20 }
|
||||
src = { IconCheck } />
|
||||
}
|
||||
</View>
|
||||
<TouchableHighlight
|
||||
onPress = { onLanguageSelectedWrapper }
|
||||
underlayColor = { 'transparent' } >
|
||||
<Text
|
||||
style = { [
|
||||
styles.languageItemText,
|
||||
selected && styles.activeLanguageItemText ] }>
|
||||
{ t(lang) }
|
||||
</Text>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(LanguageListItem);
|
||||
@@ -0,0 +1,45 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
// @ts-ignore
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import { goBack }
|
||||
// @ts-ignore
|
||||
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import AbstractLanguageSelectorDialog, {
|
||||
IAbstractLanguageSelectorDialogProps
|
||||
} from '../AbstractLanguageSelectorDialog';
|
||||
|
||||
import LanguageList from './LanguageList';
|
||||
// @ts-ignore
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
const LanguageSelectorDialog = (props: IAbstractLanguageSelectorDialogProps) => {
|
||||
const { language, listItems, onLanguageSelected, subtitles } = props;
|
||||
|
||||
const onSelected = useCallback((e: string) => {
|
||||
onLanguageSelected(e);
|
||||
goBack();
|
||||
}, [ language ]);
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<JitsiScreen
|
||||
disableForcedKeyboardDismiss = { true }
|
||||
style = { styles.subtitlesContainer }>
|
||||
<LanguageList
|
||||
items = { listItems }
|
||||
onLanguageSelected = { onSelected }
|
||||
selectedLanguage = { subtitles } />
|
||||
</JitsiScreen>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* We apply AbstractLanguageSelector to fill in the AbstractProps common
|
||||
* to both the web and native implementations.
|
||||
*/
|
||||
// eslint-disable-next-line new-cap
|
||||
export default AbstractLanguageSelectorDialog(LanguageSelectorDialog);
|
||||
63
react/features/subtitles/components/native/styles.js
Normal file
63
react/features/subtitles/components/native/styles.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { BoxModel } from '../../../base/styles/components/styles/BoxModel';
|
||||
import {
|
||||
ColorPalette
|
||||
} from '../../../base/styles/components/styles/ColorPalette';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
/**
|
||||
* The styles of the React {@code Component}s of the feature subtitles.
|
||||
*/
|
||||
export default {
|
||||
languageItemWrapper: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
||||
iconWrapper: {
|
||||
width: 32
|
||||
},
|
||||
|
||||
activeLanguageItemText: {
|
||||
...BaseTheme.typography.bodyShortBoldLarge
|
||||
},
|
||||
|
||||
languageItemText: {
|
||||
...BaseTheme.typography.bodyShortRegularLarge,
|
||||
color: BaseTheme.palette.text01,
|
||||
marginLeft: BaseTheme.spacing[2],
|
||||
marginVertical: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
subtitlesContainer: {
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for subtitle paragraph.
|
||||
*/
|
||||
captionsSubtitles: {
|
||||
backgroundColor: ColorPalette.black,
|
||||
borderRadius: BoxModel.margin / 4,
|
||||
color: ColorPalette.white,
|
||||
marginBottom: BoxModel.margin,
|
||||
padding: BoxModel.padding / 2
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the subtitles container.
|
||||
*/
|
||||
captionsSubtitlesContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 0,
|
||||
justifyContent: 'flex-end',
|
||||
margin: BoxModel.margin
|
||||
},
|
||||
|
||||
itemsContainer: {
|
||||
marginHorizontal: BaseTheme.spacing[4],
|
||||
marginVertical: BaseTheme.spacing[4]
|
||||
}
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { BoxModel } from '../../base/styles/components/styles/BoxModel';
|
||||
import { ColorPalette } from '../../base/styles/components/styles/ColorPalette';
|
||||
import { createStyleSheet } from '../../base/styles/functions.any';
|
||||
|
||||
/**
|
||||
* The styles of the React {@code Component}s of the feature subtitles.
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
|
||||
/**
|
||||
* Style for subtitle paragraph.
|
||||
*/
|
||||
subtitle: {
|
||||
backgroundColor: ColorPalette.black,
|
||||
borderRadius: BoxModel.margin / 4,
|
||||
color: ColorPalette.white,
|
||||
marginBottom: BoxModel.margin,
|
||||
padding: BoxModel.padding / 2
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the subtitles container.
|
||||
*/
|
||||
subtitlesContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 0,
|
||||
justifyContent: 'flex-end',
|
||||
margin: BoxModel.margin
|
||||
}
|
||||
});
|
||||
@@ -1,23 +1,23 @@
|
||||
import React from 'react';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { getLocalParticipant } from '../../base/participants/functions';
|
||||
import { getLargeVideoParticipant } from '../../large-video/functions';
|
||||
import { isLayoutTileView } from '../../video-layout/functions.web';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import { getLargeVideoParticipant } from '../../../large-video/functions';
|
||||
import { isLayoutTileView } from '../../../video-layout/functions.web';
|
||||
import {
|
||||
AbstractCaptions,
|
||||
type AbstractCaptionsProps,
|
||||
type IAbstractCaptionsProps,
|
||||
_abstractMapStateToProps
|
||||
} from './AbstractCaptions';
|
||||
} from '../AbstractCaptions';
|
||||
|
||||
interface IProps extends AbstractCaptionsProps {
|
||||
|
||||
interface IProps extends IAbstractCaptionsProps {
|
||||
|
||||
/**
|
||||
* Whether the subtitles container is lifted above the invite box.
|
||||
*/
|
||||
_isLifted: boolean;
|
||||
_isLifted: boolean | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,9 +34,9 @@ class Captions extends AbstractCaptions<IProps> {
|
||||
* @param {string} text - Subtitles text formatted with the participant's
|
||||
* name.
|
||||
* @protected
|
||||
* @returns {React$Element} - The React element which displays the text.
|
||||
* @returns {ReactElement} - The React element which displays the text.
|
||||
*/
|
||||
_renderParagraph(id: string, text: string) {
|
||||
_renderParagraph(id: string, text: string): ReactElement {
|
||||
return (
|
||||
<p key = { id }>
|
||||
<span>{ text }</span>
|
||||
@@ -47,14 +47,15 @@ class Captions extends AbstractCaptions<IProps> {
|
||||
/**
|
||||
* Renders the subtitles container.
|
||||
*
|
||||
* @param {Array<React$Element>} paragraphs - An array of elements created
|
||||
* @param {Array<ReactElement>} paragraphs - An array of elements created
|
||||
* for each subtitle using the {@link _renderParagraph} method.
|
||||
* @protected
|
||||
* @returns {React$Element} - The subtitles container.
|
||||
* @returns {ReactElement} - The subtitles container.
|
||||
*/
|
||||
_renderSubtitlesContainer(paragraphs: Array<React.ReactElement>) {
|
||||
|
||||
const className = this.props._isLifted ? 'transcription-subtitles lifted' : 'transcription-subtitles';
|
||||
_renderSubtitlesContainer(paragraphs: Array<ReactElement>): ReactElement {
|
||||
const className = this.props._isLifted
|
||||
? 'transcription-subtitles lifted'
|
||||
: 'transcription-subtitles';
|
||||
|
||||
return (
|
||||
<div className = { className } >
|
||||
@@ -83,4 +84,5 @@ function mapStateToProps(state: IReduxState) {
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
export default connect(mapStateToProps)(Captions);
|
||||
@@ -1,13 +1,12 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import { IconSubtitles } from '../../base/icons/svg';
|
||||
import { toggleLanguageSelectorDialog } from '../actions.web';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconSubtitles } from '../../../base/icons/svg';
|
||||
import { toggleLanguageSelectorDialog } from '../../actions.web';
|
||||
import {
|
||||
AbstractClosedCaptionButton,
|
||||
_abstractMapStateToProps
|
||||
} from './AbstractClosedCaptionButton';
|
||||
} from '../AbstractClosedCaptionButton';
|
||||
|
||||
/**
|
||||
* A button which starts/stops the transcriptions.
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
|
||||
import LanguageListItem from './LanguageListItem.web';
|
||||
import LanguageListItem from './LanguageListItem';
|
||||
|
||||
interface ILanguageListProps {
|
||||
items: Array<ILanguageItem>;
|
||||
@@ -36,11 +36,13 @@ const LanguageList = ({
|
||||
onLanguageSelected
|
||||
}: ILanguageListProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
const listItems = items.map(item => (<LanguageListItem
|
||||
key = { item.id }
|
||||
lang = { item.lang }
|
||||
onLanguageSelected = { onLanguageSelected }
|
||||
selected = { item.selected } />));
|
||||
const listItems = items.map(item => (
|
||||
<LanguageListItem
|
||||
key = { item.id }
|
||||
lang = { item.lang }
|
||||
onLanguageSelected = { onLanguageSelected }
|
||||
selected = { item.selected } />
|
||||
));
|
||||
|
||||
return (
|
||||
<div className = { styles.itemsContainer }>{listItems}</div>
|
||||
@@ -2,24 +2,24 @@ import React, { useCallback } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { IconCheck } from '../../base/icons/svg';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconCheck } from '../../../base/icons/svg';
|
||||
|
||||
interface ILanguageListItemProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Whether or not the button should be full width.
|
||||
* Language string.
|
||||
*/
|
||||
lang: string;
|
||||
|
||||
/**
|
||||
* Click callback.
|
||||
* Callback for language selection.
|
||||
*/
|
||||
onLanguageSelected: (lang: string) => void;
|
||||
|
||||
/**
|
||||
* The id of the button.
|
||||
* If language item is selected or not.
|
||||
*/
|
||||
selected?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import i18next from 'i18next';
|
||||
import React, { useCallback } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { translate, translateToHTML } from '../../../base/i18n/functions';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
import { openSettingsDialog } from '../../../settings/actions.web';
|
||||
import { SETTINGS_TABS } from '../../../settings/constants';
|
||||
import { toggleLanguageSelectorDialog } from '../../actions.web';
|
||||
import AbstractLanguageSelectorDialog, {
|
||||
IAbstractLanguageSelectorDialogProps
|
||||
} from '../AbstractLanguageSelectorDialog';
|
||||
|
||||
import LanguageList from './LanguageList';
|
||||
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
paragraphWrapper: {
|
||||
fontSize: 14,
|
||||
margin: '10px 0px',
|
||||
color: theme.palette.text01
|
||||
},
|
||||
spanWrapper: {
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
color: theme.palette.link01,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.ui04,
|
||||
color: theme.palette.link01Hover
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
const LanguageSelectorDialog = (props: IAbstractLanguageSelectorDialogProps) => {
|
||||
const { dispatch, language, listItems, onLanguageSelected, subtitles, t } = props;
|
||||
|
||||
const { classes: styles } = useStyles();
|
||||
|
||||
const onSelected = useCallback((e: string) => {
|
||||
onLanguageSelected(e);
|
||||
dispatch(toggleLanguageSelectorDialog());
|
||||
}, [ language ]);
|
||||
|
||||
const onSourceLanguageClick = useCallback(() => {
|
||||
dispatch(openSettingsDialog(SETTINGS_TABS.PROFILE, false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancel = {{ hidden: true }}
|
||||
ok = {{ hidden: true }}
|
||||
titleKey = 'transcribing.subtitles'>
|
||||
<p className = { styles.paragraphWrapper } >
|
||||
{
|
||||
translateToHTML(t, 'transcribing.sourceLanguageDesc', {
|
||||
'sourceLanguage': t(`languages:${i18next.language}`).toLowerCase()
|
||||
})
|
||||
}<span
|
||||
className = { styles.spanWrapper }
|
||||
onClick = { onSourceLanguageClick }>{t('transcribing.sourceLanguageHere')}.</span>
|
||||
</p>
|
||||
<LanguageList
|
||||
items = { listItems }
|
||||
onLanguageSelected = { onSelected }
|
||||
selectedLanguage = { subtitles } />
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* We apply AbstractLanguageSelector to fill in the AbstractProps common
|
||||
* to both the web and native implementations.
|
||||
*/
|
||||
// eslint-disable-next-line new-cap
|
||||
export default translate(AbstractLanguageSelectorDialog(LanguageSelectorDialog));
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import {
|
||||
removeTranscriptMessage,
|
||||
updateTranscriptMessage
|
||||
} from './actions';
|
||||
} from './actions.any';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,7 +24,7 @@ interface ITranscriptMessage {
|
||||
export interface ISubtitlesState {
|
||||
_language: string;
|
||||
_requestingSubtitles: boolean;
|
||||
_transcriptMessages: Map<string, ITranscriptMessage>;
|
||||
_transcriptMessages: Map<string, ITranscriptMessage> | any;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,7 @@ import SecurityDialogButton
|
||||
import SharedVideoButton from '../../../shared-video/components/native/SharedVideoButton';
|
||||
import SpeakerStatsButton from '../../../speaker-stats/components/native/SpeakerStatsButton';
|
||||
import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions';
|
||||
import ClosedCaptionButton from '../../../subtitles/components/ClosedCaptionButton.native';
|
||||
import ClosedCaptionButton from '../../../subtitles/components/native/ClosedCaptionButton';
|
||||
import TileViewButton from '../../../video-layout/components/TileViewButton';
|
||||
import styles from '../../../video-menu/components/native/styles';
|
||||
import { getMovableButtons } from '../../functions.native';
|
||||
|
||||
@@ -29,17 +29,17 @@ import {
|
||||
hasRaisedHand,
|
||||
isLocalParticipantModerator
|
||||
} from '../../../base/participants/functions';
|
||||
import { getLocalVideoTrack } from '../../../base/tracks/functions';
|
||||
import { getLocalVideoTrack } from '../../../base/tracks/functions.web';
|
||||
import { ITrack } from '../../../base/tracks/types';
|
||||
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
|
||||
import ContextMenuItemGroup from '../../../base/ui/components/web/ContextMenuItemGroup';
|
||||
import { toggleChat } from '../../../chat/actions.web';
|
||||
import ChatButton from '../../../chat/components/web/ChatButton';
|
||||
import EmbedMeetingButton from '../../../embed-meeting/components/EmbedMeetingButton';
|
||||
import SharedDocumentButton from '../../../etherpad/components/SharedDocumentButton';
|
||||
import FeedbackButton from '../../../feedback/components/FeedbackButton';
|
||||
import SharedDocumentButton from '../../../etherpad/components/SharedDocumentButton.web';
|
||||
import FeedbackButton from '../../../feedback/components/FeedbackButton.web';
|
||||
import { setGifMenuVisibility } from '../../../gifs/actions';
|
||||
import { isGifEnabled } from '../../../gifs/functions';
|
||||
import { isGifEnabled } from '../../../gifs/functions.web';
|
||||
import InviteButton from '../../../invite/components/add-people-dialog/web/InviteButton';
|
||||
import { isVpaasMeeting } from '../../../jaas/functions';
|
||||
import KeyboardShortcutsButton from '../../../keyboard-shortcuts/components/web/KeyboardShortcutsButton';
|
||||
@@ -50,11 +50,13 @@ import {
|
||||
} from '../../../participants-pane/actions.web';
|
||||
import ParticipantsPaneButton from '../../../participants-pane/components/web/ParticipantsPaneButton';
|
||||
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
|
||||
import { addReactionToBuffer } from '../../../reactions/actions.any';
|
||||
import { toggleReactionsMenuVisibility } from '../../../reactions/actions.web';
|
||||
import {
|
||||
addReactionToBuffer,
|
||||
toggleReactionsMenuVisibility
|
||||
} from '../../../reactions/actions.web';
|
||||
import ReactionsMenuButton from '../../../reactions/components/web/ReactionsMenuButton';
|
||||
import { REACTIONS } from '../../../reactions/constants';
|
||||
import { isReactionsEnabled } from '../../../reactions/functions.any';
|
||||
import { isReactionsEnabled } from '../../../reactions/functions.web';
|
||||
import LiveStreamButton from '../../../recording/components/LiveStream/web/LiveStreamButton';
|
||||
import RecordButton from '../../../recording/components/Recording/web/RecordButton';
|
||||
import { isSalesforceEnabled } from '../../../salesforce/functions';
|
||||
@@ -71,12 +73,12 @@ import SettingsButton from '../../../settings/components/web/SettingsButton';
|
||||
import SharedVideoButton from '../../../shared-video/components/web/SharedVideoButton';
|
||||
import SpeakerStats from '../../../speaker-stats/components/web/SpeakerStats';
|
||||
import SpeakerStatsButton from '../../../speaker-stats/components/web/SpeakerStatsButton';
|
||||
import ClosedCaptionButton from '../../../subtitles/components/ClosedCaptionButton';
|
||||
import ClosedCaptionButton from '../../../subtitles/components/web/ClosedCaptionButton';
|
||||
import { toggleTileView } from '../../../video-layout/actions.web';
|
||||
import TileViewButton from '../../../video-layout/components/TileViewButton';
|
||||
import { shouldDisplayTileView } from '../../../video-layout/functions.web';
|
||||
import VideoQualityButton from '../../../video-quality/components/VideoQualityButton';
|
||||
import VideoQualityDialog from '../../../video-quality/components/VideoQualityDialog';
|
||||
import VideoQualityButton from '../../../video-quality/components/VideoQualityButton.web';
|
||||
import VideoQualityDialog from '../../../video-quality/components/VideoQualityDialog.web';
|
||||
import VideoBackgroundButton from '../../../virtual-background/components/VideoBackgroundButton';
|
||||
import { iAmVisitor } from '../../../visitors/functions';
|
||||
import WhiteboardButton from '../../../whiteboard/components/web/WhiteboardButton';
|
||||
@@ -87,10 +89,13 @@ import {
|
||||
setOverflowMenuVisible,
|
||||
setToolbarHovered,
|
||||
showToolbox
|
||||
} from '../../actions';
|
||||
} from '../../actions.web';
|
||||
import { NOTIFY_CLICK_MODE, NOT_APPLICABLE, THRESHOLDS } from '../../constants';
|
||||
import { isDesktopShareButtonDisabled, isToolboxVisible } from '../../functions';
|
||||
import { getJwtDisabledButtons } from '../../functions.any';
|
||||
import {
|
||||
getJwtDisabledButtons,
|
||||
isDesktopShareButtonDisabled,
|
||||
isToolboxVisible
|
||||
} from '../../functions.web';
|
||||
import DownloadButton from '../DownloadButton';
|
||||
import HangupButton from '../HangupButton';
|
||||
import HelpButton from '../HelpButton';
|
||||
|
||||
85
resources/prosody-plugins/mod_muc_auth_ban.lua
Normal file
85
resources/prosody-plugins/mod_muc_auth_ban.lua
Normal file
@@ -0,0 +1,85 @@
|
||||
-- Can be used to ban users based on external http service
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local ACCESS_MANAGER_URL = module:get_option_string("muc_prosody_jitsi_access_manager_url");
|
||||
if not ACCESS_MANAGER_URL then
|
||||
module:log("warn", "No 'muc_prosody_jitsi_access_manager_url' option set, disabling module");
|
||||
return
|
||||
end
|
||||
|
||||
local json = require "cjson.safe";
|
||||
local http = require "net.http";
|
||||
|
||||
local ban_check_count = module:measure("muc_auth_ban_check", "rate")
|
||||
local ban_check_users_banned_count = module:measure("muc_auth_ban_users_banned", "rate")
|
||||
|
||||
-- we will cache banned tokens to avoid extra requests
|
||||
-- on destroying session, websocket retries 2 more times before giving up
|
||||
local cache = require "util.cache".new(100);
|
||||
|
||||
local CACHE_DURATION = 5*60; -- 5 mins
|
||||
|
||||
local cache_timer = module:add_timer(CACHE_DURATION, function()
|
||||
for k, v in cache:items() do
|
||||
if socket.gettime() > v + CACHE_DURATION then
|
||||
cache:set(k, nil);
|
||||
end
|
||||
end
|
||||
|
||||
if cache:count() > 0 then
|
||||
-- rescheduling the timer
|
||||
return CACHE_DURATION;
|
||||
end
|
||||
|
||||
-- skipping return value stops the timer
|
||||
end);
|
||||
|
||||
local function shouldAllow(session)
|
||||
local token = session.auth_token;
|
||||
|
||||
if token ~= nil then
|
||||
module:log("debug", "Checking whether user should be banned ")
|
||||
|
||||
-- cached tokens are banned
|
||||
if cache:get(token) then
|
||||
return false;
|
||||
end
|
||||
|
||||
-- TODO: do this only for enabled customers
|
||||
ban_check_count();
|
||||
local function cb(content, code, response, request)
|
||||
if code == 200 then
|
||||
|
||||
local r = json.decode(content)
|
||||
if r['access'] ~= nil and r['access'] == false then
|
||||
module:log("debug", "user is banned")
|
||||
|
||||
ban_check_users_banned_count();
|
||||
|
||||
session:close();
|
||||
|
||||
-- if the cache is empty and the timer is not running reschedule it
|
||||
if cache:count() == 0 then
|
||||
cache_timer:reschedule(CACHE_DURATION);
|
||||
end
|
||||
|
||||
cache:set(token, socket.gettime());
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local request_headers = {}
|
||||
request_headers['Authorization'] = 'Bearer ' .. token;
|
||||
|
||||
http.request(ACCESS_MANAGER_URL, {
|
||||
headers = request_headers,
|
||||
method = "GET",
|
||||
}, cb);
|
||||
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
prosody.events.add_handler("jitsi-access-ban-check", function(session)
|
||||
return shouldAllow(session)
|
||||
end)
|
||||
113
resources/prosody-plugins/mod_muc_end_meeting.lua
Normal file
113
resources/prosody-plugins/mod_muc_end_meeting.lua
Normal file
@@ -0,0 +1,113 @@
|
||||
-- A global module which can be used as http endpoint to end meetings. The provided token
|
||||
--- in the request is verified whether it has the right to do so.
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
module:set_global();
|
||||
|
||||
local util = module:require "util";
|
||||
local async_handler_wrapper = util.async_handler_wrapper;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local starts_with = util.starts_with;
|
||||
|
||||
local neturl = require "net.url";
|
||||
local parse = neturl.parseQuery;
|
||||
|
||||
-- will be initialized once the main virtual host module is initialized
|
||||
local token_util;
|
||||
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
|
||||
local asapKeyServer = module:get_option_string("prosody_password_public_key_repo_url", "");
|
||||
|
||||
local event_count = module:measure("muc_end_meeting_rate", "rate")
|
||||
local event_count_success = module:measure("muc_end_meeting_success", "rate")
|
||||
|
||||
function verify_token(token)
|
||||
if token == nil then
|
||||
module:log("warn", "no token provided");
|
||||
return false;
|
||||
end
|
||||
|
||||
local session = {};
|
||||
session.auth_token = token;
|
||||
local verified, reason, msg = token_util:process_and_verify_token(session);
|
||||
if not verified then
|
||||
module:log("warn", "not a valid token %s %s", tostring(reason), tostring(msg));
|
||||
return false;
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
function handle_terminate_meeting (event)
|
||||
module:log("info", "Request for terminate meeting received: reqid %s", event.request.headers["request_id"])
|
||||
event_count()
|
||||
if not event.request.url.query then
|
||||
return { status_code = 400 };
|
||||
end
|
||||
local params = parse(event.request.url.query);
|
||||
local conference = params["conference"];
|
||||
local room_jid;
|
||||
|
||||
if conference then
|
||||
room_jid = room_jid_match_rewrite(conference)
|
||||
else
|
||||
module:log('warn', "conference param was not provided")
|
||||
return { status_code = 400 };
|
||||
end
|
||||
|
||||
-- verify access
|
||||
local token = event.request.headers["authorization"]
|
||||
if not token then
|
||||
module:log("error", "Authorization header was not provided for conference %s", conference)
|
||||
return { status_code = 401 };
|
||||
end
|
||||
if starts_with(token, 'Bearer ') then
|
||||
token = token:sub(8, #token)
|
||||
else
|
||||
module:log("error", "Authorization header is invalid")
|
||||
return { status_code = 401 };
|
||||
end
|
||||
|
||||
if not verify_token(token, room_jid) then
|
||||
return { status_code = 401 };
|
||||
end
|
||||
|
||||
local room = get_room_from_jid(room_jid);
|
||||
if not room then
|
||||
module:log("warn", "Room not found")
|
||||
return { status_code = 404 };
|
||||
else
|
||||
module:log("info", "Destroy room jid %s", room.jid)
|
||||
room:destroy(nil, "The meeting has been terminated")
|
||||
end
|
||||
event_count_success()
|
||||
return { status_code = 200 };
|
||||
end
|
||||
|
||||
|
||||
-- module API called on virtual host added, passing the host module
|
||||
function module.add_host(host_module)
|
||||
if host_module.host == muc_domain_base then
|
||||
-- the main virtual host
|
||||
module:log("info", "Initialize token_util using %s", host_module.host)
|
||||
|
||||
token_util = module:require "token/util".new(host_module);
|
||||
|
||||
if asapKeyServer then
|
||||
-- init token util with our asap keyserver
|
||||
token_util:set_asap_key_server(asapKeyServer)
|
||||
end
|
||||
|
||||
module:log("info", "Adding http handler for /end-meeting on %s", host_module.host);
|
||||
host_module:depends("http");
|
||||
host_module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["POST end-meeting"] = function(event)
|
||||
return async_handler_wrapper(event, handle_terminate_meeting)
|
||||
end;
|
||||
};
|
||||
});
|
||||
end
|
||||
end
|
||||
27
resources/prosody-plugins/mod_muc_filter_access.lua
Normal file
27
resources/prosody-plugins/mod_muc_filter_access.lua
Normal file
@@ -0,0 +1,27 @@
|
||||
-- Restricts access to a muc component to certain domains
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
-- a list of (authenticated)domains that can access rooms(send presence)
|
||||
local whitelist = module:get_option_set("muc_filter_whitelist");
|
||||
|
||||
if not whitelist then
|
||||
module:log("warn", "No 'muc_filter_whitelist' option set, disabling muc_filter_access, plugin inactive");
|
||||
return
|
||||
end
|
||||
|
||||
local jid_split = require "util.jid".split;
|
||||
|
||||
local function incoming_presence_filter(event)
|
||||
local stanza = event.stanza;
|
||||
local _, domain, _ = jid_split(stanza.attr.from);
|
||||
|
||||
if not stanza.attr.from or not whitelist:contains(domain) then
|
||||
-- Filter presence
|
||||
module:log("error", "Filtering unauthorized presence: %s", stanza:top_tag());
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
for _, jid_type in ipairs({ "host", "bare", "full" }) do
|
||||
module:hook("presence/"..jid_type, incoming_presence_filter, 2000);
|
||||
end
|
||||
218
resources/prosody-plugins/mod_muc_flip.lua
Normal file
218
resources/prosody-plugins/mod_muc_flip.lua
Normal file
@@ -0,0 +1,218 @@
|
||||
-- Allows flipping device. When a presence contains flip_device tag
|
||||
-- and the used jwt matches the id(session.jitsi_meet_context_user.id) of another user this is indication that the user
|
||||
-- is moving from one device to another. The flip feature should be present and enabled in the token features.
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local oss_util = module:require "util";
|
||||
local is_healthcheck_room = oss_util.is_healthcheck_room;
|
||||
local um_is_admin = require "core.usermanager".is_admin;
|
||||
local inspect = require('inspect');
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid = require "util.jid";
|
||||
local MUC_NS = "http://jabber.org/protocol/muc";
|
||||
|
||||
local lobby_host;
|
||||
local lobby_muc_service;
|
||||
|
||||
local lobby_muc_component_config = 'lobby.' .. module:get_option_string("muc_mapper_domain_base");
|
||||
if lobby_muc_component_config == nil then
|
||||
module:log('error', 'lobby not enabled missing lobby_muc config');
|
||||
return ;
|
||||
end
|
||||
|
||||
local function is_admin(occupant_jid)
|
||||
return um_is_admin(occupant_jid, module.host);
|
||||
end
|
||||
|
||||
local function remove_flip_tag(stanza)
|
||||
stanza:maptags(function(tag)
|
||||
if tag and tag.name == "flip_device" then
|
||||
module:log("debug", "Removing %s tag from presence stanza!", tag.name);
|
||||
return nil;
|
||||
else
|
||||
return tag;
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Make user that switch devices bypass lobby or password.
|
||||
-- A user is considered to join from another device if the
|
||||
-- id from jwt is the same as another occupant and the presence
|
||||
-- stanza has flip_device tag
|
||||
module:hook("muc-occupant-pre-join", function(event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
local session = event.origin;
|
||||
local stanza = event.stanza;
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
||||
return ;
|
||||
end
|
||||
local flip_device_tag = stanza:get_child("flip_device");
|
||||
if session.jitsi_meet_context_user and session.jitsi_meet_context_user.id then
|
||||
local participants = room._data.participants_details or {};
|
||||
local id = session.jitsi_meet_context_user.id;
|
||||
local first_device_occ_nick = participants[id];
|
||||
if flip_device_tag then
|
||||
if first_device_occ_nick and session.jitsi_meet_context_features.flip and (session.jitsi_meet_context_features.flip == true or session.jitsi_meet_context_features.flip == "true") then
|
||||
room._data.kicked_participant_nick = first_device_occ_nick;
|
||||
room._data.flip_participant_nick = occupant.nick;
|
||||
-- allow participant from flip device to bypass Lobby
|
||||
local occupant_jid = stanza.attr.from;
|
||||
local affiliation = room:get_affiliation(occupant_jid);
|
||||
if not affiliation or affiliation == 0 then
|
||||
module:log("debug", "Bypass lobby invitee %s", occupant_jid)
|
||||
occupant.role = "participant";
|
||||
room:set_affiliation(true, jid_bare(occupant_jid), "member")
|
||||
room:save();
|
||||
end
|
||||
-- bypass password on the flip device
|
||||
local join = stanza:get_child("x", MUC_NS);
|
||||
if not join then
|
||||
join = stanza:tag("x", { xmlns = MUC_NS });
|
||||
end
|
||||
local password = join:get_child("password", MUC_NS);
|
||||
if password then
|
||||
join:maptags(
|
||||
function(tag)
|
||||
for k, v in pairs(tag) do
|
||||
if k == "name" and v == "password" then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return tag
|
||||
end);
|
||||
end
|
||||
join:tag("password", { xmlns = MUC_NS }):text(room:get_password());
|
||||
elseif not session.jitsi_meet_context_features.flip or session.jitsi_meet_context_features.flip == false or session.jitsi_meet_context_features.flip == "false" then
|
||||
module:log("warn", "Flip device tag present without jwt permission")
|
||||
--remove flip_device tag if somebody wants to abuse this feature
|
||||
remove_flip_tag(stanza)
|
||||
else
|
||||
module:log("warn", "Flip device tag present without user from different device")
|
||||
--remove flip_device tag if somebody wants to abuse this feature
|
||||
remove_flip_tag(stanza)
|
||||
end
|
||||
end
|
||||
-- update authenticated participant list
|
||||
participants[id] = occupant.nick;
|
||||
room._data.participants_details = participants
|
||||
module:log("debug", "current details list %s", inspect(participants))
|
||||
else
|
||||
if flip_device_tag then
|
||||
module:log("warn", "Flip device tag present for a guest user")
|
||||
-- remove flip_device tag because a guest want to do a sneaky join
|
||||
remove_flip_tag(stanza)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Kick participant from the the first device from the main room and lobby if applies
|
||||
-- and transfer role from the previous participant, this will take care of the grant
|
||||
-- moderation case
|
||||
module:hook("muc-occupant-joined", function(event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
||||
return ;
|
||||
end
|
||||
|
||||
if room._data.flip_participant_nick and occupant.nick == room._data.flip_participant_nick then
|
||||
-- make joining participant from flip device have the same role and affiliation as for the previous device
|
||||
local kicked_occupant = room:get_occupant_by_nick(room._data.kicked_participant_nick);
|
||||
local initial_affiliation = room:get_affiliation(kicked_occupant.jid) or "member";
|
||||
module:log("debug", "Transfer affiliation %s to occupant jid %s", initial_affiliation, occupant.jid)
|
||||
room:set_affiliation(true, occupant.bare_jid, initial_affiliation)
|
||||
if initial_affiliation == "owner" then
|
||||
event.occupant.role = "moderator";
|
||||
elseif initial_affiliation == "member" then
|
||||
event.occupant.role = "participant";
|
||||
end
|
||||
-- Kick participant from the first device from the main room
|
||||
local kicked_participant_node_jid = jid.split(kicked_occupant.jid);
|
||||
module:log("info", "Kick participant jid %s nick %s from main room jid %s", kicked_occupant.jid, room._data.kicked_participant_nick, room.jid)
|
||||
room:set_role(true, room._data.kicked_participant_nick, 'none')
|
||||
room:save()
|
||||
-- Kick participant from the first device from the lobby room
|
||||
if room._data.lobbyroom then
|
||||
local lobby_room_jid = room._data.lobbyroom;
|
||||
local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid)
|
||||
for _, occupant in lobby_room:each_occupant() do
|
||||
local node = jid.split(occupant.jid);
|
||||
if kicked_participant_node_jid == node then
|
||||
module:log("info", "Kick participant from lobby %s", occupant.jid)
|
||||
lobby_room:set_role(true, occupant.nick, 'none')
|
||||
end
|
||||
end
|
||||
end
|
||||
event.room._data.flip_participant_nick = nil
|
||||
event.room._data.kicked_participant_nick = nil;
|
||||
end
|
||||
end,-2)
|
||||
|
||||
-- Update the local table after a participant leaves
|
||||
module:hook("muc-occupant-left", function(event)
|
||||
local occupant = event.occupant;
|
||||
local session = event.origin;
|
||||
if is_healthcheck_room(event.room.jid) or is_admin(occupant.bare_jid) then
|
||||
return ;
|
||||
end
|
||||
if session and session.jitsi_meet_context_user and session.jitsi_meet_context_user.id then
|
||||
local id = session.jitsi_meet_context_user.id
|
||||
local participants = event.room._data.participants_details or {};
|
||||
local occupant_left_nick = participants[id]
|
||||
if occupant_left_nick == occupant.nick then
|
||||
participants[id] = nil
|
||||
event.room._data.participants_details = participants
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Add a flip_device tag on the unavailable presence from the kicked participant in order to silent the notifications
|
||||
module:hook('muc-broadcast-presence', function(event)
|
||||
local kicked_participant_nick = event.room._data.kicked_participant_nick
|
||||
local stanza = event.stanza;
|
||||
if kicked_participant_nick and stanza.attr.from == kicked_participant_nick and stanza.attr.type == 'unavailable' then
|
||||
module:log("debug", "Add flip_device tag for presence unavailable from occupant nick %s", kicked_participant_nick)
|
||||
stanza:tag("flip_device"):up();
|
||||
end
|
||||
end)
|
||||
|
||||
function process_lobby_muc_loaded(lobby_muc, host_module)
|
||||
module:log('info', 'Lobby muc loaded');
|
||||
lobby_muc_service = lobby_muc;
|
||||
lobby_host = module:context(host_module);
|
||||
end
|
||||
|
||||
-- process a host module directly if loaded or hooks to wait for its load
|
||||
function process_host_module(name, callback)
|
||||
local function process_host(host)
|
||||
if host == name then
|
||||
callback(module:context(host), host);
|
||||
end
|
||||
end
|
||||
|
||||
if prosody.hosts[name] == nil then
|
||||
module:log('debug', 'No host/component found, will wait for it: %s', name)
|
||||
|
||||
-- when a host or component is added
|
||||
prosody.events.add_handler('host-activated', process_host);
|
||||
else
|
||||
process_host(name);
|
||||
end
|
||||
end
|
||||
|
||||
-- process or waits to process the lobby muc component
|
||||
process_host_module(lobby_muc_component_config, function(host_module, host)
|
||||
-- lobby muc component created
|
||||
module:log('info', 'Lobby component loaded %s', host);
|
||||
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
if muc_module then
|
||||
process_lobby_muc_loaded(muc_module, host_module);
|
||||
else
|
||||
module:log('debug', 'Will wait for muc to be available');
|
||||
prosody.hosts[host].events.add_handler('module-loaded', function(event)
|
||||
if (event.module == 'muc') then
|
||||
process_lobby_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
6
resources/prosody-plugins/mod_muc_hide_all.lua
Normal file
6
resources/prosody-plugins/mod_muc_hide_all.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
-- This module makes all MUCs in Prosody unavailable on disco#items query
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
module:hook("muc-room-pre-create", function(event)
|
||||
event.room:set_hidden(true);
|
||||
end, -1);
|
||||
203
resources/prosody-plugins/mod_muc_jigasi_invite.lua
Normal file
203
resources/prosody-plugins/mod_muc_jigasi_invite.lua
Normal file
@@ -0,0 +1,203 @@
|
||||
-- A http endpoint to invite jigasi to a meeting via http endpoint
|
||||
-- jwt is used to validate access
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local jid_split = require "util.jid".split;
|
||||
local hashes = require "util.hashes";
|
||||
local random = require "util.random";
|
||||
local st = require("util.stanza");
|
||||
local json = require "util.json";
|
||||
local util = module:require "util";
|
||||
local async_handler_wrapper = util.async_handler_wrapper;
|
||||
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
|
||||
-- This module chooses jigasi from the brewery room, so it needs information for the configured brewery
|
||||
local muc_domain = module:get_option_string("muc_internal_domain_base", 'internal.auth.' .. muc_domain_base);
|
||||
|
||||
local jigasi_brewery_room_jid = module:get_option_string("muc_jigasi_brewery_jid", 'jigasibrewery@' .. muc_domain);
|
||||
|
||||
local jigasi_bare_jid = module:get_option_string("muc_jigasi_jid", "jigasi@auth." .. muc_domain_base);
|
||||
local focus_jid = module:get_option_string("muc_jicofo_brewery_jid", jigasi_brewery_room_jid .. "/focus");
|
||||
|
||||
local main_muc_service;
|
||||
local JSON_CONTENT_TYPE = "application/json";
|
||||
|
||||
local event_count = module:measure("muc_invite_jigasi_rate", "rate")
|
||||
local event_count_success = module:measure("muc_invite_jigasi_success", "rate")
|
||||
local ASAP_KEY_SERVER = module:get_option_string("prosody_password_public_key_repo_url", "");
|
||||
local token_util = module:require "token/util".new(module);
|
||||
if ASAP_KEY_SERVER then
|
||||
-- init token util with our asap keyserver
|
||||
token_util:set_asap_key_server(ASAP_KEY_SERVER)
|
||||
end
|
||||
|
||||
local function invite_jigasi(conference, phone_no)
|
||||
local jigasi_brewery_room = main_muc_service.get_room_from_jid(jigasi_brewery_room_jid);
|
||||
if not jigasi_brewery_room then
|
||||
module:log("error", "Jigasi brewery room not found")
|
||||
return 404, 'Brewery room was not found'
|
||||
end
|
||||
module:log("info", "Invite jigasi from %s to join conference %s and outbound phone_no %s", jigasi_brewery_room.jid, conference, phone_no)
|
||||
|
||||
--select least stressed Jigasi
|
||||
local least_stressed_value = math.huge;
|
||||
local least_stressed_jigasi_jid;
|
||||
for occupant_jid, occupant in jigasi_brewery_room:each_occupant() do
|
||||
local _, _, resource = jid_split(occupant_jid);
|
||||
if resource ~= 'focus' then
|
||||
local occ = occupant:get_presence();
|
||||
local stats_child = occ:get_child("stats", "http://jitsi.org/protocol/colibri")
|
||||
|
||||
local is_sip_jigasi = true;
|
||||
for stats_tag in stats_child:children() do
|
||||
if stats_tag.attr.name == 'supports_sip' and stats_tag.attr.value == 'false' then
|
||||
is_sip_jigasi = false;
|
||||
end
|
||||
end
|
||||
|
||||
if is_sip_jigasi then
|
||||
for stats_tag in stats_child:children() do
|
||||
if stats_tag.attr.name == 'stress_level' then
|
||||
local stress_level = tonumber(stats_tag.attr.value);
|
||||
module:log("debug", "Stressed level %s %s ", stress_level, occupant_jid)
|
||||
if stress_level < least_stressed_value then
|
||||
least_stressed_jigasi_jid = occupant_jid
|
||||
least_stressed_value = stress_level
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
module:log("debug", "Least stressed jigasi selected jid %s value %s", least_stressed_jigasi_jid, least_stressed_value)
|
||||
if not least_stressed_jigasi_jid then
|
||||
module:log("error", "Cannot invite jigasi from room %s", jigasi_brewery_room.jid)
|
||||
return 404, 'Jigasi not found'
|
||||
end
|
||||
|
||||
-- invite Jigasi to join the conference
|
||||
local _, _, jigasi_res = jid_split(least_stressed_jigasi_jid)
|
||||
local jigasi_full_jid = jigasi_bare_jid .. "/" .. jigasi_res;
|
||||
local stanza_id = hashes.sha256(random.bytes(8), true);
|
||||
|
||||
local invite_jigasi_stanza = st.iq({ xmlns = "jabber:client", type = "set", to = jigasi_full_jid, from = focus_jid, id = stanza_id })
|
||||
:tag("dial", { xmlns = "urn:xmpp:rayo:1", from = "fromnumber", to = phone_no })
|
||||
:tag("header", { xmlns = "urn:xmpp:rayo:1", name = "JvbRoomName", value = conference })
|
||||
|
||||
module:log("debug", "Invite jigasi stanza %s", invite_jigasi_stanza)
|
||||
jigasi_brewery_room:route_stanza(invite_jigasi_stanza);
|
||||
return 200
|
||||
end
|
||||
|
||||
local function is_token_valid(token)
|
||||
if token == nil then
|
||||
module:log("warn", "no token provided");
|
||||
return false;
|
||||
end
|
||||
|
||||
local session = {};
|
||||
session.auth_token = token;
|
||||
local verified, reason, msg = token_util:process_and_verify_token(session);
|
||||
if not verified then
|
||||
module:log("warn", "not a valid token %s %s", tostring(reason), tostring(msg));
|
||||
return false;
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
local function handle_jigasi_invite(event)
|
||||
module:log("debug", "Request for invite jigasi received: reqId %s", event.request.headers["request_id"])
|
||||
event_count()
|
||||
local request = event.request;
|
||||
-- verify access
|
||||
local token = event.request.headers["authorization"]
|
||||
if not token then
|
||||
module:log("error", "Authorization header was not provided for conference %s", conference)
|
||||
return { status_code = 401 };
|
||||
end
|
||||
if util.starts_with(token, 'Bearer ') then
|
||||
token = token:sub(8, #token)
|
||||
else
|
||||
module:log("error", "Authorization header is invalid")
|
||||
return { status_code = 401 };
|
||||
end
|
||||
if not is_token_valid(token) then
|
||||
return { status_code = 401 };
|
||||
end
|
||||
|
||||
-- verify payload
|
||||
if request.headers.content_type ~= JSON_CONTENT_TYPE
|
||||
or (not request.body or #request.body == 0) then
|
||||
module:log("warn", "Wrong content type: %s or missing payload", request.headers.content_type);
|
||||
return { status_code = 400; }
|
||||
end
|
||||
local payload = json.decode(request.body);
|
||||
|
||||
local conference = payload["conference"];
|
||||
local phone_no = payload["phoneNo"];
|
||||
if not conference then
|
||||
module:log("warn", "Missing conference param")
|
||||
return { status_code = 400; }
|
||||
end
|
||||
if not phone_no then
|
||||
module:log("warn", "Missing phone no param")
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
--invite jigasi
|
||||
local status_code, error_msg = invite_jigasi(conference, phone_no)
|
||||
|
||||
if not error_msg then
|
||||
event_count_success()
|
||||
return { status_code = 200 }
|
||||
else
|
||||
return { status_code = status_code, body = json.encode({ error = error_msg }) }
|
||||
end
|
||||
end
|
||||
|
||||
module:log("info", "Adding http handler for /invite-jigasi on %s", module.host);
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["POST invite-jigasi"] = function(event)
|
||||
return async_handler_wrapper(event, handle_jigasi_invite)
|
||||
end;
|
||||
};
|
||||
});
|
||||
|
||||
-- process a host module directly if loaded or hooks to wait for its load
|
||||
function process_host_module(name, callback)
|
||||
local function process_host(host)
|
||||
if host == name then
|
||||
callback(module:context(host), host);
|
||||
end
|
||||
end
|
||||
|
||||
if prosody.hosts[name] == nil then
|
||||
module:log('info', 'No host/component found, will wait for it: %s', name)
|
||||
|
||||
-- when a host or component is added
|
||||
prosody.events.add_handler('host-activated', process_host);
|
||||
else
|
||||
process_host(name);
|
||||
end
|
||||
end
|
||||
|
||||
process_host_module(muc_domain, function(_, host)
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
if muc_module then
|
||||
main_muc_service = muc_module;
|
||||
module:log('info', 'Found main_muc_service: %s', main_muc_service);
|
||||
else
|
||||
module:log('info', 'Will wait for muc to be available');
|
||||
prosody.hosts[host].events.add_handler('module-loaded', function(event)
|
||||
if (event.module == 'muc') then
|
||||
main_muc_service = prosody.hosts[host].modules.muc;
|
||||
module:log('info', 'Found(on loaded) main_muc_service: %s', main_muc_service);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
158
resources/prosody-plugins/mod_muc_kick_jigasi.lua
Normal file
158
resources/prosody-plugins/mod_muc_kick_jigasi.lua
Normal file
@@ -0,0 +1,158 @@
|
||||
-- http endpoint to kick participants, access is based on provided jwt token
|
||||
-- the correct jigasi we fined based on the display name and the number provided
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local util = module:require "util";
|
||||
local async_handler_wrapper = util.async_handler_wrapper;
|
||||
local starts_with = util.starts_with;
|
||||
local formdecode = require "util.http".formdecode;
|
||||
local urlencode = require "util.http".urlencode;
|
||||
local jid = require "util.jid";
|
||||
local json = require "util.json";
|
||||
|
||||
local muc_domain_prefix = module:get_option_string("muc_mapper_domain_prefix", "conference");
|
||||
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
if not muc_domain_base then
|
||||
module:log("warn", "No 'muc_domain_base' option set, disabling kick check endpoint.");
|
||||
return ;
|
||||
end
|
||||
|
||||
local json_content_type = "application/json";
|
||||
|
||||
local token_util = module:require "token/util".new(module);
|
||||
|
||||
local asapKeyServer = module:get_option_string('prosody_password_public_key_repo_url', '');
|
||||
if asapKeyServer == '' then
|
||||
module:log('warn', 'No "prosody_password_public_key_repo_url" option set, disabling kick endpoint.');
|
||||
return ;
|
||||
end
|
||||
|
||||
token_util:set_asap_key_server(asapKeyServer);
|
||||
|
||||
--- Verifies the token
|
||||
-- @param token the token we received
|
||||
-- @param room_address the full room address jid
|
||||
-- @return true if values are ok or false otherwise
|
||||
function verify_token(token, room_address)
|
||||
|
||||
if token == nil then
|
||||
module:log("warn", "no token provided for %s", room_address);
|
||||
return false;
|
||||
end
|
||||
|
||||
local session = {};
|
||||
session.auth_token = token;
|
||||
local verified, reason, msg = token_util:process_and_verify_token(session);
|
||||
if not verified then
|
||||
module:log("warn", "not a valid token %s %s for %s", tostring(reason), tostring(msg), room_address);
|
||||
return false;
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
|
||||
-- Validates the request by checking for required url param room and
|
||||
-- validates the token provided with the request
|
||||
-- @param request - The request to validate.
|
||||
-- @return [error_code, room]
|
||||
local function validate_and_get_room(request)
|
||||
if not request.url.query then
|
||||
module:log("warn", "No query");
|
||||
return 400, nil;
|
||||
end
|
||||
|
||||
local params = formdecode(request.url.query);
|
||||
local room_name = urlencode(params.room) or "";
|
||||
local subdomain = urlencode(params.prefix) or "";
|
||||
|
||||
if not room_name then
|
||||
module:log("warn", "Missing room param for %s", room_name);
|
||||
return 400, nil;
|
||||
end
|
||||
|
||||
local room_address = jid.join(room_name, muc_domain_prefix.."."..muc_domain_base);
|
||||
|
||||
if subdomain and subdomain ~= "" then
|
||||
room_address = "["..subdomain.."]"..room_address;
|
||||
end
|
||||
|
||||
-- verify access
|
||||
local token = request.headers["authorization"]
|
||||
|
||||
if token and starts_with(token,'Bearer ') then
|
||||
token = token:sub(8,#token)
|
||||
end
|
||||
|
||||
if not verify_token(token, room_address) then
|
||||
return 403, nil;
|
||||
end
|
||||
|
||||
local room = get_room_from_jid(room_address);
|
||||
|
||||
if not room then
|
||||
module:log("warn", "No room found for %s", room_address);
|
||||
return 404, nil;
|
||||
else
|
||||
return 200, room;
|
||||
end
|
||||
end
|
||||
|
||||
function handle_kick_participant (event)
|
||||
local request = event.request;
|
||||
if request.headers.content_type ~= json_content_type
|
||||
or (not request.body or #request.body == 0) then
|
||||
module:log("warn", "Wrong content type: %s", request.headers.content_type);
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local params = json.decode(request.body);
|
||||
if not params then
|
||||
module:log("warn", "Missing params");
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local number = params["number"];
|
||||
|
||||
if not number then
|
||||
module:log("warn", "Missing number param");
|
||||
return { status_code = 400; };
|
||||
end
|
||||
|
||||
local error_code, room = validate_and_get_room(request);
|
||||
|
||||
if error_code and error_code ~= 200 then
|
||||
module:log("error", "Error validating %s", error_code);
|
||||
return { error_code = 400; }
|
||||
end
|
||||
|
||||
if not room then
|
||||
return { status_code = 404; }
|
||||
end
|
||||
|
||||
for _, occupant in room:each_occupant() do
|
||||
local pr = occupant:get_presence();
|
||||
local displayName = pr:get_child_text(
|
||||
'nick', 'http://jabber.org/protocol/nick');
|
||||
local initiator = pr:get_child('initiator', 'http://jitsi.org/protocol/jigasi');
|
||||
|
||||
if initiator and displayName and starts_with(displayName, number) then
|
||||
room:set_role(true, occupant.nick, nil);
|
||||
module:log('info', 'Occupant kicked %s from %s', occupant.nick, room.jid);
|
||||
return { status_code = 200; }
|
||||
end
|
||||
end
|
||||
|
||||
-- not found participant to kick
|
||||
return { status_code = 404; };
|
||||
end
|
||||
|
||||
module:log("info","Adding http handler for /kick-participant on %s", module.host);
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["PUT kick-participant"] = function (event) return async_handler_wrapper(event, handle_kick_participant) end;
|
||||
};
|
||||
});
|
||||
214
resources/prosody-plugins/mod_rate_limit.lua
Normal file
214
resources/prosody-plugins/mod_rate_limit.lua
Normal file
@@ -0,0 +1,214 @@
|
||||
-- Rate limits connection based on their ip address.
|
||||
-- Rate limits creating sessions (new connections),
|
||||
-- rate limits sent stanzas from same ip address (presence, iq, messages)
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local cache = require"util.cache";
|
||||
local ceil = math.ceil;
|
||||
local http_server = require "net.http.server";
|
||||
local gettime = require "util.time".now
|
||||
local filters = require "util.filters";
|
||||
local new_throttle = require "util.throttle".create;
|
||||
local timer = require "util.timer";
|
||||
local ip_util = require "util.ip";
|
||||
local new_ip = ip_util.new_ip;
|
||||
local match_ip = ip_util.match;
|
||||
local parse_cidr = ip_util.parse_cidr;
|
||||
|
||||
local config = {};
|
||||
local limits_resolution = 1;
|
||||
|
||||
local function load_config()
|
||||
-- Max allowed login rate in events per second.
|
||||
config.login_rate = module:get_option_number("rate_limit_login_rate", 3);
|
||||
-- The rate to which sessions from IPs exceeding the join rate will be limited, in bytes per second.
|
||||
config.session_rate = module:get_option_number("rate_limit_session_rate", 2000);
|
||||
-- The time in seconds, after which the limit for an IP address is lifted.
|
||||
config.timeout = module:get_option_number("rate_limit_timeout", 60);
|
||||
-- List of regular expressions for IP addresses that are not limited by this module.
|
||||
config.whitelist = module:get_option_set("rate_limit_whitelist", { "127.0.0.1", "::1" })._items;
|
||||
-- The size of the cache that saves state for IP addresses
|
||||
config.cache_size = module:get_option_number("rate_limit_cache_size", 10000);
|
||||
|
||||
-- Max allowed presence rate in events per second.
|
||||
config.presence_rate = module:get_option_number("rate_limit_presence_rate", 4);
|
||||
-- Max allowed iq rate in events per second.
|
||||
config.iq_rate = module:get_option_number("rate_limit_iq_rate", 10);
|
||||
-- Max allowed message rate in events per second.
|
||||
config.message_rate = module:get_option_number("rate_limit_message_rate", 3);
|
||||
|
||||
-- A list of jids for which sessions we ignore rate limiting
|
||||
config.whitelist_jids = module:get_option_set("rate_limit_whitelist_jids", {});
|
||||
|
||||
local wl = "";
|
||||
for ip in config.whitelist do wl = wl .. ip .. "," end
|
||||
local wl_jids = "";
|
||||
for j in config.whitelist_jids do wl_jids = wl_jids .. j .. "," end
|
||||
module:log("info", "Loaded configuration: ");
|
||||
module:log("info", "- session_rate=%s bytes/sec, timeout=%s sec, cache size=%s, whitelist=%s, whitelist_jids=%s",
|
||||
config.session_rate, config.timeout, config.cache_size, wl, wl_jids);
|
||||
module:log("info", "- login_rate=%s/sec, presence_rate=%s/sec, iq_rate=%s/sec, message_rate=%s/sec",
|
||||
config.login_rate, config.presence_rate, config.iq_rate, config.message_rate);
|
||||
end
|
||||
load_config();
|
||||
|
||||
-- Maps an IP address to a util.throttle which keeps the rate of login/join events from that IP.
|
||||
local login_rates = cache.new(config.cache_size);
|
||||
|
||||
-- Keeps the IP addresses that have exceeded the allowed login/join rate (i.e. the IP addresses whose sessions need
|
||||
-- to be limited). Mapped to the last instant at which the rate was exceeded.
|
||||
local limited_ips = cache.new(config.cache_size);
|
||||
|
||||
local function is_whitelisted(ip)
|
||||
local parsed_ip = new_ip(ip)
|
||||
for entry in config.whitelist do
|
||||
if match_ip(parsed_ip, parse_cidr(entry)) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
local function is_whitelisted_jid(jid)
|
||||
return config.whitelist_jids:contains(jid);
|
||||
end
|
||||
|
||||
-- Discover real remote IP of a session
|
||||
-- Note: http_server.get_request_from_conn() was added in Prosody 0.12.3,
|
||||
-- this code provides backwards compatibility with older versions
|
||||
local get_request_from_conn = http_server.get_request_from_conn or function (conn)
|
||||
local response = conn and conn._http_open_response;
|
||||
return response and response.request or nil;
|
||||
end;
|
||||
|
||||
-- Add an IP to the set of limied IPs
|
||||
local function limit_ip(ip)
|
||||
module:log("info", "Limiting %s due to login/join rate exceeded.", ip);
|
||||
limited_ips:set(ip, gettime());
|
||||
end
|
||||
|
||||
-- Installable as a session filter to limit the reading rate for a session. Based on mod_limits.
|
||||
local function limit_bytes_in(bytes, session)
|
||||
local sess_throttle = session.jitsi_throttle;
|
||||
if sess_throttle then
|
||||
-- if the limit timeout has elapsed let's stop the throttle
|
||||
if not sess_throttle.start or gettime() - sess_throttle.start > config.timeout then
|
||||
module:log("info", "Stop throttling session=%s, ip=%s.", session, session.ip);
|
||||
session.jitsi_throttle = nil;
|
||||
return bytes;
|
||||
end
|
||||
local ok, _, outstanding = sess_throttle:poll(#bytes, true);
|
||||
if not ok then
|
||||
session.log("debug",
|
||||
"Session over rate limit (%d) with %d (by %d), pausing", sess_throttle.max, #bytes, outstanding);
|
||||
outstanding = ceil(outstanding);
|
||||
session.conn:pause(); -- Read no more data from the connection until there is no outstanding data
|
||||
local outstanding_data = bytes:sub(-outstanding);
|
||||
bytes = bytes:sub(1, #bytes-outstanding);
|
||||
timer.add_task(limits_resolution, function ()
|
||||
if not session.conn then return; end
|
||||
if sess_throttle:peek(#outstanding_data) then
|
||||
session.log("debug", "Resuming paused session");
|
||||
session.conn:resume();
|
||||
end
|
||||
-- Handle what we can of the outstanding data
|
||||
session.data(outstanding_data);
|
||||
end);
|
||||
end
|
||||
end
|
||||
return bytes;
|
||||
end
|
||||
|
||||
-- Throttles reading from the connection of a specific session.
|
||||
local function throttle_session(session)
|
||||
if not session.jitsi_throttle then
|
||||
if (session.conn and session.conn.setlimit) then
|
||||
-- TODO: we don't have a mechanism to unthrottle a session in this case.
|
||||
module:log("info", "Enabling throttle (%s bytes/s) via setlimit, session=%s, ip=%s.", config.session_rate, session, session.ip);
|
||||
session.conn:setlimit(config.session_rate);
|
||||
else
|
||||
module:log("info", "Enabling throttle (%s bytes/s) via filter, session=%s, ip=%s.", config.session_rate, session, session.ip);
|
||||
session.jitsi_throttle = new_throttle(config.session_rate, 2);
|
||||
filters.add_filter(session, "bytes/in", limit_bytes_in, 1000);
|
||||
-- throttle.start used for stop throttling after the timeout
|
||||
session.jitsi_throttle.start = gettime();
|
||||
end
|
||||
else
|
||||
-- update the throttling start
|
||||
session.jitsi_throttle.start = gettime();
|
||||
end
|
||||
end
|
||||
|
||||
-- checks different stanzas for rate limiting (per session)
|
||||
function filter_stanza(stanza, session)
|
||||
local rate = session[stanza.name.."_rate"];
|
||||
if rate then
|
||||
local ok, _, _ = rate:poll(1, true);
|
||||
if not ok then
|
||||
module:log("info", "%s rate exceeded for %s, limiting.", stanza.name, session.full_jid);
|
||||
throttle_session(session);
|
||||
end
|
||||
end
|
||||
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local function on_login(session, ip)
|
||||
local login_rate = login_rates:get(ip);
|
||||
if not login_rate then
|
||||
module:log("debug", "Create new join rate for %s", ip);
|
||||
login_rate = new_throttle(config.login_rate, 2);
|
||||
login_rates:set(ip, login_rate);
|
||||
end
|
||||
|
||||
local ok, _, _ = login_rate:poll(1, true);
|
||||
if not ok then
|
||||
module:log("info", "Join rate exceeded for %s, limiting.", ip);
|
||||
limit_ip(ip);
|
||||
throttle_session(session);
|
||||
end
|
||||
end
|
||||
|
||||
local function filter_hook(session)
|
||||
local request = get_request_from_conn(session.conn);
|
||||
local ip = request and request.ip or session.ip;
|
||||
module:log("debug", "New session from %s", ip);
|
||||
if is_whitelisted(ip) or (session.username and is_whitelisted_jid(session.username..'@'..session.host)) then
|
||||
return;
|
||||
end
|
||||
|
||||
on_login(session, ip);
|
||||
|
||||
-- creates the stanzas rates
|
||||
session.presence_rate = new_throttle(config.presence_rate, 2);
|
||||
session.iq_rate = new_throttle(config.iq_rate, 2);
|
||||
session.message_rate = new_throttle(config.message_rate, 2);
|
||||
filters.add_filter(session, "stanzas/in", filter_stanza);
|
||||
|
||||
local oldt = limited_ips:get(ip);
|
||||
if oldt then
|
||||
local newt = gettime();
|
||||
local elapsed = newt - oldt;
|
||||
if elapsed < 5 then
|
||||
module:log("info", "IP address %s was limitted %s seconds ago, refreshing.", ip, elapsed);
|
||||
limited_ips:set(ip, newt);
|
||||
throttle_session(session);
|
||||
elseif elapsed < config.timeout then
|
||||
throttle_session(session);
|
||||
else
|
||||
module:log("info", "Removing the limit for %s", ip);
|
||||
limited_ips:set(ip, nil);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function module.load()
|
||||
filters.add_filter_hook(filter_hook);
|
||||
end
|
||||
|
||||
function module.unload()
|
||||
filters.remove_filter_hook(filter_hook);
|
||||
end
|
||||
|
||||
module:hook_global("config-reloaded", load_config);
|
||||
31
resources/prosody-plugins/mod_turncredentials_http.lua
Normal file
31
resources/prosody-plugins/mod_turncredentials_http.lua
Normal file
@@ -0,0 +1,31 @@
|
||||
-- http endpoint to expose turn credentials for other services
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local ext_services = module:depends("external_services");
|
||||
local get_services = ext_services.get_services;
|
||||
|
||||
local async_handler_wrapper = module:require "util".async_handler_wrapper;
|
||||
local json = require "util.json";
|
||||
|
||||
--- Handles request for retrieving turn credentials
|
||||
-- @param event the http event, holds the request query
|
||||
-- @return GET response, containing a json with participants details
|
||||
function handle_get_turn_credentials (event)
|
||||
local GET_response = {
|
||||
headers = {
|
||||
content_type = "application/json";
|
||||
};
|
||||
body = json.encode(get_services());
|
||||
};
|
||||
return GET_response;
|
||||
end;
|
||||
|
||||
function module.load()
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["GET turn-credentials"] = function (event) return async_handler_wrapper(event,handle_get_turn_credentials) end;
|
||||
};
|
||||
});
|
||||
end
|
||||
Reference in New Issue
Block a user