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()) {
return (
<>
<pre>{ev.Id}</pre>
<pre>Kind: {ev.Kind}</pre>
<pre>Content: {ev.Content}</pre>
<h4>Unknown event kind: {ev.Kind}</h4>
<pre>
{JSON.stringify(ev.ToObject(), undefined, ' ')}
</pre>
</>
);
}

View File

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

View File

@ -3,38 +3,77 @@ import moment from "moment";
import EventKind from "../nostr/EventKind";
import Note from "./Note";
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) {
const data = props.data;
const root = props.root;
const ev = props["data-ev"] || Event.FromObject(props.data);
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;
}
function mapReaction() {
switch (data.content) {
function mapReaction(c) {
switch (c) {
case "+": return "❤️";
case "-": return "👎";
default: {
if (data.content.length === 0) {
if (c.length === 0) {
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 (
<div className="reaction">
<div className="header flex">
<ProfileImage pubkey={data.pubkey} subHeader={<small>Reacted with {mapReaction()}</small>} />
<ProfileImage pubkey={ev.RootPubKey} subHeader={tagLine()} />
<div className="info">
{moment(data.created_at * 1000).fromNow()}
{moment(ev.CreatedAt * 1000).fromNow()}
</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>
);
}

View File

@ -1,6 +1,8 @@
import { useMemo } from "react";
import useTimelineFeed from "../feed/TimelineFeed";
import EventKind from "../nostr/EventKind";
import Note from "./Note";
import NoteReaction from "./NoteReaction";
/**
* 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.main?.sort((a, b) => b.created_at - a.created_at)
.map(a => <Note key={a.id} data={a} reactions={reaction(a.id)} deletion={reaction(a.id, EventKind.Deletion)} />)}
</>
)
const mainFeed = useMemo(() => {
return feed.main?.sort((a, b) => b.created_at - a.created_at);
}, [feed]);
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]));
return await signEvent(ev);
},
/**
* Respot a note
* @param {Event} note
* @returns
*/
repost: async (note) => {
if (typeof note.Id !== "string") {
throw "Must be parsed note in Event class";
}
let ev = Event.ForPubKey(pubKey);
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(["p", note.PubKey]));
return await signEvent(ev);

View File

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

View File

@ -42,7 +42,9 @@ export default class Tag {
ToObject() {
switch (this.Key) {
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": {
return ["p", this.PubKey];