Search improvements #552

Merged
Kieran merged 7 commits from people-search into main 2023-05-10 12:30:18 +00:00
18 changed files with 323 additions and 202 deletions

View File

@ -19,4 +19,5 @@
border: 1px solid var(--font-tertiary-color); border: 1px solid var(--font-tertiary-color);
margin-top: auto; margin-top: auto;
margin-bottom: auto; margin-bottom: auto;
overflow: hidden;
} }

View File

@ -327,10 +327,9 @@ export default function Note(props: NoteProps) {
{options.showHeader && ( {options.showHeader && (
<div className="header flex"> <div className="header flex">
<ProfileImage <ProfileImage
autoWidth={false}
pubkey={ev.pubkey} pubkey={ev.pubkey}
subHeader={replyTag() ?? undefined} subHeader={replyTag() ?? undefined}
linkToProfile={opt?.canClick === undefined} link={opt?.canClick === undefined ? undefined : ""}
/> />
{(options.showTime || options.showBookmarked) && ( {(options.showTime || options.showBookmarked) && (
<div className="info"> <div className="info">

View File

@ -293,7 +293,7 @@ export function NoteCreator() {
ev.stopPropagation = true; ev.stopPropagation = true;
LoginStore.switchAccount(a); LoginStore.switchAccount(a);
}}> }}>
<ProfileImage pubkey={a} linkToProfile={false} /> <ProfileImage pubkey={a} link={""} />
</MenuItem> </MenuItem>
)); ));
} }

View File

@ -1,6 +1,10 @@
.pfp { .pfp {
display: flex; display: grid;
grid-template-columns: min-content auto;
align-items: center; align-items: center;
text-decoration: none;
user-select: none;
min-width: 0;
} }
.pfp .avatar-wrapper { .pfp .avatar-wrapper {
@ -14,7 +18,7 @@
cursor: pointer; cursor: pointer;
} }
.pfp a { a.pfp {
text-decoration: none; text-decoration: none;
} }
@ -25,6 +29,10 @@
font-weight: 600; font-weight: 600;
} }
.pfp .subheader .about { .pfp .profile-name {
max-width: calc(100vw - 140px); max-width: stretch;
}
.pfp a {
text-decoration: none;
} }

View File

@ -1,7 +1,6 @@
import "./ProfileImage.css"; import "./ProfileImage.css";
import { useMemo } from "react"; import { useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { HexKey, NostrPrefix } from "@snort/nostr"; import { HexKey, NostrPrefix } from "@snort/nostr";
import { useUserProfile } from "Hooks/useUserProfile"; import { useUserProfile } from "Hooks/useUserProfile";
@ -9,7 +8,7 @@ import { hexToBech32, profileLink } from "Util";
import Avatar from "Element/Avatar"; import Avatar from "Element/Avatar";
import Nip05 from "Element/Nip05"; import Nip05 from "Element/Nip05";
import { MetadataCache } from "Cache"; import { MetadataCache } from "Cache";
import usePageWidth from "Hooks/usePageWidth"; import { Link } from "react-router-dom";
export interface ProfileImageProps { export interface ProfileImageProps {
pubkey: HexKey; pubkey: HexKey;
@ -17,10 +16,8 @@ export interface ProfileImageProps {
showUsername?: boolean; showUsername?: boolean;
className?: string; className?: string;
link?: string; link?: string;
autoWidth?: boolean;
defaultNip?: string; defaultNip?: string;
verifyNip?: boolean; verifyNip?: boolean;
linkToProfile?: boolean;
overrideUsername?: string; overrideUsername?: string;
} }
@ -30,50 +27,32 @@ export default function ProfileImage({
showUsername = true, showUsername = true,
className, className,
link, link,
autoWidth = true,
defaultNip, defaultNip,
verifyNip, verifyNip,
linkToProfile = true,
overrideUsername, overrideUsername,
}: ProfileImageProps) { }: ProfileImageProps) {
const navigate = useNavigate();
const user = useUserProfile(pubkey); const user = useUserProfile(pubkey);
const nip05 = defaultNip ? defaultNip : user?.nip05; const nip05 = defaultNip ? defaultNip : user?.nip05;
const width = usePageWidth();
const name = useMemo(() => { const name = useMemo(() => {
return overrideUsername ?? getDisplayName(user, pubkey); return overrideUsername ?? getDisplayName(user, pubkey);
}, [user, pubkey, overrideUsername]); }, [user, pubkey, overrideUsername]);
if (!pubkey && !link) {
link = "#";
}
const onAvatarClick = () => {
if (linkToProfile) {
navigate(link ?? profileLink(pubkey));
}
};
return ( return (
<div className={`pfp f-ellipsis${className ? ` ${className}` : ""}`} onClick={onAvatarClick}> <Link className={`pfp${className ? ` ${className}` : ""}`} to={link === undefined ? profileLink(pubkey) : ""}>
<div className="avatar-wrapper"> <div className="avatar-wrapper">
<Avatar user={user} /> <Avatar user={user} />
</div> </div>
{showUsername && ( {showUsername && (
<div className="profile-name"> <div className="f-ellipsis">
<div className="username"> <div className="username">
<div className="display-name">
<div>{name.trim()}</div> <div>{name.trim()}</div>
{nip05 && <Nip05 nip05={nip05} pubkey={pubkey} verifyNip={verifyNip} />} {nip05 && <Nip05 nip05={nip05} pubkey={pubkey} verifyNip={verifyNip} />}
</div> </div>
</div> <div className="subheader">{subHeader}</div>
<div className="subheader" style={{ width: autoWidth ? width - 190 : "" }}>
{subHeader}
</div>
</div> </div>
)} )}
</div> </Link>
); );
} }

