diff --git a/public/index.html b/public/index.html
index c5af8453..e4560f15 100644
--- a/public/index.html
+++ b/public/index.html
@@ -8,7 +8,7 @@
+ content="default-src 'self'; child-src 'none'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src wss://* 'self'; img-src * data:; font-src https://fonts.gstatic.com; media-src *;" />
diff --git a/src/element/Note.css b/src/element/Note.css
index 955a8d61..1b6bdc3b 100644
--- a/src/element/Note.css
+++ b/src/element/Note.css
@@ -3,11 +3,6 @@
border-bottom: 1px solid #333;
}
-.note > .header {
- display: flex;
- align-items: center;
-}
-
.note > .header > .pfp {
flex-grow: 1;
}
diff --git a/src/element/Note.js b/src/element/Note.js
index 7fa2a2a5..d984e9b8 100644
--- a/src/element/Note.js
+++ b/src/element/Note.js
@@ -18,6 +18,7 @@ const MentionRegex = /(#\[\d+\])/gi;
export default function Note(props) {
const navigate = useNavigate();
const data = props.data;
+ const opt = props.options;
const dataEvent = props["data-ev"];
const reactions = props.reactions;
const publisher = useEventPublisher();
@@ -25,6 +26,13 @@ export default function Note(props) {
const users = useSelector(s => s.users?.users);
const ev = dataEvent ?? Event.FromObject(data);
+ const options = {
+ showHeader: true,
+ showTime: true,
+ showFooter: true,
+ ...opt
+ };
+
function goToEvent(e, id) {
if (!window.location.pathname.startsWith("/e/")) {
e.stopPropagation();
@@ -121,28 +129,31 @@ export default function Note(props) {
return (
-
-
-
- {moment(ev.CreatedAt * 1000).fromNow()}
-
-
+ {options.showHeader ?
+
+
+ {options.showTime ?
+
+ {moment(ev.CreatedAt * 1000).fromNow()}
+
: null}
+
: null}
goToEvent(e, ev.Id)}>
{transformBody()}
-
- setShowReply(!showReply)}>
-
-
- like()}>
-
- {(reactions?.length ?? 0)}
-
- console.debug(ev)}>
-
-
-
- {showReply ?
setShowReply(false)}/> : null}
+ {options.showFooter ?
+
+ setShowReply(!showReply)}>
+
+
+ like()}>
+
+ {(reactions?.length ?? 0)}
+
+ console.debug(ev)}>
+
+
+
: null}
+ {showReply ? setShowReply(false)} /> : null}
)
}
\ No newline at end of file
diff --git a/src/element/NoteReaction.css b/src/element/NoteReaction.css
new file mode 100644
index 00000000..13984c9b
--- /dev/null
+++ b/src/element/NoteReaction.css
@@ -0,0 +1,23 @@
+.reaction {
+ margin-bottom: 10px;
+ border-bottom: 1px solid #333;
+}
+
+.reaction > .note {
+ margin: 5px;
+ border: 1px solid #333;
+ border-radius: 10px;
+ padding: 5px;
+}
+
+.reaction > .header > .pfp {
+ flex-grow: 1;
+}
+
+.reaction > .header .reply {
+ font-size: small;
+}
+
+.reaction > .header > .info {
+ font-size: small;
+}
\ No newline at end of file
diff --git a/src/element/NoteReaction.js b/src/element/NoteReaction.js
new file mode 100644
index 00000000..3317ecfd
--- /dev/null
+++ b/src/element/NoteReaction.js
@@ -0,0 +1,40 @@
+import "./NoteReaction.css";
+import moment from "moment";
+import EventKind from "../nostr/EventKind";
+import Note from "./Note";
+import ProfileImage from "./ProfileImage";
+
+export default function NoteReaction(props) {
+ const data = props.data;
+ const root = props.root;
+
+ if (data.kind !== EventKind.Reaction) {
+ return null;
+ }
+
+ function mapReaction() {
+ switch (data.content) {
+ case "+": return "❤️";
+ case "-": return "👎";
+ default: {
+ if (data.content.length === 0) {
+ return "❤️";
+ }
+ return data.content;
+ }
+ }
+ }
+
+ return (
+
+
+
Reacted with {mapReaction()}} />
+
+ {moment(data.created_at * 1000).fromNow()}
+
+
+
+ {root ?
: root}
+
+ );
+}
\ No newline at end of file
diff --git a/src/element/ProfileImage.js b/src/element/ProfileImage.js
index 976d2125..4688af11 100644
--- a/src/element/ProfileImage.js
+++ b/src/element/ProfileImage.js
@@ -4,18 +4,18 @@ import useProfile from "../feed/ProfileFeed";
import Nostrich from "../nostrich.jpg";
export default function ProfileImage(props) {
- const pubKey = props.pubkey;
+ const pubkey = props.pubkey;
const subHeader = props.subHeader;
const navigate = useNavigate();
- const user = useProfile(pubKey);
+ const user = useProfile(pubkey);
const hasImage = (user?.picture?.length ?? 0) > 0;
return (
-
navigate(`/p/${pubKey}`)} />
+
navigate(`/p/${pubkey}`)} />
- {user?.name ?? pubKey.substring(0, 8)}
- {subHeader}
+ {user?.name ?? pubkey.substring(0, 8)}
+ {subHeader ?
{subHeader}
: null}
)
diff --git a/src/feed/ThreadFeed.js b/src/feed/ThreadFeed.js
index 8c1119b4..d8a65985 100644
--- a/src/feed/ThreadFeed.js
+++ b/src/feed/ThreadFeed.js
@@ -6,7 +6,7 @@ import useSubscription from "./Subscription";
export default function useThreadFeed(id) {
const sub = useMemo(() => {
const thisSub = new Subscriptions();
- thisSub.Id = `thread:${thisSub.Id}`;
+ thisSub.Id = `thread:${id.substring(0, 8)}`;
thisSub.Ids.add(id);
// get replies to this event
@@ -26,7 +26,7 @@ export default function useThreadFeed(id) {
if (thisNote) {
let otherSubs = new Subscriptions();
- otherSubs.Id = `thread-related:${otherSubs.Id}`;
+ otherSubs.Id = `thread-related:${id.substring(0, 8)}`;
for (let e of thisNote.tags.filter(a => a[0] === "e")) {
otherSubs.Ids.add(e[1]);
}
@@ -47,8 +47,10 @@ export default function useThreadFeed(id) {
const others = useSubscription(relatedThisSub, { leaveOpen: true });
- return {
- main: main.notes,
- other: others.notes
- };
+ return useMemo(() => {
+ return {
+ main: main.notes,
+ other: others.notes,
+ };
+ }, [main, others]);
}
\ No newline at end of file
diff --git a/src/pages/EventPage.js b/src/pages/EventPage.js
index 35a44b02..6975094c 100644
--- a/src/pages/EventPage.js
+++ b/src/pages/EventPage.js
@@ -1,3 +1,4 @@
+import { useMemo } from "react";
import { useParams } from "react-router-dom";
import Thread from "../element/Thread";
import useThreadFeed from "../feed/ThreadFeed";
@@ -6,9 +7,14 @@ export default function EventPage() {
const params = useParams();
const id = params.id;
- const { main, other } = useThreadFeed(id);
- return a.indexOf(v) === i)} this={id} />;
+ const thread = useThreadFeed(id);
+
+ const filtered = useMemo(() => {
+ return [
+ ...thread.main,
+ ...thread.other
+ ].filter((v, i, a) => a.findIndex(x => x.id === v.id) === i);
+ }, [thread]);
+
+ return ;
}
\ No newline at end of file
diff --git a/src/pages/Layout.js b/src/pages/Layout.js
index 59801d69..b13fb6c8 100644
--- a/src/pages/Layout.js
+++ b/src/pages/Layout.js
@@ -17,6 +17,7 @@ export default function Layout(props) {
const key = useSelector(s => s.login.publicKey);
const relays = useSelector(s => s.login.relays);
const notifications = useSelector(s => s.login.notifications);
+ const readNotifications = useSelector(s => s.login.readNotifications);
useUsersCache();
useLoginFeed();
@@ -33,11 +34,12 @@ export default function Layout(props) {
}, []);
function accountHeader() {
+ const unreadNotifications = notifications?.filter(a => a.created_at > readNotifications).length ?? 0;
return (
<>
navigate("/notifications")}>
- {notifications?.length ?? 0}
+ {unreadNotifications}
>
diff --git a/src/pages/Notifications.js b/src/pages/Notifications.js
index d27c1ed8..40aa322b 100644
--- a/src/pages/Notifications.js
+++ b/src/pages/Notifications.js
@@ -1,6 +1,57 @@
+import { useEffect, useMemo } from "react";
+import { useDispatch, useSelector } from "react-redux"
+import Note from "../element/Note";
+import NoteReaction from "../element/NoteReaction";
+import useSubscription from "../feed/Subscription";
+import EventKind from "../nostr/EventKind";
+import { Subscriptions } from "../nostr/Subscriptions";
+import { markNotificationsRead } from "../state/Login";
+
export default function NotificationsPage() {
+ const dispatch = useDispatch();
+ const notifications = useSelector(s => s.login.notifications);
+
+ useEffect(() => {
+ dispatch(markNotificationsRead());
+ }, []);
+
+ const etagged = useMemo(() => {
+ return notifications?.filter(a => a.kind === EventKind.Reaction)
+ .map(a => a.tags.filter(b => b[0] === "e")[0][1])
+ }, [notifications]);
+
+ const subEvents = useMemo(() => {
+ let sub = new Subscriptions();
+ sub.Id = `reactions:${sub.Id}`;
+ sub.Kinds.add(EventKind.TextNote);
+ sub.Ids = new Set(etagged);
+
+ let replyReactions = new Subscriptions();
+ replyReactions.Kinds.add(EventKind.Reaction);
+ replyReactions.ETags = new Set(notifications?.filter(b => b.kind === EventKind.TextNote).map(b => b.id));
+ sub.OrSubs.push(replyReactions);
+ return sub;
+ }, [etagged]);
+
+ const otherNotes = useSubscription(subEvents, { leaveOpen: true });
+
+ const sorted = [
+ ...notifications
+ ].sort((a,b) => b.created_at - a.created_at);
+
return (
<>
+ {sorted?.map(a => {
+ if (a.kind === EventKind.TextNote) {
+ let reactions = otherNotes?.notes?.filter(c => c.tags.find(b => b[0] === "e" && b[1] === a.id));
+ return
+ } else if (a.kind === EventKind.Reaction) {
+ let reactedTo = a.tags.filter(a => a[0] === "e")[0][1];
+ let reactedNote = otherNotes?.notes?.find(c => c.id === reactedTo);
+ return
+ }
+ return null;
+ })}
>
)
}
\ No newline at end of file
diff --git a/src/state/Login.js b/src/state/Login.js
index 603181e0..b122fee5 100644
--- a/src/state/Login.js
+++ b/src/state/Login.js
@@ -3,6 +3,7 @@ import * as secp from '@noble/secp256k1';
const PrivateKeyItem = "secret";
const Nip07PublicKeyItem = "nip07:pubkey";
+const NotificationsReadItem = "notifications-read";
const LoginSlice = createSlice({
name: "Login",
@@ -35,7 +36,12 @@ const LoginSlice = createSlice({
/**
* Notifications for this login session
*/
- notifications: []
+ notifications: [],
+
+ /**
+ * Timestamp of last read notification
+ */
+ readNotifications: 0,
},
reducers: {
init: (state) => {
@@ -56,6 +62,12 @@ const LoginSlice = createSlice({
state.publicKey = nip07PubKey;
state.nip07 = true;
}
+
+ // notifications
+ let readNotif = parseInt(window.localStorage.getItem(NotificationsReadItem));
+ if (!isNaN(readNotif)) {
+ state.readNotifications = readNotif;
+ }
},
setPrivateKey: (state, action) => {
state.privateKey = action.payload;
@@ -86,7 +98,7 @@ const LoginSlice = createSlice({
n = [n];
}
- for (let x in n) {
+ for (let x of n) {
if (!state.notifications.some(a => a.id === x.id)) {
state.notifications.push(x);
}
@@ -102,9 +114,13 @@ const LoginSlice = createSlice({
state.publicKey = null;
state.follows = [];
state.notifications = [];
+ },
+ markNotificationsRead: (state) => {
+ state.readNotifications = new Date().getTime();
+ window.localStorage.setItem(NotificationsReadItem, state.readNotifications);
}
}
});
-export const { init, setPrivateKey, setPublicKey, setNip07PubKey, setRelays, setFollows, addNotifications, logout } = LoginSlice.actions;
+export const { init, setPrivateKey, setPublicKey, setNip07PubKey, setRelays, setFollows, addNotifications, logout, markNotificationsRead } = LoginSlice.actions;
export const reducer = LoginSlice.reducer;
\ No newline at end of file