diff --git a/src/components/chat/index.js b/src/components/chat/index.js index a524738c..0a2ac4d8 100644 --- a/src/components/chat/index.js +++ b/src/components/chat/index.js @@ -38,6 +38,9 @@ const Chat = () => { } }; + const scrollTo = (element) => { + handleAutoScroll(firstNode.current, element, POSITIONS.TOP, config.align); + } useEffect(() => { if (!interaction.current) { if (config.scroll) { @@ -58,6 +61,7 @@ const Chat = () => { > setRef(node, index)} /> diff --git a/src/components/chat/messages/index.js b/src/components/chat/messages/index.js index d3098b07..d2cc1a59 100644 --- a/src/components/chat/messages/index.js +++ b/src/components/chat/messages/index.js @@ -20,6 +20,7 @@ const defaultProps = { const Messages = ({ currentIndex, + scrollTo, setRef, }) => { @@ -33,15 +34,28 @@ const Messages = ({ switch (type) { case ID.USERS: + const indexOfMessageToBeReplied = (item.replyToMessageId) + ? storage.messages.findIndex((message) => message.id === item.replyToMessageId) : -1; + const messageToBeReplied = (indexOfMessageToBeReplied !== -1) + ? storage.messages[indexOfMessageToBeReplied] + : null; return ( - setRef(node, index)}> + setRef(node, index)}> diff --git a/src/components/chat/messages/index.scss b/src/components/chat/messages/index.scss index 05564d21..6d7ede65 100644 --- a/src/components/chat/messages/index.scss +++ b/src/components/chat/messages/index.scss @@ -1,12 +1,35 @@ @import 'src/styles/sizes'; :root { - --message-box-shadow-color: var(--gray-light); + --gray-light-color: var(--gray-light); + --gray-lightest-color: var(--gray-lightest); + --highlight-color: var(--blue); +} + +.reply-tag { + background-color: var(--gray-lightest-color); + display: flex; + width: 90%; + padding: $padding-small; + margin-bottom: $margin-extra-small; + border-radius: .35rem; + align-items: center; + line-height: 1; +} + +.user-message-wrapper { + transition: background-color .5s ease; +} + +.highlight { + background-color: var(--highlight-color); } .message { box-sizing: border-box; display: flex; + flex-direction: column; + width: 100%; [dir="ltr"] & { @@ -17,6 +40,10 @@ padding: 0 $padding $padding 0; } + .main-content-wrapper { + display: flex; + } + .data { display: flex; flex-direction: column; @@ -47,6 +74,20 @@ } } + .edited-tag { + color: var(--gray-light-color); + font-style: italic; + font-size: 75%; + display: flex; + align-items: center; + margin-left: 0.5rem; + line-height: 1; + } + + .edited-label { + margin-left: 0.25rem; + } + .name { font-weight: var(--font-weight-semi-bold); overflow: hidden; @@ -95,7 +136,7 @@ &:focus, &:hover { - box-shadow: 0 0 0 $margin-extra-small var(--message-box-shadow-color); + box-shadow: 0 0 0 $margin-extra-small var(--gray-light-color); } } diff --git a/src/components/chat/messages/info.js b/src/components/chat/messages/info.js index a27d20f6..abd40dc1 100644 --- a/src/components/chat/messages/info.js +++ b/src/components/chat/messages/info.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Icon from 'components/utils/icon'; import { FormattedTime } from 'react-intl'; import cx from 'classnames'; import { getTimestampAsMilliseconds } from 'utils/data'; @@ -21,6 +22,7 @@ const Info = ({ active, name, timestamp, + edited, }) => { const milliseconds = getTimestampAsMilliseconds(timestamp); @@ -39,6 +41,13 @@ const Info = ({ value={milliseconds} /> + { edited + && ( + + + (Edited) + + )} ); }; diff --git a/src/components/chat/messages/message.js b/src/components/chat/messages/message.js index 6278fe87..6649bedd 100644 --- a/src/components/chat/messages/message.js +++ b/src/components/chat/messages/message.js @@ -5,6 +5,8 @@ import Info from './info'; import Margin from './margin'; import player from 'utils/player'; import './index.scss'; +import Reply from './reply'; +import ChatMessageReactions from './reactions'; const propTypes = { active: PropTypes.bool, @@ -38,7 +40,11 @@ const Message = ({ emphasized, icon, initials, + messageToBeReplied, + scrollTo, + edited, name, + reactions, timestamp, }) => { const handleOnClick = () => { @@ -47,24 +53,41 @@ const Message = ({ return (
- handleOnClick()} - /> -
- + handleOnClick()} /> -
- {children} +
+ + {messageToBeReplied && + + } +
+ {children} +
+
); }; diff --git a/src/components/chat/messages/reactions/index.js b/src/components/chat/messages/reactions/index.js new file mode 100644 index 00000000..2de7db16 --- /dev/null +++ b/src/components/chat/messages/reactions/index.js @@ -0,0 +1,35 @@ +import React from 'react'; +import cx from 'classnames'; +import './index.scss'; + +const sortByCount = (r1, r2) => r2.number - r1.number; + +const ChatMessageReactions = (props) => { + const { + reactions, + active, + } = props; + + if (reactions.length === 0) return null; + return ( +
+ {reactions.sort(sortByCount).map((details) => ( + + {details.emoji} + {details.count} + + ) + )} +
+ ); +}; + +export default ChatMessageReactions; diff --git a/src/components/chat/messages/reactions/index.scss b/src/components/chat/messages/reactions/index.scss new file mode 100644 index 00000000..1f37158b --- /dev/null +++ b/src/components/chat/messages/reactions/index.scss @@ -0,0 +1,34 @@ +@import 'src/styles/sizes'; + +:root { + --emoji-wrapper-border-color: var(--gray-lightest); + --emoji-wrapper-border-color-hover: var(--gray-lighter); +} + +.reactions-wrapper { + display: flex; + flex-wrap: wrap; + margin-top: 0.25rem; + user-select: none; +} + +.emoji-wrapper { + background: none; + border-radius: 1rem; + padding: 0.375rem 1rem; + line-height: 1; + display: flex; + flex-wrap: nowrap; + border: 1px solid var(--emoji-wrapper-border-color); + margin-right: 0.25rem; + + em-emoji { + [dir='ltr'] & { + margin-right: 0.25rem; + } + + [dir='rtl'] & { + margin-left: 0.25rem; + } + } +} \ No newline at end of file diff --git a/src/components/chat/messages/reply/index.js b/src/components/chat/messages/reply/index.js new file mode 100644 index 00000000..e1817873 --- /dev/null +++ b/src/components/chat/messages/reply/index.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; +import '../index.scss'; + +const propTypes = { + active: PropTypes.bool, + idToReference: PropTypes.string, + text: PropTypes.string, +}; + +const defaultProps = { + active: false, + idToReference: '', + text: '', +}; + +const Reply = ({ + active, + idToReference, + scrollTo, + text, +}) => { + + const handleClickReply = () => { + const messageReplied = document.getElementById(idToReference); + scrollTo(messageReplied); + messageReplied.classList.add('highlight') + setTimeout(() => + messageReplied.classList.remove('highlight'), 800 + ); + } + + return ( + + {text} + + ); +}; + +Reply.propTypes = propTypes; +Reply.defaultProps = defaultProps; + +// Checks the message active state +const areEqual = (prevProps, nextProps) => { + if (prevProps.active !== nextProps.active) return false; + + return true; +}; + +export default React.memo(Reply, areEqual); diff --git a/src/components/chat/messages/user/index.js b/src/components/chat/messages/user/index.js index a21a1053..2a5f2ee5 100644 --- a/src/components/chat/messages/user/index.js +++ b/src/components/chat/messages/user/index.js @@ -33,7 +33,11 @@ const User = ({ name, moderator, text, + edited, timestamp, + messageToBeReplied, + scrollTo, + reactions, }) => { return ( @@ -42,14 +46,19 @@ const User = ({ circle={!moderator} emphasized={emphasized} initials={initials} + edited={edited} name={name} + reactions={reactions} timestamp={timestamp} + messageToBeReplied={messageToBeReplied} + scrollTo={scrollTo} > + ); }; diff --git a/src/components/utils/icon/index.scss b/src/components/utils/icon/index.scss index 9d292002..ff19f741 100644 --- a/src/components/utils/icon/index.scss +++ b/src/components/utils/icon/index.scss @@ -37,6 +37,10 @@ content: "\e92a"; } +.icon-pen:before { + content: "\e925"; +} + .icon-notes:before { content: "\e94a"; } diff --git a/src/utils/builder.js b/src/utils/builder.js index 9d778b63..579f176b 100644 --- a/src/utils/builder.js +++ b/src/utils/builder.js @@ -454,14 +454,25 @@ const buildChat = result => { const emphasized = chat._chatEmphasizedText === 'true'; const moderator = chat._senderRole === ROLES.MODERATOR; + // Normalize reactions to always be an array + const reactionsList = chat.reactions ? convertToArray(chat.reactions.reaction) : []; + const reactions = reactionsList.map((messageReaction) => ({ + emoji: messageReaction._emoji, + count: messageReaction._count, + }), + ); return { clear, + id: chat._id, emphasized, hyperlink: message !== chat._message, initials, name: chat._name, message, moderator, + reactions, + replyToMessageId: chat._replyToMessageId, + lastEditedTimestamp: chat._lastEditedTimestamp, timestamp: parseFloat(chat._in), }; });