View File

@ -1,28 +1,23 @@
.reactions-modal .modal-body { .reactions-modal .modal-body {
padding: 0;
max-width: 586px;
}
.reactions-view {
padding: 24px 32px; padding: 24px 32px;
background-color: #1b1b1b; background-color: #1b1b1b;
border-radius: 16px; border-radius: 16px;
position: relative; position: relative;
min-height: 33vh;
} }
.light .reactions-view { .light .reactions-modal .modal-body {
background-color: var(--note-bg); background-color: var(--note-bg);
} }
@media (max-width: 720px) { @media (max-width: 720px) {
.reactions-view { .reactions-modal .modal-body {
padding: 12px 16px; padding: 12px 16px;
margin-top: -160px;
max-width: calc(100vw - 32px); max-width: calc(100vw - 32px);
} }
} }
.reactions-view .close { .reactions-modal .modal-body .close {
position: absolute; position: absolute;
top: 12px; top: 12px;
right: 16px; right: 16px;
@ -30,18 +25,18 @@
cursor: pointer; cursor: pointer;
} }
.reactions-view .close:hover { .reactions-modal .modal-body .close:hover {
color: var(--font-tertiary-color); color: var(--font-tertiary-color);
} }
.reactions-view .reactions-header { .reactions-modal .modal-body .reactions-header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
margin-bottom: 32px; margin-bottom: 32px;
} }
.reactions-view .reactions-header h2 { .reactions-modal .modal-body .reactions-header h2 {
margin: 0; margin: 0;
flex-grow: 1; flex-grow: 1;
font-weight: 600; font-weight: 600;
@ -49,26 +44,25 @@
line-height: 19px; line-height: 19px;
} }
.reactions-view .body { .reactions-modal .modal-body .body {
overflow: scroll; overflow: scroll;
height: 320px; height: 40vh;
-ms-overflow-style: none; /* for Internet Explorer, Edge */ -ms-overflow-style: none; /* for Internet Explorer, Edge */
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */
} }
.reactions-view .body::-webkit-scrollbar { .reactions-modal .modal-body .body::-webkit-scrollbar {
display: none; display: none;
} }
.reactions-item { .reactions-item {
display: flex; display: grid;
flex-direction: row; grid-template-columns: 52px auto;
align-items: center; align-items: center;
margin-bottom: 24px; margin-bottom: 24px;
} }
.reactions-item .reaction-icon { .reactions-item .reaction-icon {
width: 52px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -93,12 +87,8 @@
line-height: 17px; line-height: 17px;
} }
.reactions-item .zap-comment {
width: 332px;
}
@media (max-width: 520px) { @media (max-width: 520px) {
.reactions-view .tab.disabled { .reactions-modal .modal-body .tab.disabled {
display: none; display: none;
} }
} }

View File

