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

View File

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

View File

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

View File

@ -13,6 +13,8 @@ export enum BUTTON_TYPES {
*/
export const BUTTON_MODES: {
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';
export * from './actions.any';
@ -6,13 +14,19 @@ export * from './actions.any';
* Displays the chat panel.
*
* @param {Object} participant - The recipient for the private chat.
* @param {boolean} disablePolls - Checks if polls are disabled.
*
* @returns {{
* participant: Participant,
* participant: participant,
* 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 {
participant,
type: OPEN_CHAT

View File

@ -1,7 +1,6 @@
// @ts-expect-error
// @ts-ignore
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
import { IStore } from '../app/types';
import { getParticipantById } from '../base/participants/functions';
import { OPEN_CHAT } from './actionTypes';
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.
*

View File

@ -4,7 +4,7 @@ import { IconMessage, IconReply } from '../../../base/icons';
import { getParticipantById } from '../../../base/participants';
import { connect } from '../../../base/redux';
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 { screen } from '../../../mobile/navigation/routes';

View File

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

View File

@ -9,7 +9,7 @@ import Label from '../../../base/label/components/web/Label';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
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 => {
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
export { default as KnockingParticipantList } from './KnockingParticipantList';
export { default as LobbyScreen } from './LobbyScreen';
export { default as LobbyChatScreen } from './LobbyChatScreen';

View File

@ -1,22 +1,40 @@
/* eslint-disable lines-around-comment */
import i18n from 'i18next';
import { batch } from 'react-redux';
import { AnyAction } from 'redux';
import { IStore } from '../app/types';
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 { JitsiConferenceErrors, JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { getFirstLoadableAvatarUrl, getParticipantDisplayName } from '../base/participants/functions';
import {
JitsiConferenceErrors,
JitsiConferenceEvents
} from '../base/lib-jitsi-meet';
import {
getFirstLoadableAvatarUrl,
getParticipantDisplayName
} from '../base/participants/functions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
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 {
hideNotification,
showNotification
} from '../notifications/actions';
playSound,
registerSound,
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 {
LOBBY_NOTIFICATION_ID,
NOTIFICATION_ICON,
@ -28,7 +46,10 @@ import { open as openParticipantsPane } from '../participants-pane/actions';
import { getParticipantsPaneOpen } from '../participants-pane/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 {
approveKnockingParticipant,
hideLobbyScreen,
@ -47,6 +68,7 @@ import { getKnockingParticipants, showLobbyChatButton } from './functions';
import { KNOCKING_PARTICIPANT_FILE } from './sounds';
import { IKnockingParticipant } from './types';
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case APP_WILL_MOUNT:
@ -112,7 +134,7 @@ StateListenerRegistry.register(
const isParticipantsPaneVisible = getParticipantsPaneOpen(getState());
if (navigator.product === 'ReactNative' || isParticipantsPaneVisible) {
if (isParticipantsPaneVisible || navigator.product === 'ReactNative') {
return;
}
@ -121,57 +143,6 @@ StateListenerRegistry.register(
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') {
APP.API.notifyKnockingParticipant({
id,
@ -227,16 +198,20 @@ function _handleLobbyNotification(store: IStore) {
let notificationTitle;
let customActionNameKey;
let customActionHandler;
let customActionType;
let descriptionKey;
let icon;
if (knockingParticipants.length === 1) {
const firstParticipant = knockingParticipants[0];
const { disablePolls } = getState()['features/base/config'];
const showChat = showLobbyChatButton(firstParticipant)(getState());
descriptionKey = 'notify.participantWantsToJoin';
notificationTitle = firstParticipant.name;
icon = NOTIFICATION_ICON.PARTICIPANT;
customActionNameKey = [ 'lobby.admit', 'lobby.reject' ];
customActionType = [ BUTTON_TYPES.PRIMARY, BUTTON_TYPES.DESTRUCTIVE ];
customActionHandler = [ () => batch(() => {
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
dispatch(approveKnockingParticipant(firstParticipant.id));
@ -245,6 +220,18 @@ function _handleLobbyNotification(store: IStore) {
dispatch(hideNotification(LOBBY_NOTIFICATION_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 {
descriptionKey = 'notify.participantsWantToJoin';
notificationTitle = i18n.t('notify.waitingParticipants', {
@ -252,16 +239,19 @@ function _handleLobbyNotification(store: IStore) {
});
icon = NOTIFICATION_ICON.PARTICIPANTS;
customActionNameKey = [ 'notify.viewLobby' ];
customActionType = [ BUTTON_TYPES.PRIMARY ];
customActionHandler = [ () => batch(() => {
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
dispatch(openParticipantsPane());
}) ];
}
dispatch(showNotification({
title: notificationTitle,
descriptionKey,
uid: LOBBY_NOTIFICATION_ID,
customActionNameKey,
customActionType,
customActionHandler,
icon
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));

View File

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

View File

@ -1,6 +1,17 @@
// @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.
@ -10,52 +21,84 @@ export default {
/**
* The content (left) column of the notification.
*/
interactiveContentColumn: {
...contentColumn
},
contentColumn: {
justifyContent: 'center',
flex: 1,
flexDirection: 'column',
paddingLeft: 1.5 * BoxModel.padding
...contentColumn,
justifyContent: 'center'
},
/**
* Test style of the notification.
*/
contentContainer: {
marginTop: BaseTheme.spacing[2]
},
contentText: {
alignSelf: 'flex-start',
color: ColorPalette.white
...contentText,
marginVertical: BaseTheme.spacing[1]
},
contentTextInteractive: {
...contentText,
marginTop: BaseTheme.spacing[1]
},
contentTextTitle: {
...contentText,
fontWeight: 'bold',
marginTop: BaseTheme.spacing[1]
},
/**
* Dismiss icon style.
*/
dismissIcon: {
color: ColorPalette.white,
fontSize: 20,
padding: 1.5 * BoxModel.padding
color: BaseTheme.palette.icon04,
fontSize: 20
},
/**
* Outermost view of a single notification.
*/
notification: {
backgroundColor: '#768898',
display: 'flex',
backgroundColor: BaseTheme.palette.ui12,
borderRadius: BaseTheme.shape.borderRadius,
flexDirection: 'row',
minHeight: 48,
marginTop: 0.5 * BoxModel.margin
},
/**
* Outermost container of a list of notifications.
*/
notificationContainer: {
flexGrow: 0,
justifyContent: 'flex-end'
maxHeight: 104,
height: 'auto',
marginBottom: BaseTheme.spacing[3],
marginHorizontal: BaseTheme.spacing[2]
},
/**
* Wrapper for the message.
*/
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) {
case CLEAR_NOTIFICATIONS: {
if (navigator.product !== 'ReactNative') {
const _notifications = getNotifications(state);
for (const notification of _notifications) {
@ -101,11 +100,9 @@ MiddlewareRegistry.register(store => next => action => {
}
}
timers.clear();
}
break;
}
case SHOW_NOTIFICATION: {
if (navigator.product !== 'ReactNative') {
if (timers.has(action.uid)) {
const timer = timers.get(action.uid);
@ -114,16 +111,13 @@ MiddlewareRegistry.register(store => next => action => {
}
createTimeoutId(action, dispatch);
}
break;
}
case HIDE_NOTIFICATION: {
if (navigator.product !== 'ReactNative') {
const timer = timers.get(action.uid);
clearTimeout(timer);
timers.delete(action.uid);
}
break;
}
case PARTICIPANT_JOINED: {

View File

@ -90,12 +90,25 @@ ReducerRegistry.register<INotificationsState>('features/notifications',
* queue.
*/
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
= 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
// insert at the front of the queue so that the user can finish acting on
// 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);
return copyOfNotifications;

View File

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

View File

@ -1,7 +1,4 @@
import {
PARTICIPANTS_PANE_CLOSE,
PARTICIPANTS_PANE_OPEN
} from './actionTypes';
import { PARTICIPANTS_PANE_CLOSE } from './actionTypes';
/**
* Action to close the participants pane.
@ -13,14 +10,3 @@ export const 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 */
import { IStore } from '../app/types';
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
import { SharedVideoMenu } from '../video-menu';
// @ts-ignore
@ -11,7 +16,7 @@ import ConnectionStatusComponent
// @ts-ignore
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';
export * from './actions.any';
@ -88,3 +93,16 @@ export function showRoomParticipantMenu(room: Object, participantJid: string, pa
participantJid,
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';
/**
* 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
style = { styles.contextMenuItemSectionAvatar }>
<Avatar
className = 'participant-avatar'
participantId = { p.id }
size = { 24 } />
<Text style = { styles.contextMenuItemName }>

View File

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

View File

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

View File

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

View File

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