forked from Kieran/snort
Thread progess
This commit is contained in:
parent
e617d6d528
commit
c454f5c7aa
@ -3,13 +3,19 @@ import Event from "../nostr/Event";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import { isFulfilled } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
const UrlRegex = /((?:http|ftp|https):\/\/(?:[\w+?\.\w+])+(?:[a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?)/;
|
||||||
|
const FileExtensionRegex = /\.([\w]+)$/;
|
||||||
|
const MentionRegex = /(#\[\d+\])/g;
|
||||||
|
|
||||||
export default function Note(props) {
|
export default function Note(props) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const data = props.data;
|
const data = props.data;
|
||||||
const [sig, setSig] = useState(false);
|
const [sig, setSig] = useState(false);
|
||||||
const user = useSelector(s => s.users?.users[data?.pubkey]);
|
const users = useSelector(s => s.users?.users);
|
||||||
|
const user = users[data?.pubkey];
|
||||||
const ev = Event.FromObject(data);
|
const ev = Event.FromObject(data);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -29,8 +35,10 @@ export default function Note(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function goToEvent(e, id) {
|
function goToEvent(e, id) {
|
||||||
e.stopPropagation();
|
if (!window.location.pathname.startsWith("/e/")) {
|
||||||
navigate(`/e/${id}`);
|
e.stopPropagation();
|
||||||
|
navigate(`/e/${id}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function replyTag() {
|
function replyTag() {
|
||||||
@ -40,13 +48,61 @@ export default function Note(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let replyId = thread?.ReplyTo?.Event;
|
let replyId = thread?.ReplyTo?.Event;
|
||||||
|
let mentions = thread?.PubKeys?.map(a => [a, users[a]])?.map(a => a[1]?.name ?? a[0].substring(0, 8));
|
||||||
return (
|
return (
|
||||||
<div className="reply" onClick={(e) => goToEvent(e, replyId)}>
|
<div className="reply" onClick={(e) => goToEvent(e, replyId)}>
|
||||||
➡️ {replyId?.substring(0, 8)}
|
➡️ {mentions?.join(", ") ?? replyId?.substring(0, 8)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function transformBody() {
|
||||||
|
let body = ev.Content;
|
||||||
|
let pTags = ev.Tags.filter(a => a.Key === "p");
|
||||||
|
|
||||||
|
let urlBody = body.split(UrlRegex);
|
||||||
|
|
||||||
|
return urlBody.map(a => {
|
||||||
|
if (a.startsWith("http")) {
|
||||||
|
let url = new URL(a);
|
||||||
|
let ext = url.pathname.match(FileExtensionRegex);
|
||||||
|
if (ext) {
|
||||||
|
switch (ext[1]) {
|
||||||
|
case "gif":
|
||||||
|
case "jpg":
|
||||||
|
case "jpeg":
|
||||||
|
case "png":
|
||||||
|
case "bmp":
|
||||||
|
case "webp": {
|
||||||
|
return <img src={url} />;
|
||||||
|
}
|
||||||
|
case "mp4":
|
||||||
|
case "mkv":
|
||||||
|
case "avi":
|
||||||
|
case "m4v": {
|
||||||
|
return <video src={url} controls />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mentions = a.split(MentionRegex).map((match) => {
|
||||||
|
if (match.startsWith("#")) {
|
||||||
|
let pref = pTags[match.match(/\[(\d+)\]/)[1]];
|
||||||
|
if (pref) {
|
||||||
|
let pUser = users[pref.PubKey]?.name ?? pref.PubKey.substring(0, 8);
|
||||||
|
return <Link to={`/p/${pref.PubKey}`}>#{pUser}</Link>;
|
||||||
|
} else {
|
||||||
|
return <pre>BROKEN REF: {match[0]}</pre>;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return mentions;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!ev.IsContent()) {
|
if (!ev.IsContent()) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -70,7 +126,7 @@ export default function Note(props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="body" onClick={(e) => goToEvent(e, ev.Id)}>
|
<div className="body" onClick={(e) => goToEvent(e, ev.Id)}>
|
||||||
{ev.Content}
|
{transformBody()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
21
src/element/Thread.js
Normal file
21
src/element/Thread.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import Event from "../nostr/Event";
|
||||||
|
import Note from "./Note";
|
||||||
|
|
||||||
|
export default function Thread(props) {
|
||||||
|
/** @type {Array<Event>} */
|
||||||
|
const notes = props.notes?.map(a => Event.FromObject(a));
|
||||||
|
|
||||||
|
// root note has no thread info
|
||||||
|
const root = notes.find(a => a.GetThread() === null);
|
||||||
|
if(root === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repliesToRoot = notes?.filter(a => a.GetThread()?.ReplyTo?.Event === root.Id);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Note data={root?.ToObject()}/>
|
||||||
|
{repliesToRoot?.map(a => <Note key={a.Id} data={a.ToObject()}/>)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -59,3 +59,8 @@ input[type="text"] {
|
|||||||
.f-grow {
|
.f-grow {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
line-height: 1.3em;
|
||||||
|
}
|
@ -2,10 +2,16 @@ import Event from "./Event";
|
|||||||
|
|
||||||
export default class Thread {
|
export default class Thread {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
/** @type {Tag} */
|
||||||
this.Root = null;
|
this.Root = null;
|
||||||
|
/** @type {Tag} */
|
||||||
this.ReplyTo = null;
|
this.ReplyTo = null;
|
||||||
|
/** @type {Array<Tag>} */
|
||||||
this.Mentions = [];
|
this.Mentions = [];
|
||||||
|
/** @type {Event} */
|
||||||
this.Reply = null;
|
this.Reply = null;
|
||||||
|
/** @type {Array<String>} */
|
||||||
|
this.PubKeys = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,6 +40,7 @@ export default class Thread {
|
|||||||
ret.Root = root;
|
ret.Root = root;
|
||||||
ret.ReplyTo = reply;
|
ret.ReplyTo = reply;
|
||||||
}
|
}
|
||||||
|
ret.PubKeys = ev.Tags.filter(a => a.Key === "p").map(a => a.PubKey);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import Note from "../element/Note";
|
import Note from "../element/Note";
|
||||||
|
import Thread from "../element/Thread";
|
||||||
import useThreadFeed from "./feed/ThreadFeed";
|
import useThreadFeed from "./feed/ThreadFeed";
|
||||||
|
|
||||||
export default function EventPage() {
|
export default function EventPage() {
|
||||||
@ -11,9 +12,7 @@ export default function EventPage() {
|
|||||||
|
|
||||||
if(notes) {
|
if(notes) {
|
||||||
return (
|
return (
|
||||||
<>
|
<Thread notes={notes}/>
|
||||||
{notes?.map(n => <Note key={n.id} data={n}/>)}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
@ -55,10 +55,8 @@ export default function useThreadFeed(id) {
|
|||||||
sub.OnEvent = (e) => {
|
sub.OnEvent = (e) => {
|
||||||
dispatch(addNote(e));
|
dispatch(addNote(e));
|
||||||
};
|
};
|
||||||
sub.OnEnd = (c) => {
|
|
||||||
c.RemoveSubscription(sub.Id);
|
|
||||||
};
|
|
||||||
system.AddSubscription(sub);
|
system.AddSubscription(sub);
|
||||||
|
return () => system.RemoveSubscription(sub.Id);
|
||||||
}
|
}
|
||||||
}, [system, notes]);
|
}, [system, notes]);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user