diff --git a/src/element/Note.js b/src/element/Note.js
index 4ce18d3e3..86426fc4e 100644
--- a/src/element/Note.js
+++ b/src/element/Note.js
@@ -3,13 +3,19 @@ import Event from "../nostr/Event";
import { useEffect, useState } from "react";
import { useSelector } from "react-redux";
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) {
const navigate = useNavigate();
const data = props.data;
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);
useEffect(() => {
@@ -29,8 +35,10 @@ export default function Note(props) {
}
function goToEvent(e, id) {
- e.stopPropagation();
- navigate(`/e/${id}`);
+ if (!window.location.pathname.startsWith("/e/")) {
+ e.stopPropagation();
+ navigate(`/e/${id}`);
+ }
}
function replyTag() {
@@ -40,13 +48,61 @@ export default function Note(props) {
}
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 (
goToEvent(e, replyId)}>
- ➡️ {replyId?.substring(0, 8)}
+ ➡️ {mentions?.join(", ") ?? replyId?.substring(0, 8)}
)
}
+ 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 ;
+ }
+ case "mp4":
+ case "mkv":
+ case "avi":
+ case "m4v": {
+ return
+ }
+ }
+ }
+ } 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 #{pUser};
+ } else {
+ return BROKEN REF: {match[0]}
;
+ }
+ } else {
+ return match;
+ }
+ });
+ return mentions;
+ }
+ });
+ }
+
if (!ev.IsContent()) {
return (
<>
@@ -70,7 +126,7 @@ export default function Note(props) {
goToEvent(e, ev.Id)}>
- {ev.Content}
+ {transformBody()}
)
diff --git a/src/element/Thread.js b/src/element/Thread.js
new file mode 100644
index 000000000..b9d73605c
--- /dev/null
+++ b/src/element/Thread.js
@@ -0,0 +1,21 @@
+import Event from "../nostr/Event";
+import Note from "./Note";
+
+export default function Thread(props) {
+ /** @type {Array} */
+ 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 (
+ <>
+
+ {repliesToRoot?.map(a => )}
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
index c94513b6a..262215f44 100644
--- a/src/index.css
+++ b/src/index.css
@@ -58,4 +58,9 @@ input[type="text"] {
.f-grow {
flex-grow: 1;
+}
+
+a {
+ color: inherit;
+ line-height: 1.3em;
}
\ No newline at end of file
diff --git a/src/nostr/Thread.js b/src/nostr/Thread.js
index dc51a244c..556dbefc3 100644
--- a/src/nostr/Thread.js
+++ b/src/nostr/Thread.js
@@ -2,10 +2,16 @@ import Event from "./Event";
export default class Thread {
constructor() {
+ /** @type {Tag} */
this.Root = null;
+ /** @type {Tag} */
this.ReplyTo = null;
+ /** @type {Array} */
this.Mentions = [];
+ /** @type {Event} */
this.Reply = null;
+ /** @type {Array} */
+ this.PubKeys = [];
}
/**
@@ -34,6 +40,7 @@ export default class Thread {
ret.Root = root;
ret.ReplyTo = reply;
}
+ ret.PubKeys = ev.Tags.filter(a => a.Key === "p").map(a => a.PubKey);
return ret;
}
diff --git a/src/pages/EventPage.js b/src/pages/EventPage.js
index da200210a..4dea02c74 100644
--- a/src/pages/EventPage.js
+++ b/src/pages/EventPage.js
@@ -1,6 +1,7 @@
import { useEffect } from "react";
import { useParams } from "react-router-dom";
import Note from "../element/Note";
+import Thread from "../element/Thread";
import useThreadFeed from "./feed/ThreadFeed";
export default function EventPage() {
@@ -11,9 +12,7 @@ export default function EventPage() {
if(notes) {
return (
- <>
- {notes?.map(n => )}
- >
+
)
}
return (
diff --git a/src/pages/feed/ThreadFeed.js b/src/pages/feed/ThreadFeed.js
index ead4eda52..5dbaee966 100644
--- a/src/pages/feed/ThreadFeed.js
+++ b/src/pages/feed/ThreadFeed.js
@@ -55,10 +55,8 @@ export default function useThreadFeed(id) {
sub.OnEvent = (e) => {
dispatch(addNote(e));
};
- sub.OnEnd = (c) => {
- c.RemoveSubscription(sub.Id);
- };
system.AddSubscription(sub);
+ return () => system.RemoveSubscription(sub.Id);
}
}, [system, notes]);