init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View 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;

View File

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

View 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%'
}
};