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);
margin-top: auto;
margin-bottom: auto;
overflow: hidden;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@ import "./Reactions.css";
import { useState, useMemo, useEffect } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { TaggedRawEvent } from "@snort/nostr";
import { formatShort } from "Number";
@ -75,72 +74,66 @@ const Reactions = ({ show, setShow, positive, negative, reposts, zaps }: Reactio
return show ? (
<Modal className="reactions-modal" onClose={onClose}>
<div className="reactions-view">
<div className="close" onClick={onClose}>
<Icon name="close" />
</div>
<div className="reactions-header">
<h2>
<FormattedMessage {...messages.ReactionsCount} values={{ n: total }} />
</h2>
</div>
<Tabs tabs={tabs} tab={tab} setTab={setTab} />
<div className="body" key={tab.value}>
{tab.value === 0 &&
likes.map(ev => {
return (
<div key={ev.id} className="reactions-item">
<div className="reaction-icon">{ev.content === "+" ? <Icon name="heart" /> : ev.content}</div>
<ProfileImage autoWidth={false} pubkey={ev.pubkey} />
</div>
);
})}
{tab.value === 1 &&
zaps.map(z => {
return (
z.sender && (
<div key={z.id} className="reactions-item">
<div className="zap-reaction-icon">
<Icon name="zap" size={20} />
<span className="zap-amount">{formatShort(z.amount)}</span>
</div>
<ProfileImage
autoWidth={false}
pubkey={z.anonZap ? "" : z.sender}
subHeader={
<div className="f-ellipsis zap-comment" title={z.content}>
{z.content}
</div>
}
overrideUsername={z.anonZap ? formatMessage({ defaultMessage: "Anonymous" }) : undefined}
/>
<div className="close" onClick={onClose}>
<Icon name="close" />
</div>
<div className="reactions-header">
<h2>
<FormattedMessage {...messages.ReactionsCount} values={{ n: total }} />
</h2>
</div>
<Tabs tabs={tabs} tab={tab} setTab={setTab} />
<div className="body" key={tab.value}>
{tab.value === 0 &&
likes.map(ev => {
return (
<div key={ev.id} className="reactions-item">
<div className="reaction-icon">{ev.content === "+" ? <Icon name="heart" /> : ev.content}</div>
<ProfileImage pubkey={ev.pubkey} />
</div>
);
})}
{tab.value === 1 &&
zaps.map(z => {
return (
z.sender && (
<div key={z.id} className="reactions-item">
<div className="zap-reaction-icon">
<Icon name="zap" size={20} />
<span className="zap-amount">{formatShort(z.amount)}</span>
</div>
)
);
})}
{tab.value === 2 &&
reposts.map(ev => {
return (
<div key={ev.id} className="reactions-item">
<div className="reaction-icon">
<Icon name="repost" size={16} />
</div>
<ProfileImage autoWidth={false} pubkey={ev.pubkey} />
<ProfileImage
pubkey={z.anonZap ? "" : z.sender}
subHeader={<div title={z.content}>{z.content}</div>}
link={z.anonZap ? "" : undefined}
overrideUsername={z.anonZap ? formatMessage({ defaultMessage: "Anonymous" }) : undefined}
/>
</div>
);
})}
{tab.value === 3 &&
dislikes.map(ev => {
return (
<div key={ev.id} className="reactions-item">
<div className="reaction-icon">
<Icon name="dislike" />
</div>
<ProfileImage autoWidth={false} pubkey={ev.pubkey} />
)
);
})}
{tab.value === 2 &&
reposts.map(ev => {
return (
<div key={ev.id} className="reactions-item">
<div className="reaction-icon">
<Icon name="repost" size={16} />
</div>
);
})}
</div>
<ProfileImage pubkey={ev.pubkey} />
</div>
);
})}
{tab.value === 3 &&
dislikes.map(ev => {
return (
<div key={ev.id} className="reactions-item f-ellipsis">
<div className="reaction-icon">
<Icon name="dislike" />
</div>
<ProfileImage pubkey={ev.pubkey} />
</div>
);
})}
</div>
</Modal>
) : null;

View File

@ -25,6 +25,7 @@ export interface TimelineProps {
relay?: string;
now?: number;
loadMore?: boolean;
noSort?: boolean;
}
/**
@ -46,8 +47,9 @@ const Timeline = (props: TimelineProps) => {
const filterPosts = useCallback(
(nts: readonly TaggedRawEvent[]) => {
return [...nts]
.sort((a, b) => b.created_at - a.created_at)
const a = [...nts];
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.ignoreModeration || !isMuted(a.pubkey));
},
@ -117,7 +119,7 @@ const Timeline = (props: TimelineProps) => {
<>
<div className="card latest-notes pointer" onClick={() => onShowLatest()} ref={ref}>
{latestAuthors.slice(0, 3).map(p => {
return <ProfileImage pubkey={p} showUsername={false} linkToProfile={false} />;
return <ProfileImage pubkey={p} showUsername={false} link={""} />;
})}
<FormattedMessage
defaultMessage="{n} new {n, plural, =1 {note} other {notes}}"
@ -128,7 +130,7 @@ const Timeline = (props: TimelineProps) => {
{!inView && (
<div className="card latest-notes latest-notes-fixed pointer fade-in" onClick={() => onShowLatest(true)}>
{latestAuthors.slice(0, 3).map(p => {
return <ProfileImage pubkey={p} showUsername={false} linkToProfile={false} />;
return <ProfileImage pubkey={p} showUsername={false} link={""} />;
})}
<FormattedMessage
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 PageSpinner from "Element/PageSpinner";
import NostrBandApi from "NostrBand";
interface TrendingUser {
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 = () => {
export default function TrendingUsers() {
const [userList, setUserList] = useState<HexKey[]>();
async function loadTrendingUsers() {
const api = new NostrBandApi();
const users = await api.trendingProfiles();
const keys = users.profiles.map(a => a.pubkey);
setUserList(keys);
}
useEffect(() => {
(async () => {
const data = await fetchTrendingUsers();
if (data) {
setUserList(data);
}
})();
loadTrendingUsers().catch(console.error);
}, []);
if (!userList) return <PageSpinner />;
@ -42,11 +25,9 @@ const TrendingUsers = () => {
return (
<>
<h3>
<FormattedMessage defaultMessage="Trending Users" />
<FormattedMessage defaultMessage="Trending People" />
</h3>
<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 ? (
<div className="zap note card">
<div className="header">
<ProfileImage autoWidth={false} pubkey={sender} />
{receiver !== pubKey && showZapped && <ProfileImage autoWidth={false} pubkey={unwrap(receiver)} />}
<ProfileImage pubkey={sender} />
{receiver !== pubKey && showZapped && <ProfileImage pubkey={unwrap(receiver)} />}
<div className="amount">
<span className="amount-number">
<FormattedMessage {...messages.Sats} values={{ n: formatShort(amount ?? 0) }} />
@ -151,7 +151,6 @@ export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
<div className="summary">
{sender && (
<ProfileImage
autoWidth={false}
pubkey={anonZap ? "" : sender}
overrideUsername={anonZap ? formatMessage({ defaultMessage: "Anonymous" }) : undefined}
/>

View File

@ -15,7 +15,7 @@ export interface TimelineFeedOptions {
}
export interface TimelineSubject {
type: "pubkey" | "hashtag" | "global" | "ptag" | "keyword";
type: "pubkey" | "hashtag" | "global" | "ptag" | "post_keyword" | "profile_keyword";
discriminator: 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 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) {
b.withOptions({
@ -58,7 +64,11 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
f.tag("p", subject.items);
break;
}
case "keyword": {
case "profile_keyword": {
f.search(subject.items[0] + " sort:popular");
break;
}
case "post_keyword": {
f.search(subject.items[0]);
break;
}
@ -105,7 +115,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
const subRealtime = useMemo(() => {
const rb = createBuilder();
if (rb && !pref.autoShowLatest) {
if (rb && !pref.autoShowLatest && options.method !== "LIMIT_UNTIL") {
rb.builder.withOptions({
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 { useParams } from "react-router-dom";
import ProfilePreview from "Element/ProfilePreview";
import Timeline from "Element/Timeline";
import { Tab, TabElement } from "Element/Tabs";
import { useEffect, useState } from "react";
import { debounce } from "Util";
import { router } from "index";
import { SearchRelays } from "Const";
import { System } from "System";
import { MetadataCache } from "Cache";
import { UserCache } from "Cache/UserCache";
import TrendingUsers from "Element/TrendingUsers";
import messages from "./messages";
import TrendingNotes from "Element/TrendingPosts";
const NOTES = 0;
const PROFILES = 1;
const SearchPage = () => {
const params = useParams();
const { formatMessage } = useIntl();
const [search, setSearch] = 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(() => {
if (keyword) {
// "navigate" changing only url
router.navigate(`/search/${encodeURIComponent(keyword)}`);
UserCache.search(keyword).then(v => setAllUsers(v));
} else {
router.navigate(`/search`);
setAllUsers([]);
}
}, [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 (
<div className="main-content">
<h2>
<FormattedMessage {...messages.Search} />
<FormattedMessage defaultMessage="Search" />
</h2>
<div className="flex mb10">
<input
type="text"
className="f-grow mr10"
placeholder={formatMessage(messages.SearchPlaceholder)}
placeholder={formatMessage({ defaultMessage: "Search..." })}
value={search}
onChange={e => setSearch(e.target.value)}
autoFocus={true}
/>
</div>
{!keyword && <TrendingUsers />}
{keyword && allUsers?.slice(0, 3).map(u => <ProfilePreview actions={<></>} className="card" pubkey={u.pubkey} />)}
{keyword && (
<Timeline
key={keyword}
subject={{
type: "keyword",
items: [keyword],
discriminator: keyword,
}}
postsOnly={false}
method={"TIME_RANGE"}
/>
)}
<div className="tabs">{[SearchTab.Posts, SearchTab.Profiles].map(renderTab)}</div>
{tabContent()}
</div>
);
};

View File

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

View File

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

View File

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

View File

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