@ -2,7 +2,6 @@ import "./Reactions.css";
import { useState, useMemo, useEffect } from "react"; import { useState, useMemo, useEffect } from "react";
import { useIntl, FormattedMessage } from "react-intl"; import { useIntl, FormattedMessage } from "react-intl";
import { TaggedRawEvent } from "@snort/nostr"; import { TaggedRawEvent } from "@snort/nostr";
import { formatShort } from "Number"; import { formatShort } from "Number";
@ -75,7 +74,6 @@ const Reactions = ({ show, setShow, positive, negative, reposts, zaps }: Reactio
return show ? ( return show ? (
<Modal className="reactions-modal" onClose={onClose}> <Modal className="reactions-modal" onClose={onClose}>
<div className="reactions-view">
<div className="close" onClick={onClose}> <div className="close" onClick={onClose}>
<Icon name="close" /> <Icon name="close" />
</div> </div>
@ -91,7 +89,7 @@ const Reactions = ({ show, setShow, positive, negative, reposts, zaps }: Reactio
return ( return (
<div key={ev.id} className="reactions-item"> <div key={ev.id} className="reactions-item">
<div className="reaction-icon">{ev.content === "+" ? <Icon name="heart" /> : ev.content}</div> <div className="reaction-icon">{ev.content === "+" ? <Icon name="heart" /> : ev.content}</div>
<ProfileImage autoWidth={false} pubkey={ev.pubkey} /> <ProfileImage pubkey={ev.pubkey} />
</div> </div>
); );
})} })}
@ -105,13 +103,9 @@ const Reactions = ({ show, setShow, positive, negative, reposts, zaps }: Reactio
<span className="zap-amount">{formatShort(z.amount)}</span> <span className="zap-amount">{formatShort(z.amount)}</span>
</div> </div>
<ProfileImage <ProfileImage
autoWidth={false}
pubkey={z.anonZap ? "" : z.sender} pubkey={z.anonZap ? "" : z.sender}
subHeader={ subHeader={<div title={z.content}>{z.content}</div>}
<div className="f-ellipsis zap-comment" title={z.content}> link={z.anonZap ? "" : undefined}
{z.content}
</div>
}
overrideUsername={z.anonZap ? formatMessage({ defaultMessage: "Anonymous" }) : undefined} overrideUsername={z.anonZap ? formatMessage({ defaultMessage: "Anonymous" }) : undefined}
/> />
</div> </div>
@ -125,23 +119,22 @@ const Reactions = ({ show, setShow, positive, negative, reposts, zaps }: Reactio
<div className="reaction-icon"> <div className="reaction-icon">
<Icon name="repost" size={16} /> <Icon name="repost" size={16} />
</div> </div>
<ProfileImage autoWidth={false} pubkey={ev.pubkey} /> <ProfileImage pubkey={ev.pubkey} />
</div> </div>
); );
})} })}
{tab.value === 3 && {tab.value === 3 &&
dislikes.map(ev => { dislikes.map(ev => {
return ( return (
<div key={ev.id} className="reactions-item"> <div key={ev.id} className="reactions-item f-ellipsis">
<div className="reaction-icon"> <div className="reaction-icon">
<Icon name="dislike" /> <Icon name="dislike" />
</div> </div>
<ProfileImage autoWidth={false} pubkey={ev.pubkey} /> <ProfileImage pubkey={ev.pubkey} />
</div> </div>
); );
})} })}
</div> </div>
</div>
</Modal> </Modal>
) : null; ) : null;
}; };

View File

