Compare commits

..

9 Commits

Author SHA1 Message Date
Leonard Kim
0734ce7ae3 feat(api): add notifications for kicked participants 2019-07-01 12:53:25 -07:00
Дамян Минков
2dc06c28e3 Adds option to be able to cancel locked rooms and leave. (#4391)
* Adds option to be able to cancel locked rooms and leave.

* Removes not needed operations when canceling password prompt.
2019-07-01 13:02:25 +01:00
paweldomas
5848669552 feat(analytics): time since last success in connection dropped
Update LJM to 9bcc2a26cc94683b8ed302418695a331b450df97 in order to bring
in the analytics update which will add a property indicating how much
time has passed since the last successful XMPP request came through.
2019-06-28 13:30:50 -05:00
Leonard Kim
c0376d238a ref(notifications): do not notify of local participant left
Join notifications are already supressed for the local
participant, so hide the left notification. For now
the notification is not being shown on mobile to keep
the same existing behavior and because a copy change
will be needed, but will be added once batching is
implemented.
2019-06-28 09:18:18 -07:00
Leonard Kim
979b773c3c ref(notifications): move join notification firing to notifications feature 2019-06-27 17:29:02 -07:00
Leonard Kim
3195a449ca ref(conference): web and native exercise same redux flow for kicked out 2019-06-27 09:34:05 -07:00
Bettenbuk Zoltan
d7483f07e3 feat: add possibility to clear invite search field 2019-06-27 18:17:37 +02:00
Дамян Минков
9c1b802997 Device selection now always shows the device that it managed to open. (#4384)
* Device selection now always shows the device that managed to open.

* Simplifies implementation, skips using state.
2019-06-27 17:04:47 +01:00
damencho
bb3a10b0fc Safe guard for removed parent node of the iframe. 2019-06-27 14:23:59 +01:00
22 changed files with 452 additions and 191 deletions

View File

@@ -18,4 +18,4 @@
# org.gradle.parallel=true
appVersion=19.2.0
sdkVersion=2.2.2
sdkVersion=2.2.0

View File

@@ -341,7 +341,7 @@ public class JitsiMeetConferenceOptions implements Parcelable {
dest.writeString(token);
dest.writeBundle(colorScheme);
dest.writeBundle(featureFlags);
dest.writeBundle(userInfo != null ? userInfo.asBundle() : new Bundle());
dest.writeBundle(userInfo.asBundle());
dest.writeByte((byte) (audioMuted == null ? 0 : audioMuted ? 1 : 2));
dest.writeByte((byte) (audioOnly == null ? 0 : audioOnly ? 1 : 2));
dest.writeByte((byte) (videoMuted == null ? 0 : videoMuted ? 1 : 2));

View File

@@ -39,6 +39,13 @@ class JitsiMeetUncaughtExceptionHandler implements Thread.UncaughtExceptionHandl
public void uncaughtException(Thread t, Throwable e) {
Log.e(this.getClass().getSimpleName(), "FATAL ERROR", e);
// Terminate all conferences
for (BaseReactView view: BaseReactView.getViews()) {
if (view instanceof JitsiMeetView) {
((JitsiMeetView) view).leave();
}
}
// Abort all ConnectionService ongoing calls
if (AudioModeModule.useConnectionService()) {
ConnectionService.abortConnections();

View File

@@ -86,10 +86,8 @@ class OngoingConferenceTracker {
}
private void updateListeners() {
synchronized (listeners) {
for (OngoingConferenceListener listener : listeners) {
listener.onCurrentConferenceChanged(currentConference);
}
for (OngoingConferenceListener listener : listeners) {
listener.onCurrentConferenceChanged(currentConference);
}
}

View File

@@ -23,7 +23,8 @@ import {
sendAnalytics
} from './react/features/analytics';
import {
redirectWithStoredParams,
maybeRedirectToWelcomePage,
redirectToStaticPage,
reloadWithStoredParams
} from './react/features/app';
@@ -43,6 +44,7 @@ import {
conferenceWillJoin,
conferenceWillLeave,
dataChannelOpened,
kickedOut,
lockStateChanged,
onStartMutedPolicyChanged,
p2pStatusChanged,
@@ -100,11 +102,7 @@ import {
trackAdded,
trackRemoved
} from './react/features/base/tracks';
import {
getLocationContextRoot,
getJitsiMeetGlobalNS
} from './react/features/base/util';
import { notifyKickedOut } from './react/features/conference';
import { getJitsiMeetGlobalNS } from './react/features/base/util';
import { addMessage } from './react/features/chat';
import { showDesktopPicker } from './react/features/desktop-picker';
import { appendSuffix } from './react/features/display-name';
@@ -211,77 +209,6 @@ function muteLocalVideo(muted) {
APP.store.dispatch(setVideoMuted(muted));
}
/**
* Check if the welcome page is enabled and redirects to it.
* If requested show a thank you dialog before that.
* If we have a close page enabled, redirect to it without
* showing any other dialog.
*
* @param {object} options used to decide which particular close page to show
* or if close page is disabled, whether we should show the thankyou dialog
* @param {boolean} options.showThankYou - whether we should
* show thank you dialog
* @param {boolean} options.feedbackSubmitted - whether feedback was submitted
*/
function maybeRedirectToWelcomePage(options) {
// if close page is enabled redirect to it, without further action
if (config.enableClosePage) {
const { isGuest } = APP.store.getState()['features/base/jwt'];
// save whether current user is guest or not, before navigating
// to close page
window.sessionStorage.setItem('guest', isGuest);
redirectToStaticPage(`static/${
options.feedbackSubmitted ? 'close.html' : 'close2.html'}`);
return;
}
// else: show thankYou dialog only if there is no feedback
if (options.showThankYou) {
APP.store.dispatch(showNotification({
titleArguments: { appName: interfaceConfig.APP_NAME },
titleKey: 'dialog.thankYou'
}));
}
// if Welcome page is enabled redirect to welcome page after 3 sec, if
// there is a thank you message to be shown, 0.5s otherwise.
if (config.enableWelcomePage) {
setTimeout(
() => {
APP.store.dispatch(redirectWithStoredParams('/'));
},
options.showThankYou ? 3000 : 500);
}
}
/**
* Assigns a specific pathname to window.location.pathname taking into account
* the context root of the Web app.
*
* @param {string} pathname - The pathname to assign to
* window.location.pathname. If the specified pathname is relative, the context
* root of the Web app will be prepended to the specified pathname before
* assigning it to window.location.pathname.
* @return {void}
*/
function redirectToStaticPage(pathname) {
const windowLocation = window.location;
let newPathname = pathname;
if (!newPathname.startsWith('/')) {
// A pathname equal to ./ specifies the current directory. It will be
// fine but pointless to include it because contextRoot is the current
// directory.
newPathname.startsWith('./')
&& (newPathname = newPathname.substring(2));
newPathname = getLocationContextRoot(windowLocation) + newPathname;
}
windowLocation.pathname = newPathname;
}
/**
* A queue for the async replaceLocalTrack action so that multiple audio
* replacements cannot happen simultaneously. This solves the issue where
@@ -347,7 +274,7 @@ class ConferenceConnector {
case JitsiConferenceErrors.NOT_ALLOWED_ERROR: {
// let's show some auth not allowed page
redirectToStaticPage('static/authError.html');
APP.store.dispatch(redirectToStaticPage('static/authError.html'));
break;
}
@@ -1781,11 +1708,6 @@ export default {
logger.log(`USER ${id} LEFT:`, user);
APP.API.notifyUserLeft(id);
APP.UI.messageHandler.participantNotification(
user.getDisplayName(),
'notify.somebody',
'disconnected',
'notify.disconnected');
APP.UI.onSharedVideoStop(id);
});
@@ -1962,7 +1884,7 @@ export default {
room.on(JitsiConferenceEvents.KICKED, participant => {
APP.UI.hideStats();
APP.store.dispatch(notifyKickedOut(participant));
APP.store.dispatch(kickedOut(room, participant));
// FIXME close
});
@@ -2601,7 +2523,7 @@ export default {
room = undefined;
APP.API.notifyReadyToClose();
maybeRedirectToWelcomePage(values[0]);
APP.store.dispatch(maybeRedirectToWelcomePage(values[0]));
});
},

View File

@@ -387,6 +387,19 @@ changes. The listener will receive an object with the following structure:
}
```
* **participantKickedOut** - event notifications about a participants being removed from the room. The listener will receive an object with the following structure:
```javascript
{
kicked: {
id: string, // the id of the participant removed from the room
local: boolean // whether or not the participant is the local particiapnt
},
kicker: {
id: string // the id of the participant who kicked out the other participant
}
}
```
* **participantLeft** - event notifications about participants that leave the room. The listener will receive an object with the following structure:
```javascript
{

View File

@@ -658,6 +658,24 @@ class API {
});
}
/**
* Notify external application of a participant, remote or local, being
* removed from the conference by another participant.
*
* @param {string} kicked - The ID of the participant removed from the
* conference.
* @param {string} kicker - The ID of the participant that removed the
* other participant.
* @returns {void}
*/
notifyKickedOut(kicked: Object, kicker: Object) {
this._sendEvent({
name: 'participant-kicked-out',
kicked,
kicker
});
}
/**
* Notify external application of the current meeting requiring a password
* to join.

View File

@@ -63,6 +63,7 @@ const events = {
'mic-error': 'micError',
'outgoing-message': 'outgoingMessage',
'participant-joined': 'participantJoined',
'participant-kicked-out': 'participantKickedOut',
'participant-left': 'participantLeft',
'password-required': 'passwordRequired',
'proxy-connection-event': 'proxyConnectionEvent',
@@ -561,7 +562,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
this.emit('_willDispose');
this._transport.dispose();
this.removeAllListeners();
if (this._frame) {
if (this._frame && this._frame.parentNode) {
this._frame.parentNode.removeChild(this._frame);
}
}

4
package-lock.json generated
View File

@@ -8946,8 +8946,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#c0af82a215d0893f1999df299cfdfcbc9ce9e72a",
"from": "github:jitsi/lib-jitsi-meet#c0af82a215d0893f1999df299cfdfcbc9ce9e72a",
"version": "github:jitsi/lib-jitsi-meet#9bcc2a26cc94683b8ed302418695a331b450df97",
"from": "github:jitsi/lib-jitsi-meet#9bcc2a26cc94683b8ed302418695a331b450df97",
"requires": {
"@jitsi/sdp-interop": "0.1.14",
"@jitsi/sdp-simulcast": "0.2.1",

View File

@@ -52,7 +52,7 @@
"js-utils": "github:jitsi/js-utils#73a67a7a60d52f8e895f50939c8fcbd1f20fe7b5",
"jsrsasign": "8.0.12",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#c0af82a215d0893f1999df299cfdfcbc9ce9e72a",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#9bcc2a26cc94683b8ed302418695a331b450df97",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.11",
"moment": "2.19.4",

View File

@@ -13,10 +13,18 @@ import {
import { connect, disconnect, setLocationURL } from '../base/connection';
import { loadConfig } from '../base/lib-jitsi-meet';
import { createDesiredLocalTracks } from '../base/tracks';
import { parseURIString, toURLString } from '../base/util';
import {
getLocationContextRoot,
parseURIString,
toURLString
} from '../base/util';
import { showNotification } from '../notifications';
import { setFatalError } from '../overlay';
import { getDefaultURL } from './functions';
import {
getDefaultURL,
getName
} from './functions';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -136,6 +144,34 @@ export function redirectWithStoredParams(pathname: string) {
};
}
/**
* Assigns a specific pathname to window.location.pathname taking into account
* the context root of the Web app.
*
* @param {string} pathname - The pathname to assign to
* window.location.pathname. If the specified pathname is relative, the context
* root of the Web app will be prepended to the specified pathname before
* assigning it to window.location.pathname.
* @returns {Function}
*/
export function redirectToStaticPage(pathname: string) {
return () => {
const windowLocation = window.location;
let newPathname = pathname;
if (!newPathname.startsWith('/')) {
// A pathname equal to ./ specifies the current directory. It will be
// fine but pointless to include it because contextRoot is the current
// directory.
newPathname.startsWith('./')
&& (newPathname = newPathname.substring(2));
newPathname = getLocationContextRoot(windowLocation) + newPathname;
}
windowLocation.pathname = newPathname;
};
}
/**
* Reloads the page.
*
@@ -182,3 +218,58 @@ export function reloadWithStoredParams() {
}
};
}
/**
* Check if the welcome page is enabled and redirects to it.
* If requested show a thank you dialog before that.
* If we have a close page enabled, redirect to it without
* showing any other dialog.
*
* @param {Object} options - Used to decide which particular close page to show
* or if close page is disabled, whether we should show the thankyou dialog.
* @param {boolean} options.showThankYou - Whether we should
* show thank you dialog.
* @param {boolean} options.feedbackSubmitted - Whether feedback was submitted.
* @returns {Function}
*/
export function maybeRedirectToWelcomePage(options: Object = {}) {
return (dispatch: Dispatch<any>, getState: Function) => {
const {
enableClosePage
} = getState()['features/base/config'];
// if close page is enabled redirect to it, without further action
if (enableClosePage) {
const { isGuest } = getState()['features/base/jwt'];
// save whether current user is guest or not, before navigating
// to close page
window.sessionStorage.setItem('guest', isGuest);
dispatch(redirectToStaticPage(`static/${
options.feedbackSubmitted ? 'close.html' : 'close2.html'}`));
return;
}
// else: show thankYou dialog only if there is no feedback
if (options.showThankYou) {
dispatch(showNotification({
titleArguments: { appName: getName() },
titleKey: 'dialog.thankYou'
}));
}
// if Welcome page is enabled redirect to welcome page after 3 sec, if
// there is a thank you message to be shown, 0.5s otherwise.
if (getState()['features/base/config'].enableWelcomePage) {
setTimeout(
() => {
dispatch(redirectWithStoredParams('/'));
},
options.showThankYou ? 3000 : 500);
}
};
}

View File

@@ -65,6 +65,18 @@ export const PARTICIPANT_ID_CHANGED = 'PARTICIPANT_ID_CHANGED';
*/
export const PARTICIPANT_JOINED = 'PARTICIPANT_JOINED';
/**
* Action to signal that a participant has been removed from a conference by
* another participant.
*
* {
* type: PARTICIPANT_KICKED,
* kicked: Object,
* kicker: Object
* }
*/
export const PARTICIPANT_KICKED = 'PARTICIPANT_KICKED';
/**
* Action to handle case when participant lefts.
*

View File

@@ -1,5 +1,3 @@
import throttle from 'lodash/throttle';
import { set } from '../redux';
import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
@@ -11,6 +9,7 @@ import {
MUTE_REMOTE_PARTICIPANT,
PARTICIPANT_ID_CHANGED,
PARTICIPANT_JOINED,
PARTICIPANT_KICKED,
PARTICIPANT_LEFT,
PARTICIPANT_UPDATED,
PIN_PARTICIPANT
@@ -417,6 +416,12 @@ export function participantMutedUs(participant) {
export function participantKicked(kicker, kicked) {
return (dispatch, getState) => {
dispatch({
type: PARTICIPANT_KICKED,
kicked: kicked.getId(),
kicker: kicker.getId()
});
dispatch(showNotification({
titleArguments: {
kicked:
@@ -449,73 +454,3 @@ export function pinParticipant(id) {
}
};
}
/**
* An array of names of participants that have joined the conference. The array
* is replaced with an empty array as notifications are displayed.
*
* @private
* @type {string[]}
*/
let joinedParticipantsNames = [];
/**
* A throttled internal function that takes the internal list of participant
* names, {@code joinedParticipantsNames}, and triggers the display of a
* notification informing of their joining.
*
* @private
* @type {Function}
*/
const _throttledNotifyParticipantConnected = throttle(dispatch => {
const joinedParticipantsCount = joinedParticipantsNames.length;
let notificationProps;
if (joinedParticipantsCount >= 3) {
notificationProps = {
titleArguments: {
name: joinedParticipantsNames[0],
count: joinedParticipantsCount - 1
},
titleKey: 'notify.connectedThreePlusMembers'
};
} else if (joinedParticipantsCount === 2) {
notificationProps = {
titleArguments: {
first: joinedParticipantsNames[0],
second: joinedParticipantsNames[1]
},
titleKey: 'notify.connectedTwoMembers'
};
} else if (joinedParticipantsCount) {
notificationProps = {
titleArguments: {
name: joinedParticipantsNames[0]
},
titleKey: 'notify.connectedOneMember'
};
}
if (notificationProps) {
dispatch(
showNotification(notificationProps, NOTIFICATION_TIMEOUT));
}
joinedParticipantsNames = [];
}, 500, { leading: false });
/**
* Queues the display of a notification of a participant having connected to
* the meeting. The notifications are batched so that quick consecutive
* connection events are shown in one notification.
*
* @param {string} displayName - The name of the participant that connected.
* @returns {Function}
*/
export function showParticipantJoinedNotification(displayName) {
joinedParticipantsNames.push(displayName);
return dispatch => _throttledNotifyParticipantConnected(dispatch);
}

View File

@@ -20,8 +20,7 @@ import {
localParticipantJoined,
localParticipantLeft,
participantLeft,
participantUpdated,
showParticipantJoinedNotification
participantUpdated
} from './actions';
import {
DOMINANT_SPEAKER_CHANGED,
@@ -118,15 +117,7 @@ MiddlewareRegistry.register(store => next => action => {
case PARTICIPANT_JOINED: {
_maybePlaySounds(store, action);
const result = _participantJoinedOrUpdated(store, next, action);
const { participant: p } = action;
if (!p.local) {
store.dispatch(showParticipantJoinedNotification(getParticipantDisplayName(store.getState, p.id)));
}
return result;
return _participantJoinedOrUpdated(store, next, action);
}
case PARTICIPANT_LEFT:

View File

@@ -372,7 +372,8 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
label: 'settings.selectCamera',
onSelect: selectedVideoInputId =>
super._onChange({ selectedVideoInputId }),
selectedDeviceId: this.props.selectedVideoInputId
selectedDeviceId: this.state.previewVideoTrack
? this.state.previewVideoTrack.getDeviceId() : null
},
{
devices: availableDevices.audioInput,
@@ -384,7 +385,8 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
label: 'settings.selectMic',
onSelect: selectedAudioInputId =>
super._onChange({ selectedAudioInputId }),
selectedDeviceId: this.props.selectedAudioInputId
selectedDeviceId: this.state.previewAudioTrack
? this.state.previewAudioTrack.getDeviceId() : null
}
];

View File

@@ -1,9 +1,14 @@
// @flow
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../base/conference';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
KICKED_OUT
} from '../base/conference';
import { NOTIFY_CAMERA_ERROR, NOTIFY_MIC_ERROR } from '../base/devices';
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
import {
PARTICIPANT_KICKED,
getAvatarURLByParticipantId,
getLocalParticipant
} from '../base/participants';
@@ -53,6 +58,16 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case KICKED_OUT:
APP.API.notifyKickedOut(
{
id: getLocalParticipant(store.getState()).id,
local: true
},
{ id: action.participant.getId() }
);
break;
case NOTIFY_CAMERA_ERROR:
if (action.error) {
APP.API.notifyOnCameraError(
@@ -66,6 +81,15 @@ MiddlewareRegistry.register(store => next => action => {
}
break;
case PARTICIPANT_KICKED:
APP.API.notifyKickedOut(
{
id: action.kicked,
local: false
},
{ id: action.kicker });
break;
case SET_FILMSTRIP_VISIBLE:
APP.API.notifyFilmstripDisplayChanged(action.visible);
break;

View File

@@ -7,6 +7,7 @@ import {
Alert,
FlatList,
KeyboardAvoidingView,
Platform,
SafeAreaView,
TextInput,
TouchableOpacity,
@@ -51,6 +52,11 @@ type Props = AbstractProps & {
type State = AbstractState & {
/**
* State variable to keep track of the search field value.
*/
fieldValue: string,
/**
* True if a search is in progress, false otherwise.
*/
@@ -73,6 +79,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
defaultState = {
addToCallError: false,
addToCallInProgress: false,
fieldValue: '',
inviteItems: [],
searchInprogress: false,
selectableItems: []
@@ -101,6 +108,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
this._keyExtractor = this._keyExtractor.bind(this);
this._renderItem = this._renderItem.bind(this);
this._renderSeparator = this._renderSeparator.bind(this);
this._onClearField = this._onClearField.bind(this);
this._onCloseAddPeopleDialog = this._onCloseAddPeopleDialog.bind(this);
this._onInvite = this._onInvite.bind(this);
this._onPressItem = this._onPressItem.bind(this);
@@ -168,12 +176,15 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
<TextInput
autoCorrect = { false }
autoFocus = { true }
clearButtonMode = 'always' // iOS only
onChangeText = { this._onTypeQuery }
placeholder = {
this.props.t(`inviteDialog.${placeholderKey}`)
}
ref = { this._setFieldRef }
style = { styles.searchField } />
style = { styles.searchField }
value = { this.state.fieldValue } />
{ this._renderAndroidClearButton() }
</View>
<FlatList
ItemSeparatorComponent = { this._renderSeparator }
@@ -215,6 +226,22 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
return item.type === 'user' ? item.id || item.user_id : item.number;
}
_onClearField: () => void
/**
* Callback to clear the text field.
*
* @returns {void}
*/
_onClearField() {
this.setState({
fieldValue: ''
});
// Clear search results
this._onTypeQuery('');
}
_onCloseAddPeopleDialog: () => void
/**
@@ -288,6 +315,10 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
* @returns {void}
*/
_onTypeQuery(query) {
this.setState({
fieldValue: query
});
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
this.setState({
@@ -342,6 +373,31 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
_renderItem: Object => ?React$Element<*>
/**
* Renders a button to clear the text field on Android.
*
* NOTE: For the best platform experience we use the native solution on iOS.
*
* @returns {React#Element<*>}
*/
_renderAndroidClearButton() {
if (Platform.OS !== 'android' || !this.state.fieldValue.length) {
return null;
}
return (
<TouchableOpacity
onPress = { this._onClearField }
style = { styles.clearButton }>
<View style = { styles.clearIconContainer }>
<Icon
name = 'close'
style = { styles.clearIcon } />
</View>
</TouchableOpacity>
);
}
/**
* Renders a single item in the {@code FlatList}.
*

View File

@@ -7,6 +7,8 @@ export const DARK_GREY = 'rgb(28, 32, 37)';
export const LIGHT_GREY = 'rgb(209, 219, 232)';
export const ICON_SIZE = 15;
const FIELD_COLOR = 'rgb(240, 243, 247)';
export default {
avatar: {
backgroundColor: LIGHT_GREY
@@ -21,6 +23,27 @@ export default {
flex: 1
},
clearButton: {
alignItems: 'center',
justifyContent: 'center',
marginLeft: 5
},
clearIcon: {
color: DARK_GREY,
fontSize: 18,
textAlign: 'center'
},
clearIconContainer: {
alignItems: 'center',
backgroundColor: FIELD_COLOR,
borderRadius: 12,
justifyContent: 'center',
height: 24,
width: 24
},
dialogWrapper: {
alignItems: 'stretch',
backgroundColor: ColorPalette.white,
@@ -56,7 +79,7 @@ export default {
},
searchField: {
backgroundColor: 'rgb(240, 243, 247)',
backgroundColor: FIELD_COLOR,
borderBottomRightRadius: 10,
borderTopRightRadius: 10,
color: DARK_GREY,
@@ -86,7 +109,7 @@ export default {
searchIconWrapper: {
alignItems: 'center',
backgroundColor: 'rgb(240, 243, 247)',
backgroundColor: FIELD_COLOR,
borderBottomLeftRadius: 10,
borderTopLeftRadius: 10,
flexDirection: 'row',

View File

@@ -1,5 +1,9 @@
// @flow
import throttle from 'lodash/throttle';
import type { Dispatch } from 'redux';
import {
CLEAR_NOTIFICATIONS,
HIDE_NOTIFICATION,
@@ -7,7 +11,7 @@ import {
SHOW_NOTIFICATION
} from './actionTypes';
import { NOTIFICATION_TYPE } from './constants';
import { NOTIFICATION_TIMEOUT, NOTIFICATION_TYPE } from './constants';
/**
* Clears (removes) all the notifications.
@@ -102,3 +106,73 @@ export function showWarningNotification(props: Object) {
appearance: NOTIFICATION_TYPE.WARNING
});
}
/**
* An array of names of participants that have joined the conference. The array
* is replaced with an empty array as notifications are displayed.
*
* @private
* @type {string[]}
*/
let joinedParticipantsNames = [];
/**
* A throttled internal function that takes the internal list of participant
* names, {@code joinedParticipantsNames}, and triggers the display of a
* notification informing of their joining.
*
* @private
* @type {Function}
*/
const _throttledNotifyParticipantConnected = throttle((dispatch: Dispatch<any>) => {
const joinedParticipantsCount = joinedParticipantsNames.length;
let notificationProps;
if (joinedParticipantsCount >= 3) {
notificationProps = {
titleArguments: {
name: joinedParticipantsNames[0],
count: joinedParticipantsCount - 1
},
titleKey: 'notify.connectedThreePlusMembers'
};
} else if (joinedParticipantsCount === 2) {
notificationProps = {
titleArguments: {
first: joinedParticipantsNames[0],
second: joinedParticipantsNames[1]
},
titleKey: 'notify.connectedTwoMembers'
};
} else if (joinedParticipantsCount) {
notificationProps = {
titleArguments: {
name: joinedParticipantsNames[0]
},
titleKey: 'notify.connectedOneMember'
};
}
if (notificationProps) {
dispatch(
showNotification(notificationProps, NOTIFICATION_TIMEOUT));
}
joinedParticipantsNames = [];
}, 500, { leading: false });
/**
* Queues the display of a notification of a participant having connected to
* the meeting. The notifications are batched so that quick consecutive
* connection events are shown in one notification.
*
* @param {string} displayName - The name of the participant that connected.
* @returns {Function}
*/
export function showParticipantJoinedNotification(displayName: string) {
joinedParticipantsNames.push(displayName);
return (dispatch: Dispatch<any>) => _throttledNotifyParticipantConnected(dispatch);
}

View File

@@ -1,9 +1,67 @@
/* @flow */
import { getCurrentConference } from '../base/conference';
import { StateListenerRegistry } from '../base/redux';
import {
PARTICIPANT_JOINED,
PARTICIPANT_LEFT,
getParticipantById,
getParticipantDisplayName
} from '../base/participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { clearNotifications } from './actions';
import {
clearNotifications,
showNotification,
showParticipantJoinedNotification
} from './actions';
import { NOTIFICATION_TIMEOUT } from './constants';
declare var interfaceConfig: Object;
/**
* Middleware that captures actions to display notifications.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case PARTICIPANT_JOINED: {
const result = next(action);
const { participant: p } = action;
if (!p.local) {
store.dispatch(showParticipantJoinedNotification(
getParticipantDisplayName(store.getState, p.id)
));
}
return result;
}
case PARTICIPANT_LEFT: {
const participant = getParticipantById(
store.getState(),
action.participant.id
);
if (typeof interfaceConfig === 'object'
&& participant
&& !participant.local) {
store.dispatch(showNotification({
descriptionKey: 'notify.disconnected',
titleKey: 'notify.somebody',
title: participant.name
},
NOTIFICATION_TIMEOUT));
}
return next(action);
}
}
return next(action);
});
/**
* StateListenerRegistry provides a reliable way to detect the leaving of a

View File

@@ -2,7 +2,10 @@
import type { Dispatch } from 'redux';
import { appNavigate } from '../app';
import {
appNavigate,
maybeRedirectToWelcomePage
} from '../app';
import {
conferenceLeft,
JITSI_CONFERENCE_URL_KEY,
@@ -11,6 +14,8 @@ import {
import { hideDialog, openDialog } from '../base/dialog';
import { PasswordRequiredPrompt, RoomLockPrompt } from './components';
declare var APP: Object;
/**
* Begins a (user) request to lock a specific conference/room.
*
@@ -44,6 +49,16 @@ export function beginRoomLockRequest(conference: ?Object) {
*/
export function _cancelPasswordRequiredPrompt(conference: Object) {
return (dispatch: Dispatch<any>, getState: Function) => {
if (typeof APP !== 'undefined') {
// when we are redirecting the library should handle any
// unload and clean of the connection.
APP.API.notifyReadyToClose();
dispatch(maybeRedirectToWelcomePage());
return;
}
// Canceling PasswordRequiredPrompt is to navigate the app/user to
// WelcomePage. In other words, the canceling invalidates the
// locationURL. Make sure that the canceling indeed has the intent to

View File

@@ -9,6 +9,8 @@ import { Dialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
import { connect } from '../../base/redux';
import { _cancelPasswordRequiredPrompt } from '../actions';
/**
* The type of the React {@code Component} props of
* {@link PasswordRequiredPrompt}.
@@ -63,6 +65,7 @@ class PasswordRequiredPrompt extends Component<Props, State> {
// Bind event handlers so they are only bound once per instance.
this._onPasswordChanged = this._onPasswordChanged.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onSubmit = this._onSubmit.bind(this);
}
@@ -75,7 +78,9 @@ class PasswordRequiredPrompt extends Component<Props, State> {
render() {
return (
<Dialog
isModal = { true }
disableBlanketClickDismiss = { true }
isModal = { false }
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
titleKey = 'dialog.passwordRequired'
width = 'small'>
@@ -121,6 +126,22 @@ class PasswordRequiredPrompt extends Component<Props, State> {
});
}
_onCancel: () => boolean;
/**
* Dispatches action to cancel and dismiss this dialog.
*
* @private
* @returns {boolean}
*/
_onCancel() {
this.props.dispatch(
_cancelPasswordRequiredPrompt(this.props.conference));
return true;
}
_onSubmit: () => boolean;
/**