fuzzy search on search page
This commit is contained in:
@ -1,8 +1,6 @@
|
||||
import "./SearchBox.css";
|
||||
|
||||
import { unixNow } from "@snort/shared";
|
||||
import { NostrLink, tryParseNostrLink } from "@snort/system";
|
||||
import { socialGraphInstance } from "@snort/system";
|
||||
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
@ -10,11 +8,9 @@ import { useLocation, useNavigate } from "react-router-dom";
|
||||
import Icon from "@/Components/Icons/Icon";
|
||||
import Spinner from "@/Components/Icons/Spinner";
|
||||
import ProfileImage from "@/Components/User/ProfileImage";
|
||||
import fuzzySearch, { FuzzySearchResult } from "@/Db/FuzzySearch";
|
||||
import useProfileSearch from "@/Hooks/useProfileSearch";
|
||||
import { fetchNip05Pubkey } from "@/Utils/Nip05/Verifier";
|
||||
|
||||
import useTimelineFeed, { TimelineFeedOptions, TimelineSubject } from "../../Feed/TimelineFeed";
|
||||
|
||||
const MAX_RESULTS = 3;
|
||||
|
||||
export default function SearchBox() {
|
||||
@ -29,53 +25,7 @@ export default function SearchBox() {
|
||||
const [activeIndex, setActiveIndex] = useState<number>(-1);
|
||||
const resultListRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const options: TimelineFeedOptions = {
|
||||
method: "LIMIT_UNTIL",
|
||||
window: undefined,
|
||||
now: unixNow(),
|
||||
};
|
||||
|
||||
const subject: TimelineSubject = {
|
||||
type: "profile_keyword",
|
||||
discriminator: search,
|
||||
items: search ? [search] : [],
|
||||
relay: undefined,
|
||||
streams: false,
|
||||
};
|
||||
|
||||
const { main } = useTimelineFeed(subject, options);
|
||||
|
||||
const [results, setResults] = useState<FuzzySearchResult[]>([]);
|
||||
useEffect(() => {
|
||||
const searchString = search.trim();
|
||||
const fuseResults = fuzzySearch.search(searchString);
|
||||
|
||||
const followDistanceNormalizationFactor = 3;
|
||||
|
||||
const combinedResults = fuseResults.map(result => {
|
||||
const fuseScore = result.score === undefined ? 1 : result.score;
|
||||
const followDistance =
|
||||
socialGraphInstance.getFollowDistance(result.item.pubkey) / followDistanceNormalizationFactor;
|
||||
|
||||
const startsWithSearchString = [result.item.name, result.item.display_name, result.item.nip05].some(
|
||||
field => field && field.toLowerCase?.().startsWith(searchString.toLowerCase()),
|
||||
);
|
||||
|
||||
const boostFactor = startsWithSearchString ? 0.25 : 1;
|
||||
|
||||
const weightForFuseScore = 0.8;
|
||||
const weightForFollowDistance = 0.2;
|
||||
|
||||
const combinedScore = (fuseScore * weightForFuseScore + followDistance * weightForFollowDistance) * boostFactor;
|
||||
|
||||
return { ...result, combinedScore };
|
||||
});
|
||||
|
||||
// Sort by combined score, lower is better
|
||||
combinedResults.sort((a, b) => a.combinedScore - b.combinedScore);
|
||||
|
||||
setResults(combinedResults.map(r => r.item));
|
||||
}, [search, main]);
|
||||
const results = useProfileSearch(search);
|
||||
|
||||
useEffect(() => {
|
||||
const handleGlobalKeyDown = (e: KeyboardEvent) => {
|
||||
|
@ -17,7 +17,7 @@ import useLogin from "@/Hooks/useLogin";
|
||||
import useModeration from "@/Hooks/useModeration";
|
||||
import { System } from "@/system";
|
||||
|
||||
export default function TrendingNotes({ count = Infinity, small = false }: { count: number; small: boolean }) {
|
||||
export default function TrendingNotes({ count = Infinity, small = false }: { count?: number; small?: boolean }) {
|
||||
const api = new NostrBandApi();
|
||||
const { lang } = useLocale();
|
||||
const trendingNotesUrl = api.trendingNotesUrl(lang);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { dedupe } from "@snort/shared";
|
||||
import { HexKey } from "@snort/system";
|
||||
import { ReactNode } from "react";
|
||||
import { ReactNode, useMemo } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { FollowsFeed } from "@/Cache";
|
||||
@ -35,6 +35,8 @@ export default function FollowListBase({
|
||||
const { id, follows } = useLogin(s => ({ id: s.id, follows: s.follows }));
|
||||
const login = useLogin();
|
||||
|
||||
const profilePreviewOptions = useMemo(() => ({ about: showAbout, profileCards: true }), [showAbout]);
|
||||
|
||||
async function followAll() {
|
||||
if (publisher) {
|
||||
const newFollows = dedupe([...pubkeys, ...follows.item]);
|
||||
@ -57,12 +59,13 @@ export default function FollowListBase({
|
||||
</div>
|
||||
)}
|
||||
<div className={className}>
|
||||
{pubkeys?.map(a => (
|
||||
{pubkeys?.map((a, index) => (
|
||||
<ProfilePreview
|
||||
pubkey={a}
|
||||
key={a}
|
||||
options={{ about: showAbout, profileCards: true }}
|
||||
options={profilePreviewOptions}
|
||||
actions={profileActions?.(a)}
|
||||
waitUntilInView={index > 10}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@ import "./ProfilePreview.css";
|
||||
|
||||
import { HexKey, UserMetadata } from "@snort/system";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { ReactNode } from "react";
|
||||
import { memo, ReactNode } from "react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
|
||||
import FollowButton from "@/Components/User/FollowButton";
|
||||
@ -19,10 +19,11 @@ export interface ProfilePreviewProps {
|
||||
actions?: ReactNode;
|
||||
className?: string;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
waitUntilInView?: boolean;
|
||||
}
|
||||
export default function ProfilePreview(props: ProfilePreviewProps) {
|
||||
export default memo(function ProfilePreview(props: ProfilePreviewProps) {
|
||||
const pubkey = props.pubkey;
|
||||
const { ref, inView } = useInView({ triggerOnce: true });
|
||||
const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "500px" });
|
||||
const user = useUserProfile(inView ? pubkey : undefined);
|
||||
const options = {
|
||||
about: true,
|
||||
@ -43,7 +44,7 @@ export default function ProfilePreview(props: ProfilePreviewProps) {
|
||||
className={`justify-between profile-preview${props.className ? ` ${props.className}` : ""}`}
|
||||
ref={ref}
|
||||
onClick={handleClick}>
|
||||
{inView && (
|
||||
{(!props.waitUntilInView || inView) && (
|
||||
<>
|
||||
<ProfileImage
|
||||
pubkey={pubkey}
|
||||
@ -62,4 +63,4 @@ export default function ProfilePreview(props: ProfilePreviewProps) {
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user