@ -25,6 +25,7 @@ export interface TimelineProps {
relay?: string; relay?: string;
now?: number; now?: number;
loadMore?: boolean; loadMore?: boolean;
noSort?: boolean;
} }
/** /**
@ -46,8 +47,9 @@ const Timeline = (props: TimelineProps) => {
const filterPosts = useCallback( const filterPosts = useCallback(
(nts: readonly TaggedRawEvent[]) => { (nts: readonly TaggedRawEvent[]) => {
return [...nts] const a = [...nts];
.sort((a, b) => b.created_at - a.created_at) props.noSort || a.sort((a, b) => b.created_at - a.created_at);
return a
?.filter(a => (props.postsOnly ? !a.tags.some(b => b[0] === "e") : true)) ?.filter(a => (props.postsOnly ? !a.tags.some(b => b[0] === "e") : true))
.filter(a => props.ignoreModeration || !isMuted(a.pubkey)); .filter(a => props.ignoreModeration || !isMuted(a.pubkey));
}, },
@ -117,7 +119,7 @@ const Timeline = (props: TimelineProps) => {
<> <>
<div className="card latest-notes pointer" onClick={() => onShowLatest()} ref={ref}> <div className="card latest-notes pointer" onClick={() => onShowLatest()} ref={ref}>
{latestAuthors.slice(0, 3).map(p => { {latestAuthors.slice(0, 3).map(p => {
return <ProfileImage pubkey={p} showUsername={false} linkToProfile={false} />; return <ProfileImage pubkey={p} showUsername={false} link={""} />;
})} })}
<FormattedMessage <FormattedMessage
defaultMessage="{n} new {n, plural, =1 {note} other {notes}}" defaultMessage="{n} new {n, plural, =1 {note} other {notes}}"
@ -128,7 +130,7 @@ const Timeline = (props: TimelineProps) => {
{!inView && ( {!inView && (
<div className="card latest-notes latest-notes-fixed pointer fade-in" onClick={() => onShowLatest(true)}> <div className="card latest-notes latest-notes-fixed pointer fade-in" onClick={() => onShowLatest(true)}>
{latestAuthors.slice(0, 3).map(p => { {latestAuthors.slice(0, 3).map(p => {
return <ProfileImage pubkey={p} showUsername={false} linkToProfile={false} />; return <ProfileImage pubkey={p} showUsername={false} link={""} />;
})} })}
<FormattedMessage <FormattedMessage
defaultMessage="{n} new {n, plural, =1 {note} other {notes}}" defaultMessage="{n} new {n, plural, =1 {note} other {notes}}"

View File

@ -0,0 +1,34 @@
import { useEffect, useState } from "react";
import { RawEvent, TaggedRawEvent } from "@snort/nostr";
import { FormattedMessage } from "react-intl";
import PageSpinner from "Element/PageSpinner";
import Note from "Element/Note";
import NostrBandApi from "NostrBand";
export default function TrendingNotes() {
const [posts, setPosts] = useState<Array<RawEvent>>();
async function loadTrendingNotes() {
const api = new NostrBandApi();
const trending = await api.trendingNotes();
setPosts(trending.notes.map(a => a.event));
}
useEffect(() => {
loadTrendingNotes().catch(console.error);
}, []);
if (!posts) return <PageSpinner />;
return (
<>
<h3>
<FormattedMessage defaultMessage="Trending Notes" />
</h3>
{posts.map(e => (
<Note key={e.id} data={e as TaggedRawEvent} related={[]} depth={0} />
))}
</>
);
}

View File

@ -4,37 +4,20 @@ import { FormattedMessage } from "react-intl";
import FollowListBase from "Element/FollowListBase"; import FollowListBase from "Element/FollowListBase";
import PageSpinner from "Element/PageSpinner"; import PageSpinner from "Element/PageSpinner";
import NostrBandApi from "NostrBand";
interface TrendingUser { export default function TrendingUsers() {
pubkey: HexKey;
}
interface TrendingUserResponse {
profiles: Array<TrendingUser>;
}
async function fetchTrendingUsers() {
try {
const res = await fetch(`https://api.nostr.band/v0/trending/profiles`);
if (res.ok) {
const data = (await res.json()) as TrendingUserResponse;
return data.profiles.map(a => a.pubkey);
}
} catch (e) {
console.warn(`Failed to load link preview`);
}
}
const TrendingUsers = () => {
const [userList, setUserList] = useState<HexKey[]>(); const [userList, setUserList] = useState<HexKey[]>();
useEffect(() => { async function loadTrendingUsers() {
(async () => { const api = new NostrBandApi();
const data = await fetchTrendingUsers(); const users = await api.trendingProfiles();
if (data) { const keys = users.profiles.map(a => a.pubkey);
setUserList(data); setUserList(keys);
} }
})();
useEffect(() => {
loadTrendingUsers().catch(console.error);
}, []); }, []);
if (!userList) return <PageSpinner />; if (!userList) return <PageSpinner />;
@ -42,11 +25,9 @@ const TrendingUsers = () => {
return ( return (
<> <>
<h3> <h3>
<FormattedMessage defaultMessage="Trending Users" /> <FormattedMessage defaultMessage="Trending People" />
</h3> </h3>
<FollowListBase pubkeys={userList} showAbout={true} /> <FollowListBase pubkeys={userList} showAbout={true} />
</> </>
); );
}; }
export default TrendingUsers;

View File

@ -107,8 +107,8 @@ const Zap = ({ zap, showZapped = true }: { zap: ParsedZap; showZapped?: boolean
return valid && sender ? ( return valid && sender ? (
<div className="zap note card"> <div className="zap note card">
<div className="header"> <div className="header">
<ProfileImage autoWidth={false} pubkey={sender} /> <ProfileImage pubkey={sender} />
{receiver !== pubKey && showZapped && <ProfileImage autoWidth={false} pubkey={unwrap(receiver)} />} {receiver !== pubKey && showZapped && <ProfileImage pubkey={unwrap(receiver)} />}
<div className="amount"> <div className="amount">
<span className="amount-number"> <span className="amount-number">
<FormattedMessage {...messages.Sats} values={{ n: formatShort(amount ?? 0) }} /> <FormattedMessage {...messages.Sats} values={{ n: formatShort(amount ?? 0) }} />
@ -151,7 +151,6 @@ export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
<div className="summary"> <div className="summary">
{sender && ( {sender && (
<ProfileImage <ProfileImage
autoWidth={false}
pubkey={anonZap ? "" : sender} pubkey={anonZap ? "" : sender}
overrideUsername={anonZap ? formatMessage({ defaultMessage: "Anonymous" }) : undefined} overrideUsername={anonZap ? formatMessage({ defaultMessage: "Anonymous" }) : undefined}
/> />

View File

@ -15,7 +15,7 @@ export interface TimelineFeedOptions {
} }
export interface TimelineSubject { export interface TimelineSubject {
type: "pubkey" | "hashtag" | "global" | "ptag" | "keyword"; type: "pubkey" | "hashtag" | "global" | "ptag" | "post_keyword" | "profile_keyword";
discriminator: string; discriminator: string;
items: string[]; items: string[];
} }
@ -37,7 +37,13 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
} }
const b = new RequestBuilder(`timeline:${subject.type}:${subject.discriminator}`); const b = new RequestBuilder(`timeline:${subject.type}:${subject.discriminator}`);
const f = b.withFilter().kinds([EventKind.TextNote, EventKind.Repost, EventKind.Polls]); const f = b
.withFilter()
.kinds(
subject.type === "profile_keyword"
? [EventKind.SetMetadata]
: [EventKind.TextNote, EventKind.Repost, EventKind.Polls]
);
if (options.relay) { if (options.relay) {
b.withOptions({ b.withOptions({
@ -58,7 +64,11 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
f.tag("p", subject.items); f.tag("p", subject.items);
break; break;
} }
case "keyword": { case "profile_keyword": {
f.search(subject.items[0] + " sort:popular");
break;
}
case "post_keyword": {
f.search(subject.items[0]); f.search(subject.items[0]);
break; break;
} }
@ -105,7 +115,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
const subRealtime = useMemo(() => { const subRealtime = useMemo(() => {
const rb = createBuilder(); const rb = createBuilder();
if (rb && !pref.autoShowLatest) { if (rb && !pref.autoShowLatest && options.method !== "LIMIT_UNTIL") {
rb.builder.withOptions({ rb.builder.withOptions({
leaveOpen: true, leaveOpen: true,
}); });

View File

@ -0,0 +1,52 @@
import { RawEvent } from "@snort/nostr";
export interface TrendingUser {
pubkey: string;
}
export interface TrendingUserResponse {
profiles: Array<TrendingUser>;
}
export interface TrendingNote {
event: RawEvent;
author: RawEvent; // kind0 event
}
export interface TrendingNoteResponse {
notes: Array<TrendingNote>;
}
export class NostrBandError extends Error {
body: string;
statusCode: number;
constructor(message: string, body: string, status: number) {
super(message);
this.body = body;
this.statusCode = status;
}
}
export default class NostrBandApi {
#url = "https://api.nostr.band";
async trendingProfiles() {
return await this.#json<TrendingUserResponse>("GET", "/v0/trending/profiles");
}
async trendingNotes() {
return await this.#json<TrendingNoteResponse>("GET", "/v0/trending/notes");
}
async #json<T>(method: string, path: string) {
const res = await fetch(`${this.#url}${path}`, {
method: method ?? "GET",
});
if (res.ok) {
return (await res.json()) as T;
} else {
throw new NostrBandError("Failed to load content from nostr.band", await res.text(), res.status);
}
}
}

View File

@ -1,33 +1,38 @@
import { useIntl, FormattedMessage } from "react-intl"; import { useIntl, FormattedMessage } from "react-intl";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import ProfilePreview from "Element/ProfilePreview";
import Timeline from "Element/Timeline"; import Timeline from "Element/Timeline";
import { Tab, TabElement } from "Element/Tabs";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { debounce } from "Util"; import { debounce } from "Util";
import { router } from "index"; import { router } from "index";
import { SearchRelays } from "Const"; import { SearchRelays } from "Const";
import { System } from "System"; import { System } from "System";
import { MetadataCache } from "Cache";
import { UserCache } from "Cache/UserCache";
import TrendingUsers from "Element/TrendingUsers"; import TrendingUsers from "Element/TrendingUsers";
import messages from "./messages"; import TrendingNotes from "Element/TrendingPosts";
const NOTES = 0;
const PROFILES = 1;
const SearchPage = () => { const SearchPage = () => {
const params = useParams(); const params = useParams();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [search, setSearch] = useState<string | undefined>(params.keyword); const [search, setSearch] = useState<string | undefined>(params.keyword);
const [keyword, setKeyword] = useState<string | undefined>(params.keyword); const [keyword, setKeyword] = useState<string | undefined>(params.keyword);
const [allUsers, setAllUsers] = useState<MetadataCache[]>(); const [sortPopular, setSortPopular] = useState<boolean>(true);
// tabs
const SearchTab = {
Posts: { text: formatMessage({ defaultMessage: "Notes" }), value: NOTES },
Profiles: { text: formatMessage({ defaultMessage: "People" }), value: PROFILES },
};
const [tab, setTab] = useState<Tab>(SearchTab.Posts);
useEffect(() => { useEffect(() => {
if (keyword) { if (keyword) {
// "navigate" changing only url // "navigate" changing only url
router.navigate(`/search/${encodeURIComponent(keyword)}`); router.navigate(`/search/${encodeURIComponent(keyword)}`);
UserCache.search(keyword).then(v => setAllUsers(v));
} else { } else {
router.navigate(`/search`); router.navigate(`/search`);
setAllUsers([]);
} }
}, [keyword]); }, [keyword]);
@ -50,35 +55,76 @@ const SearchPage = () => {
}; };
}, []); }, []);
function tabContent() {
if (!keyword) {
switch (tab.value) {
case PROFILES:
return <TrendingUsers />;
case NOTES:
return <TrendingNotes />;
}
return null;
}
const pf = tab.value == PROFILES;
return (
<>
{sortOptions()}
<Timeline
key={keyword + (pf ? "_p" : "")}
subject={{
type: pf ? "profile_keyword" : "post_keyword",
items: [keyword + (sortPopular ? " sort:popular" : "")],
discriminator: keyword,
}}
postsOnly={false}
noSort={pf && sortPopular}
method={"LIMIT_UNTIL"}
loadMore={false}
/>
</>
);
}
function sortOptions() {
if (tab.value != PROFILES) return null;
return (
<div className="flex mb10 f-end">
<FormattedMessage defaultMessage="Sort" description="Label for sorting options for people search" />
&nbsp;
<select onChange={e => setSortPopular(e.target.value == "true")} value={sortPopular ? "true" : "false"}>
<option value={"true"}>
<FormattedMessage defaultMessage="Popular" description="Sort order name" />
</option>
<option value={"false"}>
<FormattedMessage defaultMessage="Recent" description="Sort order name" />
</option>
</select>
</div>
);
}
function renderTab(v: Tab) {
return <TabElement key={v.value} t={v} tab={tab} setTab={setTab} />;
}
return ( return (
<div className="main-content"> <div className="main-content">
<h2> <h2>
<FormattedMessage {...messages.Search} /> <FormattedMessage defaultMessage="Search" />
</h2> </h2>
<div className="flex mb10"> <div className="flex mb10">
<input <input
type="text" type="text"
className="f-grow mr10" className="f-grow mr10"
placeholder={formatMessage(messages.SearchPlaceholder)} placeholder={formatMessage({ defaultMessage: "Search..." })}
value={search} value={search}
onChange={e => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
autoFocus={true} autoFocus={true}
/> />
</div> </div>
{!keyword && <TrendingUsers />} <div className="tabs">{[SearchTab.Posts, SearchTab.Profiles].map(renderTab)}</div>
{keyword && allUsers?.slice(0, 3).map(u => <ProfilePreview actions={<></>} className="card" pubkey={u.pubkey} />)} {tabContent()}
{keyword && (
<Timeline
key={keyword}
subject={{
type: "keyword",
items: [keyword],
discriminator: keyword,
}}
postsOnly={false}
method={"TIME_RANGE"}
/>
)}
</div> </div>
); );
}; };

View File

@ -22,8 +22,6 @@ export default defineMessages({
Sats: { defaultMessage: "{n} {n, plural, =1 {sat} other {sats}}" }, Sats: { defaultMessage: "{n} {n, plural, =1 {sat} other {sats}}" },
Following: { defaultMessage: "Following {n}" }, Following: { defaultMessage: "Following {n}" },
Settings: { defaultMessage: "Settings" }, Settings: { defaultMessage: "Settings" },
Search: { defaultMessage: "Search" },
SearchPlaceholder: { defaultMessage: "Search..." },
Messages: { defaultMessage: "Messages" }, Messages: { defaultMessage: "Messages" },
MarkAllRead: { defaultMessage: "Mark All Read" }, MarkAllRead: { defaultMessage: "Mark All Read" },
GetVerified: { defaultMessage: "Get Verified" }, GetVerified: { defaultMessage: "Get Verified" },
@ -46,4 +44,5 @@ export default defineMessages({
}, },
Bookmarks: { defaultMessage: "Bookmarks" }, Bookmarks: { defaultMessage: "Bookmarks" },
BookmarksCount: { defaultMessage: "{n} Bookmarks" }, BookmarksCount: { defaultMessage: "{n} Bookmarks" },
KeyPlaceholder: { defaultMessage: "nsec, npub, nip-05, hex" },
}); });

View File

@ -42,6 +42,7 @@
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
-webkit-tap-highlight-color: transparent;
} }
html.light { html.light {

View File

@ -244,6 +244,9 @@
"B6+XJy": { "B6+XJy": {
"defaultMessage": "zapped" "defaultMessage": "zapped"
}, },
"B6H7eJ": {
"defaultMessage": "nsec, npub, nip-05, hex"
},
"BGCM48": { "BGCM48": {
"defaultMessage": "Write access to Snort relay, with 1 year of event retention" "defaultMessage": "Write access to Snort relay, with 1 year of event retention"
}, },
@ -268,6 +271,9 @@
"CHTbO3": { "CHTbO3": {
"defaultMessage": "Failed to load invoice" "defaultMessage": "Failed to load invoice"
}, },
"CVWeJ6": {
"defaultMessage": "Trending People"
},
"CmZ9ls": { "CmZ9ls": {
"defaultMessage": "{n} Muted" "defaultMessage": "{n} Muted"
}, },
@ -337,9 +343,6 @@
"FS3b54": { "FS3b54": {
"defaultMessage": "Done!" "defaultMessage": "Done!"
}, },
"FSYL8G": {
"defaultMessage": "Trending Users"
},
"FdhSU2": { "FdhSU2": {
"defaultMessage": "Claim Now" "defaultMessage": "Claim Now"
}, },
@ -413,6 +416,9 @@
"Iwm6o2": { "Iwm6o2": {
"defaultMessage": "NIP-05 Shop" "defaultMessage": "NIP-05 Shop"
}, },
"Ix8l+B": {
"defaultMessage": "Trending Notes"
},
"J+dIsA": { "J+dIsA": {
"defaultMessage": "Subscriptions" "defaultMessage": "Subscriptions"
}, },
@ -422,9 +428,6 @@
"JHEHCk": { "JHEHCk": {
"defaultMessage": "Zaps ({n})" "defaultMessage": "Zaps ({n})"
}, },
"JTgbT0": {
"defaultMessage": "Find Twitter follows"
},
"JXtsQW": { "JXtsQW": {
"defaultMessage": "Fast Zap Donation" "defaultMessage": "Fast Zap Donation"
}, },
@ -516,6 +519,9 @@
"NndBJE": { "NndBJE": {
"defaultMessage": "New users page" "defaultMessage": "New users page"
}, },
"O9GTIc": {
"defaultMessage": "Profile picture"
},
"OEW7yJ": { "OEW7yJ": {
"defaultMessage": "Zaps" "defaultMessage": "Zaps"
}, },
@ -589,12 +595,20 @@
"RhDAoS": { "RhDAoS": {
"defaultMessage": "Are you sure you want to delete {id}" "defaultMessage": "Are you sure you want to delete {id}"
}, },
"RjpoYG": {
"defaultMessage": "Recent",
"description": "Sort order name"
},
"RoOyAh": { "RoOyAh": {
"defaultMessage": "Relays" "defaultMessage": "Relays"
}, },
"Rs4kCE": { "Rs4kCE": {
"defaultMessage": "Bookmark" "defaultMessage": "Bookmark"
}, },
"RwFaYs": {
"defaultMessage": "Sort",
"description": "Label for sorting options for people search"
},
"SOqbe9": { "SOqbe9": {
"defaultMessage": "Update Lightning Address" "defaultMessage": "Update Lightning Address"
}, },
@ -618,6 +632,9 @@
"defaultMessage": "Hex Salt..", "defaultMessage": "Hex Salt..",
"description": "Hexidecimal 'salt' input for imgproxy" "description": "Hexidecimal 'salt' input for imgproxy"
}, },
"Tpy00S": {
"defaultMessage": "People"
},
"TwyMau": { "TwyMau": {
"defaultMessage": "Account" "defaultMessage": "Account"
}, },
@ -627,9 +644,6 @@
"ULotH9": { "ULotH9": {
"defaultMessage": "Amount: {amount} sats" "defaultMessage": "Amount: {amount} sats"
}, },
"UQ3pOC": {
"defaultMessage": "On Nostr, many people have the same username. User names and identity are separate things. You can get a unique identifier in the next step."
},
"UUPFlt": { "UUPFlt": {
"defaultMessage": "Users must accept the content warning to show the content of your note." "defaultMessage": "Users must accept the content warning to show the content of your note."
}, },
@ -713,6 +727,9 @@
"ZUZedV": { "ZUZedV": {
"defaultMessage": "Lightning Donation:" "defaultMessage": "Lightning Donation:"
}, },
"Zr5TMx": {
"defaultMessage": "Setup profile"
},
"a5UPxh": { "a5UPxh": {
"defaultMessage": "Fund developers and platforms providing NIP-05 verification services" "defaultMessage": "Fund developers and platforms providing NIP-05 verification services"
}, },
@ -963,6 +980,10 @@
"mKhgP9": { "mKhgP9": {
"defaultMessage": "{n,plural,=0{} =1{zapped} other{zapped}}" "defaultMessage": "{n,plural,=0{} =1{zapped} other{zapped}}"
}, },
"mTJFgF": {
"defaultMessage": "Popular",
"description": "Sort order name"
},
"mfe8RW": { "mfe8RW": {
"defaultMessage": "Option: {n}" "defaultMessage": "Option: {n}"
}, },

View File

@ -79,6 +79,7 @@
"AyGauy": "Login", "AyGauy": "Login",
"B4C47Y": "name too short", "B4C47Y": "name too short",
"B6+XJy": "zapped", "B6+XJy": "zapped",
"B6H7eJ": "nsec, npub, nip-05, hex",
"BGCM48": "Write access to Snort relay, with 1 year of event retention", "BGCM48": "Write access to Snort relay, with 1 year of event retention",
"BOUMjw": "No nostr users found for {twitterUsername}", "BOUMjw": "No nostr users found for {twitterUsername}",
"BOr9z/": "Snort is an open source project built by passionate people in their free time", "BOr9z/": "Snort is an open source project built by passionate people in their free time",
@ -87,6 +88,7 @@
"C5xzTC": "Premium", "C5xzTC": "Premium",
"C81/uG": "Logout", "C81/uG": "Logout",
"CHTbO3": "Failed to load invoice", "CHTbO3": "Failed to load invoice",
"CVWeJ6": "Trending People",
"CmZ9ls": "{n} Muted", "CmZ9ls": "{n} Muted",
"Cu/K85": "Translated from {lang}", "Cu/K85": "Translated from {lang}",
"D+KzKd": "Automatically zap every note when loaded", "D+KzKd": "Automatically zap every note when loaded",
@ -110,7 +112,6 @@
"FDguSC": "{n} Zaps", "FDguSC": "{n} Zaps",
"FP+D3H": "LNURL to forward zaps to", "FP+D3H": "LNURL to forward zaps to",
"FS3b54": "Done!", "FS3b54": "Done!",
"FSYL8G": "Trending Users",
"FdhSU2": "Claim Now", "FdhSU2": "Claim Now",
"FfYsOb": "An error has occured!", "FfYsOb": "An error has occured!",
"FmXUJg": "follows you", "FmXUJg": "follows you",
@ -135,10 +136,10 @@
"INSqIz": "Twitter username...", "INSqIz": "Twitter username...",
"IUZC+0": "This means that nobody can modify notes which you have created and everybody can easily verify that the notes they are reading are created by you.", "IUZC+0": "This means that nobody can modify notes which you have created and everybody can easily verify that the notes they are reading are created by you.",
"Iwm6o2": "NIP-05 Shop", "Iwm6o2": "NIP-05 Shop",
"Ix8l+B": "Trending Notes",
"J+dIsA": "Subscriptions", "J+dIsA": "Subscriptions",
"JCIgkj": "Username", "JCIgkj": "Username",
"JHEHCk": "Zaps ({n})", "JHEHCk": "Zaps ({n})",
"JTgbT0": "Find Twitter follows",
"JXtsQW": "Fast Zap Donation", "JXtsQW": "Fast Zap Donation",
"JkLHGw": "Website", "JkLHGw": "Website",
"JymXbw": "Private Key", "JymXbw": "Private Key",
@ -169,6 +170,7 @@
"NepkXH": "Can't vote with {amount} sats, please set a different default zap amount", "NepkXH": "Can't vote with {amount} sats, please set a different default zap amount",
"NfNk2V": "Your private key", "NfNk2V": "Your private key",
"NndBJE": "New users page", "NndBJE": "New users page",
"O9GTIc": "Profile picture",
"OEW7yJ": "Zaps", "OEW7yJ": "Zaps",
"OKhRC6": "Share", "OKhRC6": "Share",
"OLEm6z": "Unknown login error", "OLEm6z": "Unknown login error",
@ -193,8 +195,10 @@
"RahCRH": "Expired", "RahCRH": "Expired",
"RfhLwC": "By: {author}", "RfhLwC": "By: {author}",
"RhDAoS": "Are you sure you want to delete {id}", "RhDAoS": "Are you sure you want to delete {id}",
"RjpoYG": "Recent",
"RoOyAh": "Relays", "RoOyAh": "Relays",
"Rs4kCE": "Bookmark", "Rs4kCE": "Bookmark",
"RwFaYs": "Sort",
"SOqbe9": "Update Lightning Address", "SOqbe9": "Update Lightning Address",
"SX58hM": "Copy", "SX58hM": "Copy",
"SYQtZ7": "LN Address Proxy", "SYQtZ7": "LN Address Proxy",
@ -202,10 +206,10 @@
"Ss0sWu": "Pay Now", "Ss0sWu": "Pay Now",
"TMfYfY": "Cashu token", "TMfYfY": "Cashu token",
"TpgeGw": "Hex Salt..", "TpgeGw": "Hex Salt..",
"Tpy00S": "People",
"TwyMau": "Account", "TwyMau": "Account",
"UDYlxu": "Pending Subscriptions", "UDYlxu": "Pending Subscriptions",
"ULotH9": "Amount: {amount} sats", "ULotH9": "Amount: {amount} sats",
"UQ3pOC": "On Nostr, many people have the same username. User names and identity are separate things. You can get a unique identifier in the next step.",
"UUPFlt": "Users must accept the content warning to show the content of your note.", "UUPFlt": "Users must accept the content warning to show the content of your note.",
"Up5U7K": "Block", "Up5U7K": "Block",
"VBadwB": "Hmm, can't find a key manager extension.. try reloading the page.", "VBadwB": "Hmm, can't find a key manager extension.. try reloading the page.",
@ -233,6 +237,7 @@
"ZKORll": "Activate Now", "ZKORll": "Activate Now",
"ZLmyG9": "Contributors", "ZLmyG9": "Contributors",
"ZUZedV": "Lightning Donation:", "ZUZedV": "Lightning Donation:",
"Zr5TMx": "Setup profile",
"a5UPxh": "Fund developers and platforms providing NIP-05 verification services", "a5UPxh": "Fund developers and platforms providing NIP-05 verification services",
"aWpBzj": "Show more", "aWpBzj": "Show more",
"b12Goz": "Mnemonic", "b12Goz": "Mnemonic",
@ -315,6 +320,7 @@
"mKAr6h": "Follow all", "mKAr6h": "Follow all",
"mKh2HS": "File upload service", "mKh2HS": "File upload service",
"mKhgP9": "{n,plural,=0{} =1{zapped} other{zapped}}", "mKhgP9": "{n,plural,=0{} =1{zapped} other{zapped}}",
"mTJFgF": "Popular",
"mfe8RW": "Option: {n}", "mfe8RW": "Option: {n}",
"n1Whvj": "Switch", "n1Whvj": "Switch",
"nDejmx": "Unblock", "nDejmx": "Unblock",