Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: highlight search messages #884

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/markups/src/elements/BoldSpan.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PlainSpan from './PlainSpan';
import ItalicSpan from './ItalicSpan';
import StrikeSpan from './StrikeSpan';
import LinkSpan from './LinkSpan';
import HighlightText from './highlightText';

const BoldSpan = ({ contents }) => (
<strong>
Expand All @@ -17,6 +18,7 @@ const BoldSpan = ({ contents }) => (

case 'ITALIC':
return <ItalicSpan key={index} contents={content.value} />;

case 'LINK':
return (
<LinkSpan
Expand All @@ -30,6 +32,9 @@ const BoldSpan = ({ contents }) => (
/>
);

case 'HIGHLIGHT_TEXT':
return <HighlightText key={index} contents={content.value} />;

default:
return null;
}
Expand Down
29 changes: 27 additions & 2 deletions packages/markups/src/elements/CodeElement.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import PlainSpan from './PlainSpan';
import HighlightText from './highlightText';
import { InlineElementsStyles } from './elements.styles';

const CodeElement = ({ contents }) => {
const styles = InlineElementsStyles();

const contentsArray = Array.isArray(contents) ? contents : [contents];
return (
<code css={styles.inlineElement}>
<PlainSpan contents={contents.value} />
{contentsArray.map((content, index) => {
switch (content.type) {
case 'PLAIN_TEXT':
return <PlainSpan key={index} contents={content.value} />;

case 'HIGHLIGHT_TEXT':
return <HighlightText key={index} contents={content.value} />;

default:
return null;
}
})}
</code>
);
};

export default CodeElement;

CodeElement.propTypes = {
contents: PropTypes.any,
contents: PropTypes.oneOfType([
PropTypes.arrayOf(
PropTypes.shape({
type: PropTypes.string.isRequired,
value: PropTypes.any.isRequired,
})
),
PropTypes.shape({
type: PropTypes.string.isRequired,
value: PropTypes.any.isRequired,
}),
]).isRequired,
};
5 changes: 5 additions & 0 deletions packages/markups/src/elements/InlineElements.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ChannelMention from '../mentions/ChannelMention';
import ColorElement from './ColorElement';
import LinkSpan from './LinkSpan';
import UserMention from '../mentions/UserMention';
import HighlightText from './highlightText';

const InlineElements = ({ contents }) =>
contents.map((content, index) => {
Expand Down Expand Up @@ -53,6 +54,10 @@ const InlineElements = ({ contents }) =>
}
/>
);

case 'HIGHLIGHT_TEXT':
return <HighlightText key={index} contents={content.value} />;

default:
return null;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/markups/src/elements/ItalicSpan.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import PlainSpan from './PlainSpan';
import BoldSpan from './BoldSpan';
import StrikeSpan from './StrikeSpan';
import HighlightText from './highlightText';

const ItalicSpan = ({ contents }) => (
<em>
Expand All @@ -17,6 +18,9 @@ const ItalicSpan = ({ contents }) => (
case 'BOLD':
return <BoldSpan key={index} contents={content.value} />;

case 'HIGHLIGHT_TEXT':
return <HighlightText key={index} contents={content.value} />;

default:
return null;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/markups/src/elements/LinkSpan.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PlainSpan from './PlainSpan';
import StrikeSpan from './StrikeSpan';
import ItalicSpan from './ItalicSpan';
import BoldSpan from './BoldSpan';
import HighlightText from './highlightText';

const getBaseURI = () => {
if (document.baseURI) {
Expand Down Expand Up @@ -44,6 +45,9 @@ const LinkSpan = ({ href, label }) => {
case 'BOLD':
return <BoldSpan key={index} contents={content.value} />;

case 'HIGHLIGHT_TEXT':
return <HighlightText key={index} contents={content.value} />;

default:
return null;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/markups/src/elements/StrikeSpan.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import PlainSpan from './PlainSpan';
import BoldSpan from './BoldSpan';
import ItalicSpan from './ItalicSpan';
import HighlightText from './highlightText';

const StrikeSpan = ({ contents }) => (
<del>
Expand All @@ -17,6 +18,9 @@ const StrikeSpan = ({ contents }) => (
case 'BOLD':
return <BoldSpan key={index} contents={content.value} />;

case 'HIGHLIGHT_TEXT':
return <HighlightText key={index} contents={content.value} />;

default:
return null;
}
Expand Down
7 changes: 7 additions & 0 deletions packages/markups/src/elements/elements.styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ export const EmojiStyles = {
`,
};

export const useHighlightTextStyles = {
highlight: css`
background-color: yellow;
font-weight: bold;
`,
};

const useMentionStyles = (contents, username) => {
const { theme } = useTheme();
const styles = {
Expand Down
16 changes: 16 additions & 0 deletions packages/markups/src/elements/highlightText.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Box } from '@embeddedchat/ui-elements';
import { useHighlightTextStyles as styles } from './elements.styles';

const HighlightText = ({ contents }) => (
<Box is="span" css={styles.highlight}>
{contents}
</Box>
);

HighlightText.propTypes = {
contents: PropTypes.string.isRequired,
};

export default HighlightText;
151 changes: 151 additions & 0 deletions packages/react/src/lib/highlightUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
const highlightText = (text, searchTerm) => {
const parts = text.split(new RegExp(`(${searchTerm})`, 'gi'));
const result = [];

parts.forEach((part) => {
if (part.toLowerCase() === searchTerm.toLowerCase()) {
result.push({ type: 'HIGHLIGHT_TEXT', value: part });
} else {
result.push({ type: 'PLAIN_TEXT', value: part });
}
});

return result;
};

export function highlightSearchTerm(messagesArr, searchedWords) {
const searchTerms = Array.isArray(searchedWords)
? searchedWords
: [searchedWords];

return messagesArr.map((message) => {
if (message.md) {
message.md = message.md.map((paragraphBlock) => {
if (paragraphBlock.type === 'PARAGRAPH') {
const updatedValue = paragraphBlock.value.reduce(
(accumulatedValue, content) => {
if (content.type === 'PLAIN_TEXT') {
let updatedContent = content.value;
searchTerms.forEach((searchTerm) => {
if (searchTerm && updatedContent) {
accumulatedValue.push(
...highlightText(updatedContent, searchTerm)
);
updatedContent = '';
}
});

if (updatedContent) {
accumulatedValue.push({
type: 'PLAIN_TEXT',
value: updatedContent,
});
}
} else if (content.type === 'LINK') {
const updatedLabel = content.value.label.reduce(
(labelAccumulatedValue, labelContent) => {
if (labelContent.type === 'PLAIN_TEXT') {
let updatedLabelContent = labelContent.value;
searchTerms.forEach((searchTerm) => {
if (searchTerm && updatedLabelContent) {
labelAccumulatedValue.push(
...highlightText(updatedLabelContent, searchTerm)
);
updatedLabelContent = '';
}
});

if (updatedLabelContent) {
labelAccumulatedValue.push({
type: 'PLAIN_TEXT',
value: updatedLabelContent,
});
}
} else {
labelAccumulatedValue.push(labelContent);
}

return labelAccumulatedValue;
},
[]
);

accumulatedValue.push({
...content,
value: {
...content.value,
label: updatedLabel,
},
});
} else if (content.type === 'INLINE_CODE') {
let updatedContent = content.value.value;
searchTerms.forEach((searchTerm) => {
if (searchTerm && updatedContent) {
accumulatedValue.push(
...highlightText(updatedContent, searchTerm)
);
updatedContent = '';
}
});

if (updatedContent) {
accumulatedValue.push({
type: 'INLINE_CODE',
value: { type: 'PLAIN_TEXT', value: updatedContent },
});
}
} else if (['STRIKE', 'BOLD', 'ITALIC'].includes(content.type)) {
const updatedContents = content.value.reduce(
(innerAccumulatedValue, innerContent) => {
if (innerContent.type === 'PLAIN_TEXT') {
let updatedContent = innerContent.value;
searchTerms.forEach((searchTerm) => {
if (searchTerm && updatedContent) {
innerAccumulatedValue.push(
...highlightText(updatedContent, searchTerm)
);
updatedContent = '';
}
});

if (updatedContent) {
innerAccumulatedValue.push({
type: 'PLAIN_TEXT',
value: updatedContent,
});
}
} else {
innerAccumulatedValue.push(innerContent);
}

return innerAccumulatedValue;
},
[]
);

accumulatedValue.push({
...content,
value: updatedContents,
});
} else {
accumulatedValue.push(content);
}

return accumulatedValue;
},
[]
);

return {
...paragraphBlock,
value: updatedValue,
};
}

return paragraphBlock;
});
}

return message;
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const SearchMessages = () => {
searchFiltered={messageList}
shouldRender={(msg) => !!msg}
viewType={viewType}
searchedText={text}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useMemo } from 'react';
import { isSameDay, format, set } from 'date-fns';
import { isSameDay, format } from 'date-fns';
import {
Box,
Sidebar,
Expand All @@ -18,6 +18,7 @@ import NoMessagesIndicator from './NoMessageIndicator';
import FileDisplay from '../../FileMessage/FileMessage';
import useSetExclusiveState from '../../../hooks/useSetExclusiveState';
import { useRCContext } from '../../../context/RCInstance';
import { highlightSearchTerm } from '../../../lib/highlightUtils';

export const MessageAggregator = ({
title,
Expand All @@ -29,6 +30,7 @@ export const MessageAggregator = ({
searchProps,
searchFiltered,
fetching,
searchedText,
type = 'message',
viewType = 'Sidebar',
}) => {
Expand All @@ -45,7 +47,11 @@ export const MessageAggregator = ({
);

const [messageRendered, setMessageRendered] = useState(false);
const { loading, messageList } = useSetMessageList(
let { messageList } = useSetMessageList(
fetchedMessageList || searchFiltered || allMessages,
shouldRender
);
const { loading } = useSetMessageList(
fetchedMessageList || searchFiltered || allMessages,
shouldRender
);
Expand Down Expand Up @@ -109,6 +115,16 @@ export const MessageAggregator = ({
const noMessages = messageList?.length === 0 || !messageRendered;
const ViewComponent = viewType === 'Popup' ? Popup : Sidebar;

if (title === 'Search Messages') {
if (messageList) {
const highlightedMessages = highlightSearchTerm(
messageList,
searchedText
);
messageList = highlightedMessages;
}
}

return (
<ViewComponent
title={title}
Expand Down
Loading