Compare commits

...

7 Commits

Author SHA1 Message Date
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
11 changed files with 229 additions and 102 deletions

View File

@@ -43,6 +43,7 @@ import {
conferenceWillJoin,
conferenceWillLeave,
dataChannelOpened,
kickedOut,
lockStateChanged,
onStartMutedPolicyChanged,
p2pStatusChanged,
@@ -104,7 +105,6 @@ import {
getLocationContextRoot,
getJitsiMeetGlobalNS
} from './react/features/base/util';
import { notifyKickedOut } from './react/features/conference';
import { addMessage } from './react/features/chat';
import { showDesktopPicker } from './react/features/desktop-picker';
import { appendSuffix } from './react/features/display-name';
@@ -1781,11 +1781,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 +1957,7 @@ export default {
room.on(JitsiConferenceEvents.KICKED, participant => {
APP.UI.hideStats();
APP.store.dispatch(notifyKickedOut(participant));
APP.store.dispatch(kickedOut(room, participant));
// FIXME close
});

View File

@@ -561,7 +561,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

@@ -1,5 +1,3 @@
import throttle from 'lodash/throttle';
import { set } from '../redux';
import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
@@ -449,73 +447,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

@@ -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