Repost with content

This commit is contained in:
Kieran 2023-01-08 18:35:36 +00:00
parent c01e0ca457
commit 0e0266c813
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
7 changed files with 93 additions and 27 deletions

View File

@ -69,9 +69,10 @@ export default function Note(props) {
if (!ev.IsContent()) { if (!ev.IsContent()) {
return ( return (
<> <>
<pre>{ev.Id}</pre> <h4>Unknown event kind: {ev.Kind}</h4>
<pre>Kind: {ev.Kind}</pre> <pre>
<pre>Content: {ev.Content}</pre> {JSON.stringify(ev.ToObject(), undefined, ' ')}
</pre>
</> </>
); );
} }

View File

@ -65,13 +65,13 @@ export default function NoteFooter(props) {
return null; return null;
} }
function reactionIcon(content) { function reactionIcon(content, reacted) {
switch (content) { switch (content) {
case Reaction.Positive: { case Reaction.Positive: {
return <FontAwesomeIcon color={hasReacted(content) ? "red" : "currentColor"} icon={faHeart} />; return <FontAwesomeIcon color={reacted ? "red" : "currentColor"} icon={faHeart} />;
} }
case Reaction.Negative: { case Reaction.Negative: {
return <FontAwesomeIcon color={hasReacted(content) ? "orange" : "currentColor"} icon={faThumbsDown} />; return <FontAwesomeIcon color={reacted ? "orange" : "currentColor"} icon={faThumbsDown} />;
} }
} }
return content; return content;
@ -90,10 +90,15 @@ export default function NoteFooter(props) {
<span className="pill" onClick={(e) => setReply(s => !s)}> <span className="pill" onClick={(e) => setReply(s => !s)}>
<FontAwesomeIcon icon={faReply} /> <FontAwesomeIcon icon={faReply} />
</span> </span>
{Object.keys(groupReactions).map((emoji) => { {Object.keys(groupReactions || {}).map((emoji) => {
let didReact = hasReacted(emoji);
return ( return (
<span className="pill" onClick={() => react(emoji)} key={emoji}> <span className="pill" onClick={() => {
{reactionIcon(emoji)} if (!didReact) {
react(emoji);
}
}} key={emoji}>
{reactionIcon(emoji, didReact)}
{groupReactions[emoji] ? <>&nbsp;{groupReactions[emoji]}</> : null} {groupReactions[emoji] ? <>&nbsp;{groupReactions[emoji]}</> : null}
</span> </span>
) )

View File

@ -3,38 +3,77 @@ import moment from "moment";
import EventKind from "../nostr/EventKind"; import EventKind from "../nostr/EventKind";
import Note from "./Note"; import Note from "./Note";
import ProfileImage from "./ProfileImage"; import ProfileImage from "./ProfileImage";
import Event from "../nostr/Event";
import { eventLink } from "../Util";
import { Link } from "react-router-dom";
import { useMemo } from "react";
export default function NoteReaction(props) { export default function NoteReaction(props) {
const data = props.data; const ev = props["data-ev"] || Event.FromObject(props.data);
const root = props.root;
if (data.kind !== EventKind.Reaction) { const refEvent = useMemo(() => {
if(ev) {
let eTags = ev.Tags.filter(a => a.Key === "e");
return eTags[0].Event;
}
return null;
}, [ev]);
if (ev.Kind !== EventKind.Reaction && ev.Kind !== EventKind.Repost) {
return null; return null;
} }
function mapReaction() { function mapReaction(c) {
switch (data.content) { switch (c) {
case "+": return "❤️"; case "+": return "❤️";
case "-": return "👎"; case "-": return "👎";
default: { default: {
if (data.content.length === 0) { if (c.length === 0) {
return "❤️"; return "❤️";
} }
return data.content; return c;
} }
} }
} }
function tagLine() {
switch (ev.Kind) {
case EventKind.Reaction: return <small>Reacted with {mapReaction(ev.Content)}</small>;
case EventKind.Repost: return <small>Reposted</small>
}
}
/**
* Some clients embed the reposted note in the content
*/
function extractRoot() {
if (ev?.Kind === EventKind.Repost && ev.Content.length > 0) {
try {
let r = JSON.parse(ev.Content);
return r;
} catch (e) {
console.error("Could not load reposted content", e);
}
}
return props.root;
}
const root = extractRoot();
const opt = {
showHeader: ev?.Kind === EventKind.Repost,
showFooter: ev?.Kind === EventKind.Repost
};
return ( return (
<div className="reaction"> <div className="reaction">
<div className="header flex"> <div className="header flex">
<ProfileImage pubkey={data.pubkey} subHeader={<small>Reacted with {mapReaction()}</small>} /> <ProfileImage pubkey={ev.RootPubKey} subHeader={tagLine()} />
<div className="info"> <div className="info">
{moment(data.created_at * 1000).fromNow()} {moment(ev.CreatedAt * 1000).fromNow()}
</div> </div>
</div> </div>
{root ? <Note data={root} options={{ showHeader: false, showFooter: false }} /> : root} {root ? <Note data={root} options={opt} /> : null}
{!root && refEvent ? <p><Link to={eventLink(refEvent)}>#{refEvent.substring(0, 8)}</Link></p> : null}
</div> </div>
); );
} }

View File

@ -1,6 +1,8 @@
import { useMemo } from "react";
import useTimelineFeed from "../feed/TimelineFeed"; import useTimelineFeed from "../feed/TimelineFeed";
import EventKind from "../nostr/EventKind"; import EventKind from "../nostr/EventKind";
import Note from "./Note"; import Note from "./Note";
import NoteReaction from "./NoteReaction";
/** /**
* A list of notes by pubkeys * A list of notes by pubkeys
@ -12,10 +14,21 @@ export default function Timeline({ global, pubkeys }) {
return feed?.others?.filter(a => a.kind === kind && a.tags.some(b => b[0] === "e" && b[1] === id)); return feed?.others?.filter(a => a.kind === kind && a.tags.some(b => b[0] === "e" && b[1] === id));
} }
return ( const mainFeed = useMemo(() => {
<> return feed.main?.sort((a, b) => b.created_at - a.created_at);
{feed.main?.sort((a, b) => b.created_at - a.created_at) }, [feed]);
.map(a => <Note key={a.id} data={a} reactions={reaction(a.id)} deletion={reaction(a.id, EventKind.Deletion)} />)}
</> function eventElement(e) {
) switch (e.kind) {
case EventKind.TextNote: {
return <Note key={e.id} data={e} reactions={reaction(e.id)} deletion={reaction(e.id, EventKind.Deletion)} />
}
case EventKind.Reaction:
case EventKind.Repost: {
return <NoteReaction data={e} key={e.id}/>
}
}
}
return mainFeed.map(eventElement);
} }

View File

@ -120,13 +120,18 @@ export default function useEventPublisher() {
ev.Tags.push(new Tag(["e", id])); ev.Tags.push(new Tag(["e", id]));
return await signEvent(ev); return await signEvent(ev);
}, },
/**
* Respot a note
* @param {Event} note
* @returns
*/
repost: async (note) => { repost: async (note) => {
if (typeof note.Id !== "string") { if (typeof note.Id !== "string") {
throw "Must be parsed note in Event class"; throw "Must be parsed note in Event class";
} }
let ev = Event.ForPubKey(pubKey); let ev = Event.ForPubKey(pubKey);
ev.Kind = EventKind.Repost; ev.Kind = EventKind.Repost;
ev.Content = ""; ev.Content = JSON.stringify(note.Original);
ev.Tags.push(new Tag(["e", note.Id])); ev.Tags.push(new Tag(["e", note.Id]));
ev.Tags.push(new Tag(["p", note.PubKey])); ev.Tags.push(new Tag(["p", note.PubKey]));
return await signEvent(ev); return await signEvent(ev);

View File

@ -18,6 +18,7 @@ export default function useTimelineFeed(pubKeys, global = false) {
sub.Id = `timeline:${subTab}`; sub.Id = `timeline:${subTab}`;
sub.Authors = new Set(global ? [] : pubKeys); sub.Authors = new Set(global ? [] : pubKeys);
sub.Kinds.add(EventKind.TextNote); sub.Kinds.add(EventKind.TextNote);
sub.Kinds.add(EventKind.Repost);
sub.Limit = 20; sub.Limit = 20;
return sub; return sub;

View File

@ -42,7 +42,9 @@ export default class Tag {
ToObject() { ToObject() {
switch (this.Key) { switch (this.Key) {
case "e": { case "e": {
return ["e", this.Event, this.Relay, this.Marker]; let ret = ["e", this.Event, this.Relay, this.Marker];
let trimEnd = ret.reverse().findIndex(a => a != null);
return ret.reverse().slice(0, ret.length - trimEnd);
} }
case "p": { case "p": {
return ["p", this.PubKey]; return ["p", this.PubKey];