feat: reaction improvements
This commit is contained in:
parent
1372c266c6
commit
96d2fdcaac
10
src/Util.ts
10
src/Util.ts
@ -1,6 +1,7 @@
|
|||||||
import * as secp from "@noble/secp256k1";
|
import * as secp from "@noble/secp256k1";
|
||||||
import { bech32 } from "bech32";
|
import { bech32 } from "bech32";
|
||||||
import { HexKey, u256 } from "./nostr";
|
import { HexKey, RawEvent, TaggedRawEvent, u256 } from "./nostr";
|
||||||
|
import EventKind from "./nostr/EventKind";
|
||||||
|
|
||||||
export async function openFile(): Promise<File | undefined> {
|
export async function openFile(): Promise<File | undefined> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -113,6 +114,13 @@ export function normalizeReaction(content: string) {
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get reactions to a specific event (#e + kind filter)
|
||||||
|
*/
|
||||||
|
export function getReactions(notes: TaggedRawEvent[], id: u256, kind = EventKind.Reaction) {
|
||||||
|
return notes?.filter(a => a.kind === kind && a.tags.some(a => a[0] === "e" && a[1] === id)) || [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts LNURL service to LN Address
|
* Converts LNURL service to LN Address
|
||||||
* @param lnurl
|
* @param lnurl
|
||||||
|
@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { default as NEvent } from "../nostr/Event";
|
import { default as NEvent } from "../nostr/Event";
|
||||||
import ProfileImage from "./ProfileImage";
|
import ProfileImage from "./ProfileImage";
|
||||||
import Text from "./Text";
|
import Text from "./Text";
|
||||||
import { eventLink, hexToBech32 } from "../Util";
|
import { eventLink, getReactions, hexToBech32 } from "../Util";
|
||||||
import NoteFooter from "./NoteFooter";
|
import NoteFooter from "./NoteFooter";
|
||||||
import NoteTime from "./NoteTime";
|
import NoteTime from "./NoteTime";
|
||||||
import EventKind from "../nostr/EventKind";
|
import EventKind from "../nostr/EventKind";
|
||||||
@ -15,8 +15,7 @@ import { TaggedRawEvent, u256 } from "../nostr";
|
|||||||
export interface NoteProps {
|
export interface NoteProps {
|
||||||
data?: TaggedRawEvent,
|
data?: TaggedRawEvent,
|
||||||
isThread?: boolean,
|
isThread?: boolean,
|
||||||
reactions: TaggedRawEvent[],
|
related: TaggedRawEvent[],
|
||||||
deletion: TaggedRawEvent[],
|
|
||||||
highlight?: boolean,
|
highlight?: boolean,
|
||||||
options?: {
|
options?: {
|
||||||
showHeader?: boolean,
|
showHeader?: boolean,
|
||||||
@ -28,11 +27,11 @@ export interface NoteProps {
|
|||||||
|
|
||||||
export default function Note(props: NoteProps) {
|
export default function Note(props: NoteProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data, isThread, reactions, deletion, highlight, options: opt, ["data-ev"]: parsedEvent } = props
|
const { data, isThread, related, highlight, options: opt, ["data-ev"]: parsedEvent } = props
|
||||||
const ev = useMemo(() => parsedEvent ?? new NEvent(data), [data]);
|
const ev = useMemo(() => parsedEvent ?? new NEvent(data), [data]);
|
||||||
const pubKeys = useMemo(() => ev.Thread?.PubKeys || [], [ev]);
|
const pubKeys = useMemo(() => ev.Thread?.PubKeys || [], [ev]);
|
||||||
|
|
||||||
const users = useProfile(pubKeys);
|
const users = useProfile(pubKeys);
|
||||||
|
const deletions = useMemo(() => getReactions(related, ev.Id, EventKind.Deletion), [related]);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
showHeader: true,
|
showHeader: true,
|
||||||
@ -43,7 +42,7 @@ export default function Note(props: NoteProps) {
|
|||||||
|
|
||||||
const transformBody = useCallback(() => {
|
const transformBody = useCallback(() => {
|
||||||
let body = ev?.Content ?? "";
|
let body = ev?.Content ?? "";
|
||||||
if (deletion?.length > 0) {
|
if (deletions?.length > 0) {
|
||||||
return (<b className="error">Deleted</b>);
|
return (<b className="error">Deleted</b>);
|
||||||
}
|
}
|
||||||
return <Text content={body} tags={ev.Tags} users={users || new Map()} />;
|
return <Text content={body} tags={ev.Tags} users={users || new Map()} />;
|
||||||
@ -106,7 +105,7 @@ export default function Note(props: NoteProps) {
|
|||||||
<div className="body" onClick={(e) => goToEvent(e, ev.Id)}>
|
<div className="body" onClick={(e) => goToEvent(e, ev.Id)}>
|
||||||
{transformBody()}
|
{transformBody()}
|
||||||
</div>
|
</div>
|
||||||
{options.showFooter ? <NoteFooter ev={ev} reactions={reactions} /> : null}
|
{options.showFooter ? <NoteFooter ev={ev} related={related} /> : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -4,33 +4,35 @@ import { faHeart, faReply, faThumbsDown, faTrash, faBolt, faRepeat } from "@fort
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import useEventPublisher from "../feed/EventPublisher";
|
import useEventPublisher from "../feed/EventPublisher";
|
||||||
import { normalizeReaction, Reaction } from "../Util";
|
import { getReactions, normalizeReaction, Reaction } from "../Util";
|
||||||
import { NoteCreator } from "./NoteCreator";
|
import { NoteCreator } from "./NoteCreator";
|
||||||
import LNURLTip from "./LNURLTip";
|
import LNURLTip from "./LNURLTip";
|
||||||
import useProfile from "../feed/ProfileFeed";
|
import useProfile from "../feed/ProfileFeed";
|
||||||
import { default as NEvent } from "../nostr/Event";
|
import { default as NEvent } from "../nostr/Event";
|
||||||
import { RootState } from "../state/Store";
|
import { RootState } from "../state/Store";
|
||||||
import { TaggedRawEvent } from "../nostr";
|
import { HexKey, TaggedRawEvent } from "../nostr";
|
||||||
|
import EventKind from "../nostr/EventKind";
|
||||||
|
|
||||||
export interface NoteFooterProps {
|
export interface NoteFooterProps {
|
||||||
reactions: TaggedRawEvent[],
|
related: TaggedRawEvent[],
|
||||||
ev: NEvent
|
ev: NEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NoteFooter(props: NoteFooterProps) {
|
export default function NoteFooter(props: NoteFooterProps) {
|
||||||
const reactions = props.reactions;
|
const { related, ev } = props;
|
||||||
const ev = props.ev;
|
|
||||||
|
|
||||||
const login = useSelector<RootState, string | undefined>(s => s.login.publicKey);
|
const login = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||||
const author = useProfile(ev.RootPubKey)?.get(ev.RootPubKey);
|
const author = useProfile(ev.RootPubKey)?.get(ev.RootPubKey);
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const [reply, setReply] = useState(false);
|
const [reply, setReply] = useState(false);
|
||||||
const [tip, setTip] = useState(false);
|
const [tip, setTip] = useState(false);
|
||||||
const isMine = ev.RootPubKey === login;
|
const isMine = ev.RootPubKey === login;
|
||||||
|
const reactions = useMemo(() => getReactions(related, ev.Id, EventKind.Reaction), [related]);
|
||||||
|
const reposts = useMemo(() => getReactions(related, ev.Id, EventKind.Repost), [related]);
|
||||||
|
|
||||||
const groupReactions = useMemo(() => {
|
const groupReactions = useMemo(() => {
|
||||||
return reactions?.reduce((acc, { content }) => {
|
return reactions?.reduce((acc, { content }) => {
|
||||||
let r = normalizeReaction(content ?? "");
|
let r = normalizeReaction(content);
|
||||||
const amount = acc[r] || 0
|
const amount = acc[r] || 0
|
||||||
return { ...acc, [r]: amount + 1 }
|
return { ...acc, [r]: amount + 1 }
|
||||||
}, {
|
}, {
|
||||||
@ -40,7 +42,11 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
}, [reactions]);
|
}, [reactions]);
|
||||||
|
|
||||||
function hasReacted(emoji: string) {
|
function hasReacted(emoji: string) {
|
||||||
return reactions?.some(({ pubkey, content }) => content === emoji && pubkey === login)
|
return reactions?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === login)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasReposted() {
|
||||||
|
return reposts.some(a => a.pubkey === login);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function react(content: string) {
|
async function react(content: string) {
|
||||||
@ -94,7 +100,8 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
</span> : null}
|
</span> : null}
|
||||||
{tipButton()}
|
{tipButton()}
|
||||||
<span className="pill" onClick={() => repost()}>
|
<span className="pill" onClick={() => repost()}>
|
||||||
<FontAwesomeIcon icon={faRepeat} />
|
<FontAwesomeIcon icon={faRepeat} color={hasReposted() ? "green" : "currenColor"} />
|
||||||
|
{reposts.length > 0 ? <> {reposts.length}</> : null}
|
||||||
</span>
|
</span>
|
||||||
<span className="pill" onClick={(e) => setReply(s => !s)}>
|
<span className="pill" onClick={(e) => setReply(s => !s)}>
|
||||||
<FontAwesomeIcon icon={faReply} />
|
<FontAwesomeIcon icon={faReply} />
|
||||||
@ -119,7 +126,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
onSend={() => setReply(false)}
|
onSend={() => setReply(false)}
|
||||||
show={reply}
|
show={reply}
|
||||||
/>
|
/>
|
||||||
<LNURLTip svc={author?.lud16 || author?.lud06 || ""} onClose={() => setTip(false)} show={tip} />
|
<LNURLTip svc={author?.lud16 || author?.lud06} onClose={() => setTip(false)} show={tip} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,7 @@ export default function NoteReaction(props: NoteReactionProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{root ? <Note data={root} options={opt} reactions={[]} deletion={[]} /> : null}
|
{root ? <Note data={root} options={opt} related={[]}/> : null}
|
||||||
{!root && refEvent ? <p><Link to={eventLink(refEvent)}>#{hexToBech32("note", refEvent).substring(0, 12)}</Link></p> : null}
|
{!root && refEvent ? <p><Link to={eventLink(refEvent)}>#{hexToBech32("note", refEvent).substring(0, 12)}</Link></p> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -13,14 +13,15 @@ export interface ThreadProps {
|
|||||||
}
|
}
|
||||||
export default function Thread(props: ThreadProps) {
|
export default function Thread(props: ThreadProps) {
|
||||||
const thisEvent = props.this;
|
const thisEvent = props.this;
|
||||||
const notes = props.notes?.map(a => new NEvent(a));
|
const notes = props.notes ?? [];
|
||||||
|
const parsedNotes = notes.map(a => new NEvent(a));
|
||||||
|
|
||||||
// root note has no thread info
|
// root note has no thread info
|
||||||
const root = useMemo(() => notes?.find(a => a.Thread === null), [notes]);
|
const root = useMemo(() => parsedNotes.find(a => a.Thread === null), [notes]);
|
||||||
|
|
||||||
const chains = useMemo(() => {
|
const chains = useMemo(() => {
|
||||||
let chains = new Map<u256, NEvent[]>();
|
let chains = new Map<u256, NEvent[]>();
|
||||||
notes?.filter(a => a.Kind === EventKind.TextNote).sort((a, b) => b.CreatedAt - a.CreatedAt).forEach((v) => {
|
parsedNotes?.filter(a => a.Kind === EventKind.TextNote).sort((a, b) => b.CreatedAt - a.CreatedAt).forEach((v) => {
|
||||||
let replyTo = v.Thread?.ReplyTo?.Event ?? v.Thread?.Root?.Event;
|
let replyTo = v.Thread?.ReplyTo?.Event ?? v.Thread?.Root?.Event;
|
||||||
if (replyTo) {
|
if (replyTo) {
|
||||||
if (!chains.has(replyTo)) {
|
if (!chains.has(replyTo)) {
|
||||||
@ -37,20 +38,19 @@ export default function Thread(props: ThreadProps) {
|
|||||||
}, [notes]);
|
}, [notes]);
|
||||||
|
|
||||||
const brokenChains = useMemo(() => {
|
const brokenChains = useMemo(() => {
|
||||||
return Array.from(chains?.keys()).filter(a => !notes?.some(b => b.Id === a));
|
return Array.from(chains?.keys()).filter(a => !parsedNotes?.some(b => b.Id === a));
|
||||||
}, [chains]);
|
}, [chains]);
|
||||||
|
|
||||||
const mentionsRoot = useMemo(() => {
|
const mentionsRoot = useMemo(() => {
|
||||||
return notes?.filter(a => a.Kind === EventKind.TextNote && a.Thread)
|
return parsedNotes?.filter(a => a.Kind === EventKind.TextNote && a.Thread)
|
||||||
}, [chains]);
|
}, [chains]);
|
||||||
|
|
||||||
function reactions(id: u256, kind = EventKind.Reaction) {
|
|
||||||
return (notes?.filter(a => a.Kind === kind && a.Tags.find(a => a.Key === "e" && a.Event === id)) || []).map(a => a.Original!);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRoot() {
|
function renderRoot() {
|
||||||
if (root) {
|
if (root) {
|
||||||
return <Note data-ev={root} reactions={reactions(root.Id)} deletion={reactions(root.Id, EventKind.Deletion)} isThread />
|
return <Note
|
||||||
|
data-ev={root}
|
||||||
|
related={notes}
|
||||||
|
isThread />
|
||||||
} else {
|
} else {
|
||||||
return <NoteGhost>
|
return <NoteGhost>
|
||||||
Loading thread root.. ({notes?.length} notes loaded)
|
Loading thread root.. ({notes?.length} notes loaded)
|
||||||
@ -69,8 +69,7 @@ export default function Thread(props: ThreadProps) {
|
|||||||
<>
|
<>
|
||||||
<Note data-ev={a}
|
<Note data-ev={a}
|
||||||
key={a.Id}
|
key={a.Id}
|
||||||
reactions={reactions(a.Id)}
|
related={notes}
|
||||||
deletion={reactions(a.Id, EventKind.Deletion)}
|
|
||||||
highlight={thisEvent === a.Id} />
|
highlight={thisEvent === a.Id} />
|
||||||
{renderChain(a.Id)}
|
{renderChain(a.Id)}
|
||||||
</>
|
</>
|
||||||
|
@ -16,10 +16,6 @@ export interface TimelineProps {
|
|||||||
export default function Timeline({ global, pubkeys }: TimelineProps) {
|
export default function Timeline({ global, pubkeys }: TimelineProps) {
|
||||||
const { main, others } = useTimelineFeed(pubkeys, global);
|
const { main, others } = useTimelineFeed(pubkeys, global);
|
||||||
|
|
||||||
function reaction(id: u256, kind = EventKind.Reaction) {
|
|
||||||
return others?.filter(a => a.kind === kind && a.tags.some(b => b[0] === "e" && b[1] === id));
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainFeed = useMemo(() => {
|
const mainFeed = useMemo(() => {
|
||||||
return main?.sort((a, b) => b.created_at - a.created_at);
|
return main?.sort((a, b) => b.created_at - a.created_at);
|
||||||
}, [main]);
|
}, [main]);
|
||||||
@ -27,7 +23,7 @@ export default function Timeline({ global, pubkeys }: TimelineProps) {
|
|||||||
function eventElement(e: TaggedRawEvent) {
|
function eventElement(e: TaggedRawEvent) {
|
||||||
switch (e.kind) {
|
switch (e.kind) {
|
||||||
case EventKind.TextNote: {
|
case EventKind.TextNote: {
|
||||||
return <Note key={e.id} data={e} reactions={reaction(e.id)} deletion={reaction(e.id, EventKind.Deletion)} />
|
return <Note key={e.id} data={e} related={others} />
|
||||||
}
|
}
|
||||||
case EventKind.Reaction:
|
case EventKind.Reaction:
|
||||||
case EventKind.Repost: {
|
case EventKind.Repost: {
|
||||||
|
@ -1,57 +1,45 @@
|
|||||||
import { useMemo } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { u256 } from "../nostr";
|
import { u256 } from "../nostr";
|
||||||
import EventKind from "../nostr/EventKind";
|
import EventKind from "../nostr/EventKind";
|
||||||
import { Subscriptions } from "../nostr/Subscriptions";
|
import { Subscriptions } from "../nostr/Subscriptions";
|
||||||
import useSubscription from "./Subscription";
|
import useSubscription from "./Subscription";
|
||||||
|
|
||||||
export default function useThreadFeed(id: u256) {
|
export default function useThreadFeed(id: u256) {
|
||||||
|
const [trackingEvents, setTrackingEvent] = useState<u256[]>([id]);
|
||||||
|
|
||||||
|
function addId(id: u256[]) {
|
||||||
|
setTrackingEvent((s) => {
|
||||||
|
let tmp = new Set([...s, ...id]);
|
||||||
|
return Array.from(tmp);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
const thisSub = new Subscriptions();
|
const thisSub = new Subscriptions();
|
||||||
thisSub.Id = `thread:${id.substring(0, 8)}`;
|
thisSub.Id = `thread:${id.substring(0, 8)}`;
|
||||||
thisSub.Ids = new Set([id]);
|
thisSub.Ids = new Set(trackingEvents);
|
||||||
|
|
||||||
// get replies to this event
|
// get replies to this event
|
||||||
const subRelated = new Subscriptions();
|
const subRelated = new Subscriptions();
|
||||||
subRelated.Kinds = new Set([EventKind.Reaction, EventKind.TextNote, EventKind.Deletion]);
|
subRelated.Kinds = new Set([EventKind.Reaction, EventKind.TextNote, EventKind.Deletion, EventKind.Repost]);
|
||||||
subRelated.ETags = thisSub.Ids;
|
subRelated.ETags = thisSub.Ids;
|
||||||
thisSub.AddSubscription(subRelated);
|
thisSub.AddSubscription(subRelated);
|
||||||
|
|
||||||
return thisSub;
|
return thisSub;
|
||||||
}, [id]);
|
}, [trackingEvents]);
|
||||||
|
|
||||||
const main = useSubscription(sub, { leaveOpen: true });
|
const main = useSubscription(sub, { leaveOpen: true });
|
||||||
|
|
||||||
const relatedThisSub = useMemo(() => {
|
useEffect(() => {
|
||||||
let thisNote = main.notes.find(a => a.id === id);
|
// debounce
|
||||||
|
let t = setTimeout(() => {
|
||||||
|
let eTags = main.notes.map(a => a.tags.filter(b => b[0] === "e").map(b => b[1])).flat();
|
||||||
|
let ids = main.notes.map(a => a.id);
|
||||||
|
let allEvents = new Set([...eTags, ...ids]);
|
||||||
|
addId(Array.from(allEvents));
|
||||||
|
}, 200);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}, [main.notes]);
|
||||||
|
|
||||||
if (thisNote) {
|
return main;
|
||||||
let otherSubs = new Subscriptions();
|
|
||||||
otherSubs.Id = `thread-related:${id.substring(0, 8)}`;
|
|
||||||
otherSubs.Ids = new Set();
|
|
||||||
for (let e of thisNote.tags.filter(a => a[0] === "e")) {
|
|
||||||
otherSubs.Ids.add(e[1]);
|
|
||||||
}
|
|
||||||
// no #e skip related
|
|
||||||
if (otherSubs.Ids.size === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let relatedSubs = new Subscriptions();
|
|
||||||
relatedSubs.Kinds = new Set([EventKind.Reaction, EventKind.TextNote, EventKind.Deletion]);
|
|
||||||
relatedSubs.ETags = otherSubs.Ids;
|
|
||||||
|
|
||||||
otherSubs.AddSubscription(relatedSubs);
|
|
||||||
return otherSubs;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [main]);
|
|
||||||
|
|
||||||
const others = useSubscription(relatedThisSub, { leaveOpen: true });
|
|
||||||
|
|
||||||
return useMemo(() => {
|
|
||||||
return {
|
|
||||||
main: main.notes,
|
|
||||||
other: others.notes,
|
|
||||||
};
|
|
||||||
}, [main, others]);
|
|
||||||
}
|
}
|
@ -1,10 +1,12 @@
|
|||||||
import { useMemo } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { HexKey } from "../nostr";
|
import { HexKey, u256 } from "../nostr";
|
||||||
import EventKind from "../nostr/EventKind";
|
import EventKind from "../nostr/EventKind";
|
||||||
import { Subscriptions } from "../nostr/Subscriptions";
|
import { Subscriptions } from "../nostr/Subscriptions";
|
||||||
import useSubscription from "./Subscription";
|
import useSubscription from "./Subscription";
|
||||||
|
|
||||||
export default function useTimelineFeed(pubKeys: HexKey | Array<HexKey>, global: boolean = false) {
|
export default function useTimelineFeed(pubKeys: HexKey | Array<HexKey>, global: boolean = false) {
|
||||||
|
const [trackingEvents, setTrackingEvent] = useState<u256[]>([]);
|
||||||
|
|
||||||
const subTab = global ? "global" : "follows";
|
const subTab = global ? "global" : "follows";
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
if (!Array.isArray(pubKeys)) {
|
if (!Array.isArray(pubKeys)) {
|
||||||
@ -27,18 +29,30 @@ export default function useTimelineFeed(pubKeys: HexKey | Array<HexKey>, global:
|
|||||||
const main = useSubscription(sub, { leaveOpen: true });
|
const main = useSubscription(sub, { leaveOpen: true });
|
||||||
|
|
||||||
const subNext = useMemo(() => {
|
const subNext = useMemo(() => {
|
||||||
return null; // TODO: spam
|
if (trackingEvents.length > 0) {
|
||||||
if (main.notes.length > 0) {
|
|
||||||
let sub = new Subscriptions();
|
let sub = new Subscriptions();
|
||||||
sub.Id = `timeline-related:${subTab}`;
|
sub.Id = `timeline-related:${subTab}`;
|
||||||
sub.Kinds = new Set([EventKind.Reaction, EventKind.Deletion]);
|
sub.Kinds = new Set([EventKind.Reaction, EventKind.Deletion, EventKind.Repost]);
|
||||||
sub.ETags = new Set(main.notes.map(a => a.id));
|
sub.ETags = new Set(trackingEvents);
|
||||||
|
|
||||||
return sub;
|
return sub;
|
||||||
}
|
}
|
||||||
}, [main]);
|
return null;
|
||||||
|
}, [trackingEvents]);
|
||||||
|
|
||||||
const others = useSubscription(subNext, { leaveOpen: true });
|
const others = useSubscription(subNext, { leaveOpen: true });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (main.notes.length > 0) {
|
||||||
|
// debounce
|
||||||
|
let t = setTimeout(() => {
|
||||||
|
setTrackingEvent(s => {
|
||||||
|
let ids = main.notes.map(a => a.id);
|
||||||
|
let temp = new Set([...s, ...ids]);
|
||||||
|
return Array.from(temp);
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}
|
||||||
|
}, [main.notes]);
|
||||||
return { main: main.notes, others: others.notes };
|
return { main: main.notes, others: others.notes };
|
||||||
}
|
}
|
@ -1,4 +1,3 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import Thread from "../element/Thread";
|
import Thread from "../element/Thread";
|
||||||
import useThreadFeed from "../feed/ThreadFeed";
|
import useThreadFeed from "../feed/ThreadFeed";
|
||||||
@ -9,12 +8,5 @@ export default function EventPage() {
|
|||||||
const id = parseId(params.id!);
|
const id = parseId(params.id!);
|
||||||
const thread = useThreadFeed(id);
|
const thread = useThreadFeed(id);
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
return <Thread notes={thread.notes} this={id} />;
|
||||||
return [
|
|
||||||
...thread.main,
|
|
||||||
...thread.other
|
|
||||||
].filter((v, i, a) => a.findIndex(x => x.id === v.id) === i);
|
|
||||||
}, [thread]);
|
|
||||||
|
|
||||||
return <Thread notes={filtered} this={id} />;
|
|
||||||
}
|
}
|
@ -9,6 +9,7 @@ import EventKind from "../nostr/EventKind";
|
|||||||
import { Subscriptions } from "../nostr/Subscriptions";
|
import { Subscriptions } from "../nostr/Subscriptions";
|
||||||
import { markNotificationsRead } from "../state/Login";
|
import { markNotificationsRead } from "../state/Login";
|
||||||
import { RootState } from "../state/Store";
|
import { RootState } from "../state/Store";
|
||||||
|
import { getReactions } from "../Util";
|
||||||
|
|
||||||
export default function NotificationsPage() {
|
export default function NotificationsPage() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -51,8 +52,7 @@ export default function NotificationsPage() {
|
|||||||
<>
|
<>
|
||||||
{sorted?.map(a => {
|
{sorted?.map(a => {
|
||||||
if (a.kind === EventKind.TextNote) {
|
if (a.kind === EventKind.TextNote) {
|
||||||
let reactions = otherNotes?.notes?.filter(c => c.tags.find(b => b[0] === "e" && b[1] === a.id));
|
return <Note data={a} key={a.id} related={otherNotes?.notes ?? []} />
|
||||||
return <Note data={a} key={a.id} reactions={reactions} deletion={[]}/>
|
|
||||||
} else if (a.kind === EventKind.Reaction) {
|
} else if (a.kind === EventKind.Reaction) {
|
||||||
let ev = new Event(a);
|
let ev = new Event(a);
|
||||||
let reactedTo = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
|
let reactedTo = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
|
||||||
|
Loading…
Reference in New Issue
Block a user