mirror of https://github.com/jitsi/jitsi-meet
Move some styles from SCSS to JSS Convert some files to TS Implement redesignpull/12823/head
parent
e3166e6faa
commit
8e1d96cc48
@ -1,21 +1,19 @@ |
||||
// @flow
|
||||
|
||||
import punycode from 'punycode'; |
||||
import React, { Component } from 'react'; |
||||
import React, { Component, ReactNode } from 'react'; |
||||
import ReactLinkify from 'react-linkify'; |
||||
|
||||
type Props = { |
||||
interface IProps { |
||||
|
||||
/** |
||||
* The children of the component. |
||||
*/ |
||||
children: React$Node |
||||
}; |
||||
children: ReactNode; |
||||
} |
||||
|
||||
/** |
||||
* Implements a react wrapper for the react-linkify component. |
||||
*/ |
||||
export default class Linkify extends Component<Props> { |
||||
export default class Linkify extends Component<IProps> { |
||||
/** |
||||
* Implements {@Component#render}. |
||||
* |
@ -1,131 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
import { translate } from '../../../base/i18n'; |
||||
import Message from '../../../base/react/components/web/Message'; |
||||
import { connect } from '../../../base/redux'; |
||||
import { MESSAGE_TYPE_LOCAL } from '../../constants'; |
||||
import AbstractChatMessage, { type Props } from '../AbstractChatMessage'; |
||||
|
||||
import PrivateMessageButton from './PrivateMessageButton'; |
||||
|
||||
/** |
||||
* Renders a single chat message. |
||||
*/ |
||||
class ChatMessage extends AbstractChatMessage<Props> { |
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { message, t, knocking } = this.props; |
||||
|
||||
return ( |
||||
<div |
||||
className = 'chatmessage-wrapper' |
||||
id = { this.props.message.messageId } |
||||
tabIndex = { -1 }> |
||||
<div |
||||
className = { `chatmessage ${message.privateMessage ? 'privatemessage' : ''} ${ |
||||
message.lobbyChat && !knocking ? 'lobbymessage' : ''}` }>
|
||||
<div className = 'replywrapper'> |
||||
<div className = 'messagecontent'> |
||||
{ this.props.showDisplayName && this._renderDisplayName() } |
||||
<div className = 'usermessage'> |
||||
<span className = 'sr-only'> |
||||
{ this.props.message.displayName === this.props.message.recipient |
||||
? t('chat.messageAccessibleTitleMe') |
||||
: t('chat.messageAccessibleTitle', |
||||
{ user: this.props.message.displayName }) } |
||||
</span> |
||||
<Message text = { this._getMessageText() } /> |
||||
</div> |
||||
{ (message.privateMessage || (message.lobbyChat && !knocking)) |
||||
&& this._renderPrivateNotice() } |
||||
</div> |
||||
{ (message.privateMessage || (message.lobbyChat && !knocking)) |
||||
&& message.messageType !== MESSAGE_TYPE_LOCAL |
||||
&& ( |
||||
<div |
||||
className = { `messageactions ${ |
||||
message.lobbyChat ? 'lobbychatmessageactions' : ''}` }>
|
||||
<PrivateMessageButton |
||||
isLobbyMessage = { message.lobbyChat } |
||||
participantID = { message.id } |
||||
reply = { true } |
||||
showLabel = { false } /> |
||||
</div> |
||||
) } |
||||
</div> |
||||
</div> |
||||
{ this.props.showTimestamp && this._renderTimestamp() } |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
_getFormattedTimestamp: () => string; |
||||
|
||||
_getMessageText: () => string; |
||||
|
||||
_getPrivateNoticeMessage: () => string; |
||||
|
||||
/** |
||||
* Renders the display name of the sender. |
||||
* |
||||
* @returns {React$Element<*>} |
||||
*/ |
||||
_renderDisplayName() { |
||||
return ( |
||||
<div |
||||
aria-hidden = { true } |
||||
className = 'display-name'> |
||||
{ this.props.message.displayName } |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Renders the message privacy notice. |
||||
* |
||||
* @returns {React$Element<*>} |
||||
*/ |
||||
_renderPrivateNotice() { |
||||
return ( |
||||
<div className = 'privatemessagenotice'> |
||||
{ this._getPrivateNoticeMessage() } |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Renders the time at which the message was sent. |
||||
* |
||||
* @returns {React$Element<*>} |
||||
*/ |
||||
_renderTimestamp() { |
||||
return ( |
||||
<div className = 'timestamp'> |
||||
{ this._getFormattedTimestamp() } |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps part of the Redux store to the props of this component. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @returns {Props} |
||||
*/ |
||||
function _mapStateToProps(state: Object): $Shape<Props> { |
||||
const { knocking } = state['features/lobby']; |
||||
|
||||
return { |
||||
knocking |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(ChatMessage)); |
@ -0,0 +1,223 @@ |
||||
import { Theme } from '@mui/material'; |
||||
import { withStyles } from '@mui/styles'; |
||||
import clsx from 'clsx'; |
||||
import React from 'react'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { IReduxState } from '../../../app/types'; |
||||
import { translate } from '../../../base/i18n/functions'; |
||||
import Message from '../../../base/react/components/web/Message'; |
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web'; |
||||
import { MESSAGE_TYPE_LOCAL } from '../../constants'; |
||||
import AbstractChatMessage, { IProps as AbstractProps } from '../AbstractChatMessage'; |
||||
|
||||
import PrivateMessageButton from './PrivateMessageButton'; |
||||
|
||||
interface IProps extends AbstractProps { |
||||
|
||||
classes: any; |
||||
|
||||
type: string; |
||||
} |
||||
|
||||
const styles = (theme: Theme) => { |
||||
return { |
||||
chatMessageWrapper: { |
||||
maxWidth: '100%' |
||||
}, |
||||
|
||||
chatMessage: { |
||||
display: 'inline-flex', |
||||
padding: '12px', |
||||
backgroundColor: theme.palette.ui02, |
||||
borderRadius: '4px 12px 12px 12px', |
||||
boxSizing: 'border-box' as const, |
||||
maxWidth: '100%', |
||||
marginTop: '4px', |
||||
|
||||
'&.privatemessage': { |
||||
backgroundColor: theme.palette.support05 |
||||
}, |
||||
|
||||
'&.local': { |
||||
backgroundColor: theme.palette.ui04, |
||||
borderRadius: '12px 4px 12px 12px', |
||||
|
||||
'&.privatemessage': { |
||||
backgroundColor: theme.palette.support05 |
||||
} |
||||
}, |
||||
|
||||
'&.error': { |
||||
backgroundColor: 'rgb(215, 121, 118)', |
||||
borderRadius: 0, |
||||
fontWeight: 100 |
||||
}, |
||||
|
||||
'&.lobbymessage': { |
||||
backgroundColor: theme.palette.support05 |
||||
} |
||||
}, |
||||
|
||||
replyWrapper: { |
||||
display: 'flex', |
||||
flexDirection: 'row' as const, |
||||
alignItems: 'center' |
||||
}, |
||||
|
||||
messageContent: { |
||||
maxWidth: '100%', |
||||
overflow: 'hidden', |
||||
flex: 1 |
||||
}, |
||||
|
||||
replyButtonContainer: { |
||||
display: 'flex', |
||||
alignItems: 'flex-start', |
||||
height: '100%' |
||||
}, |
||||
|
||||
replyButton: { |
||||
padding: '2px' |
||||
}, |
||||
|
||||
displayName: { |
||||
...withPixelLineHeight(theme.typography.labelBold), |
||||
color: theme.palette.text02, |
||||
whiteSpace: 'nowrap', |
||||
textOverflow: 'ellipsis', |
||||
overflow: 'hidden', |
||||
marginBottom: theme.spacing(1) |
||||
}, |
||||
|
||||
userMessage: { |
||||
...withPixelLineHeight(theme.typography.bodyShortRegular), |
||||
color: theme.palette.text01, |
||||
whiteSpace: 'pre-wrap' |
||||
}, |
||||
|
||||
privateMessageNotice: { |
||||
...withPixelLineHeight(theme.typography.labelRegular), |
||||
color: theme.palette.text02, |
||||
marginTop: theme.spacing(1) |
||||
}, |
||||
|
||||
timestamp: { |
||||
...withPixelLineHeight(theme.typography.labelRegular), |
||||
color: theme.palette.text03, |
||||
marginTop: theme.spacing(1) |
||||
} |
||||
}; |
||||
}; |
||||
|
||||
/** |
||||
* Renders a single chat message. |
||||
*/ |
||||
class ChatMessage extends AbstractChatMessage<IProps> { |
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { message, t, knocking, classes, type } = this.props; |
||||
|
||||
return ( |
||||
<div |
||||
className = { classes.chatMessageWrapper } |
||||
id = { this.props.message.messageId } |
||||
tabIndex = { -1 }> |
||||
<div |
||||
className = { clsx('chatmessage', classes.chatMessage, type, |
||||
message.privateMessage && 'privatemessage', |
||||
message.lobbyChat && !knocking && 'lobbymessage') }> |
||||
<div className = { classes.replyWrapper }> |
||||
<div className = { clsx('messagecontent', classes.messageContent) }> |
||||
{ this.props.showDisplayName && this._renderDisplayName() } |
||||
<div className = { clsx('usermessage', classes.userMessage) }> |
||||
<span className = 'sr-only'> |
||||
{ this.props.message.displayName === this.props.message.recipient |
||||
? t('chat.messageAccessibleTitleMe') |
||||
: t('chat.messageAccessibleTitle', |
||||
{ user: this.props.message.displayName }) } |
||||
</span> |
||||
<Message text = { this._getMessageText() } /> |
||||
</div> |
||||
{ (message.privateMessage || (message.lobbyChat && !knocking)) |
||||
&& this._renderPrivateNotice() } |
||||
</div> |
||||
{ (message.privateMessage || (message.lobbyChat && !knocking)) |
||||
&& message.messageType !== MESSAGE_TYPE_LOCAL |
||||
&& ( |
||||
<div |
||||
className = { classes.replyButtonContainer }> |
||||
<PrivateMessageButton |
||||
isLobbyMessage = { message.lobbyChat } |
||||
participantID = { message.id } /> |
||||
</div> |
||||
) } |
||||
</div> |
||||
</div> |
||||
{ this.props.showTimestamp && this._renderTimestamp() } |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Renders the display name of the sender. |
||||
* |
||||
* @returns {React$Element<*>} |
||||
*/ |
||||
_renderDisplayName() { |
||||
return ( |
||||
<div |
||||
aria-hidden = { true } |
||||
className = { clsx('display-name', this.props.classes.displayName) }> |
||||
{ this.props.message.displayName } |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Renders the message privacy notice. |
||||
* |
||||
* @returns {React$Element<*>} |
||||
*/ |
||||
_renderPrivateNotice() { |
||||
return ( |
||||
<div className = { this.props.classes.privateMessageNotice }> |
||||
{ this._getPrivateNoticeMessage() } |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Renders the time at which the message was sent. |
||||
* |
||||
* @returns {React$Element<*>} |
||||
*/ |
||||
_renderTimestamp() { |
||||
return ( |
||||
<div className = { clsx('timestamp', this.props.classes.timestamp) }> |
||||
{ this._getFormattedTimestamp() } |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps part of the Redux store to the props of this component. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @returns {IProps} |
||||
*/ |
||||
function _mapStateToProps(state: IReduxState) { |
||||
const { knocking } = state['features/lobby']; |
||||
|
||||
return { |
||||
knocking |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(withStyles(styles)(ChatMessage))); |
@ -1,59 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
|
||||
import ChatMessage from './ChatMessage'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Additional CSS classes to apply to the root element. |
||||
*/ |
||||
className: string, |
||||
|
||||
/** |
||||
* The messages to display as a group. |
||||
*/ |
||||
messages: Array<Object>, |
||||
}; |
||||
|
||||
/** |
||||
* Displays a list of chat messages. Will show only the display name for the |
||||
* first chat message and the timestamp for the last chat message. |
||||
* |
||||
* @augments React.Component |
||||
*/ |
||||
class ChatMessageGroup extends Component<Props> { |
||||
static defaultProps = { |
||||
className: '' |
||||
}; |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { className, messages } = this.props; |
||||
|
||||
const messagesLength = messages.length; |
||||
|
||||
if (!messagesLength) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<div className = { `chat-message-group ${className}` }> |
||||
{ messages.map((message, i) => ( |
||||
<ChatMessage |
||||
key = { i } |
||||
message = { message } |
||||
showDisplayName = { i === 0 } |
||||
showTimestamp = { i === messages.length - 1 } /> |
||||
))} |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default ChatMessageGroup; |
@ -0,0 +1,81 @@ |
||||
import clsx from 'clsx'; |
||||
import React from 'react'; |
||||
import { makeStyles } from 'tss-react/mui'; |
||||
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import Avatar from '../../../base/avatar/components/Avatar'; |
||||
import { IMessage } from '../../reducer'; |
||||
|
||||
import ChatMessage from './ChatMessage'; |
||||
|
||||
interface IProps { |
||||
|
||||
/** |
||||
* Additional CSS classes to apply to the root element. |
||||
*/ |
||||
className: string; |
||||
|
||||
/** |
||||
* The messages to display as a group. |
||||
*/ |
||||
messages: Array<IMessage>; |
||||
} |
||||
|
||||
const useStyles = makeStyles()(theme => { |
||||
return { |
||||
messageGroup: { |
||||
display: 'flex', |
||||
flexDirection: 'column' |
||||
}, |
||||
|
||||
groupContainer: { |
||||
display: 'flex', |
||||
|
||||
'&.local': { |
||||
justifyContent: 'flex-end', |
||||
|
||||
'& .avatar': { |
||||
display: 'none' |
||||
} |
||||
} |
||||
}, |
||||
|
||||
avatar: { |
||||
margin: `${theme.spacing(1)} ${theme.spacing(2)} ${theme.spacing(3)} 0`, |
||||
position: 'sticky', |
||||
top: 0 |
||||
} |
||||
}; |
||||
}); |
||||
|
||||
|
||||
const ChatMessageGroup = ({ className = '', messages }: IProps) => { |
||||
const { classes } = useStyles(); |
||||
const messagesLength = messages.length; |
||||
|
||||
if (!messagesLength) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<div className = { clsx(classes.groupContainer, className) }> |
||||
<Avatar |
||||
className = { clsx(classes.avatar, 'avatar') } |
||||
participantId = { messages[0].id } |
||||
size = { 32 } /> |
||||
<div className = { `${classes.messageGroup} chat-message-group ${className}` }> |
||||
{messages.map((message, i) => ( |
||||
<ChatMessage |
||||
key = { i } |
||||
message = { message } |
||||
showDisplayName = { i === 0 } |
||||
showTimestamp = { i === messages.length - 1 } |
||||
type = { className } /> |
||||
))} |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
export default ChatMessageGroup; |
@ -1,99 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import { CHAT_ENABLED, getFeatureFlag } from '../../../base/flags'; |
||||
import { translate } from '../../../base/i18n'; |
||||
import { IconMessage, IconReply } from '../../../base/icons'; |
||||
import { getParticipantById } from '../../../base/participants'; |
||||
import { connect } from '../../../base/redux'; |
||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; |
||||
import { handleLobbyChatInitialized, openChat } from '../../actions'; |
||||
|
||||
export type Props = AbstractButtonProps & { |
||||
|
||||
/** |
||||
* True if the message is a lobby chat message. |
||||
*/ |
||||
isLobbyMessage: boolean, |
||||
|
||||
/** |
||||
* The ID of the participant that the message is to be sent. |
||||
*/ |
||||
participantID: string, |
||||
|
||||
/** |
||||
* True if the button is rendered as a reply button. |
||||
*/ |
||||
reply: boolean, |
||||
|
||||
/** |
||||
* Function to be used to translate i18n labels. |
||||
*/ |
||||
t: Function, |
||||
|
||||
/** |
||||
* The Redux dispatch function. |
||||
*/ |
||||
dispatch: Function, |
||||
|
||||
/** |
||||
* The participant object retrieved from Redux. |
||||
*/ |
||||
_participant: Object, |
||||
}; |
||||
|
||||
/** |
||||
* Class to render a button that initiates the sending of a private message through chet. |
||||
*/ |
||||
class PrivateMessageButton extends AbstractButton<Props, any> { |
||||
accessibilityLabel = 'toolbar.accessibilityLabel.privateMessage'; |
||||
icon = IconMessage; |
||||
label = 'toolbar.privateMessage'; |
||||
toggledIcon = IconReply; |
||||
|
||||
/** |
||||
* Handles clicking / pressing the button, and kicks the participant. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_handleClick() { |
||||
const { _participant, participantID, dispatch, isLobbyMessage } = this.props; |
||||
|
||||
if (isLobbyMessage) { |
||||
dispatch(handleLobbyChatInitialized(participantID)); |
||||
} else { |
||||
dispatch(openChat(_participant)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Helper function to be implemented by subclasses, which must return a |
||||
* {@code boolean} value indicating if this button is toggled or not. |
||||
* |
||||
* @protected |
||||
* @returns {boolean} |
||||
*/ |
||||
_isToggled() { |
||||
return this.props.reply; |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* Maps part of the Redux store to the props of this component. |
||||
* |
||||
* @param {Object} state - The Redux state. |
||||
* @param {Props} ownProps - The own props of the component. |
||||
* @returns {Props} |
||||
*/ |
||||
export function _mapStateToProps(state: Object, ownProps: Props): $Shape<Props> { |
||||
const enabled = getFeatureFlag(state, CHAT_ENABLED, true); |
||||
const { visible = enabled } = ownProps; |
||||
|
||||
return { |
||||
_participant: getParticipantById(state, ownProps.participantID), |
||||
visible |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(PrivateMessageButton)); |
@ -0,0 +1,72 @@ |
||||
import React, { useCallback } from 'react'; |
||||
import { useDispatch, useSelector } from 'react-redux'; |
||||
import { makeStyles } from 'tss-react/mui'; |
||||
|
||||
import { IReduxState } from '../../../app/types'; |
||||
import { CHAT_ENABLED } from '../../../base/flags/constants'; |
||||
import { getFeatureFlag } from '../../../base/flags/functions'; |
||||
import { IconReply } from '../../../base/icons/svg'; |
||||
import { getParticipantById } from '../../../base/participants/functions'; |
||||
import Button from '../../../base/ui/components/web/Button'; |
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.any'; |
||||
import { handleLobbyChatInitialized, openChat } from '../../actions.web'; |
||||
|
||||
interface IProps { |
||||
|
||||
/** |
||||
* True if the message is a lobby chat message. |
||||
*/ |
||||
isLobbyMessage: boolean; |
||||
|
||||
/** |
||||
* The ID of the participant that the message is to be sent. |
||||
*/ |
||||
participantID: string; |
||||
|
||||
/** |
||||
* Whether the button should be visible or not. |
||||
*/ |
||||
visible?: boolean; |
||||
} |
||||
|
||||
const useStyles = makeStyles()(theme => { |
||||
return { |
||||
replyButton: { |
||||
padding: '2px', |
||||
|
||||
'&:hover': { |
||||
backgroundColor: theme.palette.action03 |
||||
} |
||||
} |
||||
}; |
||||
}); |
||||
|
||||
const PrivateMessageButton = ({ participantID, isLobbyMessage, visible }: IProps) => { |
||||
const { classes } = useStyles(); |
||||
const dispatch = useDispatch(); |
||||
const participant = useSelector((state: IReduxState) => getParticipantById(state, participantID)); |
||||
const isVisible = useSelector((state: IReduxState) => getFeatureFlag(state, CHAT_ENABLED, true)) ?? visible; |
||||
|
||||
const handleClick = useCallback(() => { |
||||
if (isLobbyMessage) { |
||||
dispatch(handleLobbyChatInitialized(participantID)); |
||||
} else { |
||||
dispatch(openChat(participant)); |
||||
} |
||||
}, []); |
||||
|
||||
if (!isVisible) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<Button |
||||
accessibilityLabel = 'toolbar.accessibilityLabel.privateMessage' |
||||
className = { classes.replyButton } |
||||
icon = { IconReply } |
||||
onClick = { handleClick } |
||||
type = { BUTTON_TYPES.TERTIARY } /> |
||||
); |
||||
}; |
||||
|
||||
export default PrivateMessageButton; |
Loading…
Reference in new issue