From 640edef1ffaa375ee7ea7bdb47baa985638635f7 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Sun, 30 Jul 2023 15:19:04 -0500 Subject: [PATCH] cleanup --- .changeset/selfish-years-walk.md | 5 ++ package.json | 3 - src/helpers/embeds.ts | 2 +- src/hooks/use-user-followers.ts | 13 ---- src/services/user-followers.ts | 99 ------------------------- src/views/user/components/user-card.tsx | 19 +++-- src/views/user/followers.tsx | 74 +++++++++--------- src/views/user/following.tsx | 47 +++--------- src/views/user/likes.tsx | 4 +- vite.config.ts | 2 +- yarn.lock | 25 ------- 11 files changed, 69 insertions(+), 224 deletions(-) create mode 100644 .changeset/selfish-years-walk.md delete mode 100644 src/hooks/use-user-followers.ts delete mode 100644 src/services/user-followers.ts diff --git a/.changeset/selfish-years-walk.md b/.changeset/selfish-years-walk.md new file mode 100644 index 000000000..376b6dc7d --- /dev/null +++ b/.changeset/selfish-years-walk.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Use timeline loader for followers view diff --git a/package.json b/package.json index f7551a5fd..30ad29e37 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,6 @@ "react-router-dom": "^6.14.1", "react-singleton-hook": "^4.0.1", "react-use": "^17.4.0", - "react-virtualized-auto-sizer": "^1.0.20", - "react-window": "^1.8.9", "webln": "^0.3.2" }, "devDependencies": { @@ -45,7 +43,6 @@ "@types/identicon.js": "^2.3.1", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", - "@types/react-window": "^1.8.5", "@vitejs/plugin-react": "^4.0.1", "cypress": "^12.16.0", "prettier": "^2.8.8", diff --git a/src/helpers/embeds.ts b/src/helpers/embeds.ts index 6423ece62..a8eb1f080 100644 --- a/src/helpers/embeds.ts +++ b/src/helpers/embeds.ts @@ -53,7 +53,7 @@ export type LinkEmbedHandler = (link: URL) => JSX.Element | string | null; export function embedUrls(content: EmbedableContent, handlers: LinkEmbedHandler[]) { return embedJSX(content, { name: "embedUrls", - regexp: /https?:\/\/([\dA-z\.-]+\.[A-z\.]{2,12})(\/[\+~%\/\.\w\-_]*)?([\?#][^\s]+)?/i, + regexp: /https?:\/\/([\dA-z\.-]+\.[A-z\.]{2,12})(\/[\+~%\/\.\w\-_@]*)?([\?#][^\s]+)?/i, render: (match) => { try { const url = new URL(match[0]); diff --git a/src/hooks/use-user-followers.ts b/src/hooks/use-user-followers.ts deleted file mode 100644 index 440623489..000000000 --- a/src/hooks/use-user-followers.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useMemo } from "react"; -import userFollowersService from "../services/user-followers"; -import useSubject from "./use-subject"; - -export function useUserFollowers(pubkey: string, relays: string[], alwaysRequest = false) { - const subject = useMemo( - () => userFollowersService.requestFollowers(pubkey, relays, alwaysRequest), - [pubkey, alwaysRequest] - ); - const followers = useSubject(subject) ?? undefined; - - return followers; -} diff --git a/src/services/user-followers.ts b/src/services/user-followers.ts deleted file mode 100644 index a6c181eac..000000000 --- a/src/services/user-followers.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { NostrEvent, isPTag } from "../types/nostr-event"; -import { NostrQuery } from "../types/nostr-query"; -import { PubkeySubjectCache } from "../classes/pubkey-subject-cache"; -import { NostrMultiSubscription } from "../classes/nostr-multi-subscription"; -import db from "./db"; -import userContactsService from "./user-contacts"; -import { Subject } from "../classes/subject"; -import { Kind } from "nostr-tools"; - -const subscription = new NostrMultiSubscription([], undefined, "user-followers"); -const subjects = new PubkeySubjectCache(); -const forceRequestedKeys = new Set(); - -export type UserFollowers = Set; - -function mergeNext(subject: Subject, next: string[]) { - let arr = subject.value ? Array.from(subject.value) : []; - for (const key of next) { - if (!arr.includes(key)) arr.push(key); - } - - subject.next(arr); -} - -function requestFollowers(pubkey: string, relays: string[], alwaysRequest = false) { - let subject = subjects.getSubject(pubkey); - - if (relays.length) subjects.addRelays(pubkey, relays); - - db.getAllKeysFromIndex("userFollows", "follows", pubkey).then((cached) => { - mergeNext(subject, cached); - }); - - if (alwaysRequest) forceRequestedKeys.add(pubkey); - - return subject; -} - -function flushRequests() { - if (!subjects.dirty) return; - - const pubkeys = new Set(); - const relayUrls = new Set(); - - const pending = subjects.getAllPubkeysMissingData(Array.from(forceRequestedKeys)); - for (const key of pending.pubkeys) pubkeys.add(key); - for (const url of pending.relays) relayUrls.add(url); - - if (pubkeys.size === 0) return; - - const query: NostrQuery = { kinds: [3], "#p": Array.from(pubkeys) }; - - subscription.setRelays(Array.from(relayUrls)); - subscription.setQuery(query); - if (subscription.state !== NostrMultiSubscription.OPEN) { - subscription.open(); - } - subjects.dirty = false; -} - -function receiveEvent(event: NostrEvent) { - if (event.kind !== Kind.Contacts) return; - const follower = event.pubkey; - - const pTags = event.tags.filter(isPTag); - if (pTags.length > 0) { - for (const [_, pubkey] of pTags) { - if (subjects.hasSubject(pubkey)) { - const subject = subjects.getSubject(pubkey); - mergeNext(subject, [follower]); - } - - forceRequestedKeys.delete(pubkey); - } - } - - db.put("userFollows", { pubkey: event.pubkey, follows: pTags.map((p) => p[1]) }); -} - -subscription.onEvent.subscribe((event) => { - // pass the event to the contacts service - userContactsService.receiveEvent(event); - receiveEvent(event); -}); - -// flush requests every second -setInterval(() => { - subjects.prune(); - flushRequests(); -}, 1000 * 5); - -const userFollowersService = { requestFollowers, flushRequests, receiveEvent }; - -if (import.meta.env.DEV) { - // @ts-ignore - window.userFollowersService = userFollowersService; -} - -export default userFollowersService; diff --git a/src/views/user/components/user-card.tsx b/src/views/user/components/user-card.tsx index e2e757beb..ff00bbe7f 100644 --- a/src/views/user/components/user-card.tsx +++ b/src/views/user/components/user-card.tsx @@ -1,4 +1,4 @@ -import { Box, Code, Flex, Heading, Input, Link, Spacer, Text } from "@chakra-ui/react"; +import { Box, Flex, FlexProps, Heading, Input, Link } from "@chakra-ui/react"; import { Link as ReactRouterLink } from "react-router-dom"; import { useUserMetadata } from "../../../hooks/use-user-metadata"; @@ -7,14 +7,14 @@ import { UserAvatar } from "../../../components/user-avatar"; import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip19"; import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon"; import { UserFollowButton } from "../../../components/user-follow-button"; -import { useIsMobile } from "../../../hooks/use-is-mobile"; -export const UserCard = ({ pubkey, relay }: { pubkey: string; relay?: string }) => { - const isMobile = useIsMobile(); +export type UserCardProps = { pubkey: string; relay?: string } & Omit; + +export const UserCard = ({ pubkey, relay, ...props }: UserCardProps) => { const metadata = useUserMetadata(pubkey, relay ? [relay] : []); return ( - - + - + {getUserDisplayName(metadata, pubkey)} - {relay && !isMobile && } - + ); }; diff --git a/src/views/user/followers.tsx b/src/views/user/followers.tsx index 6a45150c8..7de43cb14 100644 --- a/src/views/user/followers.tsx +++ b/src/views/user/followers.tsx @@ -1,52 +1,58 @@ -import AutoSizer from "react-virtualized-auto-sizer"; -import { Box, Flex, Spinner } from "@chakra-ui/react"; +import { Flex, SimpleGrid } from "@chakra-ui/react"; import { useOutletContext } from "react-router-dom"; -import { FixedSizeList, ListChildComponentProps } from "react-window"; +import { Event, Kind } from "nostr-tools"; -import { UserCard } from "./components/user-card"; -import { useUserFollowers } from "../../hooks/use-user-followers"; +import { UserCard, UserCardProps } from "./components/user-card"; import { useAdditionalRelayContext } from "../../providers/additional-relay-context"; import { useReadRelayUrls } from "../../hooks/use-client-relays"; +import { useTimelineLoader } from "../../hooks/use-timeline-loader"; +import { truncatedId } from "../../helpers/nostr-event"; +import useSubject from "../../hooks/use-subject"; +import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; +import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer"; +import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status"; +import { useMemo, useRef } from "react"; -function FollowerItem({ index, style, data: followers }: ListChildComponentProps) { - const pubkey = followers[index]; +function FollowerItem({ event, ...props }: { event: Event } & Omit) { + const ref = useRef(null); + useRegisterIntersectionEntity(ref, event.id); return ( -
- +
+
); } export default function UserFollowersTab() { const { pubkey } = useOutletContext() as { pubkey: string }; + const contextRelays = useAdditionalRelayContext(); + const readRelays = useReadRelayUrls(contextRelays); - const relays = useReadRelayUrls(useAdditionalRelayContext()); - const followers = useUserFollowers(pubkey, relays, true); + const timeline = useTimelineLoader(`${truncatedId(pubkey)}-followers`, readRelays, { + "#p": [pubkey], + kinds: [Kind.Contacts], + }); + + const followerEvents = useSubject(timeline.timeline); + const callback = useTimelineCurserIntersectionCallback(timeline); + + const followers = useMemo(() => { + const dedupe = new Map(); + for (const event of followerEvents) { + dedupe.set(event.pubkey, event); + } + return Array.from(dedupe.values()); + }, [followerEvents]); return ( - - {followers ? ( - - - {({ height }: { height: number }) => ( - d[i]} - width="100%" - height={height} - overscanCount={10} - > - {FollowerItem} - - )} - - - ) : ( - - )} - + + + {followers.map((event) => ( + + ))} + + + ); } diff --git a/src/views/user/following.tsx b/src/views/user/following.tsx index 4b04e9eea..723be9786 100644 --- a/src/views/user/following.tsx +++ b/src/views/user/following.tsx @@ -1,51 +1,26 @@ -import { Box, Flex, Spinner } from "@chakra-ui/react"; +import { SimpleGrid, Spinner } from "@chakra-ui/react"; import { useOutletContext } from "react-router-dom"; -import AutoSizer from "react-virtualized-auto-sizer"; -import { FixedSizeList, ListChildComponentProps } from "react-window"; import { UserCard } from "./components/user-card"; import { useUserContacts } from "../../hooks/use-user-contacts"; import { useAdditionalRelayContext } from "../../providers/additional-relay-context"; import { UserContacts } from "../../services/user-contacts"; - -function ContactItem({ index, style, data: contacts }: ListChildComponentProps) { - const pubkey = contacts.contacts[index]; - - return ( -
- -
- ); -} +import { unique } from "../../helpers/array"; export default function UserFollowingTab() { const { pubkey } = useOutletContext() as { pubkey: string }; const contextRelays = useAdditionalRelayContext(); const contacts = useUserContacts(pubkey, contextRelays, true); + const people = unique(contacts?.contacts ?? []); + + if (!contacts) return ; + return ( - - {contacts ? ( - - - {({ height }: { height: number }) => ( - d.contacts[i]} - width="100%" - height={height} - overscanCount={10} - > - {ContactItem} - - )} - - - ) : ( - - )} - + + {people.map((pubkey) => ( + + ))} + ); } diff --git a/src/views/user/likes.tsx b/src/views/user/likes.tsx index 60f279bff..ef10894eb 100644 --- a/src/views/user/likes.tsx +++ b/src/views/user/likes.tsx @@ -58,7 +58,7 @@ export default function UserLikesTab() { const timeline = useTimelineLoader(`${truncatedId(pubkey)}-likes`, readRelays, { authors: [pubkey], kinds: [7] }); - const lines = useSubject(timeline.timeline); + const likes = useSubject(timeline.timeline); const callback = useTimelineCurserIntersectionCallback(timeline); @@ -66,7 +66,7 @@ export default function UserLikesTab() { - {lines.map((event) => ( + {likes.map((event) => ( ))} diff --git a/vite.config.ts b/vite.config.ts index f7db1ff45..fdd554a3e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ name: "noStrudel", short_name: "noStrudel", description: "A simple PWA nostr client", - orientation: "portrait-primary", + orientation: "any", theme_color: "#8DB600", categories: ["nostr"], icons: [ diff --git a/yarn.lock b/yarn.lock index d365e18a0..655b4f5b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2621,13 +2621,6 @@ dependencies: "@types/react" "*" -"@types/react-window@^1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1" - integrity sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw== - dependencies: - "@types/react" "*" - "@types/react@*", "@types/react@^16.9.35", "@types/react@^18.2.14": version "18.2.15" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.15.tgz#14792b35df676c20ec3cf595b262f8c615a73066" @@ -4852,11 +4845,6 @@ mdn-data@2.0.14: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== -"memoize-one@>=3.1.1 <6": - version "5.2.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" - integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== - meow@^6.0.0: version "6.1.1" resolved "https://registry.yarnpkg.com/meow/-/meow-6.1.1.tgz#1ad64c4b76b2a24dfb2f635fddcadf320d251467" @@ -5474,24 +5462,11 @@ react-use@^17.4.0: ts-easing "^0.2.0" tslib "^2.1.0" -react-virtualized-auto-sizer@^1.0.20: - version "1.0.20" - resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.20.tgz#d9a907253a7c221c52fa57dc775a6ef40c182645" - integrity sha512-OdIyHwj4S4wyhbKHOKM1wLSj/UDXm839Z3Cvfg2a9j+He6yDa6i5p0qQvEiCnyQlGO/HyfSnigQwuxvYalaAXA== - react-webcam@^5.0.1: version "5.2.4" resolved "https://registry.yarnpkg.com/react-webcam/-/react-webcam-5.2.4.tgz#714b4460ea43ac7ed081824299cd2a580f764478" integrity sha512-Qqj14t68Ke1eoEYjFde+N48HtuIJg0ePIQRpFww9eZt5oBcDpe/l60h+m3VRFJAR5/E3dOhSU5R8EJEcdCq/Eg== -react-window@^1.8.9: - version "1.8.9" - resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.9.tgz#24bc346be73d0468cdf91998aac94e32bc7fa6a8" - integrity sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q== - dependencies: - "@babel/runtime" "^7.0.0" - memoize-one ">=3.1.1 <6" - react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"