Search improvements #552
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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}}"
|
||||||
|
34
packages/app/src/Element/TrendingPosts.tsx
Normal file
34
packages/app/src/Element/TrendingPosts.tsx
Normal 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} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
52
packages/app/src/NostrBand.tsx
Normal file
52
packages/app/src/NostrBand.tsx
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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" />
|
||||||
|
|
||||||
|
<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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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" },
|
||||||
});
|
});
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.light {
|
html.light {
|
||||||
|
@ -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}"
|
||||||
},
|
},
|
||||||
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user