Compare commits

...

39 Commits

Author SHA1 Message Date
Calin-Teodor
2039feae94 feat(base): TERTIARY type rework 2022-07-08 13:49:39 +03:00
Calin-Teodor
2842d3acef feat(conf/gifs/participants): created ParticipantsPaneFooter and updated comments 2022-07-08 13:49:31 +03:00
Calin-Teodor
00e1a1d79d feat(base): fixed height for buttons 2022-07-08 13:49:22 +03:00
Calinteodor
1843516ec7 feat(mobile/navigation): revert to stack navigator (#11811)
* feat(mobile/navigation): replaced native stack with stack navigator and other ui fixes
2022-07-07 18:15:49 +03:00
Calinteodor
7f34818cd4 feat(base/native): button abstractions (#11795)
* feat(base): created Button.tsx and IconButton.tsx
2022-07-07 18:15:41 +03:00
Calin-Teodor
c8229a590c feat(gifs/native): fixed linter 2022-07-07 18:15:31 +03:00
Calin-Teodor
e339325afc feat(gifs/native): created GifsMenuFooter 2022-07-07 18:15:22 +03:00
Calin-Teodor
2aecbcaab1 feat(conference/native): created CarModeFooter 2022-07-07 18:15:13 +03:00
Calin-Teodor
f8037966f4 feat(base): removed PagedList because it is not used anymore 2022-07-07 18:15:04 +03:00
Calin-Teodor
057dceb178 feat(etherpad): ui fixes 2022-07-07 18:14:47 +03:00
Saúl Ibarra Corretgé
f8917b2d3f chore(rn,versions) bump appp and sdk versions 2022-07-06 23:01:17 +02:00
Boris Grozev
2dd3c72473 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1461.0.0+96664436...v1464.0.0+28aab9fc
2022-07-06 14:40:59 -04:00
Calinteodor
4a4856f3de feat(conference/native): disabled PiP on WelcomePage (#11801)
* feat(conference/native): disabled PiP on WelcomePage
2022-07-06 18:14:41 +03:00
Titus Moldovan
47bdf800e7 fix(ios) removes scope from sendEvent parameter in ExternalAPI 2022-07-06 17:11:55 +02:00
Robert Pintilii
f6d088149c fix(video-constraints) Fix video constraints for resizable top panel (#11794) 2022-07-05 17:23:01 +03:00
Saúl Ibarra Corretgé
cbe3d6d505 feat(rn) remove use of externalAPIScope
Use the system broadcasting mechanism instead.

On Android I took the chance and removed the no longer needed
BaseReactView and implemented it on JitsiMeetView instead.
2022-07-05 11:40:03 +02:00
Calin-Teodor
b41c71e80b feat(conference): fixed linter 2022-07-05 10:16:13 +03:00
Calin-Teodor
a4b997362a feat(conference): disabled pip if we are not in conference room 2022-07-05 10:16:13 +03:00
Saúl Ibarra Corretgé
a5da90ddaf fix(prejoin) don't hide during auth
Fix the focus issue by disabling autofocus in case an auth (login, ait
for owner or password) dialog is shown.

Fixes: https://github.com/jitsi/docker-jitsi-meet/issues/1336
2022-07-05 07:49:35 +02:00
Saúl Ibarra Corretgé
dffa71666c fix(rn) fix mobile build
Looks like a transform error of some sort, it chokes on the ??=
shorthand syntax.
2022-07-04 21:09:32 +03:00
Calin-Teodor
892751154c feat(etherpad/native): fixed header left close button 2022-07-04 17:48:28 +03:00
Robert Pintilii
935e4d3261 ref(config) Convert config to TypeScript (#11774) 2022-07-04 14:12:12 +03:00
Saúl Ibarra Corretgé
f115028961 fix(rn,dynamic-branding) fix extracting fqdn from URL
On mobile we don't want to look in window.location.
2022-07-04 10:52:13 +03:00
Robert Pintilii
d910b9db57 fix(filmstrip) Fix screensharing filmstrip (#11775) 2022-07-04 10:32:59 +03:00
Robert Pintilii
b2b576f6fb ref(reducers) Convert some reducers to TS (#11768) 2022-07-01 12:33:03 +03:00
Robert Pintilii
a39d9f283d ref(reducers) Convert some reducers to TS (#11768) 2022-07-01 12:32:39 +03:00
Saúl Ibarra Corretgé
0913cf2c4f fix(android) make ongoing service public
Those using the view API may want to integrate iit in their own
Activity.
2022-07-01 11:58:18 +03:00
tmoldovan8x8
51f7b46628 fix(android) explicitly sets the theme for JitsiMeetActivity 2022-06-30 16:44:52 +02:00
George Politis
d029045fda fix: Do not send the videoType for audio tracks (#11742) 2022-06-30 16:21:42 +02:00
Alex Bumbu
ddab27e292 fix(ios, pip): update view hierarchy to present the rn view with view controller 2022-06-29 17:57:51 +02:00
Calin-Teodor
6df2e4009c feat(dynamic-branding): get branding data from state 2022-06-29 18:47:38 +03:00
Robert Pintilii
21cf7f23c2 feat: Add screenshare filmstrip (#11714)
Add new screen share layout with resizable top panel
Only enable new layout in large meetings (min 50 participants - configurable)
2022-06-29 16:59:49 +03:00
Calin-Teodor
bac1347961 feat(lobby/prejoin/native): display name input text color update 2022-06-29 15:56:03 +03:00
Robert Pintilii
c4f39e9c34 feat(recording) Add config to hide storage warning (#11761) 2022-06-29 15:28:20 +03:00
Saúl Ibarra Corretgé
ee266160f9 fix(external-api) fix error if setting some options too early
Specifically: display-name, email and avatar. These are the most common
ones, and the ones currently used by Spot for example.
2022-06-29 13:28:53 +03:00
Robert Pintilii
730d42cba1 fix(local-recording) Improvements (#11754)
Show Start rec button if local rec is enabled but fileRecordings is disabled
Add warning for users to stop the recording
2022-06-29 10:05:55 +03:00
Calin Chitu
3f795cd1ff feat(gifs/native): fixed gify search input 2022-06-28 18:23:14 +03:00
Robert Pintilii
252441da29 feat(transcription) Enable for all (#11739)
Move all transcription configs into new object
2022-06-28 14:11:26 +03:00
Saúl Ibarra Corretgé
b89c470366 chore(deps) react-native-screens@3.13.1 2022-06-28 12:02:47 +03:00
141 changed files with 3073 additions and 2479 deletions

View File

@@ -26,5 +26,5 @@ android.useAndroidX=true
android.enableJetifier=true
android.bundle.enableUncompressedNativeLibs=false
appVersion=22.3.0
sdkVersion=5.2.0
appVersion=22.4.0
sdkVersion=6.0.0

View File

@@ -32,6 +32,7 @@
android:name=".JitsiMeetActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:theme="@style/JitsiMeetActivityStyle"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:windowSoftInputMode="adjustResize"/>

View File

@@ -1,240 +0,0 @@
/*
* Copyright @ 2018-present 8x8, Inc.
* Copyright @ 2018 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.ReactRootView;
import com.facebook.react.bridge.ReadableMap;
import com.rnimmersive.RNImmersiveModule;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
/**
* Base class for all views which are backed by a React Native view.
*/
public abstract class BaseReactView<ListenerT>
extends FrameLayout {
/**
* Background color used by {@code BaseReactView} and the React Native root
* view.
*/
protected static int BACKGROUND_COLOR = 0xFF111111;
/**
* The collection of all existing {@code BaseReactView}s. Used to find the
* {@code BaseReactView} when delivering events coming from
* {@link ExternalAPIModule}.
*/
static final Set<BaseReactView> views
= Collections.newSetFromMap(new WeakHashMap<BaseReactView, Boolean>());
/**
* Finds a {@code BaseReactView} which matches a specific external API
* scope.
*
* @param externalAPIScope - The external API scope associated with the
* {@code BaseReactView} to find.
* @return The {@code BaseReactView}, if any, associated with the specified
* {@code externalAPIScope}; otherwise, {@code null}.
*/
public static BaseReactView findViewByExternalAPIScope(
String externalAPIScope) {
synchronized (views) {
for (BaseReactView view : views) {
if (view.externalAPIScope.equals(externalAPIScope)) {
return view;
}
}
}
return null;
}
/**
* Gets all registered React views.
*
* @return An {@link ArrayList} containing all views currently held by React.
*/
static ArrayList<BaseReactView> getViews() {
return new ArrayList<>(views);
}
/**
* The unique identifier of this {@code BaseReactView} within the process
* for the purposes of {@link ExternalAPIModule}. The name scope was
* inspired by postis which we use on Web for the similar purposes of the
* iframe-based external API.
*/
protected String externalAPIScope;
/**
* The listener (e.g. {@link JitsiMeetViewListener}) instance for reporting
* events occurring in Jitsi Meet.
*/
@Deprecated
private ListenerT listener;
/**
* React Native root view.
*/
private ReactRootView reactRootView;
public BaseReactView(@NonNull Context context) {
super(context);
initialize((Activity)context);
}
public BaseReactView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize((Activity)context);
}
public BaseReactView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize((Activity)context);
}
/**
* Creates the {@code ReactRootView} for the given app name with the given
* props. Once created it's set as the view of this {@code FrameLayout}.
*
* @param appName - The name of the "app" (in React Native terms) to load.
* @param props - The React Component props to pass to the app.
*/
public void createReactRootView(String appName, @Nullable Bundle props) {
if (props == null) {
props = new Bundle();
}
props.putString("externalAPIScope", externalAPIScope);
if (reactRootView == null) {
reactRootView = new ReactRootView(getContext());
reactRootView.startReactApplication(
ReactInstanceManagerHolder.getReactInstanceManager(),
appName,
props);
reactRootView.setBackgroundColor(BACKGROUND_COLOR);
addView(reactRootView);
} else {
reactRootView.setAppProperties(props);
}
}
/**
* Releases the React resources (specifically the {@link ReactRootView})
* associated with this view.
*
* MUST be called when the {@link Activity} holding this view is destroyed,
* typically in the {@code onDestroy} method.
*/
public void dispose() {
if (reactRootView != null) {
removeView(reactRootView);
reactRootView.unmountReactApplication();
reactRootView = null;
}
}
/**
* Gets the listener set on this {@code BaseReactView}.
*
* @return The listener set on this {@code BaseReactView}.
*/
@Deprecated
public ListenerT getListener() {
return listener;
}
/**
* Abstract method called by {@link ExternalAPIModule} when an event is
* received for this view.
*
* @param name - The name of the event.
* @param data - The details of the event associated with/specific to the
* specified {@code name}.
*/
@Deprecated
protected abstract void onExternalAPIEvent(String name, ReadableMap data);
@Deprecated
protected void onExternalAPIEvent(
Map<String, Method> listenerMethods,
String name, ReadableMap data) {
ListenerT listener = getListener();
if (listener != null) {
ListenerUtils.runListenerMethod(
listener, listenerMethods, name, data);
}
}
/**
* Called when the window containing this view gains or loses focus.
*
* @param hasFocus If the window of this view now has focus, {@code true};
* otherwise, {@code false}.
*/
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
// https://github.com/mockingbot/react-native-immersive#restore-immersive-state
RNImmersiveModule immersive = RNImmersiveModule.getInstance();
if (hasFocus && immersive != null) {
immersive.emitImmersiveStateChangeEvent();
}
}
/**
* Sets a specific listener on this {@code BaseReactView}.
*
* @param listener The listener to set on this {@code BaseReactView}.
*/
@Deprecated
public void setListener(ListenerT listener) {
this.listener = listener;
}
private void initialize(Activity activity) {
setBackgroundColor(BACKGROUND_COLOR);
ReactInstanceManagerHolder.initReactInstanceManager(activity);
// Hook this BaseReactView into ExternalAPI.
externalAPIScope = UUID.randomUUID().toString();
synchronized (views) {
views.add(this);
}
}
}

View File

@@ -102,31 +102,18 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
/**
* Dispatches an event that occurred on the JavaScript side of the SDK to
* the specified {@link BaseReactView}'s listener.
* the native side.
*
* @param name The name of the event.
* @param data The details/specifics of the event to send determined
* by/associated with the specified {@code name}.
* @param scope
*/
@ReactMethod
public void sendEvent(String name, ReadableMap data, String scope) {
public void sendEvent(String name, ReadableMap data) {
// Keep track of the current ongoing conference.
OngoingConferenceTracker.getInstance().onExternalAPIEvent(name, data);
// The JavaScript App needs to provide uniquely identifying information
// to the native ExternalAPI module so that the latter may match the
// former to the native BaseReactView which hosts it.
BaseReactView view = BaseReactView.findViewByExternalAPIScope(scope);
if (view != null) {
JitsiMeetLogger.d(TAG + " Sending event: " + name + " with data: " + data);
try {
view.onExternalAPIEvent(name, data);
broadcastEmitter.sendBroadcast(name, data);
} catch (Exception e) {
JitsiMeetLogger.e(e, TAG + " onExternalAPIEvent: error sending event");
}
}
JitsiMeetLogger.d(TAG + " Sending event: " + name + " with data: " + data);
broadcastEmitter.sendBroadcast(name, data);
}
}

View File

@@ -167,12 +167,9 @@ public class JitsiMeetActivity extends AppCompatActivity
}
}
public void leave() {
if (this.jitsiView != null) {
this.jitsiView .leave();
} else {
JitsiMeetLogger.w("Cannot leave, view is null");
}
protected void leave() {
Intent hangupBroadcastIntent = BroadcastIntentHelper.buildHangUpIntent();
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(hangupBroadcastIntent);
}
private @Nullable

View File

@@ -1,82 +0,0 @@
/*
* Copyright @ 2019-present 8x8, Inc.
* Copyright @ 2017-2018 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
/**
* Base {@link Fragment} for applications integrating Jitsi Meet at a higher level. It
* contains all the required wiring between the {@code JitsiMeetView} and
* the Fragment lifecycle methods already implemented.
*
* In this fragment we use a single {@code JitsiMeetView} instance. This
* instance gives us access to a view which displays the welcome page and the
* conference itself. All lifecycle methods associated with this Fragment are
* hooked to the React Native subsystem via proxy calls through the
* {@code JitsiMeetActivityDelegate} static methods.
*
* @deprecated use {@link JitsiMeetActivity} or directly {@link JitsiMeetView}
*/
@Deprecated
public class JitsiMeetFragment extends Fragment {
/**
* Instance of the {@link JitsiMeetView} which this activity will display.
*/
private JitsiMeetView view;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return this.view = new JitsiMeetView(getActivity());
}
public JitsiMeetView getJitsiView() {
return view;
}
@Override
public void onDestroy() {
super.onDestroy();
JitsiMeetActivityDelegate.onHostDestroy(getActivity());
}
@Override
public void onResume() {
super.onResume();
JitsiMeetActivityDelegate.onHostResume(getActivity());
}
@Override
public void onStop() {
super.onStop();
JitsiMeetActivityDelegate.onHostPause(getActivity());
}
}

View File

@@ -51,7 +51,7 @@ public class JitsiMeetOngoingConferenceService extends Service
private boolean isAudioMuted;
static void launch(Context context, HashMap<String, Object> extraData) {
public static void launch(Context context, HashMap<String, Object> extraData) {
OngoingNotification.createOngoingConferenceNotificationChannel();
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
@@ -80,7 +80,7 @@ public class JitsiMeetOngoingConferenceService extends Service
}
}
static void abort(Context context) {
public static void abort(Context context) {
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
context.stopService(intent);
}

View File

@@ -16,36 +16,33 @@
package org.jitsi.meet.sdk;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.ReactRootView;
import com.rnimmersive.RNImmersiveModule;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.lang.reflect.Method;
import java.util.Map;
public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener>
implements OngoingConferenceTracker.OngoingConferenceListener {
public class JitsiMeetView extends FrameLayout {
/**
* The {@code Method}s of {@code JitsiMeetViewListener} by event name i.e.
* redux action types.
* Background color used by {@code BaseReactView} and the React Native root
* view.
*/
private static final Map<String, Method> LISTENER_METHODS
= ListenerUtils.mapListenerMethods(JitsiMeetViewListener.class);
private static final int BACKGROUND_COLOR = 0xFF111111;
/**
* The URL of the current conference.
* React Native root view.
*/
// XXX Currently, one thread writes and one thread reads, so it should be
// fine to have this field volatile without additional synchronization.
private volatile String url;
private ReactRootView reactRootView;
/**
* Helper method to recursively merge 2 {@link Bundle} objects representing React Native props.
@@ -109,10 +106,19 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener>
initialize(context);
}
@Override
/**
* Releases the React resources (specifically the {@link ReactRootView})
* associated with this view.
*
* MUST be called when the {@link Activity} holding this view is destroyed,
* typically in the {@code onDestroy} method.
*/
public void dispose() {
OngoingConferenceTracker.getInstance().removeListener(this);
super.dispose();
if (reactRootView != null) {
removeView(reactRootView);
reactRootView.unmountReactApplication();
reactRootView = null;
}
}
/**
@@ -130,8 +136,7 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener>
PictureInPictureModule.class);
if (pipModule != null
&& pipModule.isPictureInPictureSupported()
&& !JitsiMeetActivityDelegate.arePermissionsBeingRequested()
&& this.url != null) {
&& !JitsiMeetActivityDelegate.arePermissionsBeingRequested()) {
try {
pipModule.enterPictureInPicture();
} catch (RuntimeException re) {
@@ -151,10 +156,40 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener>
}
/**
* Leaves the currently active conference.
* Creates the {@code ReactRootView} for the given app name with the given
* props. Once created it's set as the view of this {@code FrameLayout}.
*
* @param appName - The name of the "app" (in React Native terms) to load.
* @param props - The React Component props to pass to the app.
*/
public void leave() {
setProps(new Bundle());
private void createReactRootView(String appName, @Nullable Bundle props) {
if (props == null) {
props = new Bundle();
}
if (reactRootView == null) {
reactRootView = new ReactRootView(getContext());
reactRootView.startReactApplication(
ReactInstanceManagerHolder.getReactInstanceManager(),
appName,
props);
reactRootView.setBackgroundColor(BACKGROUND_COLOR);
addView(reactRootView);
} else {
reactRootView.setAppProperties(props);
}
}
private void initialize(@NonNull Context context) {
// Check if the parent Activity implements JitsiMeetActivityInterface,
// otherwise things may go wrong.
if (!(context instanceof JitsiMeetActivityInterface)) {
throw new RuntimeException("Enclosing Activity must implement JitsiMeetActivityInterface");
}
setBackgroundColor(BACKGROUND_COLOR);
ReactInstanceManagerHolder.initReactInstanceManager((Activity) context);
}
/**
@@ -179,46 +214,27 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener>
createReactRootView("App", props);
}
/**
* Handler for {@link OngoingConferenceTracker} events.
* @param conferenceUrl
*/
@Override
public void onCurrentConferenceChanged(String conferenceUrl) {
// This property was introduced in order to address
// an exception in the Picture-in-Picture functionality which arose
// because of delays related to bridging between JavaScript and Java. To
// reduce these delays do not wait for the call to be transferred to the
// UI thread.
this.url = conferenceUrl;
}
/**
* Handler for {@link ExternalAPIModule} events.
*
* @param name The name of the event.
* @param data The details/specifics of the event to send determined
* by/associated with the specified {@code name}.
*/
@Override
@Deprecated
protected void onExternalAPIEvent(String name, ReadableMap data) {
onExternalAPIEvent(LISTENER_METHODS, name, data);
}
@Override
protected void onDetachedFromWindow() {
dispose();
super.onDetachedFromWindow();
}
private void initialize(@NonNull Context context) {
// Check if the parent Activity implements JitsiMeetActivityInterface,
// otherwise things may go wrong.
if (!(context instanceof JitsiMeetActivityInterface)) {
throw new RuntimeException("Enclosing Activity must implement JitsiMeetActivityInterface");
}
/**
* Called when the window containing this view gains or loses focus.
*
* @param hasFocus If the window of this view now has focus, {@code true};
* otherwise, {@code false}.
*/
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
OngoingConferenceTracker.getInstance().addListener(this);
// https://github.com/mockingbot/react-native-immersive#restore-immersive-state
RNImmersiveModule immersive = RNImmersiveModule.getInstance();
if (hasFocus && immersive != null) {
immersive.emitImmersiveStateChangeEvent();
}
}
}

View File

@@ -1,51 +0,0 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import java.util.Map;
/**
* Interface for listening to events coming from Jitsi Meet.
*/
@Deprecated
public interface JitsiMeetViewListener {
/**
* Called when a conference was joined.
*
* @param data Map with a "url" key with the conference URL.
*/
void onConferenceJoined(Map<String, Object> data);
/**
* Called when the active conference ends, be it because of user choice or
* because of a failure.
*
* @param data Map with an "error" key with the error and a "url" key with
* the conference URL. If the conference finished gracefully no `error`
* key will be present. The possible values for "error" are described here:
* https://github.com/jitsi/lib-jitsi-meet/blob/master/JitsiConnectionErrors.js
* https://github.com/jitsi/lib-jitsi-meet/blob/master/JitsiConferenceErrors.js
*/
void onConferenceTerminated(Map<String, Object> data);
/**
* Called before the conference is joined.
*
* @param data Map with a "url" key with the conference URL.
*/
void onConferenceWillJoin(Map<String, Object> data);
}

View File

@@ -1,167 +0,0 @@
/*
* Copyright @ 2018-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.UiThreadUtil;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Utility methods for helping with transforming {@link ExternalAPIModule}
* events into listener methods. Used with descendants of {@link BaseReactView}.
*/
@Deprecated
public final class ListenerUtils {
/**
* Extracts the methods defined in a listener and creates a mapping of this
* form: event name -> method.
*
* @param listener - The listener whose methods we want to slurp.
* @return A mapping with event names - methods.
*/
public static Map<String, Method> mapListenerMethods(Class listener) {
Map<String, Method> methods = new HashMap<>();
// Figure out the mapping between the listener methods
// and the events i.e. redux action types.
Pattern onPattern = Pattern.compile("^on[A-Z]+");
Pattern camelcasePattern = Pattern.compile("([a-z0-9]+)([A-Z0-9]+)");
for (Method method : listener.getDeclaredMethods()) {
// * The method must be public (because it is declared by an
// interface).
// * The method must be/return void.
if (!Modifier.isPublic(method.getModifiers())
|| !Void.TYPE.equals(method.getReturnType())) {
continue;
}
// * The method name must start with "on" followed by a
// capital/uppercase letter (in agreement with the camelcase
// coding style customary to Java in general and the projects of
// the Jitsi community in particular).
String name = method.getName();
if (!onPattern.matcher(name).find()) {
continue;
}
// * The method must accept/have exactly 1 parameter of a type
// assignable from HashMap.
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length != 1
|| !parameterTypes[0].isAssignableFrom(HashMap.class)) {
continue;
}
// Convert the method name to an event name.
name
= camelcasePattern.matcher(name.substring(2))
.replaceAll("$1_$2")
.toUpperCase(Locale.ROOT);
methods.put(name, method);
}
return methods;
}
/**
* Executes the right listener method for the given event.
* NOTE: This function will run asynchronously on the UI thread.
*
* @param listener - The listener on which the method will be called.
* @param listenerMethods - Mapping with event names and the matching
* methods.
* @param eventName - Name of the event.
* @param eventData - Data associated with the event.
*/
public static void runListenerMethod(
final Object listener,
final Map<String, Method> listenerMethods,
final String eventName,
final ReadableMap eventData) {
// Make sure listener methods are invoked on the UI thread. It
// was requested by SDK consumers.
if (UiThreadUtil.isOnUiThread()) {
runListenerMethodOnUiThread(
listener, listenerMethods, eventName, eventData);
} else {
UiThreadUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
runListenerMethodOnUiThread(
listener, listenerMethods, eventName, eventData);
}
});
}
}
/**
* Helper companion for {@link ListenerUtils#runListenerMethod} which runs
* in the UI thread.
*/
private static void runListenerMethodOnUiThread(
Object listener,
Map<String, Method> listenerMethods,
String eventName,
ReadableMap eventData) {
UiThreadUtil.assertOnUiThread();
Method method = listenerMethods.get(eventName);
if (method != null) {
try {
method.invoke(listener, toHashMap(eventData));
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
/**
* Initializes a new {@code HashMap} instance with the key-value
* associations of a specific {@code ReadableMap}.
*
* @param readableMap the {@code ReadableMap} specifying the key-value
* associations with which the new {@code HashMap} instance is to be
* initialized.
* @return a new {@code HashMap} instance initialized with the key-value
* associations of the specified {@code readableMap}.
*/
private static HashMap<String, Object> toHashMap(ReadableMap readableMap) {
HashMap<String, Object> hashMap = new HashMap<>();
for (ReadableMapKeySetIterator i = readableMap.keySetIterator();
i.hasNextKey();) {
String key = i.nextKey();
hashMap.put(key, readableMap.getString(key));
}
return hashMap;
}
}

View File

@@ -0,0 +1,3 @@
<resources>
<style name="JitsiMeetActivityStyle" parent="Theme.AppCompat.Light.NoActionBar"/>
</resources>

View File

@@ -3094,34 +3094,12 @@ export default {
* @param email {string} the new email
*/
changeLocalEmail(email = '') {
const localParticipant = getLocalParticipant(APP.store.getState());
const formattedEmail = String(email).trim();
if (formattedEmail === localParticipant.email) {
return;
}
const localId = localParticipant.id;
APP.store.dispatch(participantUpdated({
// XXX Only the local participant is allowed to update without
// stating the JitsiConference instance (i.e. participant property
// `conference` for a remote participant) because the local
// participant is uniquely identified by the very fact that there is
// only one local participant.
id: localId,
local: true,
email: formattedEmail
}));
APP.store.dispatch(updateSettings({
email: formattedEmail
}));
APP.API.notifyEmailChanged(localId, {
email: formattedEmail
});
sendData(commands.EMAIL, formattedEmail);
},
@@ -3130,29 +3108,12 @@ export default {
* @param url {string} the new url
*/
changeLocalAvatarUrl(url = '') {
const { avatarURL, id } = getLocalParticipant(APP.store.getState());
const formattedUrl = String(url).trim();
if (formattedUrl === avatarURL) {
return;
}
APP.store.dispatch(participantUpdated({
// XXX Only the local participant is allowed to update without
// stating the JitsiConference instance (i.e. participant property
// `conference` for a remote participant) because the local
// participant is uniquely identified by the very fact that there is
// only one local participant.
id,
local: true,
avatarURL: formattedUrl
}));
APP.store.dispatch(updateSettings({
avatarURL: formattedUrl
}));
sendData(commands.AVATAR_URL, url);
},
@@ -3193,23 +3154,6 @@ export default {
*/
changeLocalDisplayName(nickname = '') {
const formattedNickname = getNormalizedDisplayName(nickname);
const { id, name } = getLocalParticipant(APP.store.getState());
if (formattedNickname === name) {
return;
}
APP.store.dispatch(participantUpdated({
// XXX Only the local participant is allowed to update without
// stating the JitsiConference instance (i.e. participant property
// `conference` for a remote participant) because the local
// participant is uniquely identified by the very fact that there is
// only one local participant.
id,
local: true,
name: formattedNickname
}));
APP.store.dispatch(updateSettings({
displayName: formattedNickname

View File

@@ -143,6 +143,7 @@ var config = {
// Disable measuring of audio levels.
// disableAudioLevels: false,
// audioLevelsInterval: 200,
// Enabling this will run the lib-jitsi-meet no audio detection module which
@@ -271,8 +272,9 @@ var config = {
// Recording
// Whether to enable file recording or not.
// DEPRECATED. Use recordingService.enabled instead.
// fileRecordingsEnabled: false,
// Enable the dropbox integration.
// dropbox: {
// appKey: '<APP_KEY>' // Specify your app key here.
@@ -282,14 +284,27 @@ var config = {
// redirectURI:
// 'https://jitsi-meet.example.com/subfolder/static/oauth.html'
// },
// When integrations like dropbox are enabled only that will be shown,
// by enabling fileRecordingsServiceEnabled, we show both the integrations
// and the generic recording service (its configuration and storage type
// depends on jibri configuration)
// recordingService: {
// // When integrations like dropbox are enabled only that will be shown,
// // by enabling fileRecordingsServiceEnabled, we show both the integrations
// // and the generic recording service (its configuration and storage type
// // depends on jibri configuration)
// enabled: false,
// // Whether to show the possibility to share file recording with other people
// // (e.g. meeting participants), based on the actual implementation
// // on the backend.
// sharingEnabled: false,
// // Hide the warning that says we only store the recording for 24 hours.
// hideStorageWarning: false
// },
// DEPRECATED. Use recordingService.enabled instead.
// fileRecordingsServiceEnabled: false,
// Whether to show the possibility to share file recording with other people
// (e.g. meeting participants), based on the actual implementation
// on the backend.
// DEPRECATED. Use recordingService.sharingEnabled instead.
// fileRecordingsServiceSharingEnabled: false,
// Whether to enable live streaming or not.
@@ -303,25 +318,43 @@ var config = {
// notifyAllParticipants: false
// },
// Transcription (in interface_config,
// subtitles and buttons can be configured)
// DEPRECATED. Use transcription.enabled instead.
// transcribingEnabled: false,
// If true transcriber will use the application language.
// The application language is either explicitly set by participants in their settings or automatically
// detected based on the environment, e.g. if the app is opened in a chrome instance which is using french as its
// default language then transcriptions for that participant will be in french.
// Defaults to true.
// DEPRECATED. Use transcription.useAppLanguage instead.
// transcribeWithAppLanguage: true,
// Transcriber language. This settings will only work if "transcribeWithAppLanguage" is explicitly set to false.
// Available languages can be found in
// ./src/react/features/transcribing/transcriber-langs.json.
// DEPRECATED. Use transcription.preferredLanguage instead.
// preferredTranscribeLanguage: 'en-US',
// Enables automatic turning on captions when recording is started
// DEPRECATED. Use transcription.autoCaptionOnRecord instead.
// autoCaptionOnRecord: false,
// Transcription options.
// transcription: {
// // Whether the feature should be enabled or not.
// enabled: false,
// // If true transcriber will use the application language.
// // The application language is either explicitly set by participants in their settings or automatically
// // detected based on the environment, e.g. if the app is opened in a chrome instance which
// // is using french as its default language then transcriptions for that participant will be in french.
// // Defaults to true.
// useAppLanguage: true,
// // Transcriber language. This settings will only work if "useAppLanguage"
// // is explicitly set to false.
// // Available languages can be found in
// // ./src/react/features/transcribing/transcriber-langs.json.
// preferredLanguage: 'en-US',
// // Disable start transcription for all participants.
// disableStartForAll: false,
// // Enables automatic turning on captions when recording is started
// autoCaptionOnRecord: false
// },
// Misc
// Default value for the channel "last N" attribute. -1 for unlimited.
@@ -383,7 +416,7 @@ var config = {
// // This will result in Safari not being able to decode video from endpoints sending VP9 video.
// // When set to false, the conference falls back to VP8 whenever there is an endpoint that doesn't support the
// // preferred codec and goes back to the preferred codec when that endpoint leaves.
// // enforcePreferredCodec: false,
// enforcePreferredCodec: false,
//
// // Provides a way to configure the maximum bitrates that will be enforced on the simulcast streams for
// // video tracks. The keys in the object represent the type of the stream (LD, SD or HD) and the values
@@ -551,8 +584,8 @@ var config = {
// enableFeaturesBasedOnToken: false,
// When enabled the password used for locking a room is restricted to up to the number of digits specified
// roomPasswordNumberOfDigits: 10,
// default: roomPasswordNumberOfDigits: false,
// roomPasswordNumberOfDigits: 10,
// Message to show the users. Example: 'The service will be down for
// maintenance at 01:00 AM GMT,
@@ -1173,7 +1206,8 @@ var config = {
// 'transcribing',
// 'video-quality',
// 'insecure-room',
// 'highlight-moment'
// 'highlight-moment',
// 'top-panel-toggle'
// ]
// },
@@ -1367,7 +1401,14 @@ var config = {
// // Disables the stage filmstrip
// // (displaying multiple participants on stage besides the vertical filmstrip)
// disableStageFilmstrip: false
// disableStageFilmstrip: false,
// // Disables the top panel (only shown when a user is sharing their screen).
// disableTopPanel: false,
// // The minimum number of participants that must be in the call for
// // the top panel layout to be used.
// minParticipantCountForTopPanel: 50
// },
// Tile view related config options.

View File

@@ -21,8 +21,7 @@ import { isFatalJitsiConnectionError } from './react/features/base/lib-jitsi-mee
import { getCustomerDetails } from './react/features/jaas/actions.any';
import { isVpaasMeeting, getJaasJWT } from './react/features/jaas/functions';
import {
setPrejoinDisplayNameRequired,
setPrejoinPageVisibility
setPrejoinDisplayNameRequired
} from './react/features/prejoin/actions';
const logger = Logger.getLogger(__filename);
@@ -247,7 +246,6 @@ function requestAuth(roomName) {
resolve(connection);
};
APP.store.dispatch(setPrejoinPageVisibility(false));
APP.store.dispatch(
openDialog(LoginDialog, { onSuccess,
roomName })

View File

@@ -467,7 +467,7 @@ PODS:
- React-Core
- RNReanimated (1.13.4):
- React-Core
- RNScreens (3.10.1):
- RNScreens (3.13.1):
- React-Core
- React-RCTImage
- RNSound (0.11.1):
@@ -767,7 +767,7 @@ SPEC CHECKSUMS:
RNGestureHandler: e5c7cab5f214503dcefd6b2b0cefb050e1f51c4a
RNGoogleSignin: c4381751eefd73c552b923ba347a9bfc6f18771c
RNReanimated: c1b56d030d1616239861534d9adb531f8cffab68
RNScreens: 522705f2e5c9d27efb17f24aceb2bf8335bc7b8e
RNScreens: 40a2cb40a02a609938137a1e0acfbf8fc9eebf19
RNSound: 27e8268bdb0a1f191f219a33267f7e0445e8d62f
RNSVG: ce9d996113475209013317e48b05c21ee988d42e
RNWatch: 99637948ec9b5c9ec5a41920642594ad5ba07e80

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>22.3.0</string>
<string>22.4.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>22.3.0</string>
<string>22.4.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>22.3.0</string>
<string>22.4.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UISupportedInterfaceOrientations</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>22.3.0</string>
<string>22.4.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CLKComplicationPrincipalClass</key>

View File

@@ -24,8 +24,14 @@
0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */ = {isa = PBXBuildFile; fileRef = 0BD906E81EC0C00300C8C18E /* JitsiMeet.h */; settings = {ATTRIBUTES = (Public, ); }; };
4E51B76425E5345E0038575A /* ScheenshareEventEmiter.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E51B76225E5345E0038575A /* ScheenshareEventEmiter.h */; };
4E51B76525E5345E0038575A /* ScheenshareEventEmiter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E51B76325E5345E0038575A /* ScheenshareEventEmiter.m */; };
4EBA6E61286072E300B31882 /* JitsiMeetViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EBA6E5F286072E300B31882 /* JitsiMeetViewController.h */; };
4EBA6E62286072E300B31882 /* JitsiMeetViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA6E60286072E300B31882 /* JitsiMeetViewController.m */; };
4EBA6E652860B1E800B31882 /* JitsiMeetRenderingView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EBA6E632860B1E800B31882 /* JitsiMeetRenderingView.h */; };
4EBA6E662860B1E800B31882 /* JitsiMeetRenderingView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA6E642860B1E800B31882 /* JitsiMeetRenderingView.m */; };
4ED4FFF32721B9B90074E620 /* JitsiAudioSession.h in Headers */ = {isa = PBXBuildFile; fileRef = 4ED4FFF12721B9B90074E620 /* JitsiAudioSession.h */; settings = {ATTRIBUTES = (Public, ); }; };
4ED4FFF42721B9B90074E620 /* JitsiAudioSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ED4FFF22721B9B90074E620 /* JitsiAudioSession.m */; };
4EEC9630286C73A2008705FA /* JitsiMeetView+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EEC962E286C73A2008705FA /* JitsiMeetView+Private.h */; };
4EEC9631286C73A2008705FA /* JitsiMeetView+Private.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EEC962F286C73A2008705FA /* JitsiMeetView+Private.m */; };
6F08DF7D4458EE3CF3F36F6D /* libPods-JitsiMeetSDK.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E4376CA6886DE68FD7A4294B /* libPods-JitsiMeetSDK.a */; };
A4A934E9212F3ADB001E9388 /* Dropbox.m in Sources */ = {isa = PBXBuildFile; fileRef = A4A934E8212F3ADB001E9388 /* Dropbox.m */; };
C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5B2053091D0040BE68 /* image-resize@2x.png */; };
@@ -79,9 +85,15 @@
0BD906E91EC0C00300C8C18E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
4E51B76225E5345E0038575A /* ScheenshareEventEmiter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ScheenshareEventEmiter.h; sourceTree = "<group>"; };
4E51B76325E5345E0038575A /* ScheenshareEventEmiter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ScheenshareEventEmiter.m; sourceTree = "<group>"; };
4EBA6E5F286072E300B31882 /* JitsiMeetViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetViewController.h; sourceTree = "<group>"; };
4EBA6E60286072E300B31882 /* JitsiMeetViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetViewController.m; sourceTree = "<group>"; };
4EBA6E632860B1E800B31882 /* JitsiMeetRenderingView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetRenderingView.h; sourceTree = "<group>"; };
4EBA6E642860B1E800B31882 /* JitsiMeetRenderingView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetRenderingView.m; sourceTree = "<group>"; };
4ED4FFF12721B9B90074E620 /* JitsiAudioSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiAudioSession.h; sourceTree = "<group>"; };
4ED4FFF22721B9B90074E620 /* JitsiAudioSession.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiAudioSession.m; sourceTree = "<group>"; };
4ED4FFF52721BAE10074E620 /* JitsiAudioSession+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiAudioSession+Private.h"; sourceTree = "<group>"; };
4EEC962E286C73A2008705FA /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
4EEC962F286C73A2008705FA /* JitsiMeetView+Private.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "JitsiMeetView+Private.m"; sourceTree = "<group>"; };
891FE43DAD30BC8976683100 /* Pods-JitsiMeetSDK.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeetSDK.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeetSDK/Pods-JitsiMeetSDK.release.xcconfig"; sourceTree = "<group>"; };
98E09B5C73D9036B4ED252FC /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = "<group>"; };
9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.release.xcconfig"; sourceTree = "<group>"; };
@@ -94,7 +106,6 @@
C69EFA0B209A0F660027712B /* JMCallKitListener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JMCallKitListener.swift; sourceTree = "<group>"; };
C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = "<group>"; };
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = "<group>"; };
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
C81E9AB825AC5AD800B134D9 /* ExternalAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExternalAPI.h; sourceTree = "<group>"; };
C8AFD27D2462C613000293D2 /* InfoPlistUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InfoPlistUtil.h; sourceTree = "<group>"; };
C8AFD27E2462C613000293D2 /* InfoPlistUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InfoPlistUtil.m; sourceTree = "<group>"; };
@@ -194,12 +205,17 @@
DE65AACB2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h */,
DE81A2DD2317ED5400AE1940 /* JitsiMeetBaseLogHandler.m */,
0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */,
4EEC962E286C73A2008705FA /* JitsiMeetView+Private.h */,
4EEC962F286C73A2008705FA /* JitsiMeetView+Private.m */,
0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */,
4EBA6E632860B1E800B31882 /* JitsiMeetRenderingView.h */,
4EBA6E642860B1E800B31882 /* JitsiMeetRenderingView.m */,
4EBA6E5F286072E300B31882 /* JitsiMeetViewController.h */,
4EBA6E60286072E300B31882 /* JitsiMeetViewController.m */,
DE81A2D72316AC7600AE1940 /* LogBridge.m */,
DE65AAC92317FFCD00290BEC /* LogUtils.h */,
DEAFA777229EAD3B0033A7FA /* RNRootView.h */,
DEAFA778229EAD520033A7FA /* RNRootView.m */,
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */,
0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */,
DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */,
C6A3426B204F127900E062DD /* picture-in-picture */,
@@ -284,11 +300,14 @@
DEA9F284258A5D9900D4CD74 /* JitsiMeetSDK.h in Headers */,
4E51B76425E5345E0038575A /* ScheenshareEventEmiter.h in Headers */,
DE65AACC2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h in Headers */,
4EBA6E652860B1E800B31882 /* JitsiMeetRenderingView.h in Headers */,
0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */,
4ED4FFF32721B9B90074E620 /* JitsiAudioSession.h in Headers */,
4EEC9630286C73A2008705FA /* JitsiMeetView+Private.h in Headers */,
0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */,
DE81A2D42316AC4D00AE1940 /* JitsiMeetLogger.h in Headers */,
DE65AACA2317FFCD00290BEC /* LogUtils.h in Headers */,
4EBA6E61286072E300B31882 /* JitsiMeetViewController.h in Headers */,
DEAD3226220C497000E93636 /* JitsiMeetConferenceOptions.h in Headers */,
C81E9AB925AC5AD800B134D9 /* ExternalAPI.h in Headers */,
C8AFD27F2462C613000293D2 /* InfoPlistUtil.h in Headers */,
@@ -449,6 +468,7 @@
files = (
0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */,
DE81A2DF2317ED5400AE1940 /* JitsiMeetBaseLogHandler.m in Sources */,
4EBA6E662860B1E800B31882 /* JitsiMeetRenderingView.m in Sources */,
4ED4FFF42721B9B90074E620 /* JitsiAudioSession.m in Sources */,
0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */,
DE81A2D92316AC7600AE1940 /* LogBridge.m in Sources */,
@@ -465,9 +485,11 @@
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
C69EFA0C209A0F660027712B /* JMCallKitEmitter.swift in Sources */,
DEFE535621FB2E8300011A3A /* ReactUtils.m in Sources */,
4EEC9631286C73A2008705FA /* JitsiMeetView+Private.m in Sources */,
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */,
4E51B76525E5345E0038575A /* ScheenshareEventEmiter.m in Sources */,
A4A934E9212F3ADB001E9388 /* Dropbox.m in Sources */,
4EBA6E62286072E300B31882 /* JitsiMeetViewController.m in Sources */,
C69EFA0D209A0F660027712B /* JMCallKitProxy.swift in Sources */,
DE81A2D52316AC4D00AE1940 /* JitsiMeetLogger.m in Sources */,
C69EFA0E209A0F660027712B /* JMCallKitListener.swift in Sources */,

View File

@@ -16,6 +16,8 @@
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
static NSString * const sendEventNotificationName = @"org.jitsi.meet.SendEvent";
@interface ExternalAPI : RCTEventEmitter<RCTBridgeModule>
- (void)sendHangUp;

View File

@@ -15,7 +15,6 @@
*/
#import "ExternalAPI.h"
#import "JitsiMeetView+Private.h"
// Events
static NSString * const hangUpAction = @"org.jitsi.meet.HANG_UP";
@@ -89,33 +88,15 @@ RCT_EXPORT_MODULE();
* @param scope
*/
RCT_EXPORT_METHOD(sendEvent:(NSString *)name
data:(NSDictionary *)data
scope:(NSString *)scope) {
// The JavaScript App needs to provide uniquely identifying information to
// the native ExternalAPI module so that the latter may match the former
// to the native JitsiMeetView which hosts it.
JitsiMeetView *view = [JitsiMeetView viewForExternalAPIScope:scope];
if (!view) {
return;
}
id delegate = view.delegate;
if (!delegate) {
return;
}
data:(NSDictionary *)data) {
if ([name isEqual: @"PARTICIPANTS_INFO_RETRIEVED"]) {
[self onParticipantsInfoRetrieved: data];
return;
}
SEL sel = NSSelectorFromString([self methodNameFromEventName:name]);
if (sel && [delegate respondsToSelector:sel]) {
[delegate performSelector:sel withObject:data];
}
[[NSNotificationCenter defaultCenter] postNotificationName:sendEventNotificationName
object:nil
userInfo:@{@"name": name, @"data": data}];
}
- (void) onParticipantsInfoRetrieved:(NSDictionary *)data {
@@ -127,28 +108,6 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
[participantInfoCompletionHandlers removeObjectForKey:completionHandlerId];
}
/**
* Converts a specific event name i.e. redux action type description to a
* method name.
*
* @param eventName The event name to convert to a method name.
* @return A method name constructed from the specified `eventName`.
*/
- (NSString *)methodNameFromEventName:(NSString *)eventName {
NSMutableString *methodName
= [NSMutableString stringWithCapacity:eventName.length];
for (NSString *c in [eventName componentsSeparatedByString:@"_"]) {
if (c.length) {
[methodName appendString:
methodName.length ? c.capitalizedString : c.lowercaseString];
}
}
[methodName appendString:@":"];
return methodName;
}
- (void)sendHangUp {
[self sendEventWithName:hangUpAction body:nil];
}

View File

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

View File

@@ -15,6 +15,8 @@
*/
#import <Intents/Intents.h>
#import <RNGoogleSignin/RNGoogleSignin.h>
#import <WebRTC/RTCLogging.h>
#import "Dropbox.h"
#import "JitsiMeet+Private.h"
@@ -25,9 +27,6 @@
#import "RNSplashScreen.h"
#import "ScheenshareEventEmiter.h"
#import <RNGoogleSignin/RNGoogleSignin.h>
#import <WebRTC/RTCLogging.h>
@implementation JitsiMeet {
RCTBridgeWrapper *_bridgeWrapper;
NSDictionary *_launchOptions;
@@ -87,8 +86,12 @@
restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> *))restorationHandler {
JitsiMeetConferenceOptions *options = [self optionsFromUserActivity:userActivity];
if (options) {
[JitsiMeetView updateProps:[options asProps]];
return true;
}
return options && [JitsiMeetView setPropsInViews:[options asProps]];
return false;
}
- (BOOL)application:(UIApplication *)app
@@ -112,8 +115,9 @@
JitsiMeetConferenceOptions *conferenceOptions = [JitsiMeetConferenceOptions fromBuilder:^(JitsiMeetConferenceOptionsBuilder *builder) {
builder.room = [url absoluteString];
}];
[JitsiMeetView updateProps:[conferenceOptions asProps]];
return [JitsiMeetView setPropsInViews:[conferenceOptions asProps]];
return true;
}
#pragma mark - Utility methods

View File

@@ -0,0 +1,30 @@
/*
* Copyright @ 2022-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <UIKit/UIKit.h>
#import "JitsiMeetViewDelegate.h"
NS_ASSUME_NONNULL_BEGIN
@interface JitsiMeetRenderingView : UIView
@property (nonatomic, assign) BOOL isPiPEnabled;
- (void)setProps:(NSDictionary *_Nonnull)newProps;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,83 @@
/*
* Copyright @ 2022-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <mach/mach_time.h>
#import "JitsiMeetRenderingView.h"
#import "ReactUtils.h"
#import "RNRootView.h"
#import "JitsiMeet+Private.h"
/**
* Backwards compatibility: turn the boolean prop into a feature flag.
*/
static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
@interface JitsiMeetRenderingView ()
@end
@implementation JitsiMeetRenderingView {
/**
* React Native view where the entire content will be rendered.
*/
RNRootView *rootView;
}
/**
* Passes the given props to the React Native application. The props which we pass
* are a combination of 3 different sources:
*
* - JitsiMeet.defaultConferenceOptions
* - This function's parameters
* - Some extras which are added by this function
*/
- (void)setProps:(NSDictionary *_Nonnull)newProps {
NSMutableDictionary *props = mergeProps([[JitsiMeet sharedInstance] getDefaultProps], newProps);
// Set the PiP flag if it wasn't manually set.
NSMutableDictionary *featureFlags = props[@"flags"];
// TODO: temporary implementation
if (featureFlags[PiPEnabledFeatureFlag] == nil) {
featureFlags[PiPEnabledFeatureFlag] = @(self.isPiPEnabled);
}
// This method is supposed to be imperative i.e. a second
// invocation with one and the same URL is expected to join the respective
// conference again if the first invocation was followed by leaving the
// conference. However, React and, respectively,
// appProperties/initialProperties are declarative expressions i.e. one and
// the same URL will not trigger an automatic re-render in the JavaScript
// source code. The workaround implemented below introduces imperativeness
// in React Component props by defining a unique value per invocation.
props[@"timestamp"] = @(mach_absolute_time());
if (rootView) {
// Update props with the new URL.
rootView.appProperties = props;
} else {
RCTBridge *bridge = [[JitsiMeet sharedInstance] getReactBridge];
rootView = [[RNRootView alloc] initWithBridge:bridge
moduleName:@"App"
initialProperties:props];
rootView.backgroundColor = self.backgroundColor;
// Add rootView as a subview which completely covers this one.
[rootView setFrame:[self bounds]];
rootView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self addSubview:rootView];
}
}
@end

View File

@@ -1,6 +1,5 @@
/*
* Copyright @ 2018-present 8x8, Inc.
* Copyright @ 2017-2018 Atlassian Pty Ltd
* Copyright @ 2022-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,11 +14,16 @@
* limitations under the License.
*/
#import "JitsiMeetView.h"
#import <JitsiMeetSDK/JitsiMeetSDK.h>
@interface JitsiMeetView ()
NS_ASSUME_NONNULL_BEGIN
+ (instancetype _Nullable)viewForExternalAPIScope:(NSString *_Nonnull)externalAPIScope;
+ (BOOL)setPropsInViews:(NSDictionary *_Nonnull)newProps;
static NSString * const updateViewPropsNotificationName = @"org.jitsi.meet.UpdateViewProps";
@interface JitsiMeetView (Private)
+ (void)updateProps:(NSDictionary *_Nonnull)newProps;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,25 @@
/*
* Copyright @ 2022-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "JitsiMeetView+Private.h"
@implementation JitsiMeetView (Private)
+ (void)updateProps:(NSDictionary *_Nonnull)newProps {
[[NSNotificationCenter defaultCenter] postNotificationName:updateViewPropsNotificationName object:nil userInfo:@{@"props": newProps}];
}
@end

View File

@@ -20,43 +20,22 @@
#import "ExternalAPI.h"
#import "JitsiMeet+Private.h"
#import "JitsiMeetConferenceOptions+Private.h"
#import "JitsiMeetView+Private.h"
#import "JitsiMeetView.h"
#import "JitsiMeetViewController.h"
#import "ReactUtils.h"
#import "RNRootView.h"
@interface JitsiMeetView ()
/**
* Backwards compatibility: turn the boolean prop into a feature flag.
*/
static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
@property (nonatomic, strong) JitsiMeetViewController *jitsiMeetViewController;
@property (nonatomic, strong) UINavigationController *navController;
@property (nonatomic, readonly) BOOL isPiPEnabled;
@end
@implementation JitsiMeetView {
/**
* The unique identifier of this `JitsiMeetView` within the process for the
* purposes of `ExternalAPI`. The name scope was inspired by postis which we
* use on Web for the similar purposes of the iframe-based external API.
*/
NSString *externalAPIScope;
@implementation JitsiMeetView
/**
* React Native view where the entire content will be rendered.
*/
RNRootView *rootView;
}
/**
* The `JitsiMeetView`s associated with their `ExternalAPI` scopes (i.e. unique
* identifiers within the process).
*/
static NSMapTable<NSString *, JitsiMeetView *> *views;
/**
* This gets called automagically when the program starts.
*/
__attribute__((constructor))
static void initializeViewsMap() {
views = [NSMapTable strongToWeakObjectsMapTable];
}
@dynamic isPiPEnabled;
#pragma mark Initializers
@@ -87,6 +66,10 @@ static void initializeViewsMap() {
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
/**
* Internal initialization:
*
@@ -94,70 +77,57 @@ static void initializeViewsMap() {
* - initializes the external API scope
*/
- (void)initWithXXX {
// Hook this JitsiMeetView into ExternalAPI.
externalAPIScope = [NSUUID UUID].UUIDString;
[views setObject:self forKey:externalAPIScope];
// Set a background color which is in accord with the JavaScript and Android
// parts of the application and causes less perceived visual flicker than
// the default background color.
self.backgroundColor
= [UIColor colorWithRed:.07f green:.07f blue:.07f alpha:1];
self.jitsiMeetViewController = [[JitsiMeetViewController alloc] init];
self.jitsiMeetViewController.view.frame = [self bounds];
[self addSubview:self.jitsiMeetViewController.view];
[self registerObservers];
}
#pragma mark API
- (void)join:(JitsiMeetConferenceOptions *)options {
[self setProps:options == nil ? @{} : [options asProps]];
[self.jitsiMeetViewController join:options withPiP:self.isPiPEnabled];
}
- (void)leave {
[self setProps:@{}];
[self.jitsiMeetViewController leave];
}
- (void)hangUp {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendHangUp];
[self.jitsiMeetViewController hangUp];
}
- (void)setAudioMuted:(BOOL)muted {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendSetAudioMuted:muted];
[self.jitsiMeetViewController setAudioMuted:muted];
}
- (void)sendEndpointTextMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendEndpointTextMessage:message :to];
[self.jitsiMeetViewController sendEndpointTextMessage:message :to];
}
- (void)toggleScreenShare:(BOOL)enabled {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI toggleScreenShare:enabled];
[self.jitsiMeetViewController toggleScreenShare:enabled];
}
- (void)retrieveParticipantsInfo:(void (^ _Nonnull)(NSArray * _Nullable))completionHandler {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI retrieveParticipantsInfo:completionHandler];
[self.jitsiMeetViewController retrieveParticipantsInfo:completionHandler];
}
- (void)openChat:(NSString*)to {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI openChat:to];
[self.jitsiMeetViewController openChat:to];
}
- (void)closeChat {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI closeChat];
[self.jitsiMeetViewController closeChat];
}
- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendChatMessage:message :to];
[self.jitsiMeetViewController sendChatMessage:message :to];
}
- (void)setVideoMuted:(BOOL)muted {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendSetVideoMuted:muted];
[self.jitsiMeetViewController setVideoMuted:muted];
}
- (void)setClosedCaptionsEnabled:(BOOL)enabled {
@@ -165,79 +135,47 @@ static void initializeViewsMap() {
[externalAPI sendSetClosedCaptionsEnabled:enabled];
}
#pragma mark Private methods
#pragma mark Private
- (BOOL)isPiPEnabled {
return self.delegate && [self.delegate respondsToSelector:@selector(enterPictureInPicture:)];
}
- (void)registerObservers {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleSendEventNotification:) name:sendEventNotificationName object:nil];
}
- (void)handleSendEventNotification:(NSNotification *)notification {
NSString *eventName = notification.userInfo[@"name"];
NSString *eventData = notification.userInfo[@"data"];
SEL sel = NSSelectorFromString([self methodNameFromEventName:eventName]);
if (sel && [self.delegate respondsToSelector:sel]) {
[self.delegate performSelector:sel withObject:eventData];
}
}
/**
* Passes the given props to the React Native application. The props which we pass
* are a combination of 3 different sources:
* Converts a specific event name i.e. redux action type description to a
* method name.
*
* - JitsiMeet.defaultConferenceOptions
* - This function's parameters
* - Some extras which are added by this function
* @param eventName The event name to convert to a method name.
* @return A method name constructed from the specified `eventName`.
*/
- (void)setProps:(NSDictionary *_Nonnull)newProps {
NSMutableDictionary *props = mergeProps([[JitsiMeet sharedInstance] getDefaultProps], newProps);
- (NSString *)methodNameFromEventName:(NSString *)eventName {
NSMutableString *methodName
= [NSMutableString stringWithCapacity:eventName.length];
// Set the PiP flag if it wasn't manually set.
NSMutableDictionary *featureFlags = props[@"flags"];
if (featureFlags[PiPEnabledFeatureFlag] == nil) {
featureFlags[PiPEnabledFeatureFlag]
= [NSNumber numberWithBool:
self.delegate && [self.delegate respondsToSelector:@selector(enterPictureInPicture:)]];
}
for (NSString *c in [eventName componentsSeparatedByString:@"_"]) {
if (c.length) {
[methodName appendString:
methodName.length ? c.capitalizedString : c.lowercaseString];
}
}
[methodName appendString:@":"];
props[@"externalAPIScope"] = externalAPIScope;
// This method is supposed to be imperative i.e. a second
// invocation with one and the same URL is expected to join the respective
// conference again if the first invocation was followed by leaving the
// conference. However, React and, respectively,
// appProperties/initialProperties are declarative expressions i.e. one and
// the same URL will not trigger an automatic re-render in the JavaScript
// source code. The workaround implemented below introduces imperativeness
// in React Component props by defining a unique value per invocation.
props[@"timestamp"] = @(mach_absolute_time());
if (rootView) {
// Update props with the new URL.
rootView.appProperties = props;
} else {
RCTBridge *bridge = [[JitsiMeet sharedInstance] getReactBridge];
rootView
= [[RNRootView alloc] initWithBridge:bridge
moduleName:@"App"
initialProperties:props];
rootView.backgroundColor = self.backgroundColor;
// Add rootView as a subview which completely covers this one.
[rootView setFrame:[self bounds]];
rootView.autoresizingMask
= UIViewAutoresizingFlexibleWidth
| UIViewAutoresizingFlexibleHeight;
[self addSubview:rootView];
}
}
+ (BOOL)setPropsInViews:(NSDictionary *_Nonnull)newProps {
BOOL handled = NO;
if (views) {
for (NSString *externalAPIScope in views) {
JitsiMeetView *view
= [self viewForExternalAPIScope:externalAPIScope];
if (view) {
[view setProps:newProps];
handled = YES;
}
}
}
return handled;
}
+ (instancetype)viewForExternalAPIScope:(NSString *)externalAPIScope {
return [views objectForKey:externalAPIScope];
return methodName;
}
@end

View File

@@ -0,0 +1,38 @@
/*
* Copyright @ 2022-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <UIKit/UIKit.h>
#import "JitsiMeetConferenceOptions.h"
NS_ASSUME_NONNULL_BEGIN
@interface JitsiMeetViewController : UIViewController
- (void)join:(JitsiMeetConferenceOptions *)options withPiP:(BOOL)enablePiP;
- (void)leave;
- (void)hangUp;
- (void)setAudioMuted:(BOOL)muted;
- (void)sendEndpointTextMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to;
- (void)toggleScreenShare:(BOOL)enabled;
- (void)retrieveParticipantsInfo:(void (^ _Nonnull)(NSArray * _Nullable))completionHandler;
- (void)openChat:(NSString*)to;
- (void)closeChat;
- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to;
- (void)setVideoMuted:(BOOL)muted;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,127 @@
/*
* Copyright @ 2022-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "JitsiMeetViewController.h"
#import "JitsiMeet+Private.h"
#import "JitsiMeetConferenceOptions+Private.h"
#import "JitsiMeetRenderingView.h"
#import "JitsiMeetView+Private.h"
@interface JitsiMeetViewController ()
@property (strong, nonatomic) JitsiMeetRenderingView *view;
@end
@implementation JitsiMeetViewController
@dynamic view;
- (instancetype)init {
self = [super init];
if (self) {
[self registerObservers];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)loadView {
[super loadView];
self.view = [[JitsiMeetRenderingView alloc] init];
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Set a background color which is in accord with the JavaScript and Android
// parts of the application and causes less perceived visual flicker than
// the default background color.
self.view.backgroundColor = [UIColor colorWithRed:.07f green:.07f blue:.07f alpha:1];
}
- (void)join:(JitsiMeetConferenceOptions *)options withPiP:(BOOL)enablePiP {
self.view.isPiPEnabled = enablePiP;
[self.view setProps:options == nil ? @{} : [options asProps]];
}
- (void)leave {
[self.view setProps:@{}];
}
- (void)hangUp {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendHangUp];
}
- (void)setAudioMuted:(BOOL)muted {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendSetAudioMuted:muted];
}
- (void)sendEndpointTextMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendEndpointTextMessage:message :to];
}
- (void)toggleScreenShare:(BOOL)enabled {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI toggleScreenShare:enabled];
}
- (void)retrieveParticipantsInfo:(void (^ _Nonnull)(NSArray * _Nullable))completionHandler {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI retrieveParticipantsInfo:completionHandler];
}
- (void)openChat:(NSString*)to {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI openChat:to];
}
- (void)closeChat {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI closeChat];
}
- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendChatMessage:message :to];
}
- (void)setVideoMuted:(BOOL)muted {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI sendSetVideoMuted:muted];
}
#pragma mark Private
- (void)registerObservers {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleUpdateViewPropsNotification:) name:updateViewPropsNotificationName object:nil];
}
- (void)handleUpdateViewPropsNotification:(NSNotification *)notification {
NSDictionary *props = [notification.userInfo objectForKey:@"props"];
[self.view setProps:props];
}
@end

View File

@@ -896,6 +896,8 @@
"live": "LIVE",
"localRecordingNoNotificationWarning": "The recording will not be announced to other participants. You will need to let them know that the meeting is recorded.",
"localRecordingNoVideo": "Video is not being recorded",
"localRecordingStartWarning": "Please make sure you stop the recording before exiting the meeting in order to save it.",
"localRecordingStartWarningTitle": "Stop the recording to save it",
"localRecordingVideoStop": "Stopping your video will also stop the local recording. Are you sure you want to continue?",
"localRecordingVideoWarning": "To record your video you must have it on when starting the recording",
"localRecordingWarning": "Make sure you select the current tab in order to use the right video and audio. The recording is currently limited to 1GB, which is around 100 minutes.",
@@ -1031,6 +1033,7 @@
"termsView": {
"header": "Terms"
},
"toggleTopPanelLabel": "Toggle top panel",
"toolbar": {
"Settings": "Settings",
"accessibilityLabel": {

View File

@@ -17,7 +17,6 @@ import {
import { getReplaceParticipant } from '../../../react/features/base/config/functions';
import { isDialogOpen } from '../../../react/features/base/dialog';
import { setJWT } from '../../../react/features/base/jwt';
import { setPrejoinPageVisibility } from '../../../react/features/prejoin';
import UIUtil from '../util/UIUtil';
import ExternalLoginDialog from './LoginDialog';
@@ -181,7 +180,6 @@ function authenticate(room: Object, lockPassword: string) {
if (isTokenAuthEnabled(config) || room.isExternalAuthEnabled()) {
doExternalAuth(room, lockPassword);
} else {
APP.store.dispatch(setPrejoinPageVisibility(false));
APP.store.dispatch(openLoginDialog());
}
}

190
package-lock.json generated
View File

@@ -49,7 +49,7 @@
"@react-navigation/elements": "1.2.1",
"@react-navigation/material-top-tabs": "6.0.6",
"@react-navigation/native": "6.0.6",
"@react-navigation/native-stack": "6.6.2",
"@react-navigation/stack": "6.2.2",
"@svgr/webpack": "4.3.2",
"@tensorflow/tfjs-backend-wasm": "3.13.0",
"@tensorflow/tfjs-core": "3.13.0",
@@ -74,7 +74,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/v1461.0.0+96664436/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1464.0.0+28aab9fc/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.2",
"moment-duration-format": "2.2.2",
@@ -104,7 +104,7 @@
"react-native-performance": "2.1.0",
"react-native-reanimated": "https://git@github.com/software-mansion/react-native-reanimated#c4a6b6f687ede090f6081064abe83a2ef9a05784",
"react-native-safe-area-context": "3.3.2",
"react-native-screens": "3.10.1",
"react-native-screens": "3.13.1",
"react-native-sound": "0.11.1",
"react-native-splash-screen": "3.3.0",
"react-native-svg": "12.1.0",
@@ -143,6 +143,7 @@
"@babel/preset-react": "7.16.0",
"@babel/runtime": "7.16.0",
"@jitsi/eslint-config": "4.0.0",
"@types/lodash": "4.14.182",
"@types/react": "17.0.14",
"@types/react-native": "0.67.6",
"@types/react-redux": "7.1.24",
@@ -5025,33 +5026,6 @@
"react-native": "*"
}
},
"node_modules/@react-navigation/native-stack": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-6.6.2.tgz",
"integrity": "sha512-pFMuzhxbPml5MBvJVAzHWoaUkQaefAOKpuUnAs/AxNQuHQwwnxRmDit1PQLuIPo7g7DlfwFXagDHE1R0tbnS8Q==",
"dependencies": {
"@react-navigation/elements": "^1.3.3",
"warn-once": "^0.1.0"
},
"peerDependencies": {
"@react-navigation/native": "^6.0.0",
"react": "*",
"react-native": "*",
"react-native-safe-area-context": ">= 3.0.0",
"react-native-screens": ">= 3.0.0"
}
},
"node_modules/@react-navigation/native-stack/node_modules/@react-navigation/elements": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.3.tgz",
"integrity": "sha512-Lv2lR7si5gNME8dRsqz57d54m4FJtrwHRjNQLOyQO546ZxO+g864cSvoLC6hQedQU0+IJnPTsZiEI2hHqfpEpw==",
"peerDependencies": {
"@react-navigation/native": "^6.0.0",
"react": "*",
"react-native": "*",
"react-native-safe-area-context": ">= 3.0.0"
}
},
"node_modules/@react-navigation/routers": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.0.tgz",
@@ -5060,6 +5034,63 @@
"nanoid": "^3.1.23"
}
},
"node_modules/@react-navigation/stack": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-6.2.2.tgz",
"integrity": "sha512-P9ZfmluOXNmbs7YdG1UWS1fAh87Yse9aX8TgqOz4FlHEm5q7g5eaM35QgWByt+wif3UiqE40D8wXpqRQvMgPWg==",
"dependencies": {
"@react-navigation/elements": "^1.3.4",
"color": "^4.2.3",
"warn-once": "^0.1.0"
},
"peerDependencies": {
"@react-navigation/native": "^6.0.0",
"react": "*",
"react-native": "*",
"react-native-gesture-handler": ">= 1.0.0",
"react-native-safe-area-context": ">= 3.0.0",
"react-native-screens": ">= 3.0.0"
}
},
"node_modules/@react-navigation/stack/node_modules/@react-navigation/elements": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.4.tgz",
"integrity": "sha512-O0jICpjn3jskVo4yiWzZozmj7DZy1ZBbn3O7dbenuUjZSj/cscjwaapmZZFGcI/IMmjmx8UTKsybhCFEIbGf3g==",
"peerDependencies": {
"@react-navigation/native": "^6.0.0",
"react": "*",
"react-native": "*",
"react-native-safe-area-context": ">= 3.0.0"
}
},
"node_modules/@react-navigation/stack/node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/@react-navigation/stack/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/@react-navigation/stack/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/@sideway/address": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
@@ -5443,6 +5474,12 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.14.182",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
"integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==",
"dev": true
},
"node_modules/@types/long": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
@@ -7888,9 +7925,9 @@
"integrity": "sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA=="
},
"node_modules/debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
@@ -12169,8 +12206,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1461.0.0+96664436/lib-jitsi-meet.tgz",
"integrity": "sha512-DbtYpqJ9qsZtugeQIGDEmQOactpGQLSjCHd2obVU+1gdYxp2N6STVTxx7pw7zzlvW7S2pwkIXK/b5B6NFrt2iA==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1464.0.0+28aab9fc/lib-jitsi-meet.tgz",
"integrity": "sha512-7kQM0uGBW0m0x5651i3Z7qj8eF0jNF4ZdiOemMcXhxb/NnwBaIb6a9cqPaCkWuwY2m5ZFSB2YogcHgvcwqWbBw==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.0.0",
@@ -15535,9 +15572,9 @@
}
},
"node_modules/react-native-screens": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.10.1.tgz",
"integrity": "sha512-ZF/XHnRsuinvDY1XiCWLXxoUoSf+NgsAes2SZfX9rFQQcv128zmh/+19SSavGrSf6rQNzqytEMdRGI6yr4Gbjw==",
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.13.1.tgz",
"integrity": "sha512-xcrnuUs0qUrGpc2gOTDY4VgHHADQwp80mwR1prU/Q0JqbZN5W3koLhuOsT6FkSRKjR5t40l+4LcjhHdpqRB2HA==",
"dependencies": {
"react-freeze": "^1.0.0",
"warn-once": "^0.1.0"
@@ -23759,22 +23796,6 @@
"nanoid": "^3.1.23"
}
},
"@react-navigation/native-stack": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-6.6.2.tgz",
"integrity": "sha512-pFMuzhxbPml5MBvJVAzHWoaUkQaefAOKpuUnAs/AxNQuHQwwnxRmDit1PQLuIPo7g7DlfwFXagDHE1R0tbnS8Q==",
"requires": {
"@react-navigation/elements": "^1.3.3",
"warn-once": "^0.1.0"
},
"dependencies": {
"@react-navigation/elements": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.3.tgz",
"integrity": "sha512-Lv2lR7si5gNME8dRsqz57d54m4FJtrwHRjNQLOyQO546ZxO+g864cSvoLC6hQedQU0+IJnPTsZiEI2hHqfpEpw=="
}
}
},
"@react-navigation/routers": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.0.tgz",
@@ -23783,6 +23804,45 @@
"nanoid": "^3.1.23"
}
},
"@react-navigation/stack": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-6.2.2.tgz",
"integrity": "sha512-P9ZfmluOXNmbs7YdG1UWS1fAh87Yse9aX8TgqOz4FlHEm5q7g5eaM35QgWByt+wif3UiqE40D8wXpqRQvMgPWg==",
"requires": {
"@react-navigation/elements": "^1.3.4",
"color": "^4.2.3",
"warn-once": "^0.1.0"
},
"dependencies": {
"@react-navigation/elements": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.4.tgz",
"integrity": "sha512-O0jICpjn3jskVo4yiWzZozmj7DZy1ZBbn3O7dbenuUjZSj/cscjwaapmZZFGcI/IMmjmx8UTKsybhCFEIbGf3g=="
},
"color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"requires": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
}
}
},
"@sideway/address": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
@@ -24109,6 +24169,12 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
"@types/lodash": {
"version": "4.14.182",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
"integrity": "sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==",
"dev": true
},
"@types/long": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
@@ -26086,9 +26152,9 @@
"integrity": "sha512-ER7EjqVAMkRRsxNCC5YqJ9d9VQYuWdGt7aiH2qA5R5wt8ZmWaP2dLUSIK6y/kVzLMlmh1Tvu5xUf4M/wdGJ5KA=="
},
"debug": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
"integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"requires": {
"ms": "2.1.2"
}
@@ -29375,8 +29441,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1461.0.0+96664436/lib-jitsi-meet.tgz",
"integrity": "sha512-DbtYpqJ9qsZtugeQIGDEmQOactpGQLSjCHd2obVU+1gdYxp2N6STVTxx7pw7zzlvW7S2pwkIXK/b5B6NFrt2iA==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1464.0.0+28aab9fc/lib-jitsi-meet.tgz",
"integrity": "sha512-7kQM0uGBW0m0x5651i3Z7qj8eF0jNF4ZdiOemMcXhxb/NnwBaIb6a9cqPaCkWuwY2m5ZFSB2YogcHgvcwqWbBw==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",
@@ -32024,9 +32090,9 @@
"integrity": "sha512-yOwiiPJ1rk+/nfK13eafbpW6sKW0jOnsRem2C1LPJjM3tfTof6hlvV5eWHATye3XOpu2cJ7N+HdkUvUDGwFD2Q=="
},
"react-native-screens": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.10.1.tgz",
"integrity": "sha512-ZF/XHnRsuinvDY1XiCWLXxoUoSf+NgsAes2SZfX9rFQQcv128zmh/+19SSavGrSf6rQNzqytEMdRGI6yr4Gbjw==",
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.13.1.tgz",
"integrity": "sha512-xcrnuUs0qUrGpc2gOTDY4VgHHADQwp80mwR1prU/Q0JqbZN5W3koLhuOsT6FkSRKjR5t40l+4LcjhHdpqRB2HA==",
"requires": {
"react-freeze": "^1.0.0",
"warn-once": "^0.1.0"

View File

@@ -54,7 +54,7 @@
"@react-navigation/elements": "1.2.1",
"@react-navigation/material-top-tabs": "6.0.6",
"@react-navigation/native": "6.0.6",
"@react-navigation/native-stack": "6.6.2",
"@react-navigation/stack": "6.2.2",
"@svgr/webpack": "4.3.2",
"@tensorflow/tfjs-backend-wasm": "3.13.0",
"@tensorflow/tfjs-core": "3.13.0",
@@ -79,7 +79,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/v1461.0.0+96664436/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1464.0.0+28aab9fc/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.2",
"moment-duration-format": "2.2.2",
@@ -109,7 +109,7 @@
"react-native-performance": "2.1.0",
"react-native-reanimated": "https://git@github.com/software-mansion/react-native-reanimated#c4a6b6f687ede090f6081064abe83a2ef9a05784",
"react-native-safe-area-context": "3.3.2",
"react-native-screens": "3.10.1",
"react-native-screens": "3.13.1",
"react-native-sound": "0.11.1",
"react-native-splash-screen": "3.3.0",
"react-native-svg": "12.1.0",
@@ -148,6 +148,7 @@
"@babel/preset-react": "7.16.0",
"@babel/runtime": "7.16.0",
"@jitsi/eslint-config": "4.0.0",
"@types/lodash": "4.14.182",
"@types/react": "17.0.14",
"@types/react-native": "0.67.6",
"@types/react-redux": "7.1.24",

View File

@@ -1,6 +1,4 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { UPDATE_LOCAL_TRACKS_DURATION } from './actionTypes';
@@ -30,6 +28,22 @@ const DEFAULT_STATE = {
}
};
interface Value {
startedTime: number,
value: number
}
export interface IAnalyticsState {
localTracksDuration: {
audio: Value,
video: {
camera: Value,
desktop: Value
},
conference: Value
}
}
/**
* Listen for actions which changes the state of the analytics feature.
*
@@ -38,7 +52,7 @@ const DEFAULT_STATE = {
* @param {string} action.type - Type of action.
* @returns {Object}
*/
ReducerRegistry.register('features/analytics', (state = DEFAULT_STATE, action) => {
ReducerRegistry.register('features/analytics', (state: IAnalyticsState = DEFAULT_STATE, action: any) => {
switch (action.type) {
case UPDATE_LOCAL_TRACKS_DURATION:
return {

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { FullWindowOverlay } from 'react-native-screens';
import SplashScreen from 'react-native-splash-screen';
import { DialogContainer } from '../../base/dialog';
@@ -24,7 +23,6 @@ import '../reducers';
declare var __DEV__;
const DialogContainerWrapper = Platform.select({
ios: FullWindowOverlay,
default: View
});
@@ -33,11 +31,6 @@ const DialogContainerWrapper = Platform.select({
*/
type Props = AbstractAppProps & {
/**
* Identifier for this app on the native side.
*/
externalAPIScope: string,
/**
* An object with the feature flags.
*/

View File

@@ -1,4 +1,22 @@
import { IAnalyticsState } from "../analytics/reducer"
import { IAuthenticationState } from "../authentication/reducer"
import { IAVModerationState } from "../av-moderation/reducer"
import { IAppState } from "../base/app/reducer"
import { IAudioOnlyState } from "../base/audio-only/reducer"
import { IConferenceState } from "../base/conference/reducer"
import { IConfig } from "../base/config/configType"
export interface IStore {
getState: Function,
dispatch: Function
}
export interface IState {
'features/analytics': IAnalyticsState,
'features/authentication': IAuthenticationState,
'features/av-moderation': IAVModerationState,
'features/base/app': IAppState,
'features/base/audio-only': IAudioOnlyState,
'features/base/conference': IConferenceState,
'features/base/config': IConfig,
}

View File

@@ -15,7 +15,6 @@ import {
JitsiConnectionErrors
} from '../base/lib-jitsi-meet';
import { MiddlewareRegistry } from '../base/redux';
import { setPrejoinPageVisibility } from '../prejoin';
import {
CANCEL_LOGIN,
@@ -121,7 +120,6 @@ MiddlewareRegistry.register(store => next => action => {
&& error.name === JitsiConnectionErrors.PASSWORD_REQUIRED
&& typeof error.recoverable === 'undefined') {
error.recoverable = true;
store.dispatch(setPrejoinPageVisibility(false));
store.dispatch(openLoginDialog());
}
break;

View File

@@ -1,6 +1,6 @@
// @flow
import { assign, ReducerRegistry } from '../base/redux';
// @ts-ignore
import { assign } from '../base/redux/functions';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import {
CANCEL_LOGIN,
@@ -10,6 +10,13 @@ import {
WAIT_FOR_OWNER
} from './actionTypes';
export interface IAuthenticationState {
error?: Object|undefined;
progress?: number|undefined;
thenableWithCancel?: Object|undefined;
waitForOwnerTimeoutID?: number;
}
/**
* Listens for actions which change the state of the authentication feature.
*
@@ -18,7 +25,7 @@ import {
* @param {string} action.type - Type of action.
* @returns {Object}
*/
ReducerRegistry.register('features/authentication', (state = {}, action) => {
ReducerRegistry.register('features/authentication', (state: IAuthenticationState = {}, action: any) => {
switch (action.type) {
case CANCEL_LOGIN:
return assign(state, {

View File

@@ -1,11 +1,9 @@
// @flow
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
/**
* Mapping between a media type and the witelist reducer key.
*/
export const MEDIA_TYPE_TO_WHITELIST_STORE_KEY: {[key: MediaType]: string} = {
export const MEDIA_TYPE_TO_WHITELIST_STORE_KEY: { [key: string]: string } = {
[MEDIA_TYPE.AUDIO]: 'audioWhitelist',
[MEDIA_TYPE.VIDEO]: 'videoWhitelist'
};
@@ -13,7 +11,7 @@ export const MEDIA_TYPE_TO_WHITELIST_STORE_KEY: {[key: MediaType]: string} = {
/**
* Mapping between a media type and the pending reducer key.
*/
export const MEDIA_TYPE_TO_PENDING_STORE_KEY: {[key: MediaType]: string} = {
export const MEDIA_TYPE_TO_PENDING_STORE_KEY: {[key: string]: string} = {
[MEDIA_TYPE.AUDIO]: 'pendingAudio',
[MEDIA_TYPE.VIDEO]: 'pendingVideo'
};

View File

@@ -1,12 +1,11 @@
/* @flow */
import { MEDIA_TYPE } from '../base/media/constants';
import type { MediaType } from '../base/media/constants';
import {
PARTICIPANT_LEFT,
PARTICIPANT_UPDATED
// @ts-ignore
} from '../base/participants';
import { ReducerRegistry } from '../base/redux';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import {
DISABLE_MODERATION,
@@ -29,8 +28,19 @@ const initialState = {
pendingVideo: []
};
export interface IAVModerationState {
audioModerationEnabled: boolean;
videoModerationEnabled: boolean;
audioWhitelist: { [id: string]: boolean };
videoWhitelist: { [id: string]: boolean };
pendingAudio: Array<{ id: string }>;
pendingVideo: Array<{ id: string }>;
audioUnmuteApproved?: boolean|undefined;
videoUnmuteApproved?: boolean|undefined;
}
/**
Updates a participant in the state for the specified media type.
* Updates a participant in the state for the specified media type.
*
* @param {MediaType} mediaType - The media type.
* @param {Object} participant - Information about participant to be modified.
@@ -38,11 +48,11 @@ const initialState = {
* @private
* @returns {boolean} - Whether state instance was modified.
*/
function _updatePendingParticipant(mediaType: MediaType, participant, state: Object = {}) {
function _updatePendingParticipant(mediaType: MediaType, participant: any, state: any = {}) {
let arrayItemChanged = false;
const storeKey = MEDIA_TYPE_TO_PENDING_STORE_KEY[mediaType];
const arr = state[storeKey];
const newArr = arr.map(pending => {
const newArr = arr.map((pending: { id: string} ) => {
if (pending.id === participant.id) {
arrayItemChanged = true;
@@ -64,7 +74,7 @@ function _updatePendingParticipant(mediaType: MediaType, participant, state: Obj
return false;
}
ReducerRegistry.register('features/av-moderation', (state = initialState, action) => {
ReducerRegistry.register('features/av-moderation', (state: IAVModerationState = initialState, action: any) => {
switch (action.type) {
case DISABLE_MODERATION: {

View File

@@ -1,10 +1,12 @@
// @flow
import { ReducerRegistry } from '../redux';
import ReducerRegistry from '../redux/ReducerRegistry';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
ReducerRegistry.register('features/base/app', (state = {}, action) => {
export interface IAppState {
app?: Object|undefined;
}
ReducerRegistry.register('features/base/app', (state: IAppState = {}, action) => {
switch (action.type) {
case APP_WILL_MOUNT: {
const { app } = action;

View File

@@ -1,16 +1,17 @@
// @flow
import { ReducerRegistry } from '../redux';
import ReducerRegistry from '../redux/ReducerRegistry';
import { SET_AUDIO_ONLY } from './actionTypes';
export interface IAudioOnlyState {
enabled: boolean
}
const DEFAULT_STATE = {
enabled: false
};
ReducerRegistry.register('features/base/audio-only', (state = DEFAULT_STATE, action) => {
ReducerRegistry.register('features/base/audio-only', (state: IAudioOnlyState = DEFAULT_STATE, action) => {
switch (action.type) {
case SET_AUDIO_ONLY:
return {

View File

@@ -46,10 +46,7 @@ MiddlewareRegistry.register(store => next => action => {
case CONFERENCE_FAILED: {
const errorName = action.error?.name;
if (errorName === JitsiConferenceErrors.MEMBERS_ONLY_ERROR
|| errorName === JitsiConferenceErrors.PASSWORD_REQUIRED) {
dispatch(setPrejoinPageVisibility(false));
} else if (enableForcedReload && errorName === JitsiConferenceErrors.CONFERENCE_RESTARTED) {
if (enableForcedReload && errorName === JitsiConferenceErrors.CONFERENCE_RESTARTED) {
dispatch(setSkipPrejoinOnReload(true));
}

View File

@@ -1,9 +1,12 @@
// @flow
// @ts-ignore
import { LOCKED_LOCALLY, LOCKED_REMOTELY } from '../../room-lock';
// @ts-ignore
import { CONNECTION_WILL_CONNECT, SET_LOCATION_URL } from '../connection';
// @ts-ignore
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
import { assign, ReducerRegistry, set } from '../redux';
// @ts-ignore
import { assign, set } from '../redux';
import ReducerRegistry from '../redux/ReducerRegistry';
import {
AUTH_STATUS_CHANGED,
@@ -25,6 +28,7 @@ import {
SET_START_MUTED_POLICY,
SET_START_REACTIONS_MUTED
} from './actionTypes';
// @ts-ignore
import { isRoomValid } from './functions';
const DEFAULT_STATE = {
@@ -38,13 +42,26 @@ const DEFAULT_STATE = {
passwordRequired: undefined
};
export interface IConferenceState {
conference: Object|undefined;
e2eeSupported: boolean|undefined;
joining: Object|undefined;
leaving: Object|undefined;
locked: string|undefined;
membersOnly: boolean|undefined;
password: string|undefined;
passwordRequired: boolean|undefined;
authEnabled?: boolean|undefined;
authLogin?: string|undefined;
}
/**
* Listen for actions that contain the conference object, so that it can be
* stored for use by other action creators.
*/
ReducerRegistry.register(
'features/base/conference',
(state = DEFAULT_STATE, action) => {
(state: IConferenceState = DEFAULT_STATE, action: any) => {
switch (action.type) {
case AUTH_STATUS_CHANGED:
return _authStatusChanged(state, action);
@@ -125,7 +142,7 @@ ReducerRegistry.register(
* @returns {Object} The new state of the feature base/conference after the
* reduction of the specified action.
*/
function _authStatusChanged(state, { authEnabled, authLogin }) {
function _authStatusChanged(state: any, { authEnabled, authLogin }: {authEnabled: boolean, authLogin: string}) {
return assign(state, {
authEnabled,
authLogin
@@ -142,7 +159,7 @@ function _authStatusChanged(state, { authEnabled, authLogin }) {
* @returns {Object} The new state of the feature base/conference after the
* reduction of the specified action.
*/
function _conferenceFailed(state, { conference, error }) {
function _conferenceFailed(state: any, { conference, error }: {conference: Object, error: any}) {
// The current (similar to getCurrentConference in
// base/conference/functions.any.js) conference which is joining or joined:
const conference_ = state.conference || state.joining;
@@ -208,7 +225,7 @@ function _conferenceFailed(state, { conference, error }) {
* @returns {Object} The new state of the feature base/conference after the
* reduction of the specified action.
*/
function _conferenceJoined(state, { conference }) {
function _conferenceJoined(state: any, { conference }: {conference: any}) {
// FIXME The indicator which determines whether a JitsiConference is locked
// i.e. password-protected is private to lib-jitsi-meet. However, the
// library does not fire LOCK_STATE_CHANGED upon joining a JitsiConference
@@ -254,7 +271,7 @@ function _conferenceJoined(state, { conference }) {
* @returns {Object} The next/new state of the feature base/conference after the
* reduction of the specified action.
*/
function _conferenceLeftOrWillLeave(state, { conference, type }) {
function _conferenceLeftOrWillLeave(state: any, { conference, type }: {conference: Object, type: string}) {
const nextState = { ...state };
// The redux action CONFERENCE_LEFT is the last time that we should be
@@ -308,7 +325,7 @@ function _conferenceLeftOrWillLeave(state, { conference, type }) {
* @returns {Object} The new state of the feature base/conference after the
* reduction of the specified action.
*/
function _conferenceWillJoin(state, { conference }) {
function _conferenceWillJoin(state: any, { conference }: {conference: Object}) {
return assign(state, {
error: undefined,
joining: conference
@@ -325,7 +342,7 @@ function _conferenceWillJoin(state, { conference }) {
* @returns {Object} The new state of the feature base/conference after the
* reduction of the specified action.
*/
function _lockStateChanged(state, { conference, locked }) {
function _lockStateChanged(state: any, { conference, locked }: {conference: Object, locked: boolean}) {
if (state.conference !== conference) {
return state;
}
@@ -346,7 +363,7 @@ function _lockStateChanged(state, { conference, locked }) {
* @returns {Object} The new state of the feature base/conference after the
* reduction of the specified action.
*/
function _p2pStatusChanged(state, action) {
function _p2pStatusChanged(state: any, action: any) {
return set(state, 'p2p', action.p2p);
}
@@ -359,7 +376,7 @@ function _p2pStatusChanged(state, action) {
* @returns {Object} The new state of the feature base/conference after the
* reduction of the specified action.
*/
function _setPassword(state, { conference, method, password }) {
function _setPassword(state: any, { conference, method, password }: {conference: any, method: Object, password: string}) {
switch (method) {
case conference.join:
return assign(state, {
@@ -406,7 +423,7 @@ function _setPassword(state, { conference, method, password }) {
* @returns {Object} The new state of the feature base/conference after the
* reduction of the specified action.
*/
function _setRoom(state, action) {
function _setRoom(state: any, action: any) {
let { room } = action;
if (!isRoomValid(room)) {

View File

@@ -0,0 +1,467 @@
type ToolbarButtons = 'camera' |
'chat' |
'closedcaptions' |
'desktop' |
'dock-iframe' |
'download' |
'embedmeeting' |
'etherpad' |
'feedback' |
'filmstrip' |
'fullscreen' |
'hangup' |
'help' |
'highlight' |
'invite' |
'linktosalesforce' |
'livestreaming' |
'microphone' |
'participants-pane' |
'profile' |
'raisehand' |
'recording' |
'security' |
'select-background' |
'settings' |
'shareaudio' |
'sharedvideo' |
'shortcuts' |
'stats' |
'tileview' |
'toggle-camera' |
'undock-iframe' |
'videoquality' |
'__end';
type ButtonsWithNotifyClick = 'camera' |
'chat' |
'closedcaptions' |
'desktop' |
'download' |
'embedmeeting' |
'etherpad' |
'feedback' |
'filmstrip' |
'fullscreen' |
'hangup' |
'help' |
'invite' |
'livestreaming' |
'microphone' |
'mute-everyone' |
'mute-video-everyone' |
'participants-pane' |
'profile' |
'raisehand' |
'recording' |
'security' |
'select-background' |
'settings' |
'shareaudio' |
'sharedvideo' |
'shortcuts' |
'stats' |
'tileview' |
'toggle-camera' |
'videoquality' |
'add-passcode' |
'__end';
type Sounds = 'ASKED_TO_UNMUTE_SOUND' |
'E2EE_OFF_SOUND' |
'E2EE_ON_SOUND' |
'INCOMING_MSG_SOUND' |
'KNOCKING_PARTICIPANT_SOUND' |
'LIVE_STREAMING_OFF_SOUND' |
'LIVE_STREAMING_ON_SOUND' |
'NO_AUDIO_SIGNAL_SOUND' |
'NOISY_AUDIO_INPUT_SOUND' |
'OUTGOING_CALL_EXPIRED_SOUND' |
'OUTGOING_CALL_REJECTED_SOUND' |
'OUTGOING_CALL_RINGING_SOUND' |
'OUTGOING_CALL_START_SOUND' |
'PARTICIPANT_JOINED_SOUND' |
'PARTICIPANT_LEFT_SOUND' |
'RAISE_HAND_SOUND' |
'REACTION_SOUND' |
'RECORDING_OFF_SOUND' |
'RECORDING_ON_SOUND' |
'TALK_WHILE_MUTED_SOUND';
export interface IConfig {
hosts?: {
domain: string;
anonymousdomain?: string;
authdomain?: string;
focus?: string;
muc: string;
};
bosh?: string;
websocket?: string;
focusUserJid?: string;
testing?: {
disableE2EE?: boolean;
enableThumbnailReordering?: boolean;
mobileXmppWsThreshold?: number;
p2pTestMode?: boolean;
testMode?: boolean;
noAutoPlayVideo?: boolean;
capScreenshareBitrate?: number;
setScreenSharingResolutionConstraints?: boolean;
callStatsThreshold?: number;
};
flags?: {
sourceNameSignaling?: boolean;
sendMultipleVideoStreams?: boolean;
};
disableModeratorIndicator?: boolean;
disableReactions?: boolean;
disableReactionsModeration?: boolean;
disablePolls?: boolean;
disableSelfView?: boolean;
disableSelfViewSettings?: boolean;
screenshotCapture?: {
enabled?: boolean;
mode?: 'always' | 'recording';
};
webrtcIceUdpDisable?: boolean;
webrtcIceTcpDisable?: boolean;
enableUnifiedOnChrome?: boolean;
disableAudioLevels?: boolean;
audioLevelsInterval?: number;
enableNoAudioDetection?: boolean;
enableSaveLogs?: boolean;
disableShowMoreStats?: boolean;
enableNoisyMicDetection?: boolean;
startAudioOnly?: boolean;
startAudioMuted?: boolean;
startWithAudioMuted?: boolean;
startSilent?: boolean;
enableOpusRed?: boolean;
audioQuality?: {
stereo?: boolean;
opusMaxAverageBitrate?: number|null;
};
stereo?: boolean;
opusMaxAverageBitrate?: number;
resolution?: number;
disableRemoveRaisedHandOnFocus?: boolean;
disableSpeakerStatsSearch?: boolean;
speakerStatsOrder?: Array<'role'|'name'|'hasLeft'>;
maxFullResolutionParticipants?: number;
constraints?: {
video?: {
height?: {
ideal?: number;
max?: number;
min?: number;
}
}
};
disableSimulcast?: boolean;
enableLayerSuspension?: boolean;
startVideoMuted?: number;
startWithVideoMuted?: boolean;
preferH264?: boolean;
disableH264?: boolean;
desktopSharingFrameRate?: {
min?: number;
max?: number;
};
startScreenSharing?: boolean;
fileRecordingsEnabled?: boolean;
dropbox?: {
appKey: string;
redirectURI?: string;
};
recordingService?: {
enabled?: boolean;
sharingEnabled?: boolean;
hideStorageWarning?: boolean;
};
fileRecordingsServiceEnabled?: boolean;
fileRecordingsServiceSharingEnabled?: boolean;
liveStreamingEnabled?: boolean;
localRecording?: {
disable?: boolean;
notifyAllParticipants?: boolean;
};
transcribingEnabled?: boolean;
transcribeWithAppLanguage?: boolean;
preferredTranscribeLanguage?: string;
autoCaptionOnRecord?: boolean;
transcription?: {
enabled?: boolean;
useAppLanguage?: boolean;
preferredLanguage?: string;
disableStartForAll?: boolean;
autoCaptionOnRecord?: boolean;
};
channelLastN?: number;
connectionIndicators?: {
autoHide?: boolean;
autoHideTimeout?: number;
disabled?: boolean;
disableDetails?: boolean;
inactiveDisabled?: boolean;
};
startLastN?: number;
lastNLimits?: {
[key: number]: number;
};
useNewBandwidthAllocationStrategy?: boolean;
videoQuality?: {
disabledCodec?: string;
preferredCodec?: string;
enforcePreferredCodec?: boolean;
maxBitratesVideo?: {
[key: string]: {
low?: number;
standard?: number;
high?: number;
}
};
minHeightForQualityLvl: {
[key: number]: string;
};
resizeDesktopForPresenter?: boolean;
};
notificationTimeouts?: {
short?: number;
medium?: number;
long?: number;
};
recordingLimit?: {
limit?: number;
appName?: string;
appURL?: string;
};
disableRtx?: boolean;
disableBeforeUnloadHandlers?: boolean;
enableTcc?: boolean;
enableRemb?: boolean;
enableIceRestart?: boolean;
enableForcedReload?: boolean;
useTurnUdp?: boolean;
enableEncodedTransformSupport?: boolean;
disableResponsiveTiles?: boolean;
hideLobbyButton?: boolean;
autoKnockLobby?: boolean;
enableLobbyChat?: boolean;
hideAddRoomButton?: boolean;
requireDisplayName?: boolean;
enableWelcomePage?: boolean;
disableShortcuts?: boolean;
disableInitialGUM?: boolean;
enableClosePage?: boolean;
disable1On1Mode?: boolean|null;
defaultLocalDisplayName?: string;
defaultRemoteDisplayName?: string;
hideDisplayName?: boolean;
hideDominantSpeakerBadge?: boolean;
defaultLanguage?: string;
disableProfile?: boolean;
hideEmailInSettings?: boolean;
enableFeaturesBasedOnToken?: boolean;
roomPasswordNumberOfDigits?: number;
noticeMessage?: string;
enableCalendarIntegration?: boolean;
prejoinConfig?: {
enabled?: boolean;
hideDisplayName?: boolean;
hideExtraJoinButtons?: Array<string>;
};
prejoinPageEnabled?: boolean;
readOnlyName?: boolean;
openSharedDocumentOnJoin?: boolean;
enableInsecureRoomNameWarning?: boolean;
enableAutomaticUrlCopy?: boolean;
corsAvatarURLs?: Array<string>;
gravatarBaseURL?: string;
gravatar?: {
baseUrl?: string;
disabled?: boolean;
};
inviteAppName?: string|null;
toolbarButtons?: Array<ToolbarButtons>;
toolbarConfig?: {
initialTimeout?: number;
timeout?: number;
alwaysVisible?: boolean;
autoHideWhileChatIsOpen?: boolean;
};
buttonsWithNotifyClick?: Array<ButtonsWithNotifyClick | { key: ButtonsWithNotifyClick; preventExecution: boolean }>;
hiddenPremeetingButtons?: Array<'microphone' | 'camera' | 'select-background' | 'invite' | 'settings'>;
gatherStats?: boolean;
pcStatsInterval?: number;
callStatsID?: string;
callStatsSecret?: string;
callStatsConfigParams?: {
disableBeforeUnloadHandler?: boolean;
applicationVersion?: string;
disablePrecalltest?: boolean;
siteID?: string;
additionalIDs?: {
customerID?: string;
tenantID?: string;
productName?: string;
meetingsName?: string;
serverName?: string;
pbxID?: string;
pbxExtensionID?: string;
fqExtensionID?: string;
sessionID?: string;
};
collectLegacyStats?: boolean;
collectIP?: boolean;
};
enableDisplayNameInStats?: boolean;
enableEmailInStats?: boolean;
faceLandmarks?: {
enableFaceCentering?: boolean;
enableFaceExpressionsDetection?: boolean;
enableDisplayFaceExpressions?: boolean;
enableRTCStats?: boolean;
faceCenteringThreshold?: number;
captureInterval?: number;
};
feedbackPercentage?: number;
disableThirdPartyRequests?: boolean;
p2p?: {
enabled?: boolean;
enableUnifiedOnChrome?: boolean;
iceTransportPolicy?: string;
preferH264?: boolean;
preferredCodec?: string;
disableH264?: boolean;
disabledCodec?: string;
backToP2PDelay?: number;
stunServers?: Array<{urls: string}>;
};
analytics?: {
disabled?: boolean;
googleAnalyticsTrackingId?: string;
matomoEndpoint?: string;
matomoSiteID?: string;
amplitudeAPPKey?: string;
obfuscateRoomName?: boolean;
rtcstatsEnabled?: boolean;
rtcstatsEndpoint?: string;
rtcstatsPolIInterval?: number;
scriptURLs?: Array<string>;
};
apiLogLevels?: Array<'warn' | 'log' | 'error' | 'info' | 'debug'>;
deploymentInfo?: {
shard?: string;
region?: string;
userRegion?: string;
};
disabledSounds?: Array<Sounds>;
disableRecordAudioNotification?: boolean;
disableJoinLeaveSounds?: boolean;
disableIncomingMessageSound?: boolean;
chromeExtensionBanner?: {
url?: string;
edgeUrl?: string;
chromeExtensionsInfo?: Array<{id: string; path: string}>;
};
e2ee?: {
labels?: {
tooltip?: string;
description?: string;
label?: string;
warning?: string;
};
externallyManagedKey?: boolean;
e2eeLabels?: {
tooltip?: string;
description?: string;
label?: string;
warning?: string;
};
};
e2eeLabels?: {
tooltip?: string;
description?: string;
label?: string;
warning?: string;
};
e2eping?: {
enabled?: boolean;
numRequests?: number;
maxConferenceSize?: number;
maxMessagesPerSecond?: number;
};
_desktopSharingSourceDevice?: string;
disableDeepLinking?: boolean;
disableLocalVideoFlip?: boolean;
doNotFlipLocalVideo?: boolean;
disableInviteFunctions?: boolean;
doNotStoreRoom?: boolean;
deploymentUrls?: {
userDocumentationURL?: string;
downloadAppsUrl?: string;
};
remoteVideoMenu?: {
disabled?: boolean;
disableKick?: boolean;
disableGrantModerator?: boolean;
disablePrivateChat?: boolean;
};
salesforceUrl?: string;
disableRemoteMute?: boolean;
enableLipSync?: boolean;
dynamicBrandingUrl?: string;
participantsPane?: {
hideModeratorSettingsTab?: boolean;
hideMoreActionsButton?: boolean;
hideMuteAllButton?: boolean;
};
breakoutRooms?: {
hideAddRoomButton?: boolean;
hideAutoAssignButton?: boolean;
hideJoinRoomButton?: boolean;
};
disableAddingBackgroundImages?: boolean;
disableScreensharingVirtualBackground?: boolean;
backgroundAlpha?: number;
moderatedRoomServiceUrl?: string;
disableTileView?: boolean;
disableTileEnlargement?: boolean;
conferenceInfo?: {
alwaysVisible?: Array<string>;
autoHide?: Array<string>;
};
hideConferenceSubject?: boolean;
hideConferenceTimer?: boolean;
hideRecordingLabel?: boolean;
hideParticipantsStats?: boolean;
subject?: string;
localSubject?: string;
useHostPageLocalStorage?: boolean;
etherpad_base?: string;
dialInNumbersUrl?: string;
dialInConfCodeUrl?: string;
brandingRoomAlias?: string;
mouseMoveCallbackInterval?: number;
notifications?: Array<string>;
disabledNotifications?: Array<string>;
disableFilmstripAutohiding?: boolean;
filmstrip?: {
disableResizable?: boolean;
disableStageFilmstrip?: boolean;
disableTopPanel?: boolean;
minParticipantCountForTopPanel?: number;
};
tileView?: {
numberOfVisibleTiles?: number;
};
disableChatSmileys?: boolean;
giphy?: {
enabled?: boolean;
sdkKey?: '';
displayMode?: 'all' | 'tile' | 'chat';
tileTime?: number;
};
locationURL?: string;
}

View File

@@ -222,6 +222,7 @@ export default [
'toolbarConfig',
'tileView',
'transcribingEnabled',
'transcription',
'useHostPageLocalStorage',
'useTurnUdp',
'videoQuality',

View File

@@ -1,9 +1,9 @@
// @flow
import _ from 'lodash';
import { CONFERENCE_INFO } from '../../conference/components/constants';
import { equals, ReducerRegistry } from '../redux';
// @ts-ignore
import { equals } from '../redux';
import ReducerRegistry from '../redux/ReducerRegistry';
import {
UPDATE_CONFIG,
@@ -12,9 +12,11 @@ import {
SET_CONFIG,
OVERWRITE_CONFIG
} from './actionTypes';
import { IConfig } from './configType';
// @ts-ignore
import { _cleanupConfig } from './functions';
declare var interfaceConfig: Object;
declare var interfaceConfig: any;
/**
* The initial state of the feature base/config when executing in a
@@ -26,7 +28,7 @@ declare var interfaceConfig: Object;
*
* @type {Object}
*/
const INITIAL_NON_RN_STATE = {
const INITIAL_NON_RN_STATE: IConfig = {
};
/**
@@ -38,7 +40,7 @@ const INITIAL_NON_RN_STATE = {
*
* @type {Object}
*/
const INITIAL_RN_STATE = {
const INITIAL_RN_STATE: IConfig = {
analytics: {},
// FIXME The support for audio levels in lib-jitsi-meet polls the statistics
@@ -61,14 +63,14 @@ const INITIAL_RN_STATE = {
* Mapping between old configs controlling the conference info headers visibility and the
* new configs. Needed in order to keep backwards compatibility.
*/
const CONFERENCE_HEADER_MAPPING = {
const CONFERENCE_HEADER_MAPPING: any = {
hideConferenceTimer: [ 'conference-timer' ],
hideConferenceSubject: [ 'subject' ],
hideParticipantsStats: [ 'participants-count' ],
hideRecordingLabel: [ 'recording' ]
};
ReducerRegistry.register('features/base/config', (state = _getInitialState(), action) => {
ReducerRegistry.register('features/base/config', (state: IConfig = _getInitialState(), action: any) => {
switch (action.type) {
case UPDATE_CONFIG:
return _updateConfig(state, action);
@@ -139,12 +141,12 @@ function _getInitialState() {
* Reduces a specific Redux action SET_CONFIG of the feature
* base/lib-jitsi-meet.
*
* @param {Object} state - The Redux state of the feature base/config.
* @param {IConfig} state - The Redux state of the feature base/config.
* @param {Action} action - The Redux action SET_CONFIG to reduce.
* @private
* @returns {Object} The new state after the reduction of the specified action.
*/
function _setConfig(state, { config }) {
function _setConfig(state: IConfig, { config }: {config: IConfig}) {
// The mobile app bundles jitsi-meet and lib-jitsi-meet at build time and
// does not download them at runtime from the deployment on which it will
// join a conference. The downloading is planned for implementation in the
@@ -187,10 +189,10 @@ function _setConfig(state, { config }) {
/**
* Processes the conferenceInfo object against the defaults.
*
* @param {Object} config - The old config.
* @param {IConfig} config - The old config.
* @returns {Object} The processed conferenceInfo object.
*/
function _getConferenceInfo(config) {
function _getConferenceInfo(config: IConfig) {
const { conferenceInfo } = config;
if (conferenceInfo) {
@@ -207,11 +209,7 @@ function _getConferenceInfo(config) {
/**
* Constructs a new config {@code Object}, if necessary, out of a specific
* config {@code Object} which is in the latest format supported by jitsi-meet.
* Such a translation from an old config format to a new/the latest config
* format is necessary because the mobile app bundles jitsi-meet and
* lib-jitsi-meet at build time and does not download them at runtime from the
* deployment on which it will join a conference.
* interface_config {@code Object} which is in the latest format supported by jitsi-meet.
*
* @param {Object} oldValue - The config {@code Object} which may or may not be
* in the latest form supported by jitsi-meet and from which a new config
@@ -219,11 +217,11 @@ function _getConferenceInfo(config) {
* @returns {Object} A config {@code Object} which is in the latest format
* supported by jitsi-meet.
*/
function _translateLegacyConfig(oldValue: Object) {
function _translateInterfaceConfig(oldValue: IConfig) {
const newValue = oldValue;
if (!Array.isArray(oldValue.toolbarButtons)
&& typeof interfaceConfig === 'object' && Array.isArray(interfaceConfig.TOOLBAR_BUTTONS)) {
&& typeof interfaceConfig === 'object' && Array.isArray(interfaceConfig.TOOLBAR_BUTTONS)) {
newValue.toolbarButtons = interfaceConfig.TOOLBAR_BUTTONS;
}
@@ -231,6 +229,7 @@ function _translateLegacyConfig(oldValue: Object) {
oldValue.toolbarConfig = {};
}
newValue.toolbarConfig = oldValue.toolbarConfig || {};
if (typeof oldValue.toolbarConfig.alwaysVisible !== 'boolean'
&& typeof interfaceConfig === 'object'
&& typeof interfaceConfig.TOOLBAR_ALWAYS_VISIBLE === 'boolean') {
@@ -249,32 +248,11 @@ function _translateLegacyConfig(oldValue: Object) {
newValue.toolbarConfig.timeout = interfaceConfig.TOOLBAR_TIMEOUT;
}
const filteredConferenceInfo = Object.keys(CONFERENCE_HEADER_MAPPING).filter(key => oldValue[key]);
if (filteredConferenceInfo.length) {
newValue.conferenceInfo = _getConferenceInfo(oldValue);
filteredConferenceInfo.forEach(key => {
// hideRecordingLabel does not mean not render it at all, but autoHide it
if (key === 'hideRecordingLabel') {
newValue.conferenceInfo.alwaysVisible
= newValue.conferenceInfo.alwaysVisible.filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
newValue.conferenceInfo.autoHide
= _.union(newValue.conferenceInfo.autoHide, CONFERENCE_HEADER_MAPPING[key]);
} else {
newValue.conferenceInfo.alwaysVisible
= newValue.conferenceInfo.alwaysVisible.filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
newValue.conferenceInfo.autoHide
= newValue.conferenceInfo.autoHide.filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
}
});
}
if (!oldValue.connectionIndicators
&& typeof interfaceConfig === 'object'
&& (interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_DISABLED')
|| interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_AUTO_HIDE_ENABLED')
|| interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT'))) {
&& typeof interfaceConfig === 'object'
&& (interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_DISABLED')
|| interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_AUTO_HIDE_ENABLED')
|| interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT'))) {
newValue.connectionIndicators = {
disabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
autoHide: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
@@ -282,6 +260,68 @@ function _translateLegacyConfig(oldValue: Object) {
};
}
if (oldValue.disableModeratorIndicator === undefined
&& typeof interfaceConfig === 'object'
&& interfaceConfig.hasOwnProperty('DISABLE_FOCUS_INDICATOR')) {
newValue.disableModeratorIndicator = interfaceConfig.DISABLE_FOCUS_INDICATOR;
}
if (oldValue.defaultLocalDisplayName === undefined
&& typeof interfaceConfig === 'object'
&& interfaceConfig.hasOwnProperty('DEFAULT_LOCAL_DISPLAY_NAME')) {
newValue.defaultLocalDisplayName = interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME;
}
if (oldValue.defaultRemoteDisplayName === undefined
&& typeof interfaceConfig === 'object'
&& interfaceConfig.hasOwnProperty('DEFAULT_REMOTE_DISPLAY_NAME')) {
newValue.defaultRemoteDisplayName = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
}
return newValue;
}
/**
* Constructs a new config {@code Object}, if necessary, out of a specific
* config {@code Object} which is in the latest format supported by jitsi-meet.
* Such a translation from an old config format to a new/the latest config
* format is necessary because the mobile app bundles jitsi-meet and
* lib-jitsi-meet at build time and does not download them at runtime from the
* deployment on which it will join a conference.
*
* @param {Object} oldValue - The config {@code Object} which may or may not be
* in the latest form supported by jitsi-meet and from which a new config
* {@code Object} is to be constructed if necessary.
* @returns {Object} A config {@code Object} which is in the latest format
* supported by jitsi-meet.
*/
function _translateLegacyConfig(oldValue: IConfig) {
const newValue = _translateInterfaceConfig(oldValue);
// Translate deprecated config values to new config values.
const filteredConferenceInfo = Object.keys(CONFERENCE_HEADER_MAPPING).filter(key => oldValue[key as keyof IConfig]);
if (filteredConferenceInfo.length) {
newValue.conferenceInfo = _getConferenceInfo(oldValue);
filteredConferenceInfo.forEach(key => {
newValue.conferenceInfo = oldValue.conferenceInfo ?? {};
// hideRecordingLabel does not mean not render it at all, but autoHide it
if (key === 'hideRecordingLabel') {
newValue.conferenceInfo.alwaysVisible
= (newValue.conferenceInfo?.alwaysVisible ?? []).filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
newValue.conferenceInfo.autoHide
= _.union(newValue.conferenceInfo.autoHide, CONFERENCE_HEADER_MAPPING[key]);
} else {
newValue.conferenceInfo.alwaysVisible
= (newValue.conferenceInfo.alwaysVisible ?? []).filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
newValue.conferenceInfo.autoHide
= (newValue.conferenceInfo.autoHide ?? []).filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
}
});
}
newValue.prejoinConfig = oldValue.prejoinConfig || {};
if (oldValue.hasOwnProperty('prejoinPageEnabled')
&& !newValue.prejoinConfig.hasOwnProperty('enabled')
@@ -315,33 +355,15 @@ function _translateLegacyConfig(oldValue: Object) {
};
}
if (oldValue.disableModeratorIndicator === undefined
&& typeof interfaceConfig === 'object'
&& interfaceConfig.hasOwnProperty('DISABLE_FOCUS_INDICATOR')) {
newValue.disableModeratorIndicator = interfaceConfig.DISABLE_FOCUS_INDICATOR;
}
newValue.e2ee = newValue.e2ee || {};
if (oldValue.e2eeLabels) {
newValue.e2ee.e2eeLabels = oldValue.e2eeLabels;
}
if (oldValue.defaultLocalDisplayName === undefined
&& typeof interfaceConfig === 'object'
&& interfaceConfig.hasOwnProperty('DEFAULT_LOCAL_DISPLAY_NAME')) {
newValue.defaultLocalDisplayName = interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME;
}
newValue.defaultLocalDisplayName
= newValue.defaultLocalDisplayName || 'me';
if (oldValue.defaultRemoteDisplayName === undefined
&& typeof interfaceConfig === 'object'
&& interfaceConfig.hasOwnProperty('DEFAULT_REMOTE_DISPLAY_NAME')) {
newValue.defaultRemoteDisplayName = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
}
if (oldValue.hideAddRoomButton) {
newValue.breakoutRooms = {
/* eslint-disable-next-line no-extra-parens */
@@ -353,6 +375,46 @@ function _translateLegacyConfig(oldValue: Object) {
newValue.defaultRemoteDisplayName
= newValue.defaultRemoteDisplayName || 'Fellow Jitster';
newValue.transcription = newValue.transcription || {};
if (oldValue.transcribingEnabled !== undefined) {
newValue.transcription = {
...newValue.transcription,
enabled: oldValue.transcribingEnabled
};
}
if (oldValue.transcribeWithAppLanguage !== undefined) {
newValue.transcription = {
...newValue.transcription,
useAppLanguage: oldValue.transcribeWithAppLanguage
};
}
if (oldValue.preferredTranscribeLanguage !== undefined) {
newValue.transcription = {
...newValue.transcription,
preferredLanguage: oldValue.preferredTranscribeLanguage
};
}
if (oldValue.autoCaptionOnRecord !== undefined) {
newValue.transcription = {
...newValue.transcription,
autoCaptionOnRecord: oldValue.autoCaptionOnRecord
};
}
newValue.recordingService = newValue.recordingService || {};
if (oldValue.fileRecordingsServiceEnabled !== undefined) {
newValue.recordingService = {
...newValue.recordingService,
enabled: oldValue.fileRecordingsServiceEnabled
};
}
if (oldValue.fileRecordingsServiceSharingEnabled !== undefined) {
newValue.recordingService = {
...newValue.recordingService,
sharingEnabled: oldValue.fileRecordingsServiceSharingEnabled
};
}
return newValue;
}
@@ -364,7 +426,7 @@ function _translateLegacyConfig(oldValue: Object) {
* @private
* @returns {Object} The new state after the reduction of the specified action.
*/
function _updateConfig(state, { config }) {
function _updateConfig(state: IConfig, { config }: {config: IConfig}) {
const newState = _.merge({}, state, config);
_cleanupConfig(newState);

View File

@@ -1,5 +1,3 @@
// @flow
/**
* The set of facing modes for camera.
*
@@ -17,7 +15,7 @@ export type MediaType = 'audio' | 'video' | 'presenter' | 'screenshare';
*
* @enum {string}
*/
export const MEDIA_TYPE = {
export const MEDIA_TYPE: {[key: string]: MediaType} = {
AUDIO: 'audio',
PRESENTER: 'presenter',
SCREENSHARE: 'screenshare',

View File

@@ -1,6 +1,6 @@
// @flow
import { getDefaultHeaderHeight } from '@react-navigation/elements';
import { useHeaderHeight } from '@react-navigation/elements';
import React, { useCallback, useEffect, useState } from 'react';
import {
Keyboard,
@@ -8,7 +8,7 @@ import {
Platform,
StatusBar
} from 'react-native';
import { useSafeAreaFrame, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { StyleType } from '../../styles';
@@ -34,11 +34,6 @@ type Props = {
*/
hasTabNavigator: boolean,
/**
* Is the screen presented as a modal?
*/
isModalPresentation: boolean,
/**
* Additional style to be appended to the KeyboardAvoidingView.
*/
@@ -51,33 +46,26 @@ const JitsiKeyboardAvoidingView = (
contentContainerStyle,
hasTabNavigator,
hasBottomTextInput,
isModalPresentation,
style
}: Props) => {
const frame = useSafeAreaFrame();
const headerHeight = useHeaderHeight();
const insets = useSafeAreaInsets();
const [ bottomPadding, setBottomPadding ] = useState(insets.bottom);
const [ topPadding, setTopPadding ] = useState(insets.top);
useEffect(() => {
// This useEffect is needed because insets are undefined at first for some reason
// https://github.com/th3rdwave/react-native-safe-area-context/issues/54
setBottomPadding(insets.bottom);
setTopPadding(insets.top);
}, [ insets.bottom, insets.top ]);
}, [ insets.bottom ]);
const headerHeight = getDefaultHeaderHeight(frame, isModalPresentation, topPadding);
// Notch devices have in general a header height between 103 and 106px
const topNotchDevice = headerHeight > 100;
const deviceHeight = topNotchDevice ? headerHeight - 50 : headerHeight;
const tabNavigatorPadding
= hasTabNavigator ? deviceHeight : 0;
= hasTabNavigator ? headerHeight : 0;
const noNotchDevicePadding = bottomPadding || 10;
const iosVerticalOffset
= deviceHeight + noNotchDevicePadding + tabNavigatorPadding;
= headerHeight + noNotchDevicePadding + tabNavigatorPadding;
const androidVerticalOffset = hasBottomTextInput
? deviceHeight + StatusBar.currentHeight : deviceHeight;
? headerHeight + StatusBar.currentHeight : headerHeight;
// Tells the view what to do with taps
const shouldSetResponse = useCallback(() => true);

View File

@@ -37,11 +37,6 @@ type Props = {
*/
hasTabNavigator?: boolean,
/**
* Is the screen presented as a modal?
*/
isModalPresentation?: boolean,
/**
* Insets for the SafeAreaView.
*/
@@ -59,7 +54,6 @@ const JitsiScreen = ({
footerComponent,
hasTabNavigator = false,
hasBottomTextInput = false,
isModalPresentation = true,
safeAreaInsets = [ 'left', 'right' ],
style
}: Props) => (
@@ -69,7 +63,6 @@ const JitsiScreen = ({
contentContainerStyle = { contentContainerStyle }
hasBottomTextInput = { hasBottomTextInput }
hasTabNavigator = { hasTabNavigator }
isModalPresentation = { isModalPresentation }
style = { style }>
<SafeAreaView
edges = { safeAreaInsets }

View File

@@ -1,44 +0,0 @@
/* @flow */
import React, { Component } from 'react';
import { Text, TouchableOpacity } from 'react-native';
type Props = {
/**
* React Elements to display within the component.
*/
children: React$Node | Object,
/**
* Handler called when the user presses the button.
*/
onValueChange: Function,
/**
* The component's external style.
*/
style: Object
};
/**
* Renders a button.
*/
export default class ButtonImpl extends Component<Props> {
/**
* Implements React's {@link Component#render()}, renders the button.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<TouchableOpacity
onPress = { this.props.onValueChange } >
<Text style = { this.props.style }>
{ this.props.children }
</Text>
</TouchableOpacity>
);
}
}

View File

@@ -0,0 +1,99 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Button as NativePaperButton,
Text,
TouchableRipple
} from 'react-native-paper';
import BaseTheme from '../../../ui/components/BaseTheme.native';
import styles from './styles';
import { BUTTON_MODES, BUTTON_TYPES } from '../../constants';
import { ButtonProps } from '../../types';
const Button: React.FC<ButtonProps> = ({
accessibilityLabel,
color: buttonColor,
disabled,
icon,
label,
labelStyle,
onPress,
style,
type
}: ButtonProps) => {
const { t } = useTranslation();
const { CONTAINED } = BUTTON_MODES;
const { DESTRUCTIVE, PRIMARY, SECONDARY, TERTIARY } = BUTTON_TYPES;
let buttonLabelStyles;
let buttonStyles;
let color;
let mode;
if (type === PRIMARY) {
buttonLabelStyles = styles.buttonLabelPrimary;
color = BaseTheme.palette.action01;
mode = CONTAINED
} else if (type === SECONDARY) {
buttonLabelStyles = styles.buttonLabelSecondary;
color = BaseTheme.palette.action02;
mode = CONTAINED
} else if (type === DESTRUCTIVE) {
color = BaseTheme.palette.actionDanger;
buttonLabelStyles = styles.buttonLabelDestructive;
mode = CONTAINED
} else {
color = buttonColor;
buttonLabelStyles = styles.buttonLabel;
}
if (disabled) {
buttonLabelStyles = styles.buttonLabelDisabled;
buttonStyles = styles.buttonDisabled;
} else {
buttonStyles = styles.button;
}
if ( type === TERTIARY) {
return (
<TouchableRipple
accessibilityLabel = { accessibilityLabel }
disabled = { disabled }
onPress = { onPress }
rippleColor = 'transparent'
style = { [
buttonStyles,
style
] }>
<Text
style = { [
buttonLabelStyles,
labelStyle
] }>{ t(label) }</Text>
</TouchableRipple>
);
}
return (
<NativePaperButton
accessibilityLabel = { t(accessibilityLabel) }
children = { t(label) }
color = { color }
disabled = { disabled }
icon = { icon }
labelStyle = { [
buttonLabelStyles,
labelStyle
] }
mode = { mode }
onPress = { onPress }
style = { [
buttonStyles,
style
] } />
);
};
export default Button;

View File

@@ -0,0 +1,64 @@
import React from 'react';
import { TouchableRipple } from 'react-native-paper';
import { Icon } from '../../../icons';
import BaseTheme from '../../../ui/components/BaseTheme.native';
import styles from './styles';
import { BUTTON_TYPES } from '../../constants';
import { IconButtonProps } from '../../types';
const IconButton: React.FC<IconButtonProps> = ({
accessibilityLabel,
color: iconColor,
disabled,
onPress,
size,
src,
style,
tapColor,
type
}: IconButtonProps) => {
const { PRIMARY, SECONDARY, TERTIARY } = BUTTON_TYPES;
let color;
let rippleColor;
let iconButtonContainerStyles;
if (type === PRIMARY) {
color = BaseTheme.palette.icon01;
iconButtonContainerStyles = styles.iconButtonContainerPrimary;
rippleColor = BaseTheme.palette.action01;
} else if (type === SECONDARY) {
color = BaseTheme.palette.icon02;
iconButtonContainerStyles = styles.iconButtonContainerSecondary;
rippleColor = BaseTheme.palette.action02;
} else if ( type === TERTIARY) {
color = BaseTheme.palette.icon01;
iconButtonContainerStyles = styles.iconButtonContainer;
rippleColor = BaseTheme.palette.action03;
} else {
color = iconColor;
rippleColor = tapColor;
}
return (
<TouchableRipple
accessibilityLabel = { accessibilityLabel }
disabled = { disabled }
onPress = { onPress }
rippleColor = { rippleColor }
style = { [
iconButtonContainerStyles,
style
] }>
<Icon
color = { color }
size = { 20 || size }
src = { src } />
</TouchableRipple>
);
};
export default IconButton;

View File

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

View File

@@ -2,7 +2,6 @@
export { default as AvatarListItem } from './AvatarListItem';
export { default as BaseIndicator } from './BaseIndicator';
export { default as Button } from './Button';
export { default as Container } from './Container';
export { default as Image } from './Image';
export { default as Link } from './Link';
@@ -15,7 +14,6 @@ export { default as NavigateSectionListItem }
from './NavigateSectionListItem';
export { default as NavigateSectionListSectionHeader }
from './NavigateSectionListSectionHeader';
export { default as PagedList } from './PagedList';
export { default as Pressable } from './Pressable';
export { default as SectionList } from './SectionList';
export { default as SlidingView } from './SlidingView';

View File

@@ -5,93 +5,12 @@ import BaseTheme from '../../../ui/components/BaseTheme.native';
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
const SECONDARY_ACTION_BUTTON_SIZE = 30;
const BUTTON_HEIGHT = BaseTheme.spacing[7];
const BUTTON_WIDTH = BaseTheme.spacing[7];
export const AVATAR_SIZE = 65;
export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)';
/**
* Style classes of the PagedList-based components.
*/
const PAGED_LIST_STYLES = {
/**
* Outermost container of a page in {@code PagedList}.
*/
pageContainer: {
flex: 1
},
/**
* Style of the page indicator (Android).
*/
pageIndicator: {
alignItems: 'center',
flexDirection: 'column',
justifyContent: 'center',
padding: BoxModel.padding / 2
},
/**
* Additional style for the active indicator icon (Android).
*/
pageIndicatorActive: {
color: ColorPalette.white
},
/**
* Container for the page indicators (Android).
*/
pageIndicatorContainer: {
alignItems: 'center',
backgroundColor: ColorPalette.blue,
flexDirection: 'row',
justifyContent: 'space-around'
},
pageIndicatorContent: {
alignItems: 'center',
flexDirection: 'column',
justifyContent: 'center'
},
/**
* Icon of the page indicator (Android).
*/
pageIndicatorIcon: {
color: ColorPalette.blueHighlight,
fontSize: 24
},
/**
* Label of the page indicator (Android).
*/
pageIndicatorText: {
color: ColorPalette.blueHighlight
},
/**
* Top level style of the paged list.
*/
pagedList: {
flex: 1
},
/**
* The paged list container View.
*/
pagedListContainer: {
flex: 1,
flexDirection: 'column'
},
/**
* Disabled style for the container.
*/
pagedListContainerDisabled: {
opacity: 0.2
}
};
const SECTION_LIST_STYLES = {
/**
* The style of the avatar container that makes the avatar rounded.
@@ -217,11 +136,82 @@ export const BASE_INDICATOR = {
justifyContent: 'center'
};
const button = {
borderRadius: BaseTheme.shape.borderRadius,
height: BUTTON_HEIGHT
};
const buttonLabel = {
...BaseTheme.typography.bodyShortBold,
padding: 6,
textTransform: 'capitalize'
};
const iconButtonContainer = {
alignItems: 'center',
display: 'flex',
justifyContent: 'center',
borderRadius: BaseTheme.shape.borderRadius,
height: BUTTON_HEIGHT,
width: BUTTON_WIDTH
};
/**
* The styles of the generic React {@code Component}s implemented by the feature
* base/react.
*/
export default {
...PAGED_LIST_STYLES,
...SECTION_LIST_STYLES
...SECTION_LIST_STYLES,
button: {
...button
},
buttonLabel: {
...buttonLabel
},
buttonLabelDisabled: {
...buttonLabel,
color: BaseTheme.palette.text03
},
buttonDisabled: {
...button,
backgroundColor: BaseTheme.palette.actionDisabled
},
buttonLabelPrimary: {
...buttonLabel,
color: BaseTheme.palette.text01
},
buttonLabelSecondary: {
...buttonLabel,
color: BaseTheme.palette.text02
},
buttonLabelDestructive: {
...buttonLabel,
color: BaseTheme.palette.text01
},
buttonLabelTertiary: {
...buttonLabel,
color: BaseTheme.palette.text01
},
iconButtonContainer: {
...iconButtonContainer
},
iconButtonContainerPrimary: {
...iconButtonContainer,
backgroundColor: BaseTheme.palette.action01
},
iconButtonContainerSecondary: {
...iconButtonContainer,
backgroundColor: BaseTheme.palette.action02
}
};

View File

@@ -5,3 +5,20 @@
* everything, such as modal-type of components, or dialogs.
*/
export const OVERLAY_Z_INDEX = 1000;
/**
* The types of the buttons.
*/
export const BUTTON_TYPES = {
PRIMARY: 'primary',
SECONDARY: 'secondary',
TERTIARY: 'tertiary',
DESTRUCTIVE: 'destructive'
};
/**
* The modes of the buttons.
*/
export const BUTTON_MODES = {
CONTAINED: 'contained'
};

View File

@@ -0,0 +1,23 @@
export interface ButtonProps {
accessibilityLabel?: string;
color?: string;
disabled?: boolean;
icon?: JSX.Element;
label?: string;
labelStyle?: Object|undefined;
onPress?: Function;
style?: Object|undefined;
type?: string;
}
export interface IconButtonProps {
accessibilityLabel?: string;
color?: string;
disabled?: boolean;
onPress?: Function;
size?: number|string;
src?: Function;
style?: Object|undefined;
tapColor?: string;
type?: string;
}

View File

@@ -1,20 +1,18 @@
/* @flow */
import { combineReducers } from 'redux';
import { Action, combineReducers } from 'redux';
import type { Reducer } from 'redux';
/**
* The type of the dictionary/map which associates a reducer (function) with the
* name of he Redux state property managed by the reducer.
*/
declare type NameReducerMap<S, A> = { [name: string]: Reducer<S, A> };
declare type NameReducerMap<S, A> = { [name: string]: Reducer<S, Action<any>> };
/**
* A registry for Redux reducers, allowing features to register themselves
* without needing to create additional inter-feature dependencies.
*/
class ReducerRegistry {
_elements: NameReducerMap<*, *>;
_elements: NameReducerMap<any, any>;
/**
* Creates a ReducerRegistry instance.
@@ -37,7 +35,7 @@ class ReducerRegistry {
* included (such as reducers from third-party modules).
* @returns {Function}
*/
combineReducers(additional: NameReducerMap<*, *> = {}) {
combineReducers(additional: NameReducerMap<any, any> = {}) {
// $FlowExpectedError
return combineReducers({
...this._elements,
@@ -55,7 +53,7 @@ class ReducerRegistry {
* @param {Reducer} reducer - A Redux reducer.
* @returns {void}
*/
register(name: string, reducer: Reducer<*, *>) {
register(name: string, reducer: Reducer<any, any>) {
this._elements[name] = reducer;
}
}

View File

@@ -89,6 +89,8 @@ export const colorMap = {
// Status bar
status01Bar: 'primary11',
actionDisabled: 'surface09',
// Hover state for primary buttons
action01Hover: 'primary06',
@@ -102,7 +104,7 @@ export const colorMap = {
action01Disabled: 'primary02',
// Secondary buttons
action02: 'surface04',
action02: 'surface10',
// Hover state for secondary buttons
action02Hover: 'surface05',
@@ -156,7 +158,7 @@ export const colorMap = {
text01: 'surface11',
// Secondary text with medium contrast
text02: 'surface09',
text02: 'surface01',
// Tertiary text with low contrast placeholders, disabled actions, label for disabled buttons
text03: 'surface07',
@@ -180,7 +182,7 @@ export const colorMap = {
icon01: 'surface11',
// Secondary color for input fields
icon02: 'surface09',
icon02: 'surface01',
// Tertiary color for disabled actions
icon03: 'surface07',

View File

@@ -35,11 +35,3 @@ export const SMALL_WIDTH_THRESHOLD = 580;
* Lobby message type.
*/
export const LOBBY_CHAT_MESSAGE = 'LOBBY_CHAT_MESSAGE';
/**
* The modes of the buttons of the chat and polls tabs.
*/
export const BUTTON_MODES = {
CONTAINED: 'contained',
TEXT: 'text'
};

View File

@@ -8,6 +8,7 @@ export const CONFERENCE_INFO = {
'e2ee',
'transcribing',
'video-quality',
'insecure-room'
'insecure-room',
'top-panel-toggle'
]
};

View File

@@ -1,6 +1,7 @@
// @flow
import React from 'react';
import { useIsFocused } from '@react-navigation/native';
import React, { useEffect } from 'react';
import { BackHandler, NativeModules, SafeAreaView, StatusBar, View } from 'react-native';
import { withSafeAreaInsets } from 'react-native-safe-area-context';
@@ -192,7 +193,6 @@ class Conference extends AbstractConference<Props, State> {
*/
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
setPictureInPictureEnabled(true);
}
/**
@@ -233,7 +233,6 @@ class Conference extends AbstractConference<Props, State> {
BackHandler.removeEventListener('hardwareBackPress', this._onHardwareBackPress);
clearTimeout(this._expandedLabelTimeout.current);
setPictureInPictureEnabled(false);
}
/**
@@ -561,4 +560,21 @@ function _mapStateToProps(state) {
};
}
export default withSafeAreaInsets(connect(_mapStateToProps)(Conference));
export default withSafeAreaInsets(connect(_mapStateToProps)(props => {
const isFocused = useIsFocused();
useEffect(() => {
if (isFocused) {
setPictureInPictureEnabled(true);
} else {
setPictureInPictureEnabled(false);
}
// We also need to disable PiP when we are back on the WelcomePage
return () => setPictureInPictureEnabled(false);
}, [ isFocused ]);
return (
<Conference { ...props } />
);
}));

View File

@@ -1,10 +1,12 @@
import React, { PureComponent } from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { Text, View } from 'react-native';
import { getFeatureFlag, INVITE_ENABLED } from '../../../base/flags';
import { translate } from '../../../base/i18n';
import { Icon, IconAddPeople } from '../../../base/icons';
import { getParticipantCountWithFake } from '../../../base/participants';
import Button from '../../../base/react/components/native/Button';
import { BUTTON_TYPES } from '../../../base/react/constants';
import { connect } from '../../../base/redux';
import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { doInvitePeople } from '../../../invite/actions.native';
@@ -57,6 +59,19 @@ class LonelyMeetingExperience extends PureComponent<Props> {
this._onPress = this._onPress.bind(this);
}
/**
* Renders the "add people" icon.
*
* @returns {ReactElement}
*/
_renderAddPeopleIcon() {
return (
<Icon
size = { 20 }
src = { IconAddPeople } />
);
}
/**
* Implements {@code PureComponent#render}.
*
@@ -80,17 +95,13 @@ class LonelyMeetingExperience extends PureComponent<Props> {
{ t('lonelyMeetingExperience.youAreAlone') }
</Text>
{ !_isInviteFunctionsDiabled && !_isInBreakoutRoom && (
<TouchableOpacity
<Button
accessibilityLabel = 'lonelyMeetingExperience.button'
icon = { this._renderAddPeopleIcon }
label = 'lonelyMeetingExperience.button'
onPress = { this._onPress }
style = { styles.lonelyButton }>
<Icon
size = { 24 }
src = { IconAddPeople }
style = { styles.lonelyButtonComponents } />
<Text style = { styles.lonelyButtonComponents }>
{ t('lonelyMeetingExperience.button') }
</Text>
</TouchableOpacity>
style = { styles.lonelyButton }
type = { BUTTON_TYPES.PRIMARY } />
) }
</View>
);

View File

@@ -1,6 +1,5 @@
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Text, View } from 'react-native';
import { View } from 'react-native';
import { withSafeAreaInsets } from 'react-native-safe-area-context';
import { useDispatch, useSelector } from 'react-redux';
@@ -12,20 +11,18 @@ import { setIsCarmode } from '../../../../video-layout/actions';
import ConferenceTimer from '../../ConferenceTimer';
import { isConnecting } from '../../functions';
import EndMeetingButton from './EndMeetingButton';
import CarModeFooter from './CarModeFooter';
import MicrophoneButton from './MicrophoneButton';
import SoundDeviceButton from './SoundDeviceButton';
import TitleBar from './TitleBar';
import styles from './styles';
/**
* Implements the carmode tab.
* Implements the carmode component.
*
* @returns { JSX.Element} - The carmode tab.
* @returns { JSX.Element} - The carmode component.
*/
const CarmodeTab = (): JSX.Element => {
const CarMode = (): JSX.Element => {
const dispatch = useDispatch();
const { t } = useTranslation();
const connecting = useSelector(isConnecting);
const isSharing = useSelector(isLocalVideoTrackDesktop);
@@ -42,7 +39,9 @@ const CarmodeTab = (): JSX.Element => {
}, []);
return (
<JitsiScreen style = { styles.conference }>
<JitsiScreen
footerComponent = { CarModeFooter }
style = { styles.conference }>
{/*
* The activity/loading indicator goes above everything, except
* the toolbox/toolbars and the dialogs.
@@ -66,17 +65,8 @@ const CarmodeTab = (): JSX.Element => {
style = { styles.microphoneContainer }>
<MicrophoneButton />
</View>
<View
pointerEvents = 'box-none'
style = { styles.bottomContainer }>
<Text style = { styles.videoStoppedLabel }>
{t('carmode.labels.videoStopped')}
</Text>
<SoundDeviceButton />
<EndMeetingButton />
</View>
</JitsiScreen>
);
};
export default withSafeAreaInsets(CarmodeTab);
export default withSafeAreaInsets(CarMode);

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Text, View } from 'react-native';
import EndMeetingButton from './EndMeetingButton';
import SoundDeviceButton from './SoundDeviceButton';
import styles from './styles';
/**
* Implements the car mode footer component.
*
* @returns { JSX.Element} - The car mode footer component.
*/
const CarModeFooter = (): JSX.Element => {
const { t } = useTranslation();
return (
<View
pointerEvents = 'box-none'
style = { styles.bottomContainer }>
<Text style = { styles.videoStoppedLabel }>
{t('carmode.labels.videoStopped')}
</Text>
<SoundDeviceButton />
<EndMeetingButton />
</View>
);
};
export default CarModeFooter;

View File

@@ -1,10 +1,10 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from 'react-native-paper';
import { useDispatch } from 'react-redux';
import { createToolbarEvent, sendAnalytics } from '../../../../analytics';
import { appNavigate } from '../../../../app/actions';
import Button from '../../../../base/react/components/native/Button';
import { BUTTON_TYPES } from '../../../../base/react/constants';
import EndMeetingIcon from './EndMeetingIcon';
import styles from './styles';
@@ -15,7 +15,6 @@ import styles from './styles';
* @returns {JSX.Element} - The end meeting button.
*/
const EndMeetingButton = () : JSX.Element => {
const { t } = useTranslation();
const dispatch = useDispatch();
const onSelect = useCallback(() => {
@@ -26,13 +25,12 @@ const EndMeetingButton = () : JSX.Element => {
return (
<Button
accessibilityLabel = { t('carmode.actions.leaveMeeting') }
children = { t('carmode.actions.leaveMeeting') }
accessibilityLabel = 'carmode.actions.leaveMeeting'
icon = { EndMeetingIcon }
labelStyle = { styles.endMeetingButtonLabel }
mode = 'contained'
label = 'carmode.actions.leaveMeeting'
onPress = { onSelect }
style = { styles.endMeetingButton } />
style = { styles.endMeetingButton }
type = { BUTTON_TYPES.DESTRUCTIVE } />
);
};

View File

@@ -1,9 +1,9 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Button } from 'react-native-paper';
import { useDispatch } from 'react-redux';
import { openSheet } from '../../../../base/dialog/actions';
import Button from '../../../../base/react/components/native/Button';
import { BUTTON_TYPES } from '../../../../base/react/constants';
import AudioRoutePickerDialog from '../../../../mobile/audio-mode/components/AudioRoutePickerDialog';
import AudioIcon from './AudioIcon';
@@ -15,7 +15,6 @@ import styles from './styles';
* @returns {JSX.Element} - The sound device button.
*/
const SelectSoundDevice = () : JSX.Element => {
const { t } = useTranslation();
const dispatch = useDispatch();
const onSelect = useCallback(() =>
@@ -24,13 +23,12 @@ const SelectSoundDevice = () : JSX.Element => {
return (
<Button
accessibilityLabel = { t('carmode.actions.selectSoundDevice') }
children = { t('carmode.actions.selectSoundDevice') }
accessibilityLabel = 'carmode.actions.selectSoundDevice'
icon = { AudioIcon }
labelStyle = { styles.soundDeviceButtonLabel }
mode = 'contained'
label = 'carmode.actions.selectSoundDevice'
onPress = { onSelect }
style = { styles.soundDeviceButton } />
style = { styles.soundDeviceButton }
type = { BUTTON_TYPES.SECONDARY } />
);
};

View File

@@ -5,29 +5,6 @@ import BaseTheme from '../../../../base/ui/components/BaseTheme.native';
*/
const MICROPHONE_SIZE = 180;
/**
* Base button style.
*/
const baseButton = {
borderRadius: BaseTheme.shape.borderRadius,
height: BaseTheme.spacing[7],
marginTop: BaseTheme.spacing[3],
marginLeft: BaseTheme.spacing[10],
marginRight: BaseTheme.spacing[10],
display: 'flex',
justifyContent: 'space-around',
width: 300
};
/**
* Base label style.
*/
const baseLabel = {
display: 'flex',
fontSize: 16,
textTransform: 'capitalize'
};
/**
* The styles of the safe area view that contains the title bar.
*/
@@ -47,7 +24,7 @@ export default {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
bottom: 0,
bottom: BaseTheme.spacing[8],
left: 0,
right: 0,
position: 'absolute'
@@ -106,10 +83,8 @@ export default {
},
roomTimer: {
color: BaseTheme.palette.text01,
...BaseTheme.typography.bodyShortBold,
paddingHorizontal: 8,
paddingVertical: 6,
color: BaseTheme.palette.text01,
textAlign: 'center'
},
@@ -129,25 +104,13 @@ export default {
color: BaseTheme.palette.text02
},
soundDeviceButtonLabel: {
...baseLabel,
color: BaseTheme.palette.text06
},
soundDeviceButton: {
...baseButton,
backgroundColor: BaseTheme.palette.section01
marginBottom: BaseTheme.spacing[3],
width: 240
},
endMeetingButton: {
...baseButton,
backgroundColor: BaseTheme.palette.actionDanger,
marginBottom: 60
},
endMeetingButtonLabel: {
...baseLabel,
color: BaseTheme.palette.text01
width: 240
},
headerLabels: {
@@ -196,13 +159,14 @@ export default {
},
titleBar: {
alignSelf: 'center'
alignSelf: 'center',
marginTop: BaseTheme.spacing[1]
},
videoStoppedLabel: {
...BaseTheme.typography.bodyShortRegularLarge,
color: BaseTheme.palette.text01,
marginBottom: 32,
...BaseTheme.typography.bodyShortRegularLarge
marginBottom: BaseTheme.spacing[3]
},
connectionIndicatorIcon: {

View File

@@ -41,14 +41,20 @@ export default {
width: BaseTheme.spacing[6]
},
headerNavigationIcon: {
marginLeft: 12
},
headerNavigationText: {
color: BaseTheme.palette.text01,
marginLeft: BaseTheme.spacing[3],
fontSize: HEADER_ACTION_BUTTON_SIZE
},
headerNavigationTextBold: {
...BaseTheme.typography.labelButton,
color: BaseTheme.palette.text01,
marginRight: BaseTheme.spacing[3],
fontSize: HEADER_ACTION_BUTTON_SIZE
},
@@ -79,18 +85,8 @@ export default {
},
lonelyButton: {
alignItems: 'center',
backgroundColor: BaseTheme.palette.action01,
borderRadius: 24,
flexDirection: 'row',
height: BaseTheme.spacing[6],
justifyContent: 'space-around',
paddingHorizontal: 12
},
lonelyButtonComponents: {
color: BaseTheme.palette.text01,
marginHorizontal: 6
borderRadius: BaseTheme.spacing[4],
paddingHorizontal: BaseTheme.spacing[1]
},
lonelyMeetingContainer: {
@@ -170,7 +166,7 @@ export default {
},
roomTimerView: {
backgroundColor: BaseTheme.palette.action02,
backgroundColor: BaseTheme.palette.ui03,
borderRadius: 3,
justifyContent: 'center',
minWidth: 50

View File

@@ -11,7 +11,7 @@ import { translate } from '../../../base/i18n';
import { connect as reactReduxConnect } from '../../../base/redux';
import { setColorAlpha } from '../../../base/util';
import { Chat } from '../../../chat';
import { MainFilmstrip, StageFilmstrip } from '../../../filmstrip';
import { MainFilmstrip, StageFilmstrip, ScreenshareFilmstrip } from '../../../filmstrip';
import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { LobbyScreen } from '../../../lobby';
@@ -239,6 +239,7 @@ class Conference extends AbstractConference<Props, *> {
{
_showPrejoin || _showLobby || (<>
<StageFilmstrip />
<ScreenshareFilmstrip />
<MainFilmstrip />
</>)
}

View File

@@ -20,6 +20,7 @@ import InsecureRoomNameLabel from './InsecureRoomNameLabel';
import ParticipantsCount from './ParticipantsCount';
import RaisedHandsCountLabel from './RaisedHandsCountLabel';
import SubjectText from './SubjectText';
import ToggleTopPanelLabel from './ToggleTopPanelLabel';
/**
* The type of the React {@code Component} props of {@link Subject}.
@@ -82,6 +83,10 @@ const COMPONENTS = [
{
Component: InsecureRoomNameLabel,
id: 'insecure-room'
},
{
Component: ToggleTopPanelLabel,
id: 'top-panel-toggle'
}
];

View File

@@ -0,0 +1,31 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
// @ts-ignore
import { IconMenuDown } from '../../../base/icons';
// @ts-ignore
import { Label } from '../../../base/label';
// @ts-ignore
import { Tooltip } from '../../../base/tooltip';
// @ts-ignore
import { setTopPanelVisible } from '../../../filmstrip/actions.web';
const ToggleTopPanelLabel = () => {
const dispatch = useDispatch();
const { t } = useTranslation();
const topPanelHidden = !useSelector((state: any) => state['features/filmstrip'].topPanelVisible);
const onClick = useCallback(() => {
dispatch(setTopPanelVisible(true));
}, []);
return topPanelHidden && (<Tooltip
content={t('toggleTopPanelLabel') }
position = { 'bottom' }>
<Label
icon={IconMenuDown}
onClick = { onClick }/>
</Tooltip>);
};
export default ToggleTopPanelLabel;

View File

@@ -22,7 +22,7 @@ export function fetchCustomBrandingData() {
const { customizationReady } = state['features/dynamic-branding'];
if (!customizationReady) {
const url = await getDynamicBrandingUrl();
const url = await getDynamicBrandingUrl(state);
if (url) {
try {

View File

@@ -6,6 +6,7 @@ import {
setDynamicBrandingFailed,
setDynamicBrandingReady
} from './actions.any';
import { getDynamicBrandingUrl } from './functions.any';
import logger from './logger';
@@ -19,7 +20,7 @@ import logger from './logger';
export function fetchCustomBrandingData() {
return async function(dispatch: Function, getState: Function) {
const state = getState();
const { dynamicBrandingUrl } = state['features/base/config'];
const dynamicBrandingUrl = await getDynamicBrandingUrl(state);
if (dynamicBrandingUrl) {
try {

View File

@@ -1,6 +1,7 @@
// @flow
import { loadConfig } from '../base/lib-jitsi-meet/functions';
import { toState } from '../base/redux';
/**
* Extracts the fqn part from a path, where fqn represents
@@ -29,10 +30,13 @@ export function extractFqnFromPath(state?: Object) {
/**
* Returns the url used for fetching dynamic branding.
*
* @param {Object | Function} stateful - The redux store, state, or
* {@code getState} function.
* @returns {string}
*/
export async function getDynamicBrandingUrl() {
const config = await loadConfig(window.location.href);
export async function getDynamicBrandingUrl(stateful: Object | Function) {
const state = toState(stateful);
const config = state['features/base/config'];
const { dynamicBrandingUrl } = config;
if (dynamicBrandingUrl) {
@@ -40,7 +44,7 @@ export async function getDynamicBrandingUrl() {
}
const { brandingDataUrl: baseUrl } = config;
const fqn = extractFqnFromPath();
const fqn = extractFqnFromPath(state);
if (baseUrl && fqn) {
return `${baseUrl}?conferenceFqn=${encodeURIComponent(fqn)}`;

View File

@@ -3,13 +3,9 @@ import { View } from 'react-native';
import { WebView } from 'react-native-webview';
import { translate } from '../../../base/i18n';
import { IconArrowBack } from '../../../base/icons';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { LoadingIndicator } from '../../../base/react';
import { connect } from '../../../base/redux';
import HeaderNavigationButton
from '../../../mobile/navigation/components/HeaderNavigationButton';
import { goBack } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { getSharedDocumentUrl } from '../../functions';
import styles, { INDICATOR_COLOR } from './styles';
@@ -50,25 +46,6 @@ class SharedDocument extends PureComponent<Props> {
this._renderLoading = this._renderLoading.bind(this);
}
/**
* Implements React's {@link Component#componentDidMount()}. Invoked
* immediately after this component is mounted.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
const { navigation } = this.props;
navigation.setOptions({
headerLeft: () => (
<HeaderNavigationButton
onPress = { goBack }
src = { IconArrowBack } />
)
});
}
/**
* Implements React's {@link Component#render()}.
*
@@ -82,9 +59,11 @@ class SharedDocument extends PureComponent<Props> {
addHeaderHeightValue = { true }
style = { styles.sharedDocContainer }>
<WebView
hideKeyboardAccessoryView = { true }
renderLoading = { this._renderLoading }
source = {{ uri: _documentUrl }}
startInLoadingState = { true } />
startInLoadingState = { true }
style = { styles.sharedDoc } />
</JitsiScreen>
);
}

View File

@@ -1,22 +1,29 @@
// @flow
import { ColorPalette } from '../../../base/styles';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
export const INDICATOR_COLOR = ColorPalette.lightGrey;
export const INDICATOR_COLOR = BaseTheme.palette.indicatorColor;
export default {
indicatorWrapper: {
alignItems: 'center',
backgroundColor: ColorPalette.white,
backgroundColor: BaseTheme.palette.ui12,
height: '100%',
justifyContent: 'center'
},
sharedDocContainer: {
flex: 1
backgroundColor: BaseTheme.palette.ui12,
flex: 1,
paddingRight: BaseTheme.spacing[3]
},
sharedDoc: {
marginBottom: BaseTheme.spacing[3]
},
webView: {
backgroundColor: 'rgb(242, 242, 242)'
backgroundColor: BaseTheme.palette.ui12
}
};

View File

@@ -23,7 +23,7 @@ StateListenerRegistry.register(
const localParticipant = getLocalParticipant(store.getState());
const { defaultLocalDisplayName } = store.getState()['features/base/config'];
// Initial setting of the display name occurs happens on app
// Initial setting of the display name happens on app
// initialization, before the local participant is ready. The initial
// settings is not desired to be fired anyways, only changes.
if (localParticipant) {
@@ -39,6 +39,23 @@ StateListenerRegistry.register(
}
});
StateListenerRegistry.register(
/* selector */ state => state['features/base/settings'].email,
/* listener */ (email, store) => {
const localParticipant = getLocalParticipant(store.getState());
// Initial setting of the email happens on app
// initialization, before the local participant is ready. The initial
// settings is not desired to be fired anyways, only changes.
if (localParticipant) {
const { id } = localParticipant;
APP.API.notifyEmailChanged(id, {
email
});
}
});
/**
* Updates the on stage participant value.
*/

View File

@@ -92,6 +92,15 @@ export const SET_VOLUME = 'SET_VOLUME';
*/
export const SET_VISIBLE_REMOTE_PARTICIPANTS = 'SET_VISIBLE_REMOTE_PARTICIPANTS';
/**
* The type of action which sets the height for the top panel filmstrip.
* {
* type: SET_FILMSTRIP_HEIGHT,
* height: number
* }
*/
export const SET_FILMSTRIP_HEIGHT = 'SET_FILMSTRIP_HEIGHT';
/**
* The type of action which sets the width for the vertical filmstrip.
* {
@@ -101,6 +110,15 @@ export const SET_VISIBLE_REMOTE_PARTICIPANTS = 'SET_VISIBLE_REMOTE_PARTICIPANTS'
*/
export const SET_FILMSTRIP_WIDTH = 'SET_FILMSTRIP_WIDTH';
/**
* The type of action which sets the height for the top panel filmstrip (user resized).
* {
* type: SET_USER_FILMSTRIP_HEIGHT,
* height: number
* }
*/
export const SET_USER_FILMSTRIP_HEIGHT = 'SET_USER_FILMSTRIP_HEIGHT';
/**
* The type of action which sets the width for the vertical filmstrip (user resized).
* {
@@ -187,3 +205,19 @@ export const TOGGLE_PIN_STAGE_PARTICIPANT = 'TOGGLE_PIN_STAGE_PARTICIPANT';
* }
*/
export const CLEAR_STAGE_PARTICIPANTS = 'CLEAR_STAGE_PARTICIPANTS';
/**
* The type of Redux action which sets the dimensions of the screenshare tile.
* {
* type: SET_SCREENSHARING_TILE_DIMENSIONS
* }
*/
export const SET_SCREENSHARING_TILE_DIMENSIONS = 'SET_SCREENSHARING_TILE_DIMENSIONS';
/**
* The type of Redux action which sets the visibility of the top panel.
* {
* type: SET_TOP_PANEL_VISIBILITY
* }
*/
export const SET_TOP_PANEL_VISIBILITY = 'SET_TOP_PANEL_VISIBILITY';

View File

@@ -25,7 +25,11 @@ import {
SET_VOLUME,
SET_MAX_STAGE_PARTICIPANTS,
TOGGLE_PIN_STAGE_PARTICIPANT,
CLEAR_STAGE_PARTICIPANTS
CLEAR_STAGE_PARTICIPANTS,
SET_SCREENSHARING_TILE_DIMENSIONS,
SET_USER_FILMSTRIP_HEIGHT,
SET_FILMSTRIP_HEIGHT,
SET_TOP_PANEL_VISIBILITY
} from './actionTypes';
import {
HORIZONTAL_FILMSTRIP_MARGIN,
@@ -33,11 +37,13 @@ import {
SCROLL_SIZE,
STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER,
TILE_HORIZONTAL_MARGIN,
TILE_MIN_HEIGHT_SMALL,
TILE_VERTICAL_CONTAINER_HORIZONTAL_MARGIN,
TILE_VERTICAL_MARGIN,
TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES,
TILE_VIEW_GRID_HORIZONTAL_MARGIN,
TILE_VIEW_GRID_VERTICAL_MARGIN,
TOP_FILMSTRIP_HEIGHT,
VERTICAL_FILMSTRIP_VERTICAL_MARGIN
} from './constants';
import {
@@ -48,6 +54,7 @@ import {
getNumberOfPartipantsForTileView,
getVerticalViewMaxWidth,
isFilmstripResizable,
isStageFilmstripTopPanel,
showGridInVerticalView
} from './functions';
import { isStageFilmstripAvailable } from './functions.web';
@@ -270,7 +277,7 @@ export function setStageFilmstripViewDimensions() {
const {
tileView = {}
} = state['features/base/config'];
const { visible } = state['features/filmstrip'];
const { visible, topPanelHeight } = state['features/filmstrip'];
const verticalWidth = visible ? getVerticalViewMaxWidth(state) : 0;
const { numberOfVisibleTiles = MAX_ACTIVE_PARTICIPANTS } = tileView;
const numberOfParticipants = state['features/filmstrip'].activeParticipants.length;
@@ -280,6 +287,7 @@ export function setStageFilmstripViewDimensions() {
disableResponsiveTiles: false,
disableTileEnlargement: false
});
const topPanel = isStageFilmstripTopPanel(state);
const {
height,
@@ -288,12 +296,13 @@ export function setStageFilmstripViewDimensions() {
rows
} = calculateResponsiveTileViewDimensions({
clientWidth: availableWidth,
clientHeight,
clientHeight: topPanel ? topPanelHeight?.current || TOP_FILMSTRIP_HEIGHT : clientHeight,
disableTileEnlargement: false,
maxColumns,
noHorizontalContainerMargin: verticalWidth > 0,
numberOfParticipants,
numberOfVisibleTiles
numberOfVisibleTiles,
minTileHeight: topPanel ? TILE_MIN_HEIGHT_SMALL : null
});
const thumbnailsTotalHeight = rows * (TILE_VERTICAL_MARGIN + height);
const hasScroll = clientHeight < thumbnailsTotalHeight;
@@ -368,6 +377,22 @@ export function setVolume(participantId: string, volume: number) {
};
}
/**
* Sets the top filmstrip's height.
*
* @param {number} height - The new height of the filmstrip.
* @returns {{
* type: SET_FILMSTRIP_HEIGHT,
* height: number
* }}
*/
export function setFilmstripHeight(height: number) {
return {
type: SET_FILMSTRIP_HEIGHT,
height
};
}
/**
* Sets the filmstrip's width.
*
@@ -384,6 +409,22 @@ export function setFilmstripWidth(width: number) {
};
}
/**
* Sets the filmstrip's height and the user preferred height.
*
* @param {number} height - The new height of the filmstrip.
* @returns {{
* type: SET_USER_FILMSTRIP_WIDTH,
* height: number
* }}
*/
export function setUserFilmstripHeight(height: number) {
return {
type: SET_USER_FILMSTRIP_HEIGHT,
height
};
}
/**
* Sets the filmstrip's width and the user preferred width.
*
@@ -490,3 +531,45 @@ export function clearStageParticipants() {
type: CLEAR_STAGE_PARTICIPANTS
};
}
/**
* Set the screensharing tile dimensions.
*
* @returns {Object}
*/
export function setScreensharingTileDimensions() {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const { visible, topPanelHeight, topPanelVisible } = state['features/filmstrip'];
const verticalWidth = visible ? getVerticalViewMaxWidth(state) : 0;
const availableWidth = clientWidth - verticalWidth;
const topPanel = isStageFilmstripTopPanel(state) && topPanelVisible;
const availableHeight = clientHeight - (topPanel ? topPanelHeight?.current || TOP_FILMSTRIP_HEIGHT : 0);
dispatch({
type: SET_SCREENSHARING_TILE_DIMENSIONS,
dimensions: {
filmstripHeight: availableHeight,
filmstripWidth: availableWidth,
thumbnailSize: {
width: availableWidth - TILE_HORIZONTAL_MARGIN,
height: availableHeight - TILE_VERTICAL_MARGIN
}
}
});
};
}
/**
* Sets the visibility of the top panel.
*
* @param {boolean} visible - Whether it should be visible or not.
* @returns {Object}
*/
export function setTopPanelVisible(visible) {
return {
type: SET_TOP_PANEL_VISIBILITY,
visible
};
}

View File

@@ -23,20 +23,26 @@ import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.we
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import {
setFilmstripVisible,
setVisibleRemoteParticipants,
setUserFilmstripHeight,
setUserFilmstripWidth,
setUserIsResizing
setUserIsResizing,
setTopPanelVisible,
setVisibleRemoteParticipants
} from '../../actions';
import {
ASPECT_RATIO_BREAKPOINT,
DEFAULT_FILMSTRIP_WIDTH,
FILMSTRIP_TYPE,
MIN_STAGE_VIEW_HEIGHT,
MIN_STAGE_VIEW_WIDTH,
TILE_HORIZONTAL_MARGIN,
TILE_VERTICAL_MARGIN
TILE_VERTICAL_MARGIN,
TOP_FILMSTRIP_HEIGHT
} from '../../constants';
import {
getVerticalViewMaxWidth,
shouldRemoteVideosBeVisible
shouldRemoteVideosBeVisible,
isStageFilmstripTopPanel
} from '../../functions';
import AudioTracksContainer from './AudioTracksContainer';
@@ -112,11 +118,21 @@ type Props = {
*/
_localScreenShare: Object,
/**
* Whether or not the filmstrip videos should currently be displayed.
*/
_mainFilmstripVisible: boolean,
/**
* The maximum width of the vertical filmstrip.
*/
_maxFilmstripWidth: number,
/**
* The maximum height of the top panel.
*/
_maxTopPanelHeight: number,
/**
* The participants in the call.
*/
@@ -137,11 +153,6 @@ type Props = {
*/
_rows: number,
/**
* Whether or not this is the stage filmstrip.
*/
_stageFilmstrip: boolean,
/**
* The height of the thumbnail.
*/
@@ -157,6 +168,26 @@ type Props = {
*/
_thumbnailsReordered: Boolean,
/**
* Whether or not the filmstrip is top panel.
*/
_topPanelFilmstrip: boolean,
/**
* The max height of the top panel.
*/
_topPanelMaxHeight: number,
/**
* The height of the top panel (user resized).
*/
_topPanelHeight: ?number,
/**
* Whether or not the top panel is visible.
*/
_topPanelVisible: boolean,
/**
* The width of the vertical filmstrip (user resized).
*/
@@ -182,11 +213,6 @@ type Props = {
*/
_videosClassName: string,
/**
* Whether or not the filmstrip videos should currently be displayed.
*/
_visible: boolean,
/**
* An object containing the CSS classes.
*/
@@ -197,6 +223,11 @@ type Props = {
*/
dispatch: Dispatch<any>,
/**
* The type of filmstrip to be displayed.
*/
filmstripType: string,
/**
* Invoked to obtain translated strings.
*/
@@ -218,7 +249,12 @@ type State = {
/**
* Initial filmstrip width on drag handle mouse down.
*/
dragFilmstripWidth: ?number
dragFilmstripWidth: ?number,
/**
* Initial top panel height on drag handle mouse down.
*/
dragFilmstripHeight: ?number
}
/**
@@ -307,25 +343,45 @@ class Filmstrip extends PureComponent <Props, State> {
_currentLayout,
_disableSelfView,
_localScreenShare,
_mainFilmstripVisible,
_resizableFilmstrip,
_stageFilmstrip,
_visible,
_topPanelFilmstrip,
_topPanelMaxHeight,
_topPanelVisible,
_verticalViewBackground,
_verticalViewGrid,
_verticalViewMaxWidth,
classes
classes,
filmstripType
} = this.props;
const { isMouseDown } = this.state;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
if (_currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && _stageFilmstrip) {
if (_visible) {
if (_currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && filmstripType === FILMSTRIP_TYPE.STAGE) {
if (_topPanelFilmstrip) {
filmstripStyle.maxHeight = `${_topPanelMaxHeight}px`;
filmstripStyle.zIndex = 1;
if (!_topPanelVisible) {
filmstripStyle.top = `-${_topPanelMaxHeight}px`;
}
}
if (_mainFilmstripVisible) {
filmstripStyle.maxWidth = `calc(100% - ${_verticalViewMaxWidth}px)`;
}
} else if (_currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && filmstripType === FILMSTRIP_TYPE.SCREENSHARE) {
if (_mainFilmstripVisible) {
filmstripStyle.maxWidth = `calc(100% - ${_verticalViewMaxWidth}px)`;
}
if (_topPanelVisible) {
filmstripStyle.maxHeight = `calc(100% - ${_topPanelMaxHeight}px)`;
}
filmstripStyle.bottom = 0;
filmstripStyle.top = 'auto';
} else if (_currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
|| (_currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && !_stageFilmstrip)) {
|| (_currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && filmstripType === FILMSTRIP_TYPE.MAIN)) {
filmstripStyle.maxWidth = _verticalViewMaxWidth;
if (!_visible) {
if (!_mainFilmstripVisible) {
filmstripStyle.right = `-${filmstripStyle.maxWidth}px`;
}
}
@@ -333,14 +389,17 @@ class Filmstrip extends PureComponent <Props, State> {
let toolbar = null;
if (!this.props._iAmRecorder && this.props._isFilmstripButtonEnabled
&& _currentLayout !== LAYOUTS.TILE_VIEW && !_stageFilmstrip) {
&& _currentLayout !== LAYOUTS.TILE_VIEW && (filmstripType === FILMSTRIP_TYPE.MAIN
|| (filmstripType === FILMSTRIP_TYPE.STAGE && _topPanelFilmstrip))) {
toolbar = this._renderToggleButton();
}
const filmstrip = (<>
<div
className = { clsx(this.props._videosClassName,
!tileViewActive && !_stageFilmstrip && !_resizableFilmstrip && 'filmstrip-hover',
!tileViewActive && (filmstripType === FILMSTRIP_TYPE.MAIN
|| (filmstripType === FILMSTRIP_TYPE.STAGE && _topPanelFilmstrip))
&& !_resizableFilmstrip && 'filmstrip-hover',
_verticalViewGrid && 'vertical-view-grid') }
id = 'remoteVideos'>
{!_disableSelfView && !_verticalViewGrid && (
@@ -348,8 +407,10 @@ class Filmstrip extends PureComponent <Props, State> {
className = 'filmstrip__videos'
id = 'filmstripLocalVideo'>
{
!tileViewActive && !_stageFilmstrip && <div id = 'filmstripLocalVideoThumbnail'>
!tileViewActive && filmstripType === FILMSTRIP_TYPE.MAIN
&& <div id = 'filmstripLocalVideoThumbnail'>
<Thumbnail
filmstripType = { FILMSTRIP_TYPE.MAIN }
key = 'local' />
</div>
}
@@ -361,10 +422,9 @@ class Filmstrip extends PureComponent <Props, State> {
id = 'filmstripLocalScreenShare'>
<div id = 'filmstripLocalScreenShareThumbnail'>
{
!tileViewActive && !_stageFilmstrip && <Thumbnail
!tileViewActive && filmstripType === FILMSTRIP_TYPE.MAIN && <Thumbnail
key = 'localScreenShare'
participantID = { _localScreenShare.id } />
}
</div>
</div>
@@ -385,11 +445,14 @@ class Filmstrip extends PureComponent <Props, State> {
style = { filmstripStyle }>
{ toolbar }
{_resizableFilmstrip
? <div className = { clsx('resizable-filmstrip', classes.resizableFilmstripContainer) }>
? <div
className = { clsx('resizable-filmstrip', classes.resizableFilmstripContainer,
_topPanelFilmstrip && 'top-panel-filmstrip') }>
<div
className = { clsx('dragHandleContainer',
classes.dragHandleContainer,
isMouseDown && 'visible')
isMouseDown && 'visible',
_topPanelFilmstrip && 'top-panel')
}
onMouseDown = { this._onDragHandleMouseDown }>
<div className = { clsx(classes.dragHandle, 'dragHandle') } />
@@ -412,10 +475,13 @@ class Filmstrip extends PureComponent <Props, State> {
* @returns {void}
*/
_onDragHandleMouseDown(e) {
const { _topPanelFilmstrip, _topPanelHeight, _verticalFilmstripWidth } = this.props;
this.setState({
isMouseDown: true,
mousePosition: e.clientX,
dragFilmstripWidth: this.props._verticalFilmstripWidth || DEFAULT_FILMSTRIP_WIDTH
mousePosition: _topPanelFilmstrip ? e.clientY : e.clientX,
dragFilmstripWidth: _verticalFilmstripWidth || DEFAULT_FILMSTRIP_WIDTH,
dragFilmstripHeight: _topPanelHeight || TOP_FILMSTRIP_HEIGHT
});
this.props.dispatch(setUserIsResizing(true));
}
@@ -446,16 +512,36 @@ class Filmstrip extends PureComponent <Props, State> {
*/
_onFilmstripResize(e) {
if (this.state.isMouseDown) {
const { dispatch, _verticalFilmstripWidth, _maxFilmstripWidth } = this.props;
const { dragFilmstripWidth, mousePosition } = this.state;
const diff = mousePosition - e.clientX;
const width = Math.max(
Math.min(dragFilmstripWidth + diff, _maxFilmstripWidth),
DEFAULT_FILMSTRIP_WIDTH
);
const {
dispatch,
_verticalFilmstripWidth,
_maxFilmstripWidth,
_topPanelHeight,
_maxTopPanelHeight,
_topPanelFilmstrip
} = this.props;
const { dragFilmstripWidth, dragFilmstripHeight, mousePosition } = this.state;
if (width !== _verticalFilmstripWidth) {
dispatch(setUserFilmstripWidth(width));
if (_topPanelFilmstrip) {
const diff = e.clientY - mousePosition;
const height = Math.max(
Math.min(dragFilmstripHeight + diff, _maxTopPanelHeight),
TOP_FILMSTRIP_HEIGHT
);
if (height !== _topPanelHeight) {
dispatch(setUserFilmstripHeight(height));
}
} else {
const diff = mousePosition - e.clientX;
const width = Math.max(
Math.min(dragFilmstripWidth + diff, _maxFilmstripWidth),
DEFAULT_FILMSTRIP_WIDTH
);
if (width !== _verticalFilmstripWidth) {
dispatch(setUserFilmstripWidth(width));
}
}
}
}
@@ -495,7 +581,7 @@ class Filmstrip extends PureComponent <Props, State> {
* @returns {void}
*/
_onTabIn() {
if (!this.props._isToolboxVisible && this.props._visible) {
if (!this.props._isToolboxVisible && this.props._mainFilmstripVisible) {
this.props.dispatch(showToolbox());
}
}
@@ -605,10 +691,10 @@ class Filmstrip extends PureComponent <Props, State> {
_remoteParticipantsLength,
_resizableFilmstrip,
_rows,
_stageFilmstrip,
_thumbnailHeight,
_thumbnailWidth,
_verticalViewGrid
_verticalViewGrid,
filmstripType
} = this.props;
if (!_thumbnailWidth || isNaN(_thumbnailWidth) || !_thumbnailHeight
@@ -617,7 +703,7 @@ class Filmstrip extends PureComponent <Props, State> {
return null;
}
if (_currentLayout === LAYOUTS.TILE_VIEW || _verticalViewGrid || _stageFilmstrip) {
if (_currentLayout === LAYOUTS.TILE_VIEW || _verticalViewGrid || filmstripType !== FILMSTRIP_TYPE.MAIN) {
return (
<FixedSizeGrid
className = 'filmstrip__videos remote-videos'
@@ -626,7 +712,7 @@ class Filmstrip extends PureComponent <Props, State> {
height = { _filmstripHeight }
initialScrollLeft = { 0 }
initialScrollTop = { 0 }
itemData = {{ stageFilmstrip: _stageFilmstrip }}
itemData = {{ filmstripType }}
itemKey = { this._gridItemKey }
onItemsRendered = { this._onGridItemsRendered }
overscanRowCount = { 1 }
@@ -694,7 +780,11 @@ class Filmstrip extends PureComponent <Props, State> {
* @returns {void}
*/
_doToggleFilmstrip() {
this.props.dispatch(setFilmstripVisible(!this.props._visible));
const { dispatch, _mainFilmstripVisible, _topPanelFilmstrip, _topPanelVisible } = this.props;
_topPanelFilmstrip
? dispatch(setTopPanelVisible(!_topPanelVisible))
: dispatch(setFilmstripVisible(!_mainFilmstripVisible));
}
_onShortcutToggleFilmstrip: () => void;
@@ -710,7 +800,7 @@ class Filmstrip extends PureComponent <Props, State> {
sendAnalytics(createShortcutEvent(
'toggle.filmstrip',
{
enable: this.props._visible
enable: this.props._mainFilmstripVisible
}));
this._doToggleFilmstrip();
@@ -729,7 +819,7 @@ class Filmstrip extends PureComponent <Props, State> {
sendAnalytics(createToolbarEvent(
'toggle.filmstrip.button',
{
enable: this.props._visible
enable: this.props._mainFilmstripVisible
}));
this._doToggleFilmstrip();
@@ -758,8 +848,15 @@ class Filmstrip extends PureComponent <Props, State> {
* @returns {ReactElement}
*/
_renderToggleButton() {
const icon = this.props._visible ? IconMenuDown : IconMenuUp;
const { t, classes, _isVerticalFilmstrip } = this.props;
const {
t,
classes,
_isVerticalFilmstrip,
_mainFilmstripVisible,
_topPanelFilmstrip,
_topPanelVisible
} = this.props;
const icon = (_topPanelFilmstrip ? _topPanelVisible : _mainFilmstripVisible) ? IconMenuDown : IconMenuUp;
const actions = isMobileBrowser()
? { onTouchStart: this._onToggleButtonTouch }
: { onClick: this._onToolbarToggleFilmstrip };
@@ -768,9 +865,11 @@ class Filmstrip extends PureComponent <Props, State> {
<div
className = { clsx(classes.toggleFilmstripContainer,
_isVerticalFilmstrip && classes.toggleVerticalFilmstripContainer,
_topPanelFilmstrip && classes.toggleTopPanelContainer,
_topPanelFilmstrip && !_topPanelVisible && classes.toggleTopPanelContainerHidden,
'toggleFilmstripContainer') }>
<button
aria-expanded = { this.props._visible }
aria-expanded = { this.props._mainFilmstripVisible }
aria-label = { t('toolbar.accessibilityLabel.toggleFilmstrip') }
className = { classes.toggleFilmstripButton }
id = 'toggleFilmstripButton'
@@ -795,32 +894,38 @@ class Filmstrip extends PureComponent <Props, State> {
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const { _hasScroll = false } = ownProps;
const { _hasScroll = false, filmstripType, _topPanelFilmstrip, _remoteParticipants } = ownProps;
const toolbarButtons = getToolbarButtons(state);
const { testing = {}, iAmRecorder } = state['features/base/config'];
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
const { visible, width: verticalFilmstripWidth } = state['features/filmstrip'];
const { topPanelHeight, topPanelVisible, visible, width: verticalFilmstripWidth } = state['features/filmstrip'];
const { localScreenShare } = state['features/base/participants'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const { isOpen: shiftRight } = state['features/chat'];
const disableSelfView = shouldHideSelfView(state);
const { clientWidth } = state['features/base/responsive-ui'];
const { clientWidth, clientHeight } = state['features/base/responsive-ui'];
const collapseTileView = reduceHeight
&& isMobileBrowser()
&& clientWidth <= ASPECT_RATIO_BREAKPOINT;
const shouldReduceHeight = reduceHeight && isMobileBrowser();
const _topPanelVisible = isStageFilmstripTopPanel(state) && topPanelVisible;
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}${_hasScroll ? ' has-scroll' : ''}`;
let isVisible = visible || filmstripType !== FILMSTRIP_TYPE.MAIN;
if (_topPanelFilmstrip) {
isVisible = _topPanelVisible;
}
const videosClassName = `filmstrip__videos${isVisible ? '' : ' hidden'}${_hasScroll ? ' has-scroll' : ''}`;
const className = `${remoteVideosVisible || ownProps._verticalViewGrid ? '' : 'hide-videos'} ${
shouldReduceHeight ? 'reduce-height' : ''
} ${shiftRight ? 'shift-right' : ''} ${collapseTileView ? 'collapse' : ''} ${visible ? '' : 'hidden'}`.trim();
} ${shiftRight ? 'shift-right' : ''} ${collapseTileView ? 'collapse' : ''} ${isVisible ? '' : 'hidden'}`.trim();
const _currentLayout = getCurrentLayout(state);
const _isVerticalFilmstrip = _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
|| (!ownProps._stageFilmstrip && _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW);
|| (filmstripType === FILMSTRIP_TYPE.MAIN && _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW);
return {
_className: className,
@@ -833,8 +938,14 @@ function _mapStateToProps(state, ownProps) {
_isToolboxVisible: isToolboxVisible(state),
_isVerticalFilmstrip,
_localScreenShare: getSourceNameSignalingFeatureFlag(state) && localScreenShare,
_mainFilmstripVisible: visible,
_maxFilmstripWidth: clientWidth - MIN_STAGE_VIEW_WIDTH,
_maxTopPanelHeight: clientHeight - MIN_STAGE_VIEW_HEIGHT,
_remoteParticipantsLength: _remoteParticipants.length,
_thumbnailsReordered: enableThumbnailReordering,
_topPanelHeight: topPanelHeight.current,
_topPanelMaxHeight: topPanelHeight.current || TOP_FILMSTRIP_HEIGHT,
_topPanelVisible,
_verticalFilmstripWidth: verticalFilmstripWidth.current,
_verticalViewMaxWidth: getVerticalViewMaxWidth(state),
_videosClassName: videosClassName

View File

@@ -9,6 +9,7 @@ import {
ASPECT_RATIO_BREAKPOINT,
FILMSTRIP_BREAKPOINT,
FILMSTRIP_BREAKPOINT_OFFSET,
FILMSTRIP_TYPE,
TOOLBAR_HEIGHT,
TOOLBAR_HEIGHT_MOBILE } from '../../constants';
import { isFilmstripResizable, showGridInVerticalView } from '../../functions.web';
@@ -85,15 +86,16 @@ type Props = {
/**
* Additional CSS class names to add to the container of all the thumbnails.
*/
_videosClassName: string,
/**
* Whether or not the filmstrip videos should currently be displayed.
*/
_visible: boolean
_videosClassName: string
};
const MainFilmstrip = (props: Props) => <span><Filmstrip { ...props } /></span>;
const MainFilmstrip = (props: Props) => (
<span>
<Filmstrip
{ ...props }
filmstripType = { FILMSTRIP_TYPE.MAIN } />
</span>
);
/**
* Maps (parts of) the Redux state to the associated {@code Filmstrip}'s props.
@@ -104,7 +106,7 @@ const MainFilmstrip = (props: Props) => <span><Filmstrip { ...props } /></span>;
*/
function _mapStateToProps(state) {
const toolbarButtons = getToolbarButtons(state);
const { visible, remoteParticipants, width: verticalFilmstripWidth } = state['features/filmstrip'];
const { remoteParticipants, width: verticalFilmstripWidth } = state['features/filmstrip'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const {
gridDimensions: dimensions = {},
@@ -189,13 +191,11 @@ function _mapStateToProps(state) {
_filmstripHeight: remoteFilmstripHeight,
_filmstripWidth: remoteFilmstripWidth,
_hasScroll,
_remoteParticipantsLength: remoteParticipants.length,
_remoteParticipants: remoteParticipants,
_resizableFilmstrip,
_rows: gridDimensions.rows,
_thumbnailWidth: _thumbnailSize?.width,
_thumbnailHeight: _thumbnailSize?.height,
_visible: visible,
_verticalViewGrid,
_verticalViewBackground: verticalFilmstripWidth.current + FILMSTRIP_BREAKPOINT_OFFSET >= FILMSTRIP_BREAKPOINT
};

View File

@@ -0,0 +1,125 @@
// @flow
import React from 'react';
import { connect } from '../../../base/redux';
import { LAYOUT_CLASSNAMES } from '../../../conference/components/web/Conference';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import {
FILMSTRIP_TYPE
} from '../../constants';
import Filmstrip from './Filmstrip';
type Props = {
/**
* The current layout of the filmstrip.
*/
_currentLayout: string,
/**
* The number of columns in tile view.
*/
_columns: number,
/**
* The width of the filmstrip.
*/
_filmstripWidth: number,
/**
* The height of the filmstrip.
*/
_filmstripHeight: number,
/**
* Whether or not the current layout is vertical filmstrip.
*/
_isVerticalFilmstrip: boolean,
/**
* The participants in the call.
*/
_remoteParticipants: Array<Object>,
/**
* The length of the remote participants array.
*/
_remoteParticipantsLength: number,
/**
* Whether or not the filmstrip should be user-resizable.
*/
_resizableFilmstrip: boolean,
/**
* The number of rows in tile view.
*/
_rows: number,
/**
* The height of the thumbnail.
*/
_thumbnailHeight: number,
/**
* The width of the thumbnail.
*/
_thumbnailWidth: number,
/**
* Whether or not the vertical filmstrip should have a background color.
*/
_verticalViewBackground: boolean,
/**
* Whether or not the vertical filmstrip should be displayed as grid.
*/
_verticalViewGrid: boolean,
/**
* Additional CSS class names to add to the container of all the thumbnails.
*/
_videosClassName: string
};
const ScreenshareFilmstrip = (props: Props) => props._currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW
&& props._remoteParticipants.length === 1 && (
<span className = { LAYOUT_CLASSNAMES[LAYOUTS.TILE_VIEW] }>
<Filmstrip
{ ...props }
filmstripType = { FILMSTRIP_TYPE.SCREENSHARE } />
</span>
);
/**
* Maps (parts of) the Redux state to the associated {@code Filmstrip}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {Props}
*/
function _mapStateToProps(state) {
const {
filmstripHeight,
filmstripWidth,
thumbnailSize
} = state['features/filmstrip'].screenshareFilmstripDimensions;
const screenshares = state['features/video-layout'].remoteScreenShares;
return {
_columns: 1,
_currentLayout: getCurrentLayout(state),
_filmstripHeight: filmstripHeight,
_filmstripWidth: filmstripWidth,
_remoteParticipants: screenshares.length ? [ screenshares[0] ] : [],
_resizableFilmstrip: false,
_rows: 1,
_thumbnailWidth: thumbnailSize?.width,
_thumbnailHeight: thumbnailSize?.height,
_verticalViewGrid: false,
_verticalViewBackground: false
};
}
export default connect(_mapStateToProps)(ScreenshareFilmstrip);

View File

@@ -8,9 +8,11 @@ import { LAYOUT_CLASSNAMES } from '../../../conference/components/web/Conference
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import {
ASPECT_RATIO_BREAKPOINT,
FILMSTRIP_TYPE,
TOOLBAR_HEIGHT_MOBILE
} from '../../constants';
import { getActiveParticipantsIds } from '../../functions';
import { isFilmstripResizable, isStageFilmstripTopPanel } from '../../functions.web';
import Filmstrip from './Filmstrip';
@@ -84,19 +86,14 @@ type Props = {
/**
* Additional CSS class names to add to the container of all the thumbnails.
*/
_videosClassName: string,
/**
* Whether or not the filmstrip videos should currently be displayed.
*/
_visible: boolean
_videosClassName: string
};
const StageFilmstrip = (props: Props) => props._currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && (
<span className = { LAYOUT_CLASSNAMES[LAYOUTS.TILE_VIEW] }>
<Filmstrip
{ ...props }
_stageFilmstrip = { true } />
filmstripType = { FILMSTRIP_TYPE.STAGE } />
</span>
);
@@ -109,7 +106,6 @@ const StageFilmstrip = (props: Props) => props._currentLayout === LAYOUTS.STAGE_
*/
function _mapStateToProps(state) {
const toolbarButtons = getToolbarButtons(state);
const { visible } = state['features/filmstrip'];
const activeParticipants = getActiveParticipantsIds(state);
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const {
@@ -139,19 +135,19 @@ function _mapStateToProps(state) {
&& clientWidth <= ASPECT_RATIO_BREAKPOINT;
const remoteFilmstripHeight = filmstripHeight - (collapseTileView && filmstripPadding > 0 ? filmstripPadding : 0);
const _topPanelFilmstrip = isStageFilmstripTopPanel(state);
return {
_columns: gridDimensions.columns,
_currentLayout: getCurrentLayout(state),
_filmstripHeight: remoteFilmstripHeight,
_filmstripWidth: filmstripWidth,
_remoteParticipantsLength: activeParticipants.length,
_remoteParticipants: activeParticipants,
_resizableFilmstrip: false,
_resizableFilmstrip: isFilmstripResizable(state) && _topPanelFilmstrip,
_rows: gridDimensions.rows,
_thumbnailWidth: thumbnailSize?.width,
_thumbnailHeight: thumbnailSize?.height,
_visible: visible,
_topPanelFilmstrip,
_verticalViewGrid: false,
_verticalViewBackground: false
};

View File

@@ -37,6 +37,7 @@ import { togglePinStageParticipant } from '../../actions';
import {
DISPLAY_MODE_TO_CLASS_NAME,
DISPLAY_VIDEO,
FILMSTRIP_TYPE,
SHOW_TOOLBAR_CONTEXT_MENU_AFTER,
THUMBNAIL_TYPE,
VIDEO_TEST_EVENTS
@@ -234,6 +235,11 @@ export type Props = {|
*/
dispatch: Function,
/**
* The type of filmstrip the tile is displayed in.
*/
filmstripType: string,
/**
* The horizontal offset in px for the thumbnail. Used to center the thumbnails from the last row in tile view.
*/
@@ -244,11 +250,6 @@ export type Props = {|
*/
participantID: ?string,
/**
* Whether the tile is displayed in the stage filmstrip or not.
*/
stageFilmstrip: boolean,
/**
* Styles that will be set to the Thumbnail's main span element.
*/
@@ -993,7 +994,7 @@ class Thumbnail extends Component<Props, State> {
_thumbnailType,
_videoTrack,
classes,
stageFilmstrip
filmstripType
} = this.props;
const { id } = _participant || {};
const { isHovered, popoverVisible } = this.state;
@@ -1031,8 +1032,8 @@ class Thumbnail extends Component<Props, State> {
<span
className = { containerClassName }
id = { local
? `localVideoContainer${stageFilmstrip ? '_stage' : ''}`
: `participant_${id}${stageFilmstrip ? '_stage' : ''}`
? `localVideoContainer${filmstripType === FILMSTRIP_TYPE.MAIN ? '' : `_${filmstripType}`}`
: `participant_${id}${filmstripType === FILMSTRIP_TYPE.MAIN ? '' : `_${filmstripType}`}`
}
{ ...(_isMobile
? {
@@ -1168,7 +1169,7 @@ class Thumbnail extends Component<Props, State> {
* @returns {Props}
*/
function _mapStateToProps(state, ownProps): Object {
const { participantID, stageFilmstrip } = ownProps;
const { participantID, filmstripType = FILMSTRIP_TYPE.MAIN } = ownProps;
const participant = getParticipantByIdOrUndefined(state, participantID);
const id = participant?.id;
@@ -1199,7 +1200,7 @@ function _mapStateToProps(state, ownProps): Object {
const { localFlipX } = state['features/base/settings'];
const _isMobile = isMobileBrowser();
const activeParticipants = getActiveParticipantsIds(state);
const tileType = getThumbnailTypeFromLayout(_currentLayout, stageFilmstrip);
const tileType = getThumbnailTypeFromLayout(_currentLayout, filmstripType);
switch (tileType) {
case THUMBNAIL_TYPE.VERTICAL:
@@ -1244,7 +1245,8 @@ function _mapStateToProps(state, ownProps): Object {
const {
stageFilmstripDimensions = {
thumbnailSize: {}
}
},
screenshareFilmstripDimensions
} = state['features/filmstrip'];
size = {
@@ -1252,9 +1254,16 @@ function _mapStateToProps(state, ownProps): Object {
_height: thumbnailSize?.height
};
if (stageFilmstrip) {
if (filmstripType === FILMSTRIP_TYPE.STAGE) {
const { width: _width, height: _height } = stageFilmstripDimensions.thumbnailSize;
size = {
_width,
_height
};
} else if (filmstripType === FILMSTRIP_TYPE.SCREENSHARE) {
const { width: _width, height: _height } = screenshareFilmstripDimensions.thumbnailSize;
size = {
_width,
_height

View File

@@ -7,7 +7,7 @@ import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { shouldHideSelfView } from '../../../base/settings/functions.any';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { TILE_ASPECT_RATIO, TILE_HORIZONTAL_MARGIN } from '../../constants';
import { TILE_ASPECT_RATIO, TILE_HORIZONTAL_MARGIN, FILMSTRIP_TYPE } from '../../constants';
import { showGridInVerticalView, getActiveParticipantsIds } from '../../functions';
import Thumbnail from './Thumbnail';
@@ -22,6 +22,11 @@ type Props = {
*/
_disableSelfView: boolean,
/**
* The type of filmstrip this thumbnail is displayed in.
*/
_filmstripType: string,
/**
* The horizontal offset in px for the thumbnail. Used to center the thumbnails in the last row in tile view.
*/
@@ -37,11 +42,6 @@ type Props = {
*/
_isLocalScreenShare: boolean,
/**
* Whether or not the filmstrip is used a stage filmstrip.
*/
_stageFilmstrip: boolean,
/**
* The width of the thumbnail. Used for expanding the width of the thumbnails on last row in case
* there is empty space.
@@ -97,10 +97,10 @@ class ThumbnailWrapper extends Component<Props> {
render() {
const {
_disableSelfView,
_filmstripType = FILMSTRIP_TYPE.MAIN,
_isLocalScreenShare = false,
_horizontalOffset = 0,
_participantID,
_stageFilmstrip,
_thumbnailWidth,
style
} = this.props;
@@ -112,9 +112,9 @@ class ThumbnailWrapper extends Component<Props> {
if (_participantID === 'local') {
return _disableSelfView ? null : (
<Thumbnail
filmstripType = { _filmstripType }
horizontalOffset = { _horizontalOffset }
key = 'local'
stageFilmstrip = { _stageFilmstrip }
style = { style }
width = { _thumbnailWidth } />);
}
@@ -122,20 +122,20 @@ class ThumbnailWrapper extends Component<Props> {
if (_isLocalScreenShare) {
return _disableSelfView ? null : (
<Thumbnail
filmstripType = { _filmstripType }
horizontalOffset = { _horizontalOffset }
key = 'localScreenShare'
participantID = { _participantID }
stageFilmstrip = { _stageFilmstrip }
style = { style }
width = { _thumbnailWidth } />);
}
return (
<Thumbnail
filmstripType = { _filmstripType }
horizontalOffset = { _horizontalOffset }
key = { `remote_${_participantID}` }
participantID = { _participantID }
stageFilmstrip = { _stageFilmstrip }
style = { style }
width = { _thumbnailWidth } />);
}
@@ -158,7 +158,8 @@ function _mapStateToProps(state, ownProps) {
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
const sourceNameSignalingEnabled = getSourceNameSignalingFeatureFlag(state);
const _verticalViewGrid = showGridInVerticalView(state);
const stageFilmstrip = ownProps.data?.stageFilmstrip;
const filmstripType = ownProps.data?.filmstripType;
const stageFilmstrip = filmstripType === FILMSTRIP_TYPE.STAGE;
const sortedActiveParticipants = activeParticipants.sort();
const remoteParticipants = stageFilmstrip ? sortedActiveParticipants : remote;
const remoteParticipantsLength = remoteParticipants.length;
@@ -235,9 +236,9 @@ function _mapStateToProps(state, ownProps) {
if (stageFilmstrip) {
return {
_disableSelfView: disableSelfView,
_filmstripType: filmstripType,
_participantID: remoteParticipants[index] === localId ? 'local' : remoteParticipants[index],
_horizontalOffset: horizontalOffset,
_stageFilmstrip: stageFilmstrip,
_thumbnailWidth: thumbnailWidth
};
}
@@ -260,6 +261,7 @@ function _mapStateToProps(state, ownProps) {
if (!iAmRecorder && index === localIndex) {
return {
_disableSelfView: disableSelfView,
_filmstripType: filmstripType,
_participantID: 'local',
_horizontalOffset: horizontalOffset,
_thumbnailWidth: thumbnailWidth
@@ -269,6 +271,7 @@ function _mapStateToProps(state, ownProps) {
if (sourceNameSignalingEnabled && !iAmRecorder && localScreenShare && index === localScreenShareIndex) {
return {
_disableSelfView: disableSelfView,
_filmstripType: filmstripType,
_isLocalScreenShare: true,
_participantID: localScreenShare?.id,
_horizontalOffset: horizontalOffset,
@@ -277,12 +280,22 @@ function _mapStateToProps(state, ownProps) {
}
return {
_filmstripType: filmstripType,
_participantID: remoteParticipants[remoteIndex],
_horizontalOffset: horizontalOffset,
_thumbnailWidth: thumbnailWidth
};
}
if (_currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW && filmstripType === FILMSTRIP_TYPE.SCREENSHARE) {
const { remoteScreenShares } = state['features/video-layout'];
return {
_filmstripType: filmstripType,
_participantID: remoteScreenShares[remoteScreenShares.length - 1]
};
}
const { index } = ownProps;
if (typeof index !== 'number' || remoteParticipantsLength <= index) {

View File

@@ -5,6 +5,7 @@ export { default as Filmstrip } from './Filmstrip';
export { default as MainFilmstrip } from './MainFilmstrip';
export { default as ModeratorIndicator } from './ModeratorIndicator';
export { default as RaisedHandIndicator } from './RaisedHandIndicator';
export { default as ScreenshareFilmstrip } from './ScreenshareFilmstrip';
export { default as StageFilmstrip } from './StageFilmstrip';
export { default as StatusIndicators } from './StatusIndicators';
export { default as Thumbnail } from './Thumbnail';

View File

@@ -23,6 +23,7 @@ export const styles = theme => {
left: 'calc(50% - 16px)',
opacity: 0,
transition: 'opacity .3s',
zIndex: 1,
'&:hover': {
backgroundColor: theme.palette.ui02
@@ -53,8 +54,18 @@ export const styles = theme => {
top: 'calc(50% - 12px)'
},
toggleTopPanelContainer: {
transform: 'rotate(180deg)',
bottom: 'calc(-24px - 6px)',
top: 'auto'
},
toggleTopPanelContainerHidden: {
visibility: 'hidden'
},
filmstrip: {
transition: 'background .2s ease-in-out, right 1s, bottom 1s, height .3s ease-in',
transition: 'background .2s ease-in-out, right 1s, bottom 1s, top 1s, height .3s ease-in',
right: 0,
bottom: 0,
@@ -111,6 +122,10 @@ export const styles = theme => {
'& .avatar-container': {
maxWidth: 'initial',
maxHeight: 'initial'
},
'&.top-panel-filmstrip': {
flexDirection: 'column'
}
},
@@ -137,6 +152,18 @@ export const styles = theme => {
'& .dragHandle': {
backgroundColor: theme.palette.icon01
}
},
'&.top-panel': {
order: 2,
width: '100%',
height: '9px',
cursor: 'row-resize',
'& .dragHandle': {
height: '3px',
width: '100px'
}
}
},

View File

@@ -281,6 +281,12 @@ export const FILMSTRIP_GRID_BREAKPOINT = 300;
*/
export const FILMSTRIP_BREAKPOINT_OFFSET = 5;
/**
* The minimum height for the stage view
* (used to determine the maximum height of the user-resizable top panel).
*/
export const MIN_STAGE_VIEW_HEIGHT = 700;
/**
* The minimum width for the stage view
* (used to determine the maximum width of the user-resizable vertical filmstrip).
@@ -298,7 +304,21 @@ export const VERTICAL_VIEW_HORIZONTAL_MARGIN = VERTICAL_FILMSTRIP_MIN_HORIZONTAL
*/
export const ACTIVE_PARTICIPANT_TIMEOUT = 1000 * 60;
/**
* The types of filmstrip.
*/
export const FILMSTRIP_TYPE = {
MAIN: 'main',
STAGE: 'stage',
SCREENSHARE: 'screenshare'
};
/**
* The max number of participants to be displayed on the stage filmstrip.
*/
export const MAX_ACTIVE_PARTICIPANTS = 6;
/**
* Top filmstrip default height.
*/
export const TOP_FILMSTRIP_HEIGHT = 180;

View File

@@ -6,6 +6,7 @@ import { MEDIA_TYPE } from '../base/media';
import {
getLocalParticipant,
getParticipantById,
getParticipantCount,
getParticipantCountWithFake,
getPinnedParticipant
} from '../base/participants';
@@ -32,6 +33,7 @@ import {
DISPLAY_AVATAR,
DISPLAY_VIDEO,
FILMSTRIP_GRID_BREAKPOINT,
FILMSTRIP_TYPE,
INDICATORS_TOOLTIP_POSITION,
SCROLL_SIZE,
SQUARE_TILE_ASPECT_RATIO,
@@ -296,7 +298,8 @@ export function calculateResponsiveTileViewDimensions({
noHorizontalContainerMargin = false,
maxColumns,
numberOfParticipants,
desiredNumberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES
desiredNumberOfVisibleTiles = TILE_VIEW_DEFAULT_NUMBER_OF_VISIBLE_TILES,
minTileHeight
}) {
let height, width;
let columns, rows;
@@ -324,7 +327,8 @@ export function calculateResponsiveTileViewDimensions({
clientHeight,
disableTileEnlargement,
disableResponsiveTiles: false,
noHorizontalContainerMargin
noHorizontalContainerMargin,
minTileHeight
});
if (size) {
@@ -413,10 +417,11 @@ export function calculateThumbnailSizeForTileView({
clientHeight,
disableResponsiveTiles = false,
disableTileEnlargement = false,
noHorizontalContainerMargin = false
noHorizontalContainerMargin = false,
minTileHeight
}: Object) {
const aspectRatio = getTileDefaultAspectRatio(disableResponsiveTiles, disableTileEnlargement, clientWidth);
const minHeight = getThumbnailMinHeight(clientWidth);
const minHeight = minTileHeight || getThumbnailMinHeight(clientWidth);
const viewWidth = clientWidth - (columns * TILE_HORIZONTAL_MARGIN)
- (noHorizontalContainerMargin ? SCROLL_SIZE : TILE_VIEW_GRID_HORIZONTAL_MARGIN);
const availableHeight = clientHeight - TILE_VIEW_GRID_VERTICAL_MARGIN;
@@ -506,6 +511,7 @@ export function getVerticalFilmstripVisibleAreaWidth() {
*/
export function computeDisplayModeFromInput(input: Object) {
const {
filmstripType,
isActiveParticipant,
isAudioOnly,
isCurrentlyOnLargeVideo,
@@ -515,7 +521,6 @@ export function computeDisplayModeFromInput(input: Object) {
isRemoteParticipant,
multipleVideoSupport,
stageParticipantsVisible,
stageFilmstrip,
tileViewActive
} = input;
const adjustedIsVideoPlayable = input.isVideoPlayable && (!isRemoteParticipant || canPlayEventReceived);
@@ -534,8 +539,8 @@ export function computeDisplayModeFromInput(input: Object) {
}
}
if (!tileViewActive && ((isScreenSharing && isRemoteParticipant)
|| (stageParticipantsVisible && isActiveParticipant && !stageFilmstrip))) {
if (!tileViewActive && filmstripType === FILMSTRIP_TYPE.MAIN && ((isScreenSharing && isRemoteParticipant)
|| (stageParticipantsVisible && isActiveParticipant))) {
return DISPLAY_AVATAR;
} else if (isCurrentlyOnLargeVideo && !tileViewActive) {
// Display name is always and only displayed when user is on the stage
@@ -569,12 +574,13 @@ export function getDisplayModeInput(props: Object, state: Object) {
_participant,
_stageParticipantsVisible,
_videoTrack,
stageFilmstrip
filmstripType = FILMSTRIP_TYPE.MAIN
} = props;
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
const { canPlayEventReceived } = state;
return {
filmstripType,
isActiveParticipant: _isActiveParticipant,
isCurrentlyOnLargeVideo: _isCurrentlyOnLargeVideo,
isAudioOnly: _isAudioOnly,
@@ -588,7 +594,6 @@ export function getDisplayModeInput(props: Object, state: Object) {
isVirtualScreenshareParticipant: _isVirtualScreenshareParticipant,
multipleVideoSupport: _multipleVideoSupport,
stageParticipantsVisible: _stageParticipantsVisible,
stageFilmstrip,
videoStreamMuted: _videoTrack ? _videoTrack.muted : 'no stream'
};
}
@@ -717,8 +722,24 @@ export function isStageFilmstripAvailable(state, minParticipantCount = 0) {
const { remoteScreenShares } = state['features/video-layout'];
const sharedVideo = isSharingStatus(state['features/shared-video']?.status);
return isStageFilmstripEnabled(state) && remoteScreenShares.length === 0 && !sharedVideo
&& activeParticipants.length >= minParticipantCount;
return isStageFilmstripEnabled(state) && !sharedVideo
&& activeParticipants.length >= minParticipantCount
&& (isTopPanelEnabled(state) || remoteScreenShares.length === 0);
}
/**
* Whether the stage filmstrip should be displayed on the top.
*
* @param {Object} state - Redux state.
* @param {number} minParticipantCount - The min number of participants for the stage filmstrip
* to be displayed.
* @returns {boolean}
*/
export function isStageFilmstripTopPanel(state, minParticipantCount = 0) {
const { remoteScreenShares } = state['features/video-layout'];
return isTopPanelEnabled(state)
&& isStageFilmstripAvailable(state, minParticipantCount) && remoteScreenShares.length > 0;
}
/**
@@ -737,10 +758,10 @@ export function isStageFilmstripEnabled(state) {
* Gets the thumbnail type by filmstrip type.
*
* @param {string} currentLayout - Current app layout.
* @param {boolean} isStageFilmstrip - Whether the filmstrip is stage filmstrip or not.
* @param {string} filmstripType - The current filmstrip type.
* @returns {string}
*/
export function getThumbnailTypeFromLayout(currentLayout, isStageFilmstrip = false) {
export function getThumbnailTypeFromLayout(currentLayout, filmstripType) {
switch (currentLayout) {
case LAYOUTS.TILE_VIEW:
return THUMBNAIL_TYPE.TILE;
@@ -749,10 +770,24 @@ export function getThumbnailTypeFromLayout(currentLayout, isStageFilmstrip = fal
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
return THUMBNAIL_TYPE.HORIZONTAL;
case LAYOUTS.STAGE_FILMSTRIP_VIEW:
if (isStageFilmstrip) {
if (filmstripType !== FILMSTRIP_TYPE.MAIN) {
return THUMBNAIL_TYPE.TILE;
}
return THUMBNAIL_TYPE.VERTICAL;
}
}
/**
* Whether or not the top panel is enabled.
*
* @param {Object} state - Redux state.
* @returns {boolean}
*/
export function isTopPanelEnabled(state) {
const { filmstrip } = state['features/base/config'];
const participantsCount = getParticipantCount(state);
return !filmstrip?.disableTopPanel && participantsCount >= (filmstrip?.minParticipantCountForTopPanel ?? 50);
}

View File

@@ -31,6 +31,7 @@ import {
import {
addStageParticipant,
removeStageParticipant,
setFilmstripHeight,
setFilmstripWidth,
setStageParticipants
} from './actions';
@@ -38,7 +39,9 @@ import {
ACTIVE_PARTICIPANT_TIMEOUT,
DEFAULT_FILMSTRIP_WIDTH,
MAX_ACTIVE_PARTICIPANTS,
MIN_STAGE_VIEW_WIDTH
MIN_STAGE_VIEW_HEIGHT,
MIN_STAGE_VIEW_WIDTH,
TOP_FILMSTRIP_HEIGHT
} from './constants';
import {
isFilmstripResizable,
@@ -77,19 +80,27 @@ MiddlewareRegistry.register(store => next => action => {
const state = store.getState();
if (isFilmstripResizable(state)) {
const { width: filmstripWidth } = state['features/filmstrip'];
const { clientWidth } = action;
let width;
const { width: filmstripWidth, topPanelHeight } = state['features/filmstrip'];
const { clientWidth, clientHeight } = action;
let height, width;
if (filmstripWidth.current > clientWidth - MIN_STAGE_VIEW_WIDTH) {
width = Math.max(clientWidth - MIN_STAGE_VIEW_WIDTH, DEFAULT_FILMSTRIP_WIDTH);
} else {
width = Math.min(clientWidth - MIN_STAGE_VIEW_WIDTH, filmstripWidth.userSet);
}
if (width !== filmstripWidth.current) {
store.dispatch(setFilmstripWidth(width));
}
if (topPanelHeight.current > clientHeight - MIN_STAGE_VIEW_HEIGHT) {
height = Math.max(clientHeight - MIN_STAGE_VIEW_HEIGHT, TOP_FILMSTRIP_HEIGHT);
} else {
height = Math.min(clientHeight - MIN_STAGE_VIEW_HEIGHT, topPanelHeight.userSet);
}
if (height !== topPanelHeight.current) {
store.dispatch(setFilmstripHeight(height));
}
}
break;
}

View File

@@ -19,7 +19,11 @@ import {
SET_VISIBLE_REMOTE_PARTICIPANTS,
SET_VOLUME,
SET_MAX_STAGE_PARTICIPANTS,
CLEAR_STAGE_PARTICIPANTS
CLEAR_STAGE_PARTICIPANTS,
SET_SCREENSHARING_TILE_DIMENSIONS,
SET_USER_FILMSTRIP_HEIGHT,
SET_FILMSTRIP_HEIGHT,
SET_TOP_PANEL_VISIBILITY
} from './actionTypes';
const DEFAULT_STATE = {
@@ -76,6 +80,11 @@ const DEFAULT_STATE = {
*/
remoteParticipants: [],
/**
* The dimensions of the screenshare filmstrip.
*/
screenshareFilmstripDimensions: {},
/**
* The stage filmstrip view dimensions.
*
@@ -92,6 +101,27 @@ const DEFAULT_STATE = {
*/
tileViewDimensions: {},
/**
* The height of the resizable top panel.
*/
topPanelHeight: {
/**
* Current height. Affected by: user top panel resize,
* window resize.
*/
current: null,
/**
* Height set by user resize. Used as the preferred height.
*/
userSet: null
},
/**
* The indicator determines if the top panel is visible.
*/
topPanelVisible: true,
/**
* The vertical view dimensions.
*
@@ -227,6 +257,15 @@ ReducerRegistry.register(
...state
};
}
case SET_FILMSTRIP_HEIGHT:{
return {
...state,
topPanelHeight: {
...state.topPanelHeight,
current: action.height
}
};
}
case SET_FILMSTRIP_WIDTH: {
return {
...state,
@@ -236,6 +275,17 @@ ReducerRegistry.register(
}
};
}
case SET_USER_FILMSTRIP_HEIGHT: {
const { height } = action;
return {
...state,
topPanelHeight: {
current: height,
userSet: height
}
};
}
case SET_USER_FILMSTRIP_WIDTH: {
const { width } = action;
@@ -283,6 +333,18 @@ ReducerRegistry.register(
activeParticipants: []
};
}
case SET_SCREENSHARING_TILE_DIMENSIONS: {
return {
...state,
screenshareFilmstripDimensions: action.dimensions
};
}
case SET_TOP_PANEL_VISIBILITY: {
return {
...state,
topPanelVisible: action.visible
};
}
}
return state;

View File

@@ -14,6 +14,7 @@ import { getCurrentLayout, shouldDisplayTileView, LAYOUTS } from '../video-layou
import {
clearStageParticipants,
setHorizontalViewDimensions,
setScreensharingTileDimensions,
setStageFilmstripViewDimensions,
setTileViewDimensions,
setVerticalViewDimensions
@@ -23,7 +24,8 @@ import {
DISPLAY_DRAWER_THRESHOLD
} from './constants';
import {
isFilmstripResizable
isFilmstripResizable,
isTopPanelEnabled
} from './functions';
import './subscriber.any';
@@ -176,7 +178,8 @@ StateListenerRegistry.register(
visible: state['features/filmstrip'].visible,
clientWidth: state['features/base/responsive-ui'].clientWidth,
clientHeight: state['features/base/responsive-ui'].clientHeight,
tileView: state['features/video-layout'].tileViewEnabled
tileView: state['features/video-layout'].tileViewEnabled,
height: state['features/filmstrip'].topPanelHeight?.current
};
},
/* listener */(_, store) => {
@@ -198,3 +201,27 @@ StateListenerRegistry.register(
store.dispatch(selectParticipantInLargeVideo());
}
});
/**
* Listens for changes to determine the size of the screenshare filmstrip.
*/
StateListenerRegistry.register(
/* selector */ state => {
return {
length: state['features/video-layout'].remoteScreenShares.length,
clientWidth: state['features/base/responsive-ui'].clientWidth,
clientHeight: state['features/base/responsive-ui'].clientHeight,
height: state['features/filmstrip'].topPanelHeight?.current,
width: state['features/filmstrip'].width?.current,
visible: state['features/filmstrip'].visible,
topPanelVisible: state['features/filmstrip'].topPanelVisible
};
},
/* listener */({ length }, store) => {
if (length >= 1 && isTopPanelEnabled(store.getState())) {
store.dispatch(setScreensharingTileDimensions());
}
}, {
deepEquals: true
});

View File

@@ -1,7 +1,6 @@
import { GiphyContent, GiphyGridView, GiphyMediaType } from '@giphy/react-native-sdk';
import React, { useCallback, useState } from 'react';
import { Image, Keyboard, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { createGifSentEvent, sendAnalytics } from '../../../analytics';
@@ -11,12 +10,13 @@ import { goBack } from '../../../mobile/navigation/components/conference/Confere
import ClearableInput from '../../../participants-pane/components/native/ClearableInput';
import { formatGifUrlMessage, getGifUrl } from '../../functions';
import GifsMenuFooter from './GifsMenuFooter';
import styles from './styles';
const GifsMenu = () => {
const [ searchQuery, setSearchQuery ] = useState('');
const dispatch = useDispatch();
const insets = useSafeAreaInsets();
const { t } = useTranslation();
const content = searchQuery === ''
? GiphyContent.trending({ mediaType: GiphyMediaType.Gif })
@@ -34,33 +34,22 @@ const GifsMenu = () => {
goBack();
}, []);
const onScroll = useCallback(Keyboard.dismiss, []);
return (<JitsiScreen
style = { styles.container }>
<ClearableInput
autoFocus = { true }
customStyles = { styles.clearableInput }
onChange = { setSearchQuery }
placeholder = 'Search GIPHY'
value = { searchQuery } />
<GiphyGridView
cellPadding = { 5 }
content = { content }
onMediaSelect = { sendGif }
onScroll = { onScroll }
style = { styles.grid } />
<View
style = { [ styles.credit, {
bottom: insets.bottom,
left: insets.left,
right: insets.right
} ] }>
<Text
style = { styles.creditText }>Powered by</Text>
<Image source = { require('../../../../../images/GIPHY_logo.png') } />
</View>
</JitsiScreen>);
return (
<JitsiScreen
footerComponent = { GifsMenuFooter }
style = { styles.container }>
<ClearableInput
customStyles = { styles.clearableInput }
onChange = { setSearchQuery }
placeholder = { t('giphy.search') }
value = { searchQuery } />
<GiphyGridView
cellPadding = { 5 }
content = { content }
onMediaSelect = { sendGif }
style = { styles.grid } />
</JitsiScreen>
);
};
export default GifsMenu;

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Image, Text, View } from 'react-native';
import styles from './styles';
/**
* Implements the gifs menu footer component.
*
* @returns { JSX.Element} - The gifs menu footer component.
*/
const GifsMenuFooter = (): JSX.Element => {
const { t } = useTranslation();
return(
<View style={ styles.credit }>
<Text
style={ styles.creditText }>{ t('poweredby') }</Text>
<Image
source = { require('../../../../../images/GIPHY_logo.png') } />
</View>
)
};
export default GifsMenuFooter;

View File

@@ -12,7 +12,9 @@ export default {
marginTop: BaseTheme.spacing[3]
},
input: { textAlign: 'left' }
input: {
textAlign: 'left'
}
},
grid: {
@@ -22,19 +24,19 @@ export default {
},
credit: {
alignItems: 'center',
backgroundColor: BaseTheme.palette.ui01,
width: '100%',
height: 40,
position: 'absolute',
marginBottom: 0,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center'
height: 56,
justifyContent: 'center',
marginBottom: BaseTheme.spacing[0],
paddingBottom: BaseTheme.spacing[4],
width: '100%'
},
creditText: {
color: 'white',
color: BaseTheme.palette.text01,
fontWeight: 'bold'
}
};

View File

@@ -1,11 +1,13 @@
// @flow
import React from 'react';
import { Text, View, TouchableOpacity, TextInput } from 'react-native';
import { Text, View, TextInput } from 'react-native';
import { translate } from '../../../base/i18n';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { LoadingIndicator } from '../../../base/react';
import Button from '../../../base/react/components/native/Button';
import { BUTTON_TYPES } from '../../../base/react/constants';
import { connect } from '../../../base/redux';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui';
import BaseTheme from '../../../base/ui/components/BaseTheme';
@@ -22,7 +24,6 @@ import AbstractLobbyScreen, {
import styles from './styles';
type Props = AbstractProps & {
/**
@@ -185,31 +186,21 @@ class LobbyScreen extends AbstractLobbyScreen<Props> {
* @inheritdoc
*/
_renderPasswordJoinButtons() {
const { t } = this.props;
return (
<View style = { styles.passwordJoinButtonsWrapper }>
<TouchableOpacity
<Button
accessibilityLabel = 'lobby.backToKnockModeButton'
label = 'lobby.backToKnockModeButton'
onPress = { this._onSwitchToKnockMode }
style = { [
styles.button,
styles.primaryButton
] }>
<Text style = { styles.primaryButtonText }>
{ t('lobby.backToKnockModeButton') }
</Text>
</TouchableOpacity>
<TouchableOpacity
style = { styles.lobbyButton }
type = { BUTTON_TYPES.PRIMARY } />
<Button
accessibilityLabel = 'lobby.passwordJoinButton'
disabled = { !this.state.password }
label = 'lobby.passwordJoinButton'
onPress = { this._onJoinWithPassword }
style = { [
styles.button,
styles.primaryButton
] }>
<Text style = { styles.primaryButtonText }>
{ t('lobby.passwordJoinButton') }
</Text>
</TouchableOpacity>
style = { styles.lobbyButton }
type = { BUTTON_TYPES.PRIMARY } />
</View>
);
}
@@ -245,44 +236,39 @@ class LobbyScreen extends AbstractLobbyScreen<Props> {
* @inheritdoc
*/
_renderStandardButtons() {
const { _knocking, _renderPassword, _isLobbyChatActive, t } = this.props;
const { _knocking, _renderPassword, _isLobbyChatActive } = this.props;
const { displayName } = this.state;
const askToJoinButtonStyles
= displayName ? styles.primaryButton : styles.primaryButtonDisabled;
return (
<View style = { styles.standardButtonWrapper }>
{ _knocking && _isLobbyChatActive && <TouchableOpacity
onPress = { this._onNavigateToLobbyChat }
style = { [
styles.button,
styles.primaryButton
] }>
<Text style = { styles.primaryButtonText }>
{ t('toolbar.openChat') }
</Text>
</TouchableOpacity>}
{ _knocking || <TouchableOpacity
disabled = { !displayName }
onPress = { this._onAskToJoin }
style = { [
styles.button,
askToJoinButtonStyles
] }>
<Text style = { styles.primaryButtonText }>
{ t('lobby.knockButton') }
</Text>
</TouchableOpacity> }
{ _renderPassword && <TouchableOpacity
onPress = { this._onSwitchToPasswordMode }
style = { [
styles.button,
styles.primaryButton
] }>
<Text style = { styles.primaryButtonText }>
{ t('lobby.enterPasswordButton') }
</Text>
</TouchableOpacity> }
{
_knocking && _isLobbyChatActive
&& <Button
accessibilityLabel = 'toolbar.openChat'
label = 'toolbar.openChat'
onPress = { this._onNavigateToLobbyChat }
style = { styles.lobbyButton }
type = { BUTTON_TYPES.PRIMARY } />
}
{
_knocking
|| <Button
accessibilityLabel = 'lobby.knockButton'
disabled = { !displayName }
label = 'lobby.knockButton'
onPress = { this._onAskToJoin }
style = { styles.lobbyButton }
type = { BUTTON_TYPES.PRIMARY } />
}
{
_renderPassword
&& <Button
accessibilityLabel = 'lobby.enterPasswordButton'
label = 'lobby.enterPasswordButton'
onPress = { this._onSwitchToPasswordMode }
style = { styles.enterPasswordButton }
type = { BUTTON_TYPES.PRIMARY } />
}
</View>
);
}

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