diff --git a/package.json b/package.json
index 5f260460..83ef30c6 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"qr-code-styling": "^1.6.0-rc.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-intersection-observer": "^9.4.1",
"react-redux": "^8.0.5",
"react-router-dom": "^6.5.0",
"react-scripts": "5.0.1",
diff --git a/src/Text.js b/src/Text.js
index e2a56c06..9dbf8f11 100644
--- a/src/Text.js
+++ b/src/Text.js
@@ -3,6 +3,7 @@ import { Link } from "react-router-dom";
import Invoice from "./element/Invoice";
import { UrlRegex, FileExtensionRegex, MentionRegex, InvoiceRegex, YoutubeUrlRegex } from "./Const";
import { eventLink, hexToBech32, profileLink } from "./Util";
+import LazyImage from "./element/LazyImage";
function transformHttpLink(a) {
try {
@@ -17,7 +18,7 @@ function transformHttpLink(a) {
case "png":
case "bmp":
case "webp": {
- return ;
+ return ;
}
case "mp4":
case "mov":
diff --git a/src/element/FollowListBase.js b/src/element/FollowListBase.js
new file mode 100644
index 00000000..312fb628
--- /dev/null
+++ b/src/element/FollowListBase.js
@@ -0,0 +1,21 @@
+import useEventPublisher from "../feed/EventPublisher";
+import ProfilePreview from "./ProfilePreview";
+
+export default function FollowListBase({ pubkeys }) {
+ const publisher = useEventPublisher();
+
+ async function followAll() {
+ let ev = await publisher.addFollow(pubkeys);
+ publisher.broadcast(ev);
+ }
+
+ return (
+ <>
+
+
+
followAll()}>Follow All
+
+ {pubkeys?.map(a => )}
+ >
+ )
+}
\ No newline at end of file
diff --git a/src/element/FollowersList.js b/src/element/FollowersList.js
new file mode 100644
index 00000000..dc6101a7
--- /dev/null
+++ b/src/element/FollowersList.js
@@ -0,0 +1,15 @@
+import { useMemo } from "react";
+import useFollowersFeed from "../feed/FollowersFeed";
+import EventKind from "../nostr/EventKind";
+import FollowListBase from "./FollowListBase";
+
+export default function FollowersList({ pubkey }) {
+ const feed = useFollowersFeed(pubkey);
+
+ const pubkeys = useMemo(() => {
+ let contactLists = feed?.notes.filter(a => a.kind === EventKind.ContactList && a.tags.some(b => b[0] === "p" && b[1] === pubkey));
+ return [...new Set(contactLists?.map(a => a.pubkey))];
+ }, [feed]);
+
+ return
+}
\ No newline at end of file
diff --git a/src/element/FollowsList.js b/src/element/FollowsList.js
new file mode 100644
index 00000000..99dab381
--- /dev/null
+++ b/src/element/FollowsList.js
@@ -0,0 +1,16 @@
+import { useMemo } from "react";
+import useFollowsFeed from "../feed/FollowsFeed";
+import EventKind from "../nostr/EventKind";
+import FollowListBase from "./FollowListBase";
+
+export default function FollowsList({ pubkey }) {
+ const feed = useFollowsFeed(pubkey);
+
+ const pubkeys = useMemo(() => {
+ let contactLists = feed?.notes.filter(a => a.kind === EventKind.ContactList && a.pubkey === pubkey);
+ let pTags = contactLists?.map(a => a.tags.filter(b => b[0] === "p").map(c => c[1]));
+ return [...new Set(pTags?.flat())];
+ }, [feed]);
+
+ return
+}
\ No newline at end of file
diff --git a/src/element/LazyImage.js b/src/element/LazyImage.js
new file mode 100644
index 00000000..87f5f010
--- /dev/null
+++ b/src/element/LazyImage.js
@@ -0,0 +1,11 @@
+import { useInView } from 'react-intersection-observer';
+
+export default function LazyImage(props) {
+ const { ref, inView, entry } = useInView();
+
+ return (
+
+ {inView ?
: null}
+
+ )
+}
\ No newline at end of file
diff --git a/src/element/Note.css b/src/element/Note.css
index 3b5566ff..a8f85450 100644
--- a/src/element/Note.css
+++ b/src/element/Note.css
@@ -29,7 +29,7 @@
word-break: normal;
}
-.note > .body > img, .note > .body > video, .note > .body > iframe {
+.note > .body img, .note > .body video, .note > .body iframe {
max-width: 100%;
max-height: 500px;
margin: 10px;
@@ -38,7 +38,7 @@
display: block;
}
-.note > .header > img:hover, .note > .header > .name > .reply:hover, .note > .body:hover {
+.note > .header img:hover, .note > .header .name > .reply:hover, .note .body:hover {
cursor: pointer;
}
diff --git a/src/element/ProfileImage.css b/src/element/ProfileImage.css
index d179d356..f75504f4 100644
--- a/src/element/ProfileImage.css
+++ b/src/element/ProfileImage.css
@@ -3,7 +3,7 @@
align-items: center;
}
-.pfp > img {
+.pfp img {
width: 40px;
height: 40px;
margin-right: 10px;
diff --git a/src/element/ProfileImage.js b/src/element/ProfileImage.js
index b75c8427..bec393f8 100644
--- a/src/element/ProfileImage.js
+++ b/src/element/ProfileImage.js
@@ -4,7 +4,8 @@ import Nostrich from "../nostrich.jpg";
import { useMemo } from "react";
import { Link, useNavigate } from "react-router-dom";
import useProfile from "../feed/ProfileFeed";
-import { profileLink } from "../Util";
+import { hexToBech32, profileLink } from "../Util";
+import LazyImage from "./LazyImage";
export default function ProfileImage({ pubkey, subHeader, showUsername = true }) {
const navigate = useNavigate();
@@ -12,7 +13,7 @@ export default function ProfileImage({ pubkey, subHeader, showUsername = true })
const hasImage = (user?.picture?.length ?? 0) > 0;
const name = useMemo(() => {
- let name = pubkey.substring(0, 8);
+ let name = hexToBech32("npub", pubkey).substring(0, 12);
if (user?.display_name?.length > 0) {
name = user.display_name;
} else if (user?.name?.length > 0) {
@@ -20,11 +21,11 @@ export default function ProfileImage({ pubkey, subHeader, showUsername = true })
}
return name;
}, [user]);
+
return (
-
navigate(profileLink(pubkey))} />
- {showUsername && (
-
+
navigate(profileLink(pubkey))} />
+ {showUsername && (
{name}
{subHeader ?
{subHeader}
: null}
diff --git a/src/element/ProfilePreview.css b/src/element/ProfilePreview.css
index 4e030d7c..7c706285 100644
--- a/src/element/ProfilePreview.css
+++ b/src/element/ProfilePreview.css
@@ -6,4 +6,5 @@
.profile-preview .pfp {
flex-grow: 1;
+ min-width: 200px;
}
\ No newline at end of file
diff --git a/src/element/ProfilePreview.js b/src/element/ProfilePreview.js
index 74f5b7bc..f4d89544 100644
--- a/src/element/ProfilePreview.js
+++ b/src/element/ProfilePreview.js
@@ -1,19 +1,23 @@
import "./ProfilePreview.css";
import ProfileImage from "./ProfileImage";
-import { useSelector } from "react-redux";
import FollowButton from "./FollowButton";
+import useProfile from "../feed/ProfileFeed";
export default function ProfilePreview(props) {
const pubkey = props.pubkey;
- const user = useSelector(s => s.users.users[pubkey]);
+ const user = useProfile(pubkey);
+ const options = {
+ about: true,
+ ...props.options
+ };
return (
-
-
+
+ {options.about ?
{user?.about}
-
-
+
: null}
+
)
}
\ No newline at end of file
diff --git a/src/feed/EventPublisher.js b/src/feed/EventPublisher.js
index 2f7a9fe8..12548481 100644
--- a/src/feed/EventPublisher.js
+++ b/src/feed/EventPublisher.js
@@ -103,10 +103,15 @@ export default function useEventPublisher() {
let ev = Event.ForPubKey(pubKey);
ev.Kind = EventKind.ContactList;
ev.Content = JSON.stringify(relays);
- for (let pk of follows) {
+ let temp = new Set(follows);
+ if (Array.isArray(pkAdd)) {
+ pkAdd.forEach(a => temp.add(a));
+ } else {
+ temp.add(pkAdd);
+ }
+ for (let pk of temp) {
ev.Tags.push(new Tag(["p", pk]));
}
- ev.Tags.push(new Tag(["p", pkAdd]));
return await signEvent(ev);
},
diff --git a/src/feed/FollowersFeed.js b/src/feed/FollowersFeed.js
new file mode 100644
index 00000000..5e3f1d74
--- /dev/null
+++ b/src/feed/FollowersFeed.js
@@ -0,0 +1,17 @@
+import { useMemo } from "react";
+import EventKind from "../nostr/EventKind";
+import { Subscriptions } from "../nostr/Subscriptions";
+import useSubscription from "./Subscription";
+
+export default function useFollowersFeed(pubkey) {
+ const sub = useMemo(() => {
+ let x = new Subscriptions();
+ x.Id = "followers";
+ x.Kinds.add(EventKind.ContactList);
+ x.PTags.add(pubkey);
+
+ return x;
+ }, [pubkey]);
+
+ return useSubscription(sub);
+}
\ No newline at end of file
diff --git a/src/feed/FollowsFeed.js b/src/feed/FollowsFeed.js
new file mode 100644
index 00000000..bccd248c
--- /dev/null
+++ b/src/feed/FollowsFeed.js
@@ -0,0 +1,17 @@
+import { useMemo } from "react";
+import EventKind from "../nostr/EventKind";
+import { Subscriptions } from "../nostr/Subscriptions";
+import useSubscription from "./Subscription";
+
+export default function useFollowsFeed(pubkey) {
+ const sub = useMemo(() => {
+ let x = new Subscriptions();
+ x.Id = "follows";
+ x.Kinds.add(EventKind.ContactList);
+ x.Authors.add(pubkey);
+
+ return x;
+ }, [pubkey]);
+
+ return useSubscription(sub);
+}
\ No newline at end of file
diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js
index 16753dcc..8463e6d3 100644
--- a/src/pages/ProfilePage.js
+++ b/src/pages/ProfilePage.js
@@ -1,7 +1,7 @@
import "./ProfilePage.css";
import Nostrich from "../nostrich.jpg";
-import { useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import { useSelector } from "react-redux";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faQrcode, faGear } from "@fortawesome/free-solid-svg-icons";
@@ -15,15 +15,31 @@ import { extractLinks } from '../Text'
import LNURLTip from "../element/LNURLTip";
import Nip05 from "../element/Nip05";
import Copy from "../element/Copy";
+import ProfilePreview from "../element/ProfilePreview";
+import FollowersList from "../element/FollowersList";
+import FollowsList from "../element/FollowsList";
+
+const ProfileTab = {
+ Notes: 0,
+ //Reactions: 1,
+ Followers: 2,
+ Follows: 3
+};
export default function ProfilePage() {
const params = useParams();
const navigate = useNavigate();
- const id = parseId(params.id);
+ const id = useMemo(() => parseId(params.id), [params]);
const user = useProfile(id);
const loginPubKey = useSelector(s => s.login.publicKey);
+ const follows = useSelector(s => s.login.follows);
const isMe = loginPubKey === id;
const [showLnQr, setShowLnQr] = useState(false);
+ const [tab, setTab] = useState(ProfileTab.Notes);
+
+ useEffect(() => {
+ setTab(ProfileTab.Notes);
+ }, [params]);
function details() {
const lnurl = extractLnAddress(user?.lud16 || user?.lud06 || "");
@@ -63,6 +79,23 @@ export default function ProfilePage() {
)
}
+ function tabContent() {
+ switch (tab) {
+ case ProfileTab.Notes: return ;
+ case ProfileTab.Follows: {
+ if (isMe) {
+ return follows.map(a => )
+ } else {
+ return ;
+ }
+ }
+ case ProfileTab.Followers: {
+ return
+ }
+ }
+ return null;
+ }
+
return (
<>
@@ -75,12 +108,14 @@ export default function ProfilePage() {
-
Notes
-
Reactions
-
Followers
-
Follows
+ {
+ Object.entries(ProfileTab).map(([k, v]) => {
+ return
setTab(v)}>{k}
+ }
+ )
+ }
-
+ {tabContent()}
>
)
}
diff --git a/src/state/Users.js b/src/state/Users.js
index df23923e..d61b95cb 100644
--- a/src/state/Users.js
+++ b/src/state/Users.js
@@ -12,7 +12,7 @@ const UsersSlice = createSlice({
/**
* User objects for known pubKeys, populated async
*/
- users: {}
+ users: {},
},
reducers: {
addPubKey: (state, action) => {
diff --git a/yarn.lock b/yarn.lock
index 06242d49..900e9544 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7222,6 +7222,11 @@ react-error-overlay@^6.0.11:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb"
integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==
+react-intersection-observer@^9.4.1:
+ version "9.4.1"
+ resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-9.4.1.tgz#4ccb21e16acd0b9cf5b28d275af7055bef878f6b"
+ integrity sha512-IXpIsPe6BleFOEHKzKh5UjwRUaz/JYS0lT/HPsupWEQou2hDqjhLMStc5zyE3eQVT4Fk3FufM8Fw33qW1uyeiw==
+
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"