This commit is contained in:
222
react/features/notifications/components/native/Notification.tsx
Normal file
222
react/features/notifications/components/native/Notification.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import React, { useCallback, useContext, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Animated, Text, TextStyle, View, ViewStyle } from 'react-native';
|
||||
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import {
|
||||
IconCloseLarge,
|
||||
IconInfoCircle,
|
||||
IconUsers,
|
||||
IconWarning
|
||||
} from '../../../base/icons/svg';
|
||||
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 { CHAR_LIMIT } from '../../../chat/constants';
|
||||
import { replaceNonUnicodeEmojis } from '../../../chat/functions';
|
||||
import { NOTIFICATION_ICON, NOTIFICATION_TYPE } from '../../constants';
|
||||
import { INotificationProps } from '../../types';
|
||||
import { NotificationsTransitionContext } from '../NotificationsTransition';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
/**
|
||||
* Secondary colors for notification icons.
|
||||
*
|
||||
* @type {{error, info, normal, success, warning}}
|
||||
*/
|
||||
|
||||
const ICON_COLOR = {
|
||||
error: BaseTheme.palette.iconError,
|
||||
normal: BaseTheme.palette.iconNormal,
|
||||
success: BaseTheme.palette.iconSuccess,
|
||||
warning: BaseTheme.palette.iconWarning
|
||||
};
|
||||
|
||||
|
||||
export interface IProps extends INotificationProps {
|
||||
_participants: ArrayLike<any>;
|
||||
onDismissed: Function;
|
||||
}
|
||||
|
||||
const Notification = ({
|
||||
appearance = NOTIFICATION_TYPE.NORMAL,
|
||||
customActionHandler,
|
||||
customActionNameKey,
|
||||
customActionType,
|
||||
description,
|
||||
descriptionArguments,
|
||||
descriptionKey,
|
||||
icon,
|
||||
onDismissed,
|
||||
title,
|
||||
titleArguments,
|
||||
titleKey,
|
||||
uid
|
||||
}: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const notificationOpacityAnimation = useRef(new Animated.Value(0)).current;
|
||||
const { unmounting } = useContext(NotificationsTransitionContext);
|
||||
|
||||
useEffect(() => {
|
||||
Animated.timing(
|
||||
notificationOpacityAnimation,
|
||||
{
|
||||
toValue: 1,
|
||||
duration: 200,
|
||||
useNativeDriver: true
|
||||
})
|
||||
.start();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (unmounting.get(uid ?? '')) {
|
||||
Animated.timing(
|
||||
notificationOpacityAnimation,
|
||||
{
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true
|
||||
})
|
||||
.start();
|
||||
}
|
||||
}, [ unmounting ]);
|
||||
|
||||
const onDismiss = useCallback(() => {
|
||||
onDismissed(uid);
|
||||
}, [ onDismissed, uid ]);
|
||||
|
||||
const mapAppearanceToButtons = () => {
|
||||
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]()) {
|
||||
onDismiss();
|
||||
}
|
||||
} }
|
||||
style = { styles.btn }
|
||||
|
||||
// @ts-ignore
|
||||
type = { customActionType[index] } />
|
||||
));
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const getIcon = () => {
|
||||
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;
|
||||
};
|
||||
|
||||
const _getDescription = () => {
|
||||
const descriptionArray = [];
|
||||
|
||||
descriptionKey
|
||||
&& descriptionArray.push(t(descriptionKey, descriptionArguments));
|
||||
|
||||
description && descriptionArray.push(description);
|
||||
|
||||
return descriptionArray;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/no-multi-comp
|
||||
const _renderContent = () => {
|
||||
const titleText = title || (titleKey && t(titleKey, titleArguments));
|
||||
const descriptionArray = _getDescription();
|
||||
|
||||
if (descriptionArray?.length) {
|
||||
return (
|
||||
<>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.contentTextTitleDescription as TextStyle }>
|
||||
{ titleText }
|
||||
</Text>
|
||||
{
|
||||
descriptionArray.map((line, index) => (
|
||||
<Text
|
||||
key = { index }
|
||||
style = { styles.contentText }>
|
||||
{ line.length >= CHAR_LIMIT ? line : replaceNonUnicodeEmojis(line) }
|
||||
</Text>
|
||||
))
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.contentTextTitle as TextStyle }>
|
||||
{ titleText }
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
pointerEvents = 'box-none'
|
||||
style = { [
|
||||
_getDescription()?.length
|
||||
? styles.notificationWithDescription
|
||||
: styles.notification,
|
||||
{
|
||||
opacity: notificationOpacityAnimation
|
||||
}
|
||||
] as ViewStyle[] }>
|
||||
<View
|
||||
style = { (icon === NOTIFICATION_ICON.PARTICIPANTS
|
||||
? styles.contentColumn
|
||||
: styles.interactiveContentColumn) as ViewStyle }>
|
||||
<View style = { styles.iconContainer as ViewStyle }>
|
||||
<Icon
|
||||
color = { ICON_COLOR[appearance as keyof typeof ICON_COLOR] }
|
||||
size = { 24 }
|
||||
src = { getIcon() } />
|
||||
</View>
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.contentContainer }>
|
||||
{ _renderContent() }
|
||||
</View>
|
||||
<View style = { styles.btnContainer as ViewStyle }>
|
||||
{ mapAppearanceToButtons() }
|
||||
</View>
|
||||
</View>
|
||||
<IconButton
|
||||
color = { BaseTheme.palette.icon04 }
|
||||
onPress = { onDismiss }
|
||||
src = { IconCloseLarge }
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
</Animated.View>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default Notification;
|
||||
@@ -0,0 +1,239 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { Platform } from 'react-native';
|
||||
import { Edge, SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { hideNotification } from '../../actions';
|
||||
import { areThereNotifications } from '../../functions';
|
||||
import NotificationsTransition from '../NotificationsTransition';
|
||||
|
||||
import Notification from './Notification';
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The notifications to be displayed, with the first index being the
|
||||
* notification at the top and the rest shown below it in order.
|
||||
*/
|
||||
_notifications: Array<Object>;
|
||||
|
||||
/**
|
||||
* Invoked to update the redux store in order to remove notifications.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Whether or not the layout should change to support tile view mode.
|
||||
*/
|
||||
shouldDisplayTileView: boolean;
|
||||
|
||||
/**
|
||||
* Checks toolbox visibility.
|
||||
*/
|
||||
toolboxVisible: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays notifications and handles
|
||||
* automatic dismissal after a notification is shown for a defined timeout
|
||||
* period.
|
||||
*
|
||||
* @augments {Component}
|
||||
*/
|
||||
class NotificationsContainer extends Component<IProps> {
|
||||
|
||||
/**
|
||||
* A timeout id returned by setTimeout.
|
||||
*/
|
||||
_notificationDismissTimeout: any;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code NotificationsContainer} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
/**
|
||||
* The timeout set for automatically dismissing a displayed
|
||||
* notification. This value is set on the instance and not state to
|
||||
* avoid additional re-renders.
|
||||
*
|
||||
* @type {number|null}
|
||||
*/
|
||||
this._notificationDismissTimeout = null;
|
||||
|
||||
// Bind event handlers so they are only bound once for every instance.
|
||||
this._onDismissed = this._onDismissed.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a timeout (if applicable).
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidMount() {
|
||||
// Set the initial dismiss timeout (if any)
|
||||
// @ts-ignore
|
||||
this._manageDismissTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a timeout if the currently displayed notification has changed.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentDidUpdate(prevProps: IProps) {
|
||||
this._manageDismissTimeout(prevProps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets/clears the dismiss timeout for the top notification.
|
||||
*
|
||||
* @param {IProps} [prevProps] - The previous properties (if called from
|
||||
* {@code componentDidUpdate}).
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
_manageDismissTimeout(prevProps: IProps) {
|
||||
const { _notifications } = this.props;
|
||||
|
||||
if (_notifications.length) {
|
||||
const notification = _notifications[0];
|
||||
const previousNotification = prevProps?._notifications.length
|
||||
? prevProps._notifications[0] : undefined;
|
||||
|
||||
if (notification !== previousNotification) {
|
||||
this._clearNotificationDismissTimeout();
|
||||
|
||||
// @ts-ignore
|
||||
if (notification?.timeout) {
|
||||
|
||||
// @ts-ignore
|
||||
const { timeout, uid } = notification;
|
||||
|
||||
// @ts-ignore
|
||||
this._notificationDismissTimeout = setTimeout(() => {
|
||||
// Perform a no-op if a timeout is not specified.
|
||||
this._onDismissed(uid);
|
||||
}, timeout);
|
||||
}
|
||||
}
|
||||
} else if (this._notificationDismissTimeout) {
|
||||
// Clear timeout when all notifications are cleared (e.g external
|
||||
// call to clear them)
|
||||
this._clearNotificationDismissTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any dismissal timeout that is still active.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override componentWillUnmount() {
|
||||
this._clearNotificationDismissTimeout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the running notification dismiss timeout, if any.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_clearNotificationDismissTimeout() {
|
||||
this._notificationDismissTimeout && clearTimeout(this._notificationDismissTimeout);
|
||||
|
||||
this._notificationDismissTimeout = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an action to remove the notification from the redux store so it
|
||||
* stops displaying.
|
||||
*
|
||||
* @param {Object} uid - The id of the notification to be removed.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDismissed(uid: any) {
|
||||
const { _notifications } = this.props;
|
||||
|
||||
// Clear the timeout only if it's the top notification that's being
|
||||
// dismissed (the timeout is set only for the top one).
|
||||
// @ts-ignore
|
||||
if (!_notifications.length || _notifications[0].uid === uid) {
|
||||
this._clearNotificationDismissTimeout();
|
||||
}
|
||||
|
||||
this.props.dispatch(hideNotification(uid));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { _notifications, shouldDisplayTileView, toolboxVisible } = this.props;
|
||||
let notificationsContainerStyle;
|
||||
|
||||
if (shouldDisplayTileView) {
|
||||
|
||||
if (toolboxVisible) {
|
||||
notificationsContainerStyle = styles.withToolboxTileView;
|
||||
} else {
|
||||
notificationsContainerStyle = styles.withoutToolboxTileView;
|
||||
}
|
||||
|
||||
} else {
|
||||
notificationsContainerStyle
|
||||
= toolboxVisible ? styles.withToolbox : styles.withoutToolbox;
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
edges = { [ Platform.OS === 'ios' && 'bottom', 'left', 'right' ].filter(Boolean) as Edge[] }
|
||||
style = { notificationsContainerStyle as any }>
|
||||
<NotificationsTransition>
|
||||
{
|
||||
_notifications.map(notification => {
|
||||
// @ts-ignore
|
||||
const { props, uid } = notification;
|
||||
|
||||
return (
|
||||
<Notification
|
||||
{ ...props }
|
||||
key = { uid }
|
||||
onDismissed = { this._onDismissed }
|
||||
uid = { uid } />
|
||||
);
|
||||
})
|
||||
}
|
||||
</NotificationsTransition>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated NotificationsContainer's
|
||||
* props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
export function mapStateToProps(state: IReduxState) {
|
||||
const { notifications } = state['features/notifications'];
|
||||
const _visible = areThereNotifications(state);
|
||||
|
||||
return {
|
||||
_notifications: _visible ? notifications : []
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(NotificationsContainer);
|
||||
135
react/features/notifications/components/native/styles.ts
Normal file
135
react/features/notifications/components/native/styles.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
|
||||
const contentColumn = {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
paddingLeft: BaseTheme.spacing[2]
|
||||
};
|
||||
|
||||
const notification = {
|
||||
backgroundColor: BaseTheme.palette.ui10,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
borderLeftColor: BaseTheme.palette.link01Active,
|
||||
borderLeftWidth: BaseTheme.spacing[1],
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
marginVertical: BaseTheme.spacing[1],
|
||||
maxWidth: 416,
|
||||
width: '100%'
|
||||
};
|
||||
|
||||
/**
|
||||
* The styles of the React {@code Components} of the feature notifications.
|
||||
*/
|
||||
export default {
|
||||
|
||||
/**
|
||||
* The content (left) column of the notification.
|
||||
*/
|
||||
interactiveContentColumn: {
|
||||
...contentColumn
|
||||
},
|
||||
|
||||
contentColumn: {
|
||||
...contentColumn,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* Test style of the notification.
|
||||
*/
|
||||
|
||||
contentContainer: {
|
||||
paddingHorizontal: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
contentText: {
|
||||
color: BaseTheme.palette.text04,
|
||||
paddingLeft: BaseTheme.spacing[4],
|
||||
paddingTop: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
contentTextTitleDescription: {
|
||||
color: BaseTheme.palette.text04,
|
||||
fontWeight: 'bold',
|
||||
paddingLeft: BaseTheme.spacing[4],
|
||||
paddingTop: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
contentTextTitle: {
|
||||
color: BaseTheme.palette.text04,
|
||||
fontWeight: 'bold',
|
||||
paddingLeft: BaseTheme.spacing[4],
|
||||
paddingTop: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
/**
|
||||
* Dismiss icon style.
|
||||
*/
|
||||
dismissIcon: {
|
||||
color: BaseTheme.palette.icon04,
|
||||
fontSize: 20
|
||||
},
|
||||
|
||||
notification: {
|
||||
...notification
|
||||
},
|
||||
|
||||
notificationWithDescription: {
|
||||
...notification,
|
||||
paddingBottom: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
/**
|
||||
* Wrapper for the message.
|
||||
*/
|
||||
notificationContent: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
||||
participantName: {
|
||||
color: BaseTheme.palette.text04,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
|
||||
iconContainer: {
|
||||
position: 'absolute',
|
||||
left: BaseTheme.spacing[2],
|
||||
top: 12
|
||||
},
|
||||
|
||||
btn: {
|
||||
paddingLeft: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
btnContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
paddingLeft: BaseTheme.spacing[3],
|
||||
paddingTop: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
withToolbox: {
|
||||
bottom: 56,
|
||||
position: 'absolute',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
withToolboxTileView: {
|
||||
bottom: 56,
|
||||
position: 'absolute',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
withoutToolbox: {
|
||||
position: 'absolute',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
withoutToolboxTileView: {
|
||||
bottom: 0,
|
||||
position: 'absolute',
|
||||
width: '100%'
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user