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/FollowersList.js b/src/element/FollowersList.js new file mode 100644 index 00000000..aac73278 --- /dev/null +++ b/src/element/FollowersList.js @@ -0,0 +1,19 @@ +import { useMemo } from "react"; +import useFollowersFeed from "../feed/FollowersFeed"; +import EventKind from "../nostr/EventKind"; +import ProfilePreview from "./ProfilePreview"; + +export default function FollowersList(props) { + const feed = useFollowersFeed(props.pubkey); + + const pubKeys = useMemo(() => { + let contactLists = feed?.notes.filter(a => a.kind === EventKind.ContactList && a.tags.some(b => b[0] === "p" && b[1] === props.pubkey)); + return [...new Set(contactLists?.map(a => a.pubkey))]; + }, [feed]); + + return ( + <> + {pubKeys?.map(a => )} + + ) +} \ 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/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/pages/ProfilePage.js b/src/pages/ProfilePage.js index 16753dcc..7fcf3f69 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,30 @@ 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"; + +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 +78,21 @@ export default function ProfilePage() { ) } + function tabContent() { + switch (tab) { + case ProfileTab.Notes: return ; + case ProfileTab.Follows: { + if (isMe) { + return follows.map(a => ) + } + } + case ProfileTab.Followers: { + return + } + } + return null; + } + return ( <>
@@ -75,12 +105,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"