Cache all the things
This commit is contained in:
@ -56,7 +56,7 @@ export default function DM(props: DMProps) {
|
||||
<div className={isMe ? "dm me" : "dm other"} ref={ref}>
|
||||
<div>
|
||||
{sender()}
|
||||
<Text content={content} tags={[]} creator={otherPubkey} />
|
||||
<Text id={msg.id} content={content} tags={[]} creator={otherPubkey} />
|
||||
</div>
|
||||
<div>
|
||||
<NoteTime from={msg.created_at * 1000} fallback={formatMessage(messages.JustNow)} />
|
||||
|
@ -9,6 +9,7 @@ import AsyncButton from "Element/AsyncButton";
|
||||
import { System } from "index";
|
||||
|
||||
import messages from "./messages";
|
||||
import { FollowsFeed } from "Cache";
|
||||
|
||||
export interface FollowButtonProps {
|
||||
pubkey: HexKey;
|
||||
@ -24,6 +25,7 @@ export default function FollowButton(props: FollowButtonProps) {
|
||||
async function follow(pubkey: HexKey) {
|
||||
if (publisher) {
|
||||
const ev = await publisher.contactList([pubkey, ...follows.item], relays.item);
|
||||
await FollowsFeed.backFill(System, [pubkey]);
|
||||
System.BroadcastEvent(ev);
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import useLogin from "Hooks/useLogin";
|
||||
import { System } from "index";
|
||||
|
||||
import messages from "./messages";
|
||||
import { FollowsFeed } from "Cache";
|
||||
|
||||
export interface FollowListBaseProps {
|
||||
pubkeys: HexKey[];
|
||||
@ -34,6 +35,7 @@ export default function FollowListBase({
|
||||
async function followAll() {
|
||||
if (publisher) {
|
||||
const ev = await publisher.contactList([...pubkeys, ...follows.item], relays.item);
|
||||
await FollowsFeed.backFill(System, pubkeys);
|
||||
System.BroadcastEvent(ev);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { FormattedMessage } from "react-intl";
|
||||
import { NostrEvent, NostrLink } from "@snort/system";
|
||||
|
||||
import { findTag } from "SnortUtils";
|
||||
import useEventFeed from "Feed/EventFeed";
|
||||
import { useEventFeed } from "Feed/EventFeed";
|
||||
import PageSpinner from "Element/PageSpinner";
|
||||
import Reveal from "Element/Reveal";
|
||||
import { MediaElement } from "Element/MediaElement";
|
||||
|
@ -36,6 +36,8 @@ import { LiveEvent } from "Element/LiveEvent";
|
||||
import { NoteContextMenu, NoteTranslation } from "Element/NoteContextMenu";
|
||||
import Reactions from "Element/Reactions";
|
||||
import { ZapGoal } from "Element/ZapGoal";
|
||||
import NoteReaction from "Element/NoteReaction";
|
||||
import ProfilePreview from "Element/ProfilePreview";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
@ -81,8 +83,10 @@ const HiddenNote = ({ children }: { children: React.ReactNode }) => {
|
||||
};
|
||||
|
||||
export default function Note(props: NoteProps) {
|
||||
const { data: ev, related, highlight, options: opt, ignoreModeration = false, className } = props;
|
||||
|
||||
const { data: ev, className } = props;
|
||||
if (ev.kind === EventKind.Repost) {
|
||||
return <NoteReaction data={ev} key={ev.id} root={undefined} depth={(props.depth ?? 0) + 1} />;
|
||||
}
|
||||
if (ev.kind === EventKind.FileHeader) {
|
||||
return <NostrFileElement ev={ev} />;
|
||||
}
|
||||
@ -95,10 +99,19 @@ export default function Note(props: NoteProps) {
|
||||
if (ev.kind === EventKind.LiveEvent) {
|
||||
return <LiveEvent ev={ev} />;
|
||||
}
|
||||
if (ev.kind === EventKind.SetMetadata) {
|
||||
return <ProfilePreview actions={<></>} pubkey={ev.pubkey} className="card" />;
|
||||
}
|
||||
if (ev.kind === (9041 as EventKind)) {
|
||||
return <ZapGoal ev={ev} />;
|
||||
}
|
||||
|
||||
return <NoteInner {...props} />
|
||||
}
|
||||
|
||||
export function NoteInner(props: NoteProps) {
|
||||
const { data: ev, related, highlight, options: opt, ignoreModeration = false, className } = props;
|
||||
|
||||
const baseClassName = `note card${className ? ` ${className}` : ""}`;
|
||||
const navigate = useNavigate();
|
||||
const [showReactions, setShowReactions] = useState(false);
|
||||
@ -209,12 +222,13 @@ export default function Note(props: NoteProps) {
|
||||
)}
|
||||
</>
|
||||
}>
|
||||
<Text content={body} tags={ev.tags} creator={ev.pubkey} />
|
||||
<Text id={ev.id} content={body} tags={ev.tags} creator={ev.pubkey} />
|
||||
</Reveal>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text
|
||||
id={ev.id}
|
||||
content={body}
|
||||
tags={ev.tags}
|
||||
creator={ev.pubkey}
|
||||
@ -307,7 +321,7 @@ export default function Note(props: NoteProps) {
|
||||
if (alt) {
|
||||
return (
|
||||
<div className="note-quote">
|
||||
<Text content={alt} tags={[]} creator={ev.pubkey} />
|
||||
<Text id={ev.id} content={alt} tags={[]} creator={ev.pubkey} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
@ -379,7 +393,7 @@ export default function Note(props: NoteProps) {
|
||||
{options.showContextMenu && (
|
||||
<NoteContextMenu
|
||||
ev={ev}
|
||||
react={async () => {}}
|
||||
react={async () => { }}
|
||||
onTranslated={t => setTranslated(t)}
|
||||
setShowReactions={setShowReactions}
|
||||
/>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import useEventFeed from "Feed/EventFeed";
|
||||
import { useEventFeed } from "Feed/EventFeed";
|
||||
import { NostrLink } from "@snort/system";
|
||||
import Note from "Element/Note";
|
||||
import PageSpinner from "Element/PageSpinner";
|
||||
|
@ -14,6 +14,7 @@ import { useUserProfile } from "@snort/system-react";
|
||||
export interface NoteReactionProps {
|
||||
data: TaggedNostrEvent;
|
||||
root?: TaggedNostrEvent;
|
||||
depth?: number;
|
||||
}
|
||||
export default function NoteReaction(props: NoteReactionProps) {
|
||||
const { data: ev } = props;
|
||||
@ -47,7 +48,7 @@ export default function NoteReaction(props: NoteReactionProps) {
|
||||
try {
|
||||
const r: NostrEvent = JSON.parse(ev.content);
|
||||
EventExt.fixupEvent(r);
|
||||
if(!EventExt.verify(r)) {
|
||||
if (!EventExt.verify(r)) {
|
||||
console.debug("Event in repost is invalid");
|
||||
return undefined;
|
||||
}
|
||||
@ -78,7 +79,7 @@ export default function NoteReaction(props: NoteReactionProps) {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{root ? <Note data={root} options={opt} related={[]} /> : null}
|
||||
{root ? <Note data={root} options={opt} related={[]} depth={props.depth} /> : null}
|
||||
{!root && refEvent ? (
|
||||
<p>
|
||||
<Link to={eventLink(refEvent[1] ?? "", refEvent[2])}>
|
||||
|
@ -116,7 +116,7 @@ export default function Poll(props: PollProps) {
|
||||
{opt === voting ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Text content={desc} tags={props.ev.tags} creator={props.ev.pubkey} disableMediaSpotlight={true} />
|
||||
<Text id={props.ev.id} content={desc} tags={props.ev.tags} creator={props.ev.pubkey} disableMediaSpotlight={true} />
|
||||
)}
|
||||
</div>
|
||||
{showResults && (
|
||||
|
@ -3,10 +3,11 @@ import { useMemo } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { RelaySettings } from "@snort/system";
|
||||
import { unixNowMs } from "@snort/shared";
|
||||
|
||||
import useRelayState from "Feed/RelayState";
|
||||
import { System } from "index";
|
||||
import { getRelayName, unixNowMs, unwrap } from "SnortUtils";
|
||||
import { getRelayName, unwrap } from "SnortUtils";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { setRelays } from "Login";
|
||||
import Icon from "Icons/Icon";
|
||||
|
@ -6,10 +6,8 @@
|
||||
.text .text-frag {
|
||||
text-overflow: ellipsis;
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
overflow-x: hidden;
|
||||
overflow-y: visible;
|
||||
display: inline;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.text .text-frag > a {
|
||||
|
@ -11,70 +11,109 @@ import { ProxyImg } from "./ProxyImg";
|
||||
import { SpotlightMedia } from "./SpotlightMedia";
|
||||
|
||||
export interface TextProps {
|
||||
id: string;
|
||||
content: string;
|
||||
creator: HexKey;
|
||||
tags: Array<Array<string>>;
|
||||
disableMedia?: boolean;
|
||||
disableMediaSpotlight?: boolean;
|
||||
disableLinkPreview?: boolean;
|
||||
depth?: number;
|
||||
truncate?: number;
|
||||
className?: string;
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
export default function Text({ content, tags, creator, disableMedia, depth, disableMediaSpotlight }: TextProps) {
|
||||
const TextCache = new Map<string, Array<ParsedFragment>>();
|
||||
|
||||
export default function Text({
|
||||
id,
|
||||
content,
|
||||
tags,
|
||||
creator,
|
||||
disableMedia,
|
||||
depth,
|
||||
disableMediaSpotlight,
|
||||
disableLinkPreview,
|
||||
truncate,
|
||||
className,
|
||||
onClick
|
||||
}: TextProps) {
|
||||
const [showSpotlight, setShowSpotlight] = useState(false);
|
||||
const [imageIdx, setImageIdx] = useState(0);
|
||||
|
||||
const elements = useMemo(() => {
|
||||
return transformText(content, tags);
|
||||
}, [content]);
|
||||
const cached = TextCache.get(id);
|
||||
if (cached) return cached;
|
||||
const newCache = transformText(content, tags);
|
||||
TextCache.set(id, newCache);
|
||||
return newCache;
|
||||
}, [content, id]);
|
||||
|
||||
const images = elements.filter(a => a.type === "media" && a.mimeType?.startsWith("image")).map(a => a.content);
|
||||
|
||||
function renderChunk(a: ParsedFragment) {
|
||||
if (a.type === "media" && !a.mimeType?.startsWith("unknown")) {
|
||||
if (disableMedia ?? false) {
|
||||
return (
|
||||
<a href={a.content} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||
{a.content}
|
||||
</a>
|
||||
);
|
||||
const renderContent = () => {
|
||||
let lenCtr = 0;
|
||||
function renderChunk(a: ParsedFragment) {
|
||||
if (truncate) {
|
||||
if (lenCtr > truncate) {
|
||||
return null;
|
||||
} else if (lenCtr + a.content.length > truncate) {
|
||||
lenCtr += a.content.length;
|
||||
return <div className="text-frag">{a.content.slice(0, truncate - lenCtr)}...</div>
|
||||
} else {
|
||||
lenCtr += a.content.length;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<RevealMedia
|
||||
link={a.content}
|
||||
creator={creator}
|
||||
onMediaClick={e => {
|
||||
if (!disableMediaSpotlight) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowSpotlight(true);
|
||||
const selected = images.findIndex(b => b === a.content);
|
||||
setImageIdx(selected === -1 ? 0 : selected);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
switch (a.type) {
|
||||
case "invoice":
|
||||
return <Invoice invoice={a.content} />;
|
||||
case "hashtag":
|
||||
return <Hashtag tag={a.content} />;
|
||||
case "cashu":
|
||||
return <CashuNuts token={a.content} />;
|
||||
case "media":
|
||||
case "link":
|
||||
return <HyperText link={a.content} depth={depth} showLinkPreview={!(disableMedia ?? false)}/>;
|
||||
case "custom_emoji":
|
||||
return <ProxyImg src={a.content} size={15} className="custom-emoji" />;
|
||||
default:
|
||||
return <div className="text-frag">{a.content}</div>;
|
||||
|
||||
if (a.type === "media" && !a.mimeType?.startsWith("unknown")) {
|
||||
if (disableMedia ?? false) {
|
||||
return (
|
||||
<a href={a.content} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||
{a.content}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<RevealMedia
|
||||
link={a.content}
|
||||
creator={creator}
|
||||
onMediaClick={e => {
|
||||
if (!disableMediaSpotlight) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowSpotlight(true);
|
||||
const selected = images.findIndex(b => b === a.content);
|
||||
setImageIdx(selected === -1 ? 0 : selected);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
switch (a.type) {
|
||||
case "invoice":
|
||||
return <Invoice invoice={a.content} />;
|
||||
case "hashtag":
|
||||
return <Hashtag tag={a.content} />;
|
||||
case "cashu":
|
||||
return <CashuNuts token={a.content} />;
|
||||
case "media":
|
||||
case "link":
|
||||
return <HyperText link={a.content} depth={depth} showLinkPreview={!(disableLinkPreview ?? false)} />;
|
||||
case "custom_emoji":
|
||||
return <ProxyImg src={a.content} size={15} className="custom-emoji" />;
|
||||
default:
|
||||
return <div className="text-frag">{a.content}</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return elements.map(a => renderChunk(a));
|
||||
}
|
||||
|
||||
return (
|
||||
<div dir="auto" className="text">
|
||||
{elements.map(a => renderChunk(a))}
|
||||
<div dir="auto" className={`text${className ? ` ${className}` : ""}`} onClick={onClick}>
|
||||
{renderContent()}
|
||||
{showSpotlight && <SpotlightMedia images={images} onClose={() => setShowSpotlight(false)} idx={imageIdx} />}
|
||||
</div>
|
||||
);
|
||||
|
@ -2,18 +2,14 @@ import "./Timeline.css";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { TaggedNostrEvent, EventKind, u256, parseZap } from "@snort/system";
|
||||
import { TaggedNostrEvent, EventKind, u256 } from "@snort/system";
|
||||
|
||||
import Icon from "Icons/Icon";
|
||||
import { dedupeByPubkey, findTag, tagFilterOfTextRepost } from "SnortUtils";
|
||||
import { dedupeByPubkey, findTag } from "SnortUtils";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import useTimelineFeed, { TimelineFeed, TimelineSubject } from "Feed/TimelineFeed";
|
||||
import Zap from "Element/Zap";
|
||||
import Note from "Element/Note";
|
||||
import NoteReaction from "Element/NoteReaction";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import ProfilePreview from "Element/ProfilePreview";
|
||||
import { UserCache } from "Cache";
|
||||
import { LiveStreams } from "Element/LiveStreams";
|
||||
|
||||
export interface TimelineProps {
|
||||
@ -28,7 +24,7 @@ export interface TimelineProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of notes by pubkeys
|
||||
* A list of notes by "subject"
|
||||
*/
|
||||
const Timeline = (props: TimelineProps) => {
|
||||
const feedOptions = useMemo(() => {
|
||||
@ -70,44 +66,10 @@ const Timeline = (props: TimelineProps) => {
|
||||
return (feed.main ?? []).filter(a => a.kind === EventKind.LiveEvent && findTag(a, "status") === "live");
|
||||
}, [feed]);
|
||||
|
||||
const findRelated = useCallback(
|
||||
(id?: u256) => {
|
||||
if (!id) return undefined;
|
||||
return (feed.related ?? []).find(a => a.id === id);
|
||||
},
|
||||
[feed.related]
|
||||
);
|
||||
const latestAuthors = useMemo(() => {
|
||||
return dedupeByPubkey(latestFeed).map(e => e.pubkey);
|
||||
}, [latestFeed]);
|
||||
|
||||
function eventElement(e: TaggedNostrEvent) {
|
||||
switch (e.kind) {
|
||||
case EventKind.SetMetadata: {
|
||||
return <ProfilePreview actions={<></>} pubkey={e.pubkey} className="card" />;
|
||||
}
|
||||
case EventKind.Polls:
|
||||
case EventKind.TextNote: {
|
||||
const eRef = e.tags.find(tagFilterOfTextRepost(e))?.at(1);
|
||||
if (eRef) {
|
||||
return <NoteReaction data={e} key={e.id} root={findRelated(eRef)} />;
|
||||
}
|
||||
return (
|
||||
<Note key={e.id} data={e} related={relatedFeed(e.id)} ignoreModeration={props.ignoreModeration} depth={0} />
|
||||
);
|
||||
}
|
||||
case EventKind.ZapReceipt: {
|
||||
const zap = parseZap(e, UserCache);
|
||||
return zap.event ? null : <Zap zap={zap} key={e.id} />;
|
||||
}
|
||||
case EventKind.Reaction:
|
||||
case EventKind.Repost: {
|
||||
const eRef = findTag(e, "e");
|
||||
return <NoteReaction data={e} key={e.id} root={findRelated(eRef)} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onShowLatest(scrollToTop = false) {
|
||||
feed.showLatest();
|
||||
if (scrollToTop) {
|
||||
@ -144,7 +106,7 @@ const Timeline = (props: TimelineProps) => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{mainFeed.map(eventElement)}
|
||||
{mainFeed.map(e => <Note key={e.id} data={e} related={relatedFeed(e.id)} ignoreModeration={props.ignoreModeration} depth={0} />)}
|
||||
{(props.loadMore === undefined || props.loadMore === true) && (
|
||||
<div className="flex f-center">
|
||||
<button type="button" onClick={() => feed.loadMore()}>
|
||||
|
118
packages/app/src/Element/TimelineFollows.tsx
Normal file
118
packages/app/src/Element/TimelineFollows.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import "./Timeline.css";
|
||||
import { useCallback, useContext, useMemo, useState, useSyncExternalStore } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { TaggedNostrEvent, EventKind, u256, NostrEvent } from "@snort/system";
|
||||
import { unixNow } from "@snort/shared";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
|
||||
import { dedupeByPubkey, findTag, orderDescending } from "SnortUtils";
|
||||
import Note from "Element/Note";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { FollowsFeed } from "Cache";
|
||||
import { LiveStreams } from "Element/LiveStreams";
|
||||
import { useReactions } from "Feed/FeedReactions";
|
||||
import AsyncButton from "./AsyncButton";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import Icon from "Icons/Icon";
|
||||
|
||||
export interface TimelineFollowsProps {
|
||||
postsOnly: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of notes by "subject"
|
||||
*/
|
||||
const TimelineFollows = (props: TimelineFollowsProps) => {
|
||||
const [latest, setLatest] = useState(unixNow());
|
||||
const feed = useSyncExternalStore(cb => FollowsFeed.hook(cb, "*"), () => FollowsFeed.snapshot())
|
||||
const reactions = useReactions("follows-feed-reactions", feed.map(a => a.id));
|
||||
const system = useContext(SnortContext);
|
||||
const login = useLogin();
|
||||
const { muted, isMuted } = useModeration();
|
||||
const { ref, inView } = useInView();
|
||||
|
||||
const sortedFeed = useMemo(() => orderDescending(feed), [feed]);
|
||||
|
||||
const filterPosts = useCallback(
|
||||
function <T extends NostrEvent>(nts: Array<T>) {
|
||||
const a = nts.filter(a => a.kind !== EventKind.LiveEvent);
|
||||
return a
|
||||
?.filter(a => (props.postsOnly ? !a.tags.some(b => b[0] === "e") : true))
|
||||
.filter(a => !isMuted(a.pubkey) && login.follows.item.includes(a.pubkey));
|
||||
},
|
||||
[props.postsOnly, muted, login.follows.timestamp]
|
||||
);
|
||||
|
||||
const mainFeed = useMemo(() => {
|
||||
return filterPosts((sortedFeed ?? []).filter(a => a.created_at <= latest));
|
||||
}, [sortedFeed, filterPosts, latest, login.follows.timestamp]);
|
||||
|
||||
const latestFeed = useMemo(() => {
|
||||
return filterPosts((sortedFeed ?? []).filter(a => a.created_at > latest));
|
||||
}, [sortedFeed, latest]);
|
||||
|
||||
const relatedFeed = useCallback(
|
||||
(id: u256) => {
|
||||
return (reactions?.data ?? []).filter(a => findTag(a, "e") === id);
|
||||
},
|
||||
[reactions]
|
||||
);
|
||||
|
||||
const liveStreams = useMemo(() => {
|
||||
return (sortedFeed ?? []).filter(a => a.kind === EventKind.LiveEvent && findTag(a, "status") === "live");
|
||||
}, [sortedFeed]);
|
||||
|
||||
const latestAuthors = useMemo(() => {
|
||||
return dedupeByPubkey(latestFeed).map(e => e.pubkey);
|
||||
}, [latestFeed]);
|
||||
|
||||
function onShowLatest(scrollToTop = false) {
|
||||
setLatest(unixNow());
|
||||
if (scrollToTop) {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<LiveStreams evs={liveStreams} />
|
||||
{latestFeed.length > 0 && (
|
||||
<>
|
||||
<div className="card latest-notes" onClick={() => onShowLatest()} ref={ref}>
|
||||
{latestAuthors.slice(0, 3).map(p => {
|
||||
return <ProfileImage pubkey={p} showUsername={false} link={""} />;
|
||||
})}
|
||||
<FormattedMessage
|
||||
defaultMessage="{n} new {n, plural, =1 {note} other {notes}}"
|
||||
values={{ n: latestFeed.length }}
|
||||
/>
|
||||
<Icon name="arrowUp" />
|
||||
</div>
|
||||
{!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} link={""} />;
|
||||
})}
|
||||
<FormattedMessage
|
||||
defaultMessage="{n} new {n, plural, =1 {note} other {notes}}"
|
||||
values={{ n: latestFeed.length }}
|
||||
/>
|
||||
<Icon name="arrowUp" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{mainFeed.map(a => <Note data={a as TaggedNostrEvent} related={relatedFeed(a.id)} key={a.id} depth={0} />)}
|
||||
<div className="flex f-center p">
|
||||
<AsyncButton onClick={async () => {
|
||||
await FollowsFeed.loadMore(system, login, sortedFeed[sortedFeed.length - 1].created_at);
|
||||
}}>
|
||||
<FormattedMessage defaultMessage="Load more" />
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default TimelineFollows;
|
@ -28,7 +28,7 @@ const Zap = ({ zap, showZapped = true }: { zap: ParsedZap; showZapped?: boolean
|
||||
</div>
|
||||
{(content?.length ?? 0) > 0 && sender && (
|
||||
<div className="body">
|
||||
<Text creator={sender} content={unwrap(content)} tags={[]} />
|
||||
<Text id={zap.id} creator={sender} content={unwrap(content)} tags={[]} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user