feat(notifications): native UI updates (#12798)

* feat(notifications): native notifications UI updates
This commit is contained in:
Calinteodor 2023-02-21 11:26:04 +02:00 committed by GitHub
parent 9fa426d97f
commit f8af9c4fae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 549 additions and 485 deletions

View File

@ -13,10 +13,12 @@ import { IButtonProps } from '../types';
import styles from './buttonStyles'; import styles from './buttonStyles';
export interface IProps extends IButtonProps { export interface IProps extends IButtonProps {
color?: string; color?: string | undefined;
contentStyle?: Object | undefined; contentStyle?: Object | undefined;
labelStyle?: Object | undefined; labelStyle?: Object | undefined;
mode?: any;
style?: Object | undefined; style?: Object | undefined;
useRippleColor?: boolean;
} }
const Button: React.FC<IProps> = ({ const Button: React.FC<IProps> = ({
@ -27,31 +29,36 @@ const Button: React.FC<IProps> = ({
icon, icon,
labelKey, labelKey,
labelStyle, labelStyle,
mode = BUTTON_MODES.CONTAINED,
onClick: onPress, onClick: onPress,
style, style,
type type,
useRippleColor = true
}: IProps) => { }: IProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { CONTAINED } = BUTTON_MODES;
const { DESTRUCTIVE, PRIMARY, SECONDARY, TERTIARY } = BUTTON_TYPES; const { DESTRUCTIVE, PRIMARY, SECONDARY, TERTIARY } = BUTTON_TYPES;
const { CONTAINED, TEXT } = BUTTON_MODES;
const rippleColor
= useRippleColor ? BaseTheme.palette.action03Active : 'transparent';
let buttonLabelStyles; let buttonLabelStyles;
let buttonStyles; let buttonStyles;
let color; let color;
let mode;
if (type === PRIMARY) { if (type === PRIMARY) {
buttonLabelStyles = styles.buttonLabelPrimary; buttonLabelStyles = mode === TEXT
color = BaseTheme.palette.action01; ? styles.buttonLabelPrimaryText
mode = CONTAINED; : styles.buttonLabelPrimary;
color = mode === CONTAINED && BaseTheme.palette.action01;
} else if (type === SECONDARY) { } else if (type === SECONDARY) {
buttonLabelStyles = styles.buttonLabelSecondary; buttonLabelStyles = styles.buttonLabelSecondary;
color = BaseTheme.palette.action02; color = mode === CONTAINED && BaseTheme.palette.action02;
mode = CONTAINED;
} else if (type === DESTRUCTIVE) { } else if (type === DESTRUCTIVE) {
color = BaseTheme.palette.actionDanger; buttonLabelStyles = mode === TEXT
buttonLabelStyles = styles.buttonLabelDestructive; ? styles.buttonLabelDestructiveText
mode = CONTAINED; : styles.buttonLabelDestructive;
color = mode === CONTAINED && BaseTheme.palette.actionDanger;
} else { } else {
color = buttonColor; color = buttonColor;
buttonLabelStyles = styles.buttonLabel; buttonLabelStyles = styles.buttonLabel;
@ -65,15 +72,17 @@ const Button: React.FC<IProps> = ({
} }
if (type === TERTIARY) { if (type === TERTIARY) {
buttonLabelStyles if (useRippleColor && disabled) {
= disabled ? styles.buttonLabelTertiaryDisabled : styles.buttonLabelTertiary; buttonLabelStyles = styles.buttonLabelTertiaryDisabled;
}
buttonLabelStyles = styles.buttonLabelTertiary;
return ( return (
<TouchableRipple <TouchableRipple
accessibilityLabel = { accessibilityLabel } accessibilityLabel = { accessibilityLabel }
disabled = { disabled } disabled = { disabled }
onPress = { onPress } onPress = { onPress }
rippleColor = { BaseTheme.palette.action03Active } rippleColor = { rippleColor }
style = { [ style = { [
buttonStyles, buttonStyles,
style style

View File

@ -36,7 +36,7 @@ const IconButton: React.FC<IIconButtonProps> = ({
iconButtonContainerStyles = styles.iconButtonContainerSecondary; iconButtonContainerStyles = styles.iconButtonContainerSecondary;
rippleColor = BaseTheme.palette.action02; rippleColor = BaseTheme.palette.action02;
} else if (type === TERTIARY) { } else if (type === TERTIARY) {
color = BaseTheme.palette.icon01; color = iconColor;
iconButtonContainerStyles = styles.iconButtonContainer; iconButtonContainerStyles = styles.iconButtonContainer;
rippleColor = BaseTheme.palette.action03; rippleColor = BaseTheme.palette.action03;
} else { } else {

View File

@ -42,6 +42,11 @@ export default {
color: BaseTheme.palette.text01 color: BaseTheme.palette.text01
}, },
buttonLabelPrimaryText: {
...buttonLabel,
color: BaseTheme.palette.action01
},
buttonLabelSecondary: { buttonLabelSecondary: {
...buttonLabel, ...buttonLabel,
color: BaseTheme.palette.text04 color: BaseTheme.palette.text04
@ -52,6 +57,11 @@ export default {
color: BaseTheme.palette.text01 color: BaseTheme.palette.text01
}, },
buttonLabelDestructiveText: {
...buttonLabel,
color: BaseTheme.palette.actionDanger
},
buttonLabelTertiary: { buttonLabelTertiary: {
...buttonLabel, ...buttonLabel,
color: BaseTheme.palette.text01, color: BaseTheme.palette.text01,

View File

@ -13,6 +13,8 @@ export enum BUTTON_TYPES {
*/ */
export const BUTTON_MODES: { export const BUTTON_MODES: {
CONTAINED: 'contained'; CONTAINED: 'contained';
TEXT: 'text';
} = { } = {
CONTAINED: 'contained' CONTAINED: 'contained',
TEXT: 'text'
}; };

View File

@ -1,3 +1,11 @@
/* eslint-disable lines-around-comment, max-len */
import { navigate }
// @ts-ignore
from '../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
// @ts-ignore
import { screen } from '../mobile/navigation/routes';
import { OPEN_CHAT } from './actionTypes'; import { OPEN_CHAT } from './actionTypes';
export * from './actions.any'; export * from './actions.any';
@ -6,13 +14,19 @@ export * from './actions.any';
* Displays the chat panel. * Displays the chat panel.
* *
* @param {Object} participant - The recipient for the private chat. * @param {Object} participant - The recipient for the private chat.
* @param {boolean} disablePolls - Checks if polls are disabled.
* *
* @returns {{ * @returns {{
* participant: Participant, * participant: participant,
* type: OPEN_CHAT * type: OPEN_CHAT
* }} * }}
*/ */
export function openChat(participant: Object) { export function openChat(participant: Object, disablePolls: boolean) {
if (disablePolls) {
navigate(screen.conference.chat);
}
navigate(screen.conference.chatandpolls.main);
return { return {
participant, participant,
type: OPEN_CHAT type: OPEN_CHAT

View File

@ -1,7 +1,6 @@
// @ts-expect-error // @ts-ignore
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout'; import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
import { IStore } from '../app/types'; import { IStore } from '../app/types';
import { getParticipantById } from '../base/participants/functions';
import { OPEN_CHAT } from './actionTypes'; import { OPEN_CHAT } from './actionTypes';
import { closeChat } from './actions.any'; import { closeChat } from './actions.any';
@ -26,27 +25,6 @@ export function openChat(participant?: Object) {
}; };
} }
/**
* Displays the chat panel for a participant identified by an id.
*
* @param {string} id - The id of the participant.
* @returns {{
* participant: Participant,
* type: OPEN_CHAT
* }}
*/
export function openChatById(id: string) {
return function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
const participant = getParticipantById(getState(), id);
return dispatch({
participant,
type: OPEN_CHAT
});
};
}
/** /**
* Toggles display of the chat panel. * Toggles display of the chat panel.
* *

View File

@ -4,7 +4,7 @@ import { IconMessage, IconReply } from '../../../base/icons';
import { getParticipantById } from '../../../base/participants'; import { getParticipantById } from '../../../base/participants';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { handleLobbyChatInitialized, openChat } from '../../../chat/actions'; import { handleLobbyChatInitialized, openChat } from '../../../chat/actions.native';
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef'; import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes'; import { screen } from '../../../mobile/navigation/routes';

View File

@ -24,7 +24,6 @@ import {
import { CalleeInfoContainer } from '../../../invite'; import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video'; import { LargeVideo } from '../../../large-video';
import { startKnocking } from '../../../lobby/actions.any'; import { startKnocking } from '../../../lobby/actions.any';
import { KnockingParticipantList } from '../../../lobby/components/native';
import { getIsLobbyVisible } from '../../../lobby/functions'; import { getIsLobbyVisible } from '../../../lobby/functions';
import { navigate } import { navigate }
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef'; from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
@ -433,7 +432,13 @@ class Conference extends AbstractConference<Props, State> {
<LonelyMeetingExperience /> <LonelyMeetingExperience />
{ _shouldDisplayTileView || <><Filmstrip /><Toolbox /></> } {
_shouldDisplayTileView
|| <>
<Filmstrip />
<Toolbox />
</>
}
</View> </View>
<SafeAreaView <SafeAreaView
@ -463,10 +468,10 @@ class Conference extends AbstractConference<Props, State> {
<AlwaysOnLabels createOnPress = { this._createOnPress } /> <AlwaysOnLabels createOnPress = { this._createOnPress } />
</View> </View>
{ this._renderNotificationsContainer() } { this._renderNotificationsContainer() }
<KnockingParticipantList />
</SafeAreaView> </SafeAreaView>
<TestConnectionInfo /> <TestConnectionInfo />
{ this._renderConferenceNotification() } { this._renderConferenceNotification() }
{_shouldDisplayTileView && <Toolbox />} {_shouldDisplayTileView && <Toolbox />}

View File

@ -9,7 +9,7 @@ import Label from '../../../base/label/components/web/Label';
// eslint-disable-next-line lines-around-comment // eslint-disable-next-line lines-around-comment
// @ts-ignore // @ts-ignore
import { Tooltip } from '../../../base/tooltip'; import { Tooltip } from '../../../base/tooltip';
import { open as openParticipantsPane } from '../../../participants-pane/actions'; import { open as openParticipantsPane } from '../../../participants-pane/actions.web';
const useStyles = makeStyles()(theme => { const useStyles = makeStyles()(theme => {
return { return {

View File

@ -1,173 +0,0 @@
import React, { PureComponent } from 'react';
import { View } from 'react-native';
import { translate } from '../../../base/i18n/functions';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import { connect } from '../../../base/redux';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { handleLobbyChatInitialized } from '../../../chat/actions.native';
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import ParticipantItem
from '../../../participants-pane/components/native/ParticipantItem';
import { setKnockingParticipantApproval } from '../../actions.native';
import { getKnockingParticipants, getLobbyEnabled, showLobbyChatButton } from '../../functions';
import styles from './styles';
/**
* Props type of the component.
*/
export type Props = {
/**
* The list of participants.
*/
_participants: Array<Object>,
/**
* True if the list should be rendered.
*/
_visible: boolean,
/**
* True if the polls feature is disabled.
*/
_isPollsDisabled: boolean,
/**
* Returns true if the lobby chat button should be shown.
*/
_showChatButton: Function,
/**
* The Redux Dispatch function.
*/
dispatch: Function
};
/**
* Component to render a list for the actively knocking participants.
*/
class KnockingParticipantList extends PureComponent<Props> {
/**
* Instantiates a new component.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this._onRespondToParticipant = this._onRespondToParticipant.bind(this);
}
/**
* Implements {@code PureComponent#render}.
*
* @inheritdoc
*/
render() {
const { _participants, _visible, _showChatButton } = this.props;
if (!_visible) {
return null;
}
return (
<>
{ _participants.map(p => (
<View
key = { p.id }
style = { styles.knockingParticipantListEntry }>
<ParticipantItem
displayName = { p.name }
isKnockingParticipant = { true }
key = { p.id }
participantID = { p.id }>
<Button
labelKey = { 'lobby.admit' }
onClick = { this._onRespondToParticipant(p.id, true) }
style = { styles.lobbyButtonAdmit }
type = { BUTTON_TYPES.PRIMARY } />
{
_showChatButton(p)
? (
<Button
labelKey = { 'lobby.chat' }
onClick = { this._onInitializeLobbyChat(p.id) }
style = { styles.lobbyButtonChat }
type = { BUTTON_TYPES.SECONDARY } />
) : null
}
<Button
labelKey = { 'lobby.reject' }
onClick = { this._onRespondToParticipant(p.id, false) }
style = { styles.lobbyButtonReject }
type = { BUTTON_TYPES.DESTRUCTIVE } />
</ParticipantItem>
</View>
)) }
</>
);
}
_onRespondToParticipant: (string, boolean) => Function;
/**
* Function that constructs a callback for the response handler button.
*
* @param {string} id - The id of the knocking participant.
* @param {boolean} approve - The response for the knocking.
* @returns {Function}
*/
_onRespondToParticipant(id, approve) {
return () => {
this.props.dispatch(setKnockingParticipantApproval(id, approve));
};
}
_onInitializeLobbyChat: (string) => Function;
/**
* Function that constructs a callback for the lobby chat button.
*
* @param {string} id - The id of the knocking participant.
* @returns {Function}
*/
_onInitializeLobbyChat(id) {
return () => {
this.props.dispatch(handleLobbyChatInitialized(id));
if (this.props._isPollsDisabled) {
return navigate(screen.conference.chat);
}
navigate(screen.conference.chatandpolls.main);
};
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state): Object {
const lobbyEnabled = getLobbyEnabled(state);
const knockingParticipants = getKnockingParticipants(state);
const { disablePolls } = state['features/base/config'];
return {
_visible: lobbyEnabled && isLocalParticipantModerator(state),
_showChatButton: participant => showLobbyChatButton(participant)(state),
_isPollsDisabled: disablePolls,
// On mobile we only show a portion of the list for screen real estate reasons
_participants: knockingParticipants.slice(0, 2)
};
}
export default translate(connect(_mapStateToProps)(KnockingParticipantList));

View File

@ -1,5 +1,4 @@
// @flow // @flow
export { default as KnockingParticipantList } from './KnockingParticipantList';
export { default as LobbyScreen } from './LobbyScreen'; export { default as LobbyScreen } from './LobbyScreen';
export { default as LobbyChatScreen } from './LobbyChatScreen'; export { default as LobbyChatScreen } from './LobbyChatScreen';

View File

@ -1,22 +1,40 @@
/* eslint-disable lines-around-comment */
import i18n from 'i18next'; import i18n from 'i18next';
import { batch } from 'react-redux'; import { batch } from 'react-redux';
import { AnyAction } from 'redux'; import { AnyAction } from 'redux';
import { IStore } from '../app/types'; import { IStore } from '../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes'; import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../base/conference/actionTypes'; import {
CONFERENCE_FAILED,
CONFERENCE_JOINED
} from '../base/conference/actionTypes';
import { conferenceWillJoin } from '../base/conference/actions'; import { conferenceWillJoin } from '../base/conference/actions';
import { JitsiConferenceErrors, JitsiConferenceEvents } from '../base/lib-jitsi-meet'; import {
import { getFirstLoadableAvatarUrl, getParticipantDisplayName } from '../base/participants/functions'; JitsiConferenceErrors,
JitsiConferenceEvents
} from '../base/lib-jitsi-meet';
import {
getFirstLoadableAvatarUrl,
getParticipantDisplayName
} from '../base/participants/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry'; import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
import { isTestModeEnabled } from '../base/testing/functions';
import { handleLobbyChatInitialized, removeLobbyChatParticipant } from '../chat/actions.any';
import { import {
hideNotification, playSound,
showNotification registerSound,
} from '../notifications/actions'; unregisterSound
} from '../base/sounds/actions';
import { isTestModeEnabled } from '../base/testing/functions';
import { BUTTON_TYPES } from '../base/ui/constants.any';
// @ts-ignore
import { openChat } from '../chat/actions';
import {
handleLobbyChatInitialized,
removeLobbyChatParticipant
} from '../chat/actions.any';
import { hideNotification, showNotification } from '../notifications/actions';
import { import {
LOBBY_NOTIFICATION_ID, LOBBY_NOTIFICATION_ID,
NOTIFICATION_ICON, NOTIFICATION_ICON,
@ -28,7 +46,10 @@ import { open as openParticipantsPane } from '../participants-pane/actions';
import { getParticipantsPaneOpen } from '../participants-pane/functions'; import { getParticipantsPaneOpen } from '../participants-pane/functions';
import { shouldAutoKnock } from '../prejoin/functions'; import { shouldAutoKnock } from '../prejoin/functions';
import { KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED, KNOCKING_PARTICIPANT_LEFT } from './actionTypes'; import {
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
KNOCKING_PARTICIPANT_LEFT
} from './actionTypes';
import { import {
approveKnockingParticipant, approveKnockingParticipant,
hideLobbyScreen, hideLobbyScreen,
@ -47,6 +68,7 @@ import { getKnockingParticipants, showLobbyChatButton } from './functions';
import { KNOCKING_PARTICIPANT_FILE } from './sounds'; import { KNOCKING_PARTICIPANT_FILE } from './sounds';
import { IKnockingParticipant } from './types'; import { IKnockingParticipant } from './types';
MiddlewareRegistry.register(store => next => action => { MiddlewareRegistry.register(store => next => action => {
switch (action.type) { switch (action.type) {
case APP_WILL_MOUNT: case APP_WILL_MOUNT:
@ -112,7 +134,7 @@ StateListenerRegistry.register(
const isParticipantsPaneVisible = getParticipantsPaneOpen(getState()); const isParticipantsPaneVisible = getParticipantsPaneOpen(getState());
if (navigator.product === 'ReactNative' || isParticipantsPaneVisible) { if (isParticipantsPaneVisible || navigator.product === 'ReactNative') {
return; return;
} }
@ -121,57 +143,6 @@ StateListenerRegistry.register(
getState getState
}); });
let notificationTitle;
let customActionNameKey;
let customActionHandler;
let descriptionKey;
let icon;
const knockingParticipants = getKnockingParticipants(getState());
const firstParticipant = knockingParticipants[0];
const showChat = showLobbyChatButton(firstParticipant)(getState());
if (knockingParticipants.length > 1) {
descriptionKey = 'notify.participantsWantToJoin';
notificationTitle = i18n.t('notify.waitingParticipants', {
waitingParticipants: knockingParticipants.length
});
icon = NOTIFICATION_ICON.PARTICIPANTS;
customActionNameKey = [ 'notify.viewLobby' ];
customActionHandler = [ () => batch(() => {
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
dispatch(openParticipantsPane());
}) ];
} else {
descriptionKey = 'notify.participantWantsToJoin';
notificationTitle = firstParticipant.name;
icon = NOTIFICATION_ICON.PARTICIPANT;
customActionNameKey = [ 'lobby.admit', 'lobby.reject' ];
customActionHandler = [ () => batch(() => {
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
dispatch(approveKnockingParticipant(firstParticipant.id));
}),
() => batch(() => {
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
dispatch(rejectKnockingParticipant(firstParticipant.id));
}) ];
if (showChat) {
customActionNameKey.splice(1, 0, 'lobby.chat');
customActionHandler.splice(1, 0, () => batch(() => {
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
dispatch(handleLobbyChatInitialized(firstParticipant.id));
}));
}
}
dispatch(showNotification({
title: notificationTitle,
descriptionKey,
uid: LOBBY_NOTIFICATION_ID,
customActionNameKey,
customActionHandler,
icon
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
if (typeof APP !== 'undefined') { if (typeof APP !== 'undefined') {
APP.API.notifyKnockingParticipant({ APP.API.notifyKnockingParticipant({
id, id,
@ -227,16 +198,20 @@ function _handleLobbyNotification(store: IStore) {
let notificationTitle; let notificationTitle;
let customActionNameKey; let customActionNameKey;
let customActionHandler; let customActionHandler;
let customActionType;
let descriptionKey; let descriptionKey;
let icon; let icon;
if (knockingParticipants.length === 1) { if (knockingParticipants.length === 1) {
const firstParticipant = knockingParticipants[0]; const firstParticipant = knockingParticipants[0];
const { disablePolls } = getState()['features/base/config'];
const showChat = showLobbyChatButton(firstParticipant)(getState());
descriptionKey = 'notify.participantWantsToJoin'; descriptionKey = 'notify.participantWantsToJoin';
notificationTitle = firstParticipant.name; notificationTitle = firstParticipant.name;
icon = NOTIFICATION_ICON.PARTICIPANT; icon = NOTIFICATION_ICON.PARTICIPANT;
customActionNameKey = [ 'lobby.admit', 'lobby.reject' ]; customActionNameKey = [ 'lobby.admit', 'lobby.reject' ];
customActionType = [ BUTTON_TYPES.PRIMARY, BUTTON_TYPES.DESTRUCTIVE ];
customActionHandler = [ () => batch(() => { customActionHandler = [ () => batch(() => {
dispatch(hideNotification(LOBBY_NOTIFICATION_ID)); dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
dispatch(approveKnockingParticipant(firstParticipant.id)); dispatch(approveKnockingParticipant(firstParticipant.id));
@ -245,6 +220,18 @@ function _handleLobbyNotification(store: IStore) {
dispatch(hideNotification(LOBBY_NOTIFICATION_ID)); dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
dispatch(rejectKnockingParticipant(firstParticipant.id)); dispatch(rejectKnockingParticipant(firstParticipant.id));
}) ]; }) ];
// This checks if lobby chat button is available
// and, if so, it adds it to the customActionNameKey array
if (showChat) {
customActionNameKey.splice(1, 0, 'lobby.chat');
customActionType.splice(1, 0, BUTTON_TYPES.SECONDARY);
customActionHandler.splice(1, 0, () => batch(() => {
dispatch(handleLobbyChatInitialized(firstParticipant.id));
// @ts-ignore
dispatch(openChat(disablePolls));
}));
}
} else { } else {
descriptionKey = 'notify.participantsWantToJoin'; descriptionKey = 'notify.participantsWantToJoin';
notificationTitle = i18n.t('notify.waitingParticipants', { notificationTitle = i18n.t('notify.waitingParticipants', {
@ -252,16 +239,19 @@ function _handleLobbyNotification(store: IStore) {
}); });
icon = NOTIFICATION_ICON.PARTICIPANTS; icon = NOTIFICATION_ICON.PARTICIPANTS;
customActionNameKey = [ 'notify.viewLobby' ]; customActionNameKey = [ 'notify.viewLobby' ];
customActionType = [ BUTTON_TYPES.PRIMARY ];
customActionHandler = [ () => batch(() => { customActionHandler = [ () => batch(() => {
dispatch(hideNotification(LOBBY_NOTIFICATION_ID)); dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
dispatch(openParticipantsPane()); dispatch(openParticipantsPane());
}) ]; }) ];
} }
dispatch(showNotification({ dispatch(showNotification({
title: notificationTitle, title: notificationTitle,
descriptionKey, descriptionKey,
uid: LOBBY_NOTIFICATION_ID, uid: LOBBY_NOTIFICATION_ID,
customActionNameKey, customActionNameKey,
customActionType,
customActionHandler, customActionHandler,
icon icon
}, NOTIFICATION_TIMEOUT_TYPE.STICKY)); }, NOTIFICATION_TIMEOUT_TYPE.STICKY));

View File

@ -27,6 +27,11 @@ export type Props = {
*/ */
customActionNameKey: string[], customActionNameKey: string[],
/**
* The type of button.
*/
customActionType: ?string[],
/** /**
* The text to display in the body of the notification. If not passed * The text to display in the body of the notification. If not passed
* in, the passed in descriptionKey will be used. * in, the passed in descriptionKey will be used.

View File

@ -1,99 +0,0 @@
// @flow
import React from 'react';
import { Text, TouchableOpacity, View } from 'react-native';
import { translate } from '../../../base/i18n';
import { Icon, IconCloseLarge } from '../../../base/icons';
import { replaceNonUnicodeEmojis } from '../../../chat/functions';
import AbstractNotification, {
type Props
} from '../AbstractNotification';
import styles from './styles';
/**
* Default value for the maxLines prop.
*
* @type {number}
*/
const DEFAULT_MAX_LINES = 2;
/**
* Implements a React {@link Component} to display a notification.
*
* @augments Component
*/
class Notification extends AbstractNotification<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<View
pointerEvents = 'box-none'
style = { styles.notification }>
<View style = { styles.contentColumn }>
<View
pointerEvents = 'box-none'
style = { styles.notificationContent }>
{
this._renderContent()
}
</View>
</View>
<TouchableOpacity onPress = { this._onDismissed }>
<Icon
src = { IconCloseLarge }
style = { styles.dismissIcon } />
</TouchableOpacity>
</View>
);
}
/**
* Renders the notification's content. If the title or title key is present
* it will be just the title. Otherwise it will fallback to description.
*
* @returns {Array<ReactElement>}
* @private
*/
_renderContent() {
const { maxLines = DEFAULT_MAX_LINES, t, title, titleArguments, titleKey, concatText } = this.props;
const titleText = title || (titleKey && t(titleKey, titleArguments));
const description = this._getDescription();
const titleConcat = [];
if (concatText) {
titleConcat.push(titleText);
}
if (description && description.length) {
return [ ...titleConcat, ...description ].map((line, index) => (
<Text
key = { index }
numberOfLines = { maxLines }
style = { styles.contentText }>
{ replaceNonUnicodeEmojis(line) }
</Text>
));
}
return (
<Text
numberOfLines = { maxLines }
style = { styles.contentText } >
{ titleText }
</Text>
);
}
_getDescription: () => Array<string>;
_onDismissed: () => void;
}
export default translate(Notification);

View File

@ -0,0 +1,270 @@
/* eslint-disable lines-around-comment */
import React from 'react';
import { WithTranslation } from 'react-i18next';
import { Animated, Text, View } from 'react-native';
import { translate } from '../../../base/i18n/functions';
import {
Icon,
IconCloseLarge,
IconInfoCircle,
IconUsers,
IconWarning
// @ts-ignore
} from '../../../base/icons';
import { colors } from '../../../base/ui/Tokens';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
import Button from '../../../base/ui/components/native/Button';
import IconButton from '../../../base/ui/components/native/IconButton';
import { BUTTON_MODES, BUTTON_TYPES } from '../../../base/ui/constants.native';
import { replaceNonUnicodeEmojis } from '../../../chat/functions';
import { NOTIFICATION_ICON } from '../../constants';
import AbstractNotification, {
type Props as AbstractNotificationProps
// @ts-ignore
} from '../AbstractNotification';
// @ts-ignore
import styles from './styles';
/**
* Secondary colors for notification icons.
*
* @type {{error, info, normal, success, warning}}
*/
const ICON_COLOR = {
error: colors.error06,
normal: colors.primary06,
success: colors.success05,
warning: colors.warning05
};
type Props = AbstractNotificationProps & WithTranslation & {
_participants: ArrayLike<any>;
};
/**
* Implements a React {@link Component} to display a notification.
*
* @augments Component
*/
class Notification extends AbstractNotification<Props> {
/**
* Initializes a new {@code Notification} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
// @ts-ignore
this.state = {
notificationContainerAnimation: new Animated.Value(0)
};
}
/**
* Implements React's {@link Component#componentDidMount()}.
*
* @inheritdoc
*/
componentDidMount() {
Animated.timing(
// @ts-ignore
this.state.notificationContainerAnimation,
{
toValue: 1,
duration: 500,
useNativeDriver: true
})
.start();
}
/**
* Creates action button configurations for the notification based on
* notification appearance.
*
* @private
* @returns {Object[]}
*/
_mapAppearanceToButtons() {
const {
customActionHandler,
customActionNameKey,
customActionType
// @ts-ignore
} = this.props;
if (customActionNameKey?.length && customActionHandler?.length && customActionType?.length) {
return customActionNameKey?.map((customAction: string, index: number) => (
<Button
accessibilityLabel = { customAction }
key = { index }
labelKey = { customAction }
mode = { BUTTON_MODES.TEXT }
// eslint-disable-next-line react/jsx-no-bind
onClick = { () => {
if (customActionHandler[index]()) {
this._onDismissed();
}
} }
style = { styles.btn }
type = { customActionType[index] } />
));
}
return [];
}
/**
* Returns the Icon type component to be used, based on icon or appearance.
*
* @returns {ReactElement}
*/
_getIcon() {
const {
appearance,
icon
// @ts-ignore
} = this.props;
let src;
switch (icon || appearance) {
case NOTIFICATION_ICON.PARTICIPANT:
src = IconInfoCircle;
break;
case NOTIFICATION_ICON.PARTICIPANTS:
src = IconUsers;
break;
case NOTIFICATION_ICON.WARNING:
src = IconWarning;
break;
default:
src = IconInfoCircle;
break;
}
return src;
}
/**
* Creates an icon component depending on the configured notification
* appearance.
*
* @private
* @returns {ReactElement}
*/
_mapAppearanceToIcon() {
// @ts-ignore
const { appearance } = this.props;
// @ts-ignore
const color = ICON_COLOR[appearance];
return (
<View style = { styles.iconContainer }>
<Icon
color = { color }
size = { 24 }
src = { this._getIcon() } />
</View>
);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
// @ts-ignore
const { icon } = this.props;
const contentColumnStyles = icon === NOTIFICATION_ICON.PARTICIPANTS
? styles.contentColumn : styles.interactiveContentColumn;
return (
<Animated.View
pointerEvents = 'box-none'
style = { [
styles.notification,
{
// @ts-ignore
opacity: this.state.notificationContainerAnimation
}
] }>
<View style = { contentColumnStyles }>
{ this._mapAppearanceToIcon() }
<View
pointerEvents = 'box-none'
style = { styles.contentContainer }>
{ this._renderContent() }
</View>
<View style = { styles.btnContainer }>
{ this._mapAppearanceToButtons() }
</View>
</View>
<IconButton
color = { BaseTheme.palette.icon04 }
onPress = { this._onDismissed }
src = { IconCloseLarge }
type = { BUTTON_TYPES.TERTIARY } />
</Animated.View>
);
}
/**
* Renders the notification's content. If the title or title key is present
* it will be just the title. Otherwise it will fallback to description.
*
* @returns {Array<ReactElement>}
* @private
*/
_renderContent() {
// @ts-ignore
const { icon, t, title, titleArguments, titleKey } = this.props;
const titleText = title || (titleKey && t(titleKey, titleArguments));
const description = this._getDescription();
const descriptionStyles = icon === NOTIFICATION_ICON.PARTICIPANTS
? styles.contentTextInteractive : styles.contentText;
if (description?.length) {
return (
<>
<Text style = { styles.contentTextTitle }>
{ titleText }
</Text>
{
description.map((line, index) => (
<Text
key = { index }
style = { descriptionStyles }>
{ replaceNonUnicodeEmojis(line) }
</Text>
))
}
</>
);
}
return (
<Text style = { styles.contentTextTitle }>
{ titleText }
</Text>
);
}
_getDescription: () => Array<string>;
_onDismissed: () => void;
}
// @ts-ignore
export default translate(Notification);

View File

@ -1,14 +1,13 @@
// @flow // @flow
import React, { Component } from 'react'; import React, { Component } from 'react';
import { View } from 'react-native';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import { hideNotification } from '../../actions'; import { hideNotification } from '../../actions';
import { areThereNotifications } from '../../functions'; import { areThereNotifications } from '../../functions';
import Notification from './Notification'; import Notification from './Notification';
import styles from './styles';
type Props = { type Props = {
@ -21,12 +20,7 @@ type Props = {
/** /**
* Invoked to update the redux store in order to remove notifications. * Invoked to update the redux store in order to remove notifications.
*/ */
dispatch: Function, dispatch: Function
/**
* Any custom styling applied to the notifications container.
*/
style: Object
}; };
/** /**
@ -65,7 +59,7 @@ class NotificationsContainer extends Component<Props> {
} }
/** /**
* Sets a timeout for the first notification (if applicable). * Sets a timeout (if applicable).
* *
* @inheritdoc * @inheritdoc
*/ */
@ -173,25 +167,22 @@ class NotificationsContainer extends Component<Props> {
render() { render() {
const { _notifications } = this.props; const { _notifications } = this.props;
// Currently the native container displays only the topmost notification return (
const theNotification = _notifications[0]; <>
{
if (!theNotification) { _notifications.map((notification, index) => {
return null; const { props, uid } = notification;
}
return ( return (
<View
pointerEvents = 'box-none'
style = { [
styles.notificationContainer,
this.props.style
] } >
<Notification <Notification
{ ...theNotification.props } { ...props }
key = { index }
onDismissed = { this._onDismissed } onDismissed = { this._onDismissed }
uid = { theNotification.uid } /> uid = { uid } />
</View> );
})
}
</>
); );
} }

View File

@ -1,6 +1,17 @@
// @flow // @flow
import { BoxModel, ColorPalette } from '../../../base/styles'; import BaseTheme from '../../../base/ui/components/BaseTheme.native';
const contentColumn = {
flex: 1,
flexDirection: 'column',
marginLeft: BaseTheme.spacing[2]
};
const contentText = {
color: BaseTheme.palette.text04,
marginLeft: BaseTheme.spacing[6]
};
/** /**
* The styles of the React {@code Components} of the feature notifications. * The styles of the React {@code Components} of the feature notifications.
@ -10,52 +21,84 @@ export default {
/** /**
* The content (left) column of the notification. * The content (left) column of the notification.
*/ */
interactiveContentColumn: {
...contentColumn
},
contentColumn: { contentColumn: {
justifyContent: 'center', ...contentColumn,
flex: 1, justifyContent: 'center'
flexDirection: 'column',
paddingLeft: 1.5 * BoxModel.padding
}, },
/** /**
* Test style of the notification. * Test style of the notification.
*/ */
contentContainer: {
marginTop: BaseTheme.spacing[2]
},
contentText: { contentText: {
alignSelf: 'flex-start', ...contentText,
color: ColorPalette.white marginVertical: BaseTheme.spacing[1]
},
contentTextInteractive: {
...contentText,
marginTop: BaseTheme.spacing[1]
},
contentTextTitle: {
...contentText,
fontWeight: 'bold',
marginTop: BaseTheme.spacing[1]
}, },
/** /**
* Dismiss icon style. * Dismiss icon style.
*/ */
dismissIcon: { dismissIcon: {
color: ColorPalette.white, color: BaseTheme.palette.icon04,
fontSize: 20, fontSize: 20
padding: 1.5 * BoxModel.padding
}, },
/**
* Outermost view of a single notification.
*/
notification: { notification: {
backgroundColor: '#768898', display: 'flex',
backgroundColor: BaseTheme.palette.ui12,
borderRadius: BaseTheme.shape.borderRadius,
flexDirection: 'row', flexDirection: 'row',
minHeight: 48, maxHeight: 104,
marginTop: 0.5 * BoxModel.margin height: 'auto',
}, marginBottom: BaseTheme.spacing[3],
marginHorizontal: BaseTheme.spacing[2]
/**
* Outermost container of a list of notifications.
*/
notificationContainer: {
flexGrow: 0,
justifyContent: 'flex-end'
}, },
/** /**
* Wrapper for the message. * Wrapper for the message.
*/ */
notificationContent: { notificationContent: {
flexDirection: 'column' alignItems: 'center',
flexDirection: 'row'
},
participantName: {
color: BaseTheme.palette.text04,
overflow: 'hidden'
},
iconContainer: {
left: BaseTheme.spacing[1],
position: 'absolute',
top: BaseTheme.spacing[2]
},
btn: {
marginLeft: BaseTheme.spacing[4]
},
btnContainer: {
display: 'flex',
flexDirection: 'row',
marginLeft: BaseTheme.spacing[1]
} }
}; };

View File

@ -89,7 +89,6 @@ MiddlewareRegistry.register(store => next => action => {
switch (action.type) { switch (action.type) {
case CLEAR_NOTIFICATIONS: { case CLEAR_NOTIFICATIONS: {
if (navigator.product !== 'ReactNative') {
const _notifications = getNotifications(state); const _notifications = getNotifications(state);
for (const notification of _notifications) { for (const notification of _notifications) {
@ -101,11 +100,9 @@ MiddlewareRegistry.register(store => next => action => {
} }
} }
timers.clear(); timers.clear();
}
break; break;
} }
case SHOW_NOTIFICATION: { case SHOW_NOTIFICATION: {
if (navigator.product !== 'ReactNative') {
if (timers.has(action.uid)) { if (timers.has(action.uid)) {
const timer = timers.get(action.uid); const timer = timers.get(action.uid);
@ -114,16 +111,13 @@ MiddlewareRegistry.register(store => next => action => {
} }
createTimeoutId(action, dispatch); createTimeoutId(action, dispatch);
}
break; break;
} }
case HIDE_NOTIFICATION: { case HIDE_NOTIFICATION: {
if (navigator.product !== 'ReactNative') {
const timer = timers.get(action.uid); const timer = timers.get(action.uid);
clearTimeout(timer); clearTimeout(timer);
timers.delete(action.uid); timers.delete(action.uid);
}
break; break;
} }
case PARTICIPANT_JOINED: { case PARTICIPANT_JOINED: {

View File

@ -90,12 +90,25 @@ ReducerRegistry.register<INotificationsState>('features/notifications',
* queue. * queue.
*/ */
function _insertNotificationByPriority(notifications: INotification[], notification: INotification) { function _insertNotificationByPriority(notifications: INotification[], notification: INotification) {
// Create a copy to avoid mutation.
const copyOfNotifications = notifications.slice();
// Get the index of any queued notification that has the same id as the new notification
let insertAtLocation = copyOfNotifications.findIndex(
(queuedNotification: INotification) =>
queuedNotification?.uid === notification?.uid
);
if (insertAtLocation !== -1) {
copyOfNotifications.splice(insertAtLocation, 1, notification);
return copyOfNotifications;
}
const newNotificationPriority const newNotificationPriority
= NOTIFICATION_TYPE_PRIORITIES[notification.props.appearance ?? ''] || 0; = NOTIFICATION_TYPE_PRIORITIES[notification.props.appearance ?? ''] || 0;
// Default to putting the new notification at the end of the queue.
let insertAtLocation = notifications.length;
// Find where to insert the new notification based on priority. Do not // Find where to insert the new notification based on priority. Do not
// insert at the front of the queue so that the user can finish acting on // insert at the front of the queue so that the user can finish acting on
// any notification currently being read. // any notification currently being read.
@ -111,9 +124,6 @@ function _insertNotificationByPriority(notifications: INotification[], notificat
} }
} }
// Create a copy to avoid mutation and insert the notification.
const copyOfNotifications = notifications.slice();
copyOfNotifications.splice(insertAtLocation, 0, notification); copyOfNotifications.splice(insertAtLocation, 0, notification);
return copyOfNotifications; return copyOfNotifications;

View File

@ -5,6 +5,7 @@ export interface INotificationProps {
concatText?: boolean; concatText?: boolean;
customActionHandler?: Function[]; customActionHandler?: Function[];
customActionNameKey?: string[]; customActionNameKey?: string[];
customActionType?: string[];
description?: string | React.ReactNode; description?: string | React.ReactNode;
descriptionArguments?: Object; descriptionArguments?: Object;
descriptionKey?: string; descriptionKey?: string;

View File

@ -1,7 +1,4 @@
import { import { PARTICIPANTS_PANE_CLOSE } from './actionTypes';
PARTICIPANTS_PANE_CLOSE,
PARTICIPANTS_PANE_OPEN
} from './actionTypes';
/** /**
* Action to close the participants pane. * Action to close the participants pane.
@ -13,14 +10,3 @@ export const close = () => {
type: PARTICIPANTS_PANE_CLOSE type: PARTICIPANTS_PANE_CLOSE
}; };
}; };
/**
* Action to open the participants pane.
*
* @returns {Object}
*/
export const open = () => {
return {
type: PARTICIPANTS_PANE_OPEN
};
};

View File

@ -1,6 +1,11 @@
/* eslint-disable lines-around-comment */ /* eslint-disable lines-around-comment */
import { IStore } from '../app/types'; import { IStore } from '../app/types';
import { openSheet } from '../base/dialog/actions'; import { openSheet } from '../base/dialog/actions';
import { navigate }
// @ts-ignore
from '../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
// @ts-ignore
import { screen } from '../mobile/navigation/routes';
// @ts-ignore // @ts-ignore
import { SharedVideoMenu } from '../video-menu'; import { SharedVideoMenu } from '../video-menu';
// @ts-ignore // @ts-ignore
@ -11,7 +16,7 @@ import ConnectionStatusComponent
// @ts-ignore // @ts-ignore
import RemoteVideoMenu from '../video-menu/components/native/RemoteVideoMenu'; import RemoteVideoMenu from '../video-menu/components/native/RemoteVideoMenu';
import { SET_VOLUME } from './actionTypes'; import { PARTICIPANTS_PANE_OPEN, SET_VOLUME } from './actionTypes';
import RoomParticipantMenu from './components/native/RoomParticipantMenu'; import RoomParticipantMenu from './components/native/RoomParticipantMenu';
export * from './actions.any'; export * from './actions.any';
@ -88,3 +93,16 @@ export function showRoomParticipantMenu(room: Object, participantJid: string, pa
participantJid, participantJid,
participantName }); participantName });
} }
/**
* Action to open the participants pane.
*
* @returns {Object}
*/
export const open = () => {
navigate(screen.conference.participants);
return {
type: PARTICIPANTS_PANE_OPEN
};
};

View File

@ -1 +1,14 @@
import { PARTICIPANTS_PANE_OPEN } from './actionTypes';
export * from './actions.any'; export * from './actions.any';
/**
* Action to open the participants pane.
*
* @returns {Object}
*/
export const open = () => {
return {
type: PARTICIPANTS_PANE_OPEN
};
};

View File

@ -35,7 +35,6 @@ const ContextMenuLobbyParticipantReject = ({ participant: p }: Props) => {
<View <View
style = { styles.contextMenuItemSectionAvatar }> style = { styles.contextMenuItemSectionAvatar }>
<Avatar <Avatar
className = 'participant-avatar'
participantId = { p.id } participantId = { p.id }
size = { 24 } /> size = { 24 } />
<Text style = { styles.contextMenuItemName }> <Text style = { styles.contextMenuItemName }>

View File

@ -95,7 +95,6 @@ function ParticipantItem({
onPress = { onPress } onPress = { onPress }
style = { styles.participantContent }> style = { styles.participantContent }>
<Avatar <Avatar
className = 'participant-avatar'
displayName = { displayName } displayName = { displayName }
participantId = { participantID } participantId = { participantID }
size = { 32 } /> size = { 32 } />

View File

@ -1,7 +1,7 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { handleLobbyChatInitialized } from '../chat/actions.any'; import { handleLobbyChatInitialized } from '../chat/actions.web';
import { approveKnockingParticipant, rejectKnockingParticipant } from '../lobby/actions.web'; import { approveKnockingParticipant, rejectKnockingParticipant } from '../lobby/actions.web';
interface IDrawerParticipant { interface IDrawerParticipant {

View File

@ -53,7 +53,7 @@ import { NoiseSuppressionButton } from '../../../noise-suppression/components';
import { import {
close as closeParticipantsPane, close as closeParticipantsPane,
open as openParticipantsPane open as openParticipantsPane
} from '../../../participants-pane/actions'; } from '../../../participants-pane/actions.web';
// @ts-ignore // @ts-ignore
import { ParticipantsPaneButton } from '../../../participants-pane/components/web'; import { ParticipantsPaneButton } from '../../../participants-pane/components/web';
import { getParticipantsPaneOpen } from '../../../participants-pane/functions'; import { getParticipantsPaneOpen } from '../../../participants-pane/functions';

View File

@ -8,7 +8,7 @@ import { IconMessage } from '../../../base/icons';
import { getParticipantById } from '../../../base/participants'; import { getParticipantById } from '../../../base/participants';
import { connect } from '../../../base/redux'; import { connect } from '../../../base/redux';
import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem'; import ContextMenuItem from '../../../base/ui/components/web/ContextMenuItem';
import { openChat } from '../../../chat/'; import { openChat } from '../../../chat/actions.web';
import { import {
type Props as AbstractProps type Props as AbstractProps
} from '../../../chat/components/web/PrivateMessageButton'; } from '../../../chat/components/web/PrivateMessageButton';