mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
cleanup
This commit is contained in:
parent
68001bbe77
commit
640edef1ff
5
.changeset/selfish-years-walk.md
Normal file
5
.changeset/selfish-years-walk.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Use timeline loader for followers view
|
@ -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",
|
||||
|
@ -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]);
|
||||
|
@ -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;
|
||||
}
|
@ -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<string[]>();
|
||||
const forceRequestedKeys = new Set<string>();
|
||||
|
||||
export type UserFollowers = Set<string>;
|
||||
|
||||
function mergeNext(subject: Subject<string[] | null>, 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<string>();
|
||||
const relayUrls = new Set<string>();
|
||||
|
||||
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;
|
@ -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<FlexProps, "children">;
|
||||
|
||||
export const UserCard = ({ pubkey, relay, ...props }: UserCardProps) => {
|
||||
const metadata = useUserMetadata(pubkey, relay ? [relay] : []);
|
||||
|
||||
return (
|
||||
<Box
|
||||
<Flex
|
||||
borderWidth="1px"
|
||||
borderRadius="lg"
|
||||
pl="3"
|
||||
@ -23,20 +23,19 @@ export const UserCard = ({ pubkey, relay }: { pubkey: string; relay?: string })
|
||||
pb="2"
|
||||
overflow="hidden"
|
||||
gap="4"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
{...props}
|
||||
>
|
||||
<UserAvatar pubkey={pubkey} />
|
||||
<Flex direction="column" flex={1} overflowY="hidden" overflowX="auto">
|
||||
<Flex direction="column" flex={1} overflow="hidden">
|
||||
<Link as={ReactRouterLink} to={`/u/${normalizeToBech32(pubkey, Bech32Prefix.Pubkey)}`}>
|
||||
<Heading size="sm" whiteSpace="nowrap">
|
||||
<Heading size="sm" whiteSpace="nowrap" isTruncated>
|
||||
{getUserDisplayName(metadata, pubkey)}
|
||||
</Heading>
|
||||
</Link>
|
||||
<UserDnsIdentityIcon pubkey={pubkey} />
|
||||
</Flex>
|
||||
{relay && !isMobile && <Input readOnly value={relay} w="xs" />}
|
||||
<UserFollowButton pubkey={pubkey} size="sm" variant="outline" flexShrink={0} />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -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<string[]>) {
|
||||
const pubkey = followers[index];
|
||||
function FollowerItem({ event, ...props }: { event: Event } & Omit<UserCardProps, "pubkey">) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, event.id);
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<UserCard key={pubkey + index} pubkey={pubkey} />
|
||||
<div ref={ref}>
|
||||
<UserCard pubkey={event.pubkey} {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<string, Event>();
|
||||
for (const event of followerEvents) {
|
||||
dedupe.set(event.pubkey, event);
|
||||
}
|
||||
return Array.from(dedupe.values());
|
||||
}, [followerEvents]);
|
||||
|
||||
return (
|
||||
<Flex gap="2" direction="column" p="2" h="90vh">
|
||||
{followers ? (
|
||||
<Box flex={1}>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }: { height: number }) => (
|
||||
<FixedSizeList
|
||||
itemCount={followers.length}
|
||||
itemData={followers}
|
||||
itemSize={70}
|
||||
itemKey={(i, d) => d[i]}
|
||||
width="100%"
|
||||
height={height}
|
||||
overscanCount={10}
|
||||
>
|
||||
{FollowerItem}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</Box>
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</Flex>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<SimpleGrid minChildWidth="4in" spacing="2" py="2">
|
||||
{followers.map((event) => (
|
||||
<FollowerItem key={event.pubkey} event={event} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
||||
|
@ -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<UserContacts>) {
|
||||
const pubkey = contacts.contacts[index];
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<UserCard key={pubkey + index} pubkey={pubkey} relay={contacts.contactRelay[pubkey]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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 <Spinner />;
|
||||
|
||||
return (
|
||||
<Flex gap="2" direction="column" p="2" h="90vh">
|
||||
{contacts ? (
|
||||
<Box flex={1}>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }: { height: number }) => (
|
||||
<FixedSizeList
|
||||
itemCount={contacts.contacts.length}
|
||||
itemData={contacts}
|
||||
itemSize={70}
|
||||
itemKey={(i, d) => d.contacts[i]}
|
||||
width="100%"
|
||||
height={height}
|
||||
overscanCount={10}
|
||||
>
|
||||
{ContactItem}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</Box>
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</Flex>
|
||||
<SimpleGrid minChildWidth="4in" spacing="2" py="2">
|
||||
{people.map((pubkey) => (
|
||||
<UserCard key={pubkey} pubkey={pubkey} relay={contacts?.contactRelay[pubkey]} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
);
|
||||
}
|
||||
|
@ -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() {
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<TrustProvider trust>
|
||||
<Flex direction="column" gap="2" p="2" pb="8">
|
||||
{lines.map((event) => (
|
||||
{likes.map((event) => (
|
||||
<Like event={event} />
|
||||
))}
|
||||
|
||||
|
@ -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: [
|
||||
|
25
yarn.lock
25
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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user