feat: simple comments
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Kieran 2023-11-27 23:02:12 +00:00
parent 7bff5e384f
commit 44bde791b4
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
7 changed files with 126 additions and 15 deletions

View File

@ -215,6 +215,7 @@ export const Categories = [
] as Array<Category>;
export const TorrentKind = 2003 as EventKind;
export const TorrentCommentKind = 2004 as EventKind;
export function FormatBytes(b: number, f?: number) {
f ??= 2;

59
src/element/comments.tsx Normal file
View File

@ -0,0 +1,59 @@
import { useState } from "react";
import { NostrLink, NoteCollection, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { unwrap } from "@snort/shared";
import { ProfileImage } from "./profile-image";
import { Button } from "./button";
import { useLogin } from "../login";
import { Text } from "./text";
import { TorrentCommentKind } from "../const";
export function Comments({ link }: { link: NostrLink }) {
const rb = new RequestBuilder(`replies:${link.encode()}`);
rb.withFilter().kinds([TorrentCommentKind]).replyToLink([link]);
const comments = useRequestBuilder(NoteCollection, rb);
return (
<div className="flex flex-col gap-2">
<WriteComment link={link} />
{comments.data
?.sort((a, b) => (a.created_at > b.created_at ? -1 : 1))
.map((a) => (
<div className="flex flex-col gap-2 rounded p-2 bg-slate-900">
<ProfileImage pubkey={a.pubkey} withName={true}>
<span className="text-slate-400 text-sm">{new Date(a.created_at * 1000).toLocaleString()}</span>
</ProfileImage>
<Text content={a.content} tags={a.tags} />
</div>
))}
</div>
);
}
function WriteComment({ link }: { link: NostrLink }) {
const login = useLogin();
const [msg, setMsg] = useState("");
if (!login) return;
async function sendComment() {
const ev = await login?.builder.generic((eb) => {
return eb
.kind(TorrentCommentKind)
.content(msg)
.tag([...unwrap(link.toEventTag()), "root"]);
});
console.debug(ev);
if (ev) {
await login?.system.BroadcastEvent(ev);
}
}
return (
<div className="rounded p-2 bg-slate-900">
<h3>Write a Comment</h3>
<textarea className="w-full" value={msg} onChange={(e) => setMsg(e.target.value)}></textarea>
<Button onClick={sendComment}>Send</Button>
</div>
);
}

11
src/element/mention.tsx Normal file
View File

@ -0,0 +1,11 @@
import { hexToBech32 } from "@snort/shared";
import { NostrLink } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { Link } from "react-router-dom";
export function Mention({ link }: { link: NostrLink }) {
const profile = useUserProfile(link.id);
const npub = hexToBech32("npub", link.id);
return <Link to={`/p/${link.encode()}`}>{profile?.name ?? npub.slice(0, 12)}</Link>;
}

View File

@ -6,9 +6,10 @@ import { Link } from "react-router-dom";
type ProfileImageProps = HTMLProps<HTMLDivElement> & {
pubkey?: string;
size?: number;
withName?: boolean;
};
export function ProfileImage({ pubkey, size, ...props }: ProfileImageProps) {
export function ProfileImage({ pubkey, size, withName, children, ...props }: ProfileImageProps) {
const profile = useUserProfile(pubkey);
const v = {
backgroundImage: `url(${profile?.picture})`,
@ -18,12 +19,19 @@ export function ProfileImage({ pubkey, size, ...props }: ProfileImageProps) {
v.height = `${size}px`;
}
return (
<Link to={pubkey ? `/p/${new NostrLink(NostrPrefix.Profile, pubkey).encode()}` : ""}>
<div
{...props}
className="rounded-full aspect-square w-12 bg-slate-800 border border-slate-200 bg-cover bg-center"
style={v}
></div>
</Link>
<div className="flex items-center justify-between">
<Link
to={pubkey ? `/p/${new NostrLink(NostrPrefix.Profile, pubkey).encode()}` : ""}
className="flex items-center gap-2"
>
<div
{...props}
className="rounded-full aspect-square w-12 bg-slate-800 border border-slate-200 bg-cover bg-center"
style={v}
></div>
{withName === true && <>{profile?.name}</>}
</Link>
{children}
</div>
);
}

31
src/element/text.tsx Normal file
View File

@ -0,0 +1,31 @@
import { ParsedFragment, transformText, tryParseNostrLink } from "@snort/system";
import { useMemo } from "react";
import { Mention } from "./mention";
import { Link } from "react-router-dom";
export function Text({ content, tags }: { content: string; tags: Array<Array<string>> }) {
const frags = useMemo(() => transformText(content, tags), [content, tags]);
function renderFrag(f: ParsedFragment) {
switch (f.type) {
case "mention":
case "link": {
const link = tryParseNostrLink(f.content);
if (link) {
return <Mention link={link} />;
} else {
return (
<Link to={f.content} target="_blank">
{f.content}
</Link>
);
}
}
default: {
return <span>{f.content}</span>;
}
}
}
return <div className="text">{frags.map(renderFrag)}</div>;
}

View File

@ -1,10 +1,9 @@
import "./torrent-list.css";
import { hexToBech32 } from "@snort/shared";
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { NostrLink, NostrPrefix, TaggedNostrEvent } from "@snort/system";
import { FormatBytes } from "../const";
import { Link } from "react-router-dom";
import { MagnetLink } from "./magnet";
import { Mention } from "./mention";
export function TorrentList({ items }: { items: Array<TaggedNostrEvent> }) {
return (
@ -29,13 +28,11 @@ export function TorrentList({ items }: { items: Array<TaggedNostrEvent> }) {
}
function TorrentTableEntry({ item }: { item: TaggedNostrEvent }) {
const profile = useUserProfile(item.pubkey);
const name = item.tags.find((a) => a[0] === "title")?.at(1);
const size = item.tags
.filter((a) => a[0] === "file")
.map((a) => Number(a[2]))
.reduce((acc, v) => (acc += v), 0);
const npub = hexToBech32("npub", item.pubkey);
return (
<tr className="hover:bg-slate-800">
<td>
@ -69,7 +66,7 @@ function TorrentTableEntry({ item }: { item: TaggedNostrEvent }) {
</td>
<td>{FormatBytes(size)}</td>
<td>
<Link to={`/p/${npub}`}>{profile?.name ?? npub.slice(0, 12)}</Link>
<Mention link={new NostrLink(NostrPrefix.PublicKey, item.pubkey)} />
</td>
</tr>
);

View File

@ -1,5 +1,5 @@
import { unwrap } from "@snort/shared";
import { NoteCollection, RequestBuilder, TaggedNostrEvent, parseNostrLink } from "@snort/system";
import { NostrLink, NoteCollection, RequestBuilder, TaggedNostrEvent, parseNostrLink } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { FormatBytes, TorrentKind } from "../const";
@ -7,6 +7,7 @@ import { ProfileImage } from "../element/profile-image";
import { MagnetLink } from "../element/magnet";
import { useLogin } from "../login";
import { Button } from "../element/button";
import { Comments } from "../element/comments";
export function TorrentPage() {
const location = useLocation();
@ -28,6 +29,7 @@ export function TorrentPage() {
export function TorrentDetail({ item }: { item: TaggedNostrEvent }) {
const login = useLogin();
const navigate = useNavigate();
const link = NostrLink.fromEvent(item);
const name = item.tags.find((a) => a[0] === "title")?.at(1);
const size = item.tags
.filter((a) => a[0] === "file")
@ -83,6 +85,8 @@ export function TorrentDetail({ item }: { item: TaggedNostrEvent }) {
Delete
</Button>
)}
<h3>Comments</h3>
<Comments link={link} />
</div>
);
}