mirror of https://github.com/jitsi/jitsi-meet
feat(notifications): native UI updates (#12798)
* feat(notifications): native notifications UI updatespull/12940/head jitsi-meet_8331
parent
9fa426d97f
commit
f8af9c4fae
@ -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)); |
@ -1,5 +1,4 @@ |
||||
// @flow
|
||||
|
||||
export { default as KnockingParticipantList } from './KnockingParticipantList'; |
||||
export { default as LobbyScreen } from './LobbyScreen'; |
||||
export { default as LobbyChatScreen } from './LobbyChatScreen'; |
||||
|
@ -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); |
@ -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); |
@ -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 |
||||
}; |
||||
}; |
||||
|
Loading…
Reference in new issue