diff --git a/apps/mobile/src/components/Feed/CommentsBottomSheet/CommentsBottomSheet.tsx b/apps/mobile/src/components/Feed/CommentsBottomSheet/CommentsBottomSheet.tsx index 2e2d4bc1fe..f732a7cd58 100644 --- a/apps/mobile/src/components/Feed/CommentsBottomSheet/CommentsBottomSheet.tsx +++ b/apps/mobile/src/components/Feed/CommentsBottomSheet/CommentsBottomSheet.tsx @@ -154,17 +154,21 @@ export function CommentsBottomSheet({ {isSelectingMentions ? ( - }> - - + {aliasKeyword ? ( + }> + + + ) : ( + + )} ) : ( diff --git a/apps/mobile/src/components/Markdown.tsx b/apps/mobile/src/components/Markdown.tsx index 86febeeb27..cd6e1812ea 100644 --- a/apps/mobile/src/components/Markdown.tsx +++ b/apps/mobile/src/components/Markdown.tsx @@ -23,6 +23,9 @@ const markdownStyles = { fontSize: 14, fontFamily: 'ABCDiatypeRegular', }, + strong: { + fontFamily: 'ABCDiatypeBold', + }, }; const darkModeMarkdownStyles = { diff --git a/apps/mobile/src/components/Search/Community/CommunitySearchResult.tsx b/apps/mobile/src/components/Search/Community/CommunitySearchResult.tsx index 371467e73a..0f127fa41b 100644 --- a/apps/mobile/src/components/Search/Community/CommunitySearchResult.tsx +++ b/apps/mobile/src/components/Search/Community/CommunitySearchResult.tsx @@ -11,9 +11,10 @@ import { SearchResult } from '../SearchResult'; type Props = { communityRef: CommunitySearchResultFragment$key; + keyword: string; onSelect?: (item: MentionType) => void; }; -export function CommunitySearchResult({ communityRef, onSelect }: Props) { +export function CommunitySearchResult({ communityRef, keyword, onSelect }: Props) { const community = useFragment( graphql` fragment CommunitySearchResultFragment on Community { @@ -61,6 +62,7 @@ export function CommunitySearchResult({ communityRef, onSelect }: Props) { title={community?.name ?? ''} description={community?.description ?? ''} variant="Gallery" + keyword={keyword} /> ); } diff --git a/apps/mobile/src/components/Search/Gallery/GallerySearchResult.tsx b/apps/mobile/src/components/Search/Gallery/GallerySearchResult.tsx index 7d617128c4..fdb7ea21be 100644 --- a/apps/mobile/src/components/Search/Gallery/GallerySearchResult.tsx +++ b/apps/mobile/src/components/Search/Gallery/GallerySearchResult.tsx @@ -9,8 +9,9 @@ import { SearchResult } from '../SearchResult'; type Props = { galleryRef: GallerySearchResultFragment$key; + keyword: string; }; -export function GallerySearchResult({ galleryRef }: Props) { +export function GallerySearchResult({ galleryRef, keyword }: Props) { const gallery = useFragment( graphql` fragment GallerySearchResultFragment on Gallery { @@ -35,6 +36,7 @@ export function GallerySearchResult({ galleryRef }: Props) { title={gallery?.name ?? ''} description={gallery?.owner?.username ?? ''} variant="Gallery" + keyword={keyword} /> ); } diff --git a/apps/mobile/src/components/Search/SearchDefault.tsx b/apps/mobile/src/components/Search/SearchDefault.tsx index f53274feaf..696ce01e21 100644 --- a/apps/mobile/src/components/Search/SearchDefault.tsx +++ b/apps/mobile/src/components/Search/SearchDefault.tsx @@ -12,13 +12,14 @@ import { UserSearchResult } from './User/UserSearchResult'; type Props = { queryRef: SearchDefaultFragment$key; blurInputFocus: () => void; + keyword: string; }; type ListItemType = | { kind: 'header'; title: string } | { kind: 'user'; user: UserSearchResultFragment$key }; -export function SearchDefault({ queryRef, blurInputFocus }: Props) { +export function SearchDefault({ queryRef, blurInputFocus, keyword }: Props) { const query = useFragment( graphql` fragment SearchDefaultFragment on Query { @@ -35,25 +36,28 @@ export function SearchDefault({ queryRef, blurInputFocus }: Props) { queryRef ); - const renderItem = useCallback>(({ item }) => { - if (item.kind === 'header') { - return ( - - - {item.title} - - - ); - } else { - return ; - } - }, []); + const renderItem = useCallback>( + ({ item }) => { + if (item.kind === 'header') { + return ( + + + {item.title} + + + ); + } else { + return ; + } + }, + [keyword] + ); const items = useMemo((): ListItemType[] => { const items: ListItemType[] = []; diff --git a/apps/mobile/src/components/Search/SearchResult.tsx b/apps/mobile/src/components/Search/SearchResult.tsx index 501e8bf1c6..9875a6c25b 100644 --- a/apps/mobile/src/components/Search/SearchResult.tsx +++ b/apps/mobile/src/components/Search/SearchResult.tsx @@ -1,23 +1,21 @@ import { ReactNode, useMemo } from 'react'; import { StyleSheet, TouchableOpacityProps } from 'react-native'; import { View } from 'react-native'; -import { sanitizeMarkdown } from 'src/utils/sanitizeMarkdown'; import { contexts } from '~/shared/analytics/constants'; +import { getHighlightedDescription, getHighlightedName } from '~/shared/utils/highlighter'; import { GalleryTouchableOpacity } from '../GalleryTouchableOpacity'; import { Markdown } from '../Markdown'; -import { useSearchContext } from './SearchContext'; type Props = { title: string; description: string; variant: 'Gallery' | 'User'; profilePicture?: ReactNode; + keyword: string; } & TouchableOpacityProps; -const MAX_DESCRIPTION_CHARACTER = 150; - const markdownStyles = StyleSheet.create({ paragraph: { marginBottom: 0, @@ -27,41 +25,20 @@ const markdownStyles = StyleSheet.create({ }, }); -export function SearchResult({ title, description, variant, profilePicture, ...props }: Props) { - const { keyword } = useSearchContext(); - - const highlightedName = useMemo(() => { - if (!keyword) { - return title; - } - return title.replace(new RegExp(keyword, 'gi'), (match) => `**${match}**`); - }, [keyword, title]); - - const highlightedDescription = useMemo(() => { - const regex = new RegExp(keyword, 'gi'); - - const unformattedDescription = sanitizeMarkdown(description ?? ''); - if (!keyword) { - return unformattedDescription.substring(0, MAX_DESCRIPTION_CHARACTER); - } +export function SearchResult({ + title, + description, + keyword, + variant, + profilePicture, + ...props +}: Props) { + const highlightedName = useMemo(() => getHighlightedName(title, keyword), [keyword, title]); - const matchIndex = unformattedDescription.search(regex); - let truncatedDescription; - - const maxLength = MAX_DESCRIPTION_CHARACTER; - - if (matchIndex > -1 && matchIndex + keyword.length === unformattedDescription.length) { - const endIndex = Math.min(unformattedDescription.length, maxLength); - truncatedDescription = `...${unformattedDescription.substring( - endIndex - maxLength, - endIndex - )}`; - } else { - truncatedDescription = unformattedDescription.substring(0, maxLength); - } - // highlight keyword - return truncatedDescription.replace(regex, (match) => `**${match}**`); - }, [keyword, description]); + const highlightedDescription = useMemo( + () => getHighlightedDescription(description, keyword), + [keyword, description] + ); return ( 0) { items.push({ kind: 'search-section-header', sectionType: 'curator', @@ -221,7 +221,7 @@ export function SearchResults({ } } - if (hasGalleries && !isMentionSearch) { + if (hasGalleries && !isMentionSearch && searchGalleries.results.length > 0) { items.push({ kind: 'search-section-header', sectionType: 'gallery', @@ -240,7 +240,7 @@ export function SearchResults({ } } - if (hasCommunities) { + if (hasCommunities && searchCommunities.results.length > 0) { items.push({ kind: 'search-section-header', sectionType: 'community', @@ -293,16 +293,22 @@ export function SearchResults({ /> ); } else if (item.kind === 'user-search-result') { - return ; + return ; } else if (item.kind === 'gallery-search-result') { - return ; + return ; } else if (item.kind === 'community-search-result') { - return ; + return ( + + ); } return ; }, - [onChangeFilter, onSelect, showAllButton] + [onChangeFilter, keyword, onSelect, showAllButton] ); if (isEmpty) { diff --git a/apps/mobile/src/components/Search/User/UserSearchResult.tsx b/apps/mobile/src/components/Search/User/UserSearchResult.tsx index 850dd0b30b..9f80f32398 100644 --- a/apps/mobile/src/components/Search/User/UserSearchResult.tsx +++ b/apps/mobile/src/components/Search/User/UserSearchResult.tsx @@ -12,9 +12,10 @@ import { SearchResult } from '../SearchResult'; type Props = { userRef: UserSearchResultFragment$key; onSelect?: (item: MentionType) => void; + keyword: string; }; -export function UserSearchResult({ userRef, onSelect }: Props) { +export function UserSearchResult({ userRef, keyword, onSelect }: Props) { const user = useFragment( graphql` fragment UserSearchResultFragment on GalleryUser { @@ -51,6 +52,7 @@ export function UserSearchResult({ userRef, onSelect }: Props) { title={user?.username ?? ''} description={user?.bio ?? ''} variant="User" + keyword={keyword} /> ); } diff --git a/apps/mobile/src/hooks/useMentionableMessage.ts b/apps/mobile/src/hooks/useMentionableMessage.ts index 0ab516c56b..d557614ce2 100644 --- a/apps/mobile/src/hooks/useMentionableMessage.ts +++ b/apps/mobile/src/hooks/useMentionableMessage.ts @@ -97,6 +97,7 @@ export function useMentionableMessage(queryRef: useMentionableMessageQueryFragme setIsSelectingMentions(false); setMentions([...adjustedMentions, newMention]); + setAliasKeyword(''); }, [mentions, message, setMessage, aliasKeyword, selection.start] ); @@ -114,14 +115,17 @@ export function useMentionableMessage(queryRef: useMentionableMessageQueryFragme .split(' ') .pop(); - if (wordAtCursor && wordAtCursor[0] === '@' && wordAtCursor.length > 1) { - setAliasKeyword(wordAtCursor); + if (wordAtCursor && wordAtCursor[0] === '@' && wordAtCursor.length > 0) { setIsSelectingMentions(true); } else { setAliasKeyword(''); setIsSelectingMentions(false); } + if (wordAtCursor && wordAtCursor?.length > 1) { + setAliasKeyword(wordAtCursor.slice(1)); // Remove the @ + } + // Determine how many characters were added or removed const diff = message.length - text.length; @@ -159,6 +163,7 @@ export function useMentionableMessage(queryRef: useMentionableMessageQueryFragme setMentions([]); setIsSelectingMentions(false); setMessage(''); + setAliasKeyword(''); }, []); const handleSelectionChange = useCallback((selection: { start: number; end: number }) => { diff --git a/apps/mobile/src/screens/PostScreen/PostComposerScreen.tsx b/apps/mobile/src/screens/PostScreen/PostComposerScreen.tsx index afff1a4097..c062de9eb0 100644 --- a/apps/mobile/src/screens/PostScreen/PostComposerScreen.tsx +++ b/apps/mobile/src/screens/PostScreen/PostComposerScreen.tsx @@ -187,17 +187,23 @@ function PostComposerScreenInner() { /> {isSelectingMentions ? ( - }> - - + + {aliasKeyword ? ( + }> + + + ) : ( + + )} + ) : ( }> diff --git a/apps/mobile/src/screens/SearchScreen.tsx b/apps/mobile/src/screens/SearchScreen.tsx index 4abecdee9c..e3ce37c0b3 100644 --- a/apps/mobile/src/screens/SearchScreen.tsx +++ b/apps/mobile/src/screens/SearchScreen.tsx @@ -58,7 +58,7 @@ export function SearchScreen() { /> ) : ( - + )} diff --git a/apps/web/src/components/Search/SearchResult.tsx b/apps/web/src/components/Search/SearchResult.tsx index 9b6eae159a..55d6b3875d 100644 --- a/apps/web/src/components/Search/SearchResult.tsx +++ b/apps/web/src/components/Search/SearchResult.tsx @@ -6,6 +6,7 @@ import { useDrawerActions } from '~/contexts/globalLayout/GlobalSidebar/SidebarD import { contexts } from '~/shared/analytics/constants'; import { useTrack } from '~/shared/contexts/AnalyticsContext'; import colors from '~/shared/theme/colors'; +import { getHighlightedDescription, getHighlightedName } from '~/shared/utils/highlighter'; import GalleryLink from '../core/GalleryLink/GalleryLink'; import Markdown from '../core/Markdown/Markdown'; @@ -22,8 +23,6 @@ type Props = { profilePicture?: ReactNode; }; -const MAX_DESCRIPTION_CHARACTER = 150; - export default function SearchResult({ name, description, @@ -50,35 +49,12 @@ export default function SearchResult({ }); }, [hideDrawer, keyword, path, track, type]); - const highlightedName = useMemo(() => { - return name.replace(new RegExp(keyword, 'gi'), (match) => `**${match}**`); - }, [keyword, name]); - - const highlightedDescription = useMemo(() => { - const regex = new RegExp(keyword, 'gi'); - - // Remove bold & link markdown tag from description - const unformattedDescription = description - .replace(/\*\*/g, '') - .replace(/\[([^[]*)\]\([^)]*\)/g, '$1'); - - const matchIndex = unformattedDescription.search(regex); - let truncatedDescription; - - const maxLength = MAX_DESCRIPTION_CHARACTER; - - if (matchIndex > -1 && matchIndex + keyword.length === unformattedDescription.length) { - const endIndex = Math.min(unformattedDescription.length, maxLength); - truncatedDescription = `...${unformattedDescription.substring( - endIndex - maxLength, - endIndex - )}`; - } else { - truncatedDescription = unformattedDescription.substring(0, maxLength); - } - // highlight keyword - return truncatedDescription.replace(regex, (match) => `**${match}**`); - }, [keyword, description]); + const highlightedName = useMemo(() => getHighlightedName(name, keyword), [keyword, name]); + + const highlightedDescription = useMemo( + () => getHighlightedDescription(description, keyword), + [keyword, description] + ); return ( diff --git a/packages/shared/src/utils/highlighter.ts b/packages/shared/src/utils/highlighter.ts new file mode 100644 index 0000000000..d955d2dfad --- /dev/null +++ b/packages/shared/src/utils/highlighter.ts @@ -0,0 +1,35 @@ +const MAX_DESCRIPTION_CHARACTER = 150; + +export function getHighlightedName(text: string, keyword: string) { + return text.replace(new RegExp(keyword, 'gi'), (match) => `**${match}**`); +} + +export function getHighlightedDescription(text: string, keyword: string) { + const regex = new RegExp(keyword, 'gi'); + + const unformattedDescription = sanitizeMarkdown(text ?? ''); + if (!keyword) { + return unformattedDescription.substring(0, MAX_DESCRIPTION_CHARACTER); + } + + const matchIndex = unformattedDescription.search(regex); + let truncatedDescription; + + const maxLength = MAX_DESCRIPTION_CHARACTER; + + if (matchIndex > -1 && matchIndex + keyword.length === unformattedDescription.length) { + const endIndex = Math.min(unformattedDescription.length, maxLength); + truncatedDescription = `...${unformattedDescription.substring(endIndex - maxLength, endIndex)}`; + } else { + truncatedDescription = unformattedDescription.substring(0, maxLength); + } + // highlight keyword + return truncatedDescription.replace(regex, (match) => `**${match}**`); +} + +function sanitizeMarkdown(text: string) { + return text + .replace(/\*\*/g, '') // bold + .replace(/\[.*\]\(.*\)/g, '') // link markdown tag from description + .replace(/\n/g, ' '); // break line +}