feat(notifications): native UI updates (#12798)
* feat(notifications): native notifications UI updates
This commit is contained in:
parent
9fa426d97f
commit
f8af9c4fae
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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'
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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 />}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
// @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';
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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,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>
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -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 }>
|
||||||
|
|
|
@ -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 } />
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
Loading…
Reference in New Issue