mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
add blindspots feed
This commit is contained in:
parent
6d33b0d19d
commit
5add2819a9
5
.changeset/nasty-scissors-mate.md
Normal file
5
.changeset/nasty-scissors-mate.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add blindspots discovery feed
|
25
src/app.tsx
25
src/app.tsx
@ -7,11 +7,14 @@ import Layout from "./components/layout";
|
||||
import DrawerSubViewProvider from "./providers/drawer-sub-view-provider";
|
||||
import useSetColorMode from "./hooks/use-set-color-mode";
|
||||
import { RouteProviders } from "./providers/route";
|
||||
import RequireCurrentAccount from "./providers/route/require-current-account";
|
||||
import GlobalStyles from "./styles";
|
||||
|
||||
import HomeView from "./views/home/index";
|
||||
const DVMFeedHomeView = lazy(() => import("./views/dvm-feed/index"));
|
||||
const DVMFeedView = lazy(() => import("./views/dvm-feed/feed"));
|
||||
const DiscoveryHomeView = lazy(() => import("./views/discovery/index"));
|
||||
const DVMFeedView = lazy(() => import("./views/discovery/dvm-feed/feed"));
|
||||
const BlindspotHomeView = lazy(() => import("./views/discovery/blindspot"));
|
||||
const BlindspotFeedView = lazy(() => import("./views/discovery/blindspot/feed"));
|
||||
import SettingsView from "./views/settings";
|
||||
import NostrLinkView from "./views/link";
|
||||
import ProfileView from "./views/profile";
|
||||
@ -316,10 +319,22 @@ const router = createHashRouter([
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "dvm",
|
||||
path: "discovery",
|
||||
children: [
|
||||
{ path: ":addr", element: <DVMFeedView /> },
|
||||
{ path: "", element: <DVMFeedHomeView /> },
|
||||
{ path: "", element: <DiscoveryHomeView /> },
|
||||
{ path: "dvm/:addr", element: <DVMFeedView /> },
|
||||
{
|
||||
path: "blindspot",
|
||||
element: (
|
||||
<RequireCurrentAccount>
|
||||
<Outlet />
|
||||
</RequireCurrentAccount>
|
||||
),
|
||||
children: [
|
||||
{ path: "", element: <BlindspotHomeView /> },
|
||||
{ path: ":pubkey", element: <BlindspotFeedView /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: "search", element: <SearchView /> },
|
||||
|
@ -4,9 +4,16 @@ import { Nip07Signer } from "../../types/nostr-extensions";
|
||||
export class Account {
|
||||
readonly type: string = "unknown";
|
||||
pubkey: string;
|
||||
signer?: Nip07Signer;
|
||||
localSettings?: AppSettings;
|
||||
|
||||
protected _signer?: Nip07Signer | undefined;
|
||||
public get signer(): Nip07Signer | undefined {
|
||||
return this._signer;
|
||||
}
|
||||
public set signer(value: Nip07Signer | undefined) {
|
||||
this._signer = value;
|
||||
}
|
||||
|
||||
get readonly() {
|
||||
return !this.signer;
|
||||
}
|
||||
|
@ -3,16 +3,16 @@ import { Account } from "./account";
|
||||
|
||||
export default class ExtensionAccount extends Account {
|
||||
readonly type = "extension";
|
||||
signer?: Nip07Signer;
|
||||
|
||||
constructor(pubkey: string) {
|
||||
super(pubkey);
|
||||
this.signer = window.nostr;
|
||||
override get signer() {
|
||||
if (!window.nostr) throw new Error("Missing NIP-07 signer extension");
|
||||
return window.nostr;
|
||||
}
|
||||
set signer(signer: Nip07Signer) {
|
||||
throw new Error("Cant update signer");
|
||||
}
|
||||
|
||||
fromJSON(data: any): this {
|
||||
if (!window.nostr) throw new Error("Missing NIP-07 signer extension");
|
||||
this.signer = window.nostr;
|
||||
return super.fromJSON(data);
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,14 @@ import { Account } from "./account";
|
||||
|
||||
export default class NsecAccount extends Account {
|
||||
readonly type = "nsec";
|
||||
declare signer?: SimpleSigner;
|
||||
|
||||
protected declare _signer?: SimpleSigner | undefined;
|
||||
public get signer(): SimpleSigner | undefined {
|
||||
return this._signer;
|
||||
}
|
||||
public set signer(value: SimpleSigner | undefined) {
|
||||
this._signer = value;
|
||||
}
|
||||
|
||||
constructor(pubkey: string) {
|
||||
super(pubkey);
|
||||
|
@ -1,12 +1,19 @@
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import _throttle from "lodash.throttle";
|
||||
import { logger } from "../helpers/debug";
|
||||
|
||||
export class PubkeyGraph {
|
||||
import { logger } from "../helpers/debug";
|
||||
import EventEmitter from "eventemitter3";
|
||||
|
||||
type EventMap = {
|
||||
computed: [];
|
||||
};
|
||||
|
||||
export class PubkeyGraph extends EventEmitter<EventMap> {
|
||||
/** the pubkey at the center of it all */
|
||||
root: string;
|
||||
log = logger.extend("PubkeyGraph");
|
||||
|
||||
/** a map of what pubkeys follow other pubkeys */
|
||||
connections = new Map<string, string[]>();
|
||||
distance = new Map<string, number>();
|
||||
|
||||
@ -14,6 +21,7 @@ export class PubkeyGraph {
|
||||
connectionCount = new Map<string, number>();
|
||||
|
||||
constructor(root: string) {
|
||||
super();
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
@ -128,6 +136,8 @@ export class PubkeyGraph {
|
||||
next.add(this.root);
|
||||
walkLevel(0);
|
||||
console.timeEnd("walk");
|
||||
|
||||
this.emit("computed");
|
||||
}
|
||||
|
||||
getPaths(pubkey: string, maxLength = 2) {
|
||||
|
@ -48,7 +48,7 @@ export default function NavItems() {
|
||||
else if (location.pathname === "/") active = "notes";
|
||||
else if (location.pathname.startsWith("/notifications")) active = "notifications";
|
||||
else if (location.pathname.startsWith("/launchpad")) active = "launchpad";
|
||||
else if (location.pathname.startsWith("/dvm")) active = "dvm";
|
||||
else if (location.pathname.startsWith("/discovery")) active = "discovery";
|
||||
else if (location.pathname.startsWith("/dm")) active = "dm";
|
||||
else if (location.pathname.startsWith("/streams")) active = "streams";
|
||||
else if (location.pathname.startsWith("/relays")) active = "relays";
|
||||
@ -120,9 +120,9 @@ export default function NavItems() {
|
||||
</Button>
|
||||
<Button
|
||||
as={RouterLink}
|
||||
to="/dvm"
|
||||
to="/discovery"
|
||||
leftIcon={<PuzzlePiece01 boxSize={6} />}
|
||||
colorScheme={active === "dvm" ? "primary" : undefined}
|
||||
colorScheme={active === "discovery" ? "primary" : undefined}
|
||||
{...buttonProps}
|
||||
>
|
||||
Discover
|
||||
|
@ -7,6 +7,7 @@ import PasswordAccount from "../classes/accounts/password-account";
|
||||
import PubkeyAccount from "../classes/accounts/pubkey-account";
|
||||
import SerialPortAccount from "../classes/accounts/serial-port-account";
|
||||
import { PersistentSubject } from "../classes/subject";
|
||||
import { logger } from "../helpers/debug";
|
||||
import db from "./db";
|
||||
import { AppSettings } from "./settings/migrations";
|
||||
|
||||
@ -23,6 +24,7 @@ export type LocalAccount = CommonAccount & {
|
||||
};
|
||||
|
||||
class AccountService {
|
||||
log = logger.extend("AccountService");
|
||||
loading = new PersistentSubject(true);
|
||||
accounts = new PersistentSubject<Account[]>([]);
|
||||
current = new PersistentSubject<Account | null>(null);
|
||||
@ -36,7 +38,9 @@ class AccountService {
|
||||
try {
|
||||
const account = this.createAccountFromDatabaseRecord(data);
|
||||
if (account) accounts.push(account);
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
this.log("Failed to load account", data, error);
|
||||
}
|
||||
}
|
||||
|
||||
this.accounts.next(accounts);
|
||||
|
107
src/views/discovery/blindspot/feed.tsx
Normal file
107
src/views/discovery/blindspot/feed.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { Divider, Flex, Heading, Spacer, Spinner, useDisclosure } from "@chakra-ui/react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { kinds, NostrEvent } from "nostr-tools";
|
||||
|
||||
import VerticalPageLayout from "../../../components/vertical-page-layout";
|
||||
import useParamsProfilePointer from "../../../hooks/use-params-pubkey-pointer";
|
||||
import useTimelineLoader from "../../../hooks/use-timeline-loader";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import useUserContactList from "../../../hooks/use-user-contact-list";
|
||||
import { getPubkeysFromList } from "../../../helpers/nostr/lists";
|
||||
import { useReadRelays } from "../../../hooks/use-client-relays";
|
||||
import TimelinePage, { useTimelinePageEventFilter } from "../../../components/timeline-page";
|
||||
import KindSelectionProvider, { useKindSelectionContext } from "../../../providers/local/kind-selection-provider";
|
||||
import NoteFilterTypeButtons from "../../../components/note-filter-type-buttons";
|
||||
import TimelineViewTypeButtons from "../../../components/timeline-page/timeline-view-type";
|
||||
import Telescope from "../../../components/icons/telescope";
|
||||
import UserAvatar from "../../../components/user/user-avatar";
|
||||
import UserAvatarLink from "../../../components/user/user-avatar-link";
|
||||
import BackButton from "../../../components/router/back-button";
|
||||
import UserLink from "../../../components/user/user-link";
|
||||
import useClientSideMuteFilter from "../../../hooks/use-client-side-mute-filter";
|
||||
import { isReply, isRepost } from "../../../helpers/nostr/event";
|
||||
|
||||
function BlindspotFeedPage({ pubkey }: { pubkey: string }) {
|
||||
const account = useCurrentAccount()!;
|
||||
const contacts = useUserContactList(account.pubkey);
|
||||
const otherContacts = useUserContactList(pubkey);
|
||||
const readRelays = useReadRelays();
|
||||
|
||||
const blindspot = useMemo(() => {
|
||||
if (!contacts || !otherContacts) return [];
|
||||
|
||||
const mine = new Set(getPubkeysFromList(contacts).map((p) => p.pubkey));
|
||||
const other = new Set(getPubkeysFromList(otherContacts).map((p) => p.pubkey));
|
||||
|
||||
return Array.from(other).filter((p) => !mine.has(p) && p !== account.pubkey);
|
||||
}, [contacts, otherContacts, account.pubkey]);
|
||||
|
||||
const showReplies = useDisclosure({ defaultIsOpen: localStorage.getItem("show-replies") === "true" });
|
||||
const showReposts = useDisclosure({ defaultIsOpen: localStorage.getItem("show-reposts") !== "false" });
|
||||
|
||||
const timelinePageEventFilter = useTimelinePageEventFilter();
|
||||
const muteFilter = useClientSideMuteFilter();
|
||||
const eventFilter = useCallback(
|
||||
(event: NostrEvent) => {
|
||||
if (muteFilter(event)) return false;
|
||||
if (!showReplies.isOpen && isReply(event)) return false;
|
||||
if (!showReposts.isOpen && isRepost(event)) return false;
|
||||
return timelinePageEventFilter(event);
|
||||
},
|
||||
[timelinePageEventFilter, showReplies.isOpen, showReposts.isOpen, muteFilter],
|
||||
);
|
||||
|
||||
const { kinds } = useKindSelectionContext();
|
||||
const timeline = useTimelineLoader(
|
||||
`blnidspot-${account.pubkey}-${pubkey}-${kinds.join(",")}`,
|
||||
readRelays,
|
||||
blindspot.length > 0 ? [{ authors: blindspot, kinds }] : undefined,
|
||||
{ eventFilter },
|
||||
);
|
||||
|
||||
if (!contacts)
|
||||
return (
|
||||
<Flex gap="4" alignItems="center" justifyContent="center" flex={1}>
|
||||
<Spinner />
|
||||
<Heading size="lg">Loading your contacts...</Heading>
|
||||
</Flex>
|
||||
);
|
||||
if (!otherContacts)
|
||||
return (
|
||||
<Flex gap="4" alignItems="center" justifyContent="center" flex={1}>
|
||||
<Spinner />
|
||||
<Heading size="lg">Loading other users contacts...</Heading>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
return (
|
||||
<VerticalPageLayout>
|
||||
<Flex gap="2" alignItems="center">
|
||||
<BackButton />
|
||||
<Telescope boxSize={6} />
|
||||
<UserAvatarLink size="sm" pubkey={pubkey} />
|
||||
<UserLink pubkey={pubkey} isTruncated fontWeight="bold" />
|
||||
<NoteFilterTypeButtons showReplies={showReplies} showReposts={showReposts} />
|
||||
<Spacer />
|
||||
<TimelineViewTypeButtons />
|
||||
</Flex>
|
||||
|
||||
<TimelinePage timeline={timeline} />
|
||||
</VerticalPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const defaultKinds = [kinds.ShortTextNote, kinds.Repost, kinds.GenericRepost];
|
||||
|
||||
export default function BlindspotFeedView() {
|
||||
const pointer = useParamsProfilePointer("pubkey");
|
||||
|
||||
if (!pointer) return <Navigate to="/discovery/blindspot" />;
|
||||
|
||||
return (
|
||||
<KindSelectionProvider initKinds={defaultKinds}>
|
||||
<BlindspotFeedPage pubkey={pointer.pubkey} />
|
||||
</KindSelectionProvider>
|
||||
);
|
||||
}
|
121
src/views/discovery/blindspot/index.tsx
Normal file
121
src/views/discovery/blindspot/index.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import { memo, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Card,
|
||||
Heading,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Text,
|
||||
useForceUpdate,
|
||||
} from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import useUserContactList from "../../../hooks/use-user-contact-list";
|
||||
import { useWebOfTrust } from "../../../providers/global/web-of-trust-provider";
|
||||
import UserAvatar from "../../../components/user/user-avatar";
|
||||
import UserName from "../../../components/user/user-name";
|
||||
import HoverLinkOverlay from "../../../components/hover-link-overlay";
|
||||
import VerticalPageLayout from "../../../components/vertical-page-layout";
|
||||
|
||||
const UserCard = memo(({ pubkey, blindspot }: { pubkey: string; blindspot: string[] }) => {
|
||||
return (
|
||||
<Card display="block" p="4">
|
||||
<UserAvatar pubkey={pubkey} mr="4" float="left" />
|
||||
<Heading size="md" isTruncated>
|
||||
<HoverLinkOverlay as={RouterLink} to={`/discovery/blindspot/${nip19.npubEncode(pubkey)}`}>
|
||||
<UserName pubkey={pubkey} />
|
||||
</HoverLinkOverlay>
|
||||
</Heading>
|
||||
<Text>Following {blindspot.length} users you don't follow</Text>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
export default function BlindspotHomeView() {
|
||||
const account = useCurrentAccount()!;
|
||||
const [sort, setSort] = useState("quality"); // follows, quality
|
||||
|
||||
const contacts = useUserContactList(account.pubkey);
|
||||
const graph = useWebOfTrust();
|
||||
|
||||
const update = useForceUpdate();
|
||||
useEffect(() => {
|
||||
graph?.on("computed", update);
|
||||
return () => {
|
||||
graph?.off("computed", update);
|
||||
};
|
||||
}, [graph]);
|
||||
|
||||
const pubkeys = useMemo(() => graph?.connections.get(account.pubkey), [contacts]);
|
||||
|
||||
const blindspots = useMemo(() => {
|
||||
if (!contacts || !pubkeys) return [];
|
||||
|
||||
const arr = Array.from(pubkeys)
|
||||
.map((pubkey) => {
|
||||
const following = graph?.connections.get(pubkey);
|
||||
const blindspot = following?.filter((p) => !pubkeys.includes(p) && p !== account.pubkey) ?? [];
|
||||
|
||||
return { pubkey, blindspot };
|
||||
})
|
||||
.filter((p) => p.blindspot.length > 2);
|
||||
|
||||
if (sort === "follows") {
|
||||
return arr.sort((a, b) => b.blindspot.length - a.blindspot.length);
|
||||
} else {
|
||||
// the average distance to pubkeys in the blindspot
|
||||
const quality = new Map<string, number>();
|
||||
for (const { pubkey, blindspot } of arr) {
|
||||
const total = blindspot.reduce((t, p) => t + (graph?.distance.get(p) ?? 0), 0);
|
||||
quality.set(pubkey, total / blindspot.length);
|
||||
}
|
||||
|
||||
return arr.sort((a, b) => quality.get(a.pubkey)! - quality.get(b.pubkey)!);
|
||||
}
|
||||
}, [account.pubkey, pubkeys, graph, sort]);
|
||||
|
||||
return (
|
||||
<VerticalPageLayout>
|
||||
<Box>
|
||||
<Heading>Blind spots</Heading>
|
||||
<Text fontStyle="italic">Pick another user and see what they are seeing that your not.</Text>
|
||||
</Box>
|
||||
<Select ml="auto" maxW="48" value={sort} onChange={(e) => setSort(e.target.value)}>
|
||||
<option value="quality">Quality</option>
|
||||
<option value="follows">Follows</option>
|
||||
</Select>
|
||||
|
||||
{blindspots.length > 0 ? (
|
||||
<SimpleGrid columns={{ base: 1, md: 2, xl: 3 }} spacing="4">
|
||||
{blindspots.map(({ pubkey, blindspot }) => (
|
||||
<UserCard key={pubkey} blindspot={blindspot} pubkey={pubkey} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : (
|
||||
<Alert
|
||||
status="info"
|
||||
variant="subtle"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
textAlign="center"
|
||||
height="200px"
|
||||
>
|
||||
<AlertIcon boxSize="40px" mr={0} />
|
||||
<AlertTitle mt={4} mb={1} fontSize="lg">
|
||||
No blind spots!
|
||||
</AlertTitle>
|
||||
<AlertDescription maxWidth="sm">
|
||||
Unable to find any blind spots. maybe try following some people?
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</VerticalPageLayout>
|
||||
);
|
||||
}
|
@ -14,12 +14,12 @@ import {
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { ChainedDVMJob, getEventIdsFromJobs, getRequestInput, getRequestRelays } from "../../../helpers/nostr/dvm";
|
||||
import { ChainedDVMJob, getEventIdsFromJobs, getRequestInput, getRequestRelays } from "../../../../helpers/nostr/dvm";
|
||||
import dayjs from "dayjs";
|
||||
import { truncatedId } from "../../../helpers/nostr/event";
|
||||
import { CopyIconButton } from "../../../components/copy-icon-button";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import UserLink from "../../../components/user/user-link";
|
||||
import { truncatedId } from "../../../../helpers/nostr/event";
|
||||
import { CopyIconButton } from "../../../../components/copy-icon-button";
|
||||
import { NostrEvent } from "../../../../types/nostr-event";
|
||||
import UserLink from "../../../../components/user/user-link";
|
||||
|
||||
function JobResult({ result }: { result: NostrEvent }) {
|
||||
return (
|
@ -3,10 +3,10 @@ import { Link } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { Box, BoxProps } from "@chakra-ui/react";
|
||||
|
||||
import useUserMetadata from "../../../hooks/use-user-metadata";
|
||||
import useUserMetadata from "../../../../hooks/use-user-metadata";
|
||||
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
import useDVMMetadata from "../../../hooks/use-dvm-metadata";
|
||||
import useDVMMetadata from "../../../../hooks/use-dvm-metadata";
|
||||
|
||||
type DVMAvatarProps = {
|
||||
pointer: AddressPointer;
|
@ -3,13 +3,13 @@ import { Link as RouterLink, To } from "react-router-dom";
|
||||
import { useMemo } from "react";
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import HoverLinkOverlay from "../../../components/hover-link-overlay";
|
||||
import { NostrEvent } from "../../../../types/nostr-event";
|
||||
import HoverLinkOverlay from "../../../../components/hover-link-overlay";
|
||||
import { DVMAvatarLink } from "./dvm-avatar";
|
||||
import { getEventAddressPointer } from "../../../helpers/nostr/event";
|
||||
import { getEventAddressPointer } from "../../../../helpers/nostr/event";
|
||||
import { DVMName } from "./dvm-name";
|
||||
import DebugEventButton from "../../../components/debug-modal/debug-event-button";
|
||||
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
|
||||
import DebugEventButton from "../../../../components/debug-modal/debug-event-button";
|
||||
import useEventIntersectionRef from "../../../../hooks/use-event-intersection-ref";
|
||||
|
||||
export default function DVMCard({
|
||||
appData,
|
@ -2,10 +2,10 @@ import { Link, LinkProps, Text, TextProps } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import useUserMetadata from "../../../hooks/use-user-metadata";
|
||||
import { getDisplayName } from "../../../helpers/nostr/user-metadata";
|
||||
import useUserMetadata from "../../../../hooks/use-user-metadata";
|
||||
import { getDisplayName } from "../../../../helpers/nostr/user-metadata";
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
import useDVMMetadata from "../../../hooks/use-dvm-metadata";
|
||||
import useDVMMetadata from "../../../../hooks/use-dvm-metadata";
|
||||
|
||||
export function DVMName({
|
||||
pointer,
|
@ -1,5 +1,5 @@
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
import useDVMMetadata from "../../../hooks/use-dvm-metadata";
|
||||
import useDVMMetadata from "../../../../hooks/use-dvm-metadata";
|
||||
import { Select } from "@chakra-ui/react";
|
||||
|
||||
export default function DVMParams({
|
@ -20,15 +20,15 @@ import {
|
||||
DVM_CONTENT_DISCOVERY_JOB_KIND,
|
||||
getJobStatusType,
|
||||
getResponseFromDVM,
|
||||
} from "../../../helpers/nostr/dvm";
|
||||
import { InlineInvoiceCard } from "../../../components/lightning/inline-invoice-card";
|
||||
import { DraftNostrEvent } from "../../../types/nostr-event";
|
||||
import { useReadRelays } from "../../../hooks/use-client-relays";
|
||||
} from "../../../../helpers/nostr/dvm";
|
||||
import { InlineInvoiceCard } from "../../../../components/lightning/inline-invoice-card";
|
||||
import { DraftNostrEvent } from "../../../../types/nostr-event";
|
||||
import { useReadRelays } from "../../../../hooks/use-client-relays";
|
||||
import { DVMAvatarLink } from "./dvm-avatar";
|
||||
import DVMLink from "./dvm-name";
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
import useUserMailboxes from "../../../hooks/use-user-mailboxes";
|
||||
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
import useUserMailboxes from "../../../../hooks/use-user-mailboxes";
|
||||
import { usePublishEvent } from "../../../../providers/global/publish-provider";
|
||||
|
||||
function NextPageButton({ chain, pointer }: { pointer: AddressPointer; chain: ChainedDVMJob[] }) {
|
||||
const publish = usePublishEvent();
|
@ -1,8 +1,8 @@
|
||||
import { ChainedDVMJob, getEventIdsFromJobs } from "../../../helpers/nostr/dvm";
|
||||
import { ChainedDVMJob, getEventIdsFromJobs } from "../../../../helpers/nostr/dvm";
|
||||
import FeedStatus from "./feed-status";
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
import useSingleEvents from "../../../hooks/use-single-events";
|
||||
import TimelineItem from "../../../components/timeline-page/generic-note-timeline/timeline-item";
|
||||
import useSingleEvents from "../../../../hooks/use-single-events";
|
||||
import TimelineItem from "../../../../components/timeline-page/generic-note-timeline/timeline-item";
|
||||
|
||||
function FeedEvents({ chain }: { chain: ChainedDVMJob[] }) {
|
||||
const eventIds = getEventIdsFromJobs(chain);
|
@ -22,23 +22,23 @@ import {
|
||||
flattenJobChain,
|
||||
chainJobs,
|
||||
groupEventsIntoJobs,
|
||||
} from "../../helpers/nostr/dvm";
|
||||
import { DraftNostrEvent } from "../../types/nostr-event";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import RequireCurrentAccount from "../../providers/route/require-current-account";
|
||||
import { CodeIcon } from "../../components/icons";
|
||||
} from "../../../helpers/nostr/dvm";
|
||||
import { DraftNostrEvent } from "../../../types/nostr-event";
|
||||
import VerticalPageLayout from "../../../components/vertical-page-layout";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import useTimelineLoader from "../../../hooks/use-timeline-loader";
|
||||
import { useReadRelays } from "../../../hooks/use-client-relays";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import RequireCurrentAccount from "../../../providers/route/require-current-account";
|
||||
import { CodeIcon } from "../../../components/icons";
|
||||
import DebugChains from "./components/debug-chains";
|
||||
import Feed from "./components/feed";
|
||||
import { AddressPointer } from "nostr-tools/nip19";
|
||||
import useParamsAddressPointer from "../../hooks/use-params-address-pointer";
|
||||
import useParamsAddressPointer from "../../../hooks/use-params-address-pointer";
|
||||
import DVMParams from "./components/dvm-params";
|
||||
import useUserMailboxes from "../../hooks/use-user-mailboxes";
|
||||
import { usePublishEvent } from "../../providers/global/publish-provider";
|
||||
import { getHumanReadableCoordinate } from "../../services/replaceable-events";
|
||||
import useUserMailboxes from "../../../hooks/use-user-mailboxes";
|
||||
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
import { getHumanReadableCoordinate } from "../../../services/replaceable-events";
|
||||
|
||||
function DVMFeedPage({ pointer }: { pointer: AddressPointer }) {
|
||||
const [since] = useState(() => dayjs().subtract(1, "hour").unix());
|
||||
@ -95,7 +95,7 @@ function DVMFeedPage({ pointer }: { pointer: AddressPointer }) {
|
||||
return (
|
||||
<VerticalPageLayout>
|
||||
<Flex gap="2">
|
||||
<Button leftIcon={<ChevronLeftIcon />} onClick={() => navigate(-1)}>
|
||||
<Button leftIcon={<ChevronLeftIcon boxSize={6} />} onClick={() => navigate(-1)}>
|
||||
Back
|
||||
</Button>
|
||||
<DVMParams pointer={pointer} params={params} onChange={setParams} />
|
@ -1,7 +1,8 @@
|
||||
import { Heading, Link, SimpleGrid, Text } from "@chakra-ui/react";
|
||||
import { Card, Flex, Heading, Link, LinkBox, SimpleGrid, Text } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import DVMCard from "./components/dvm-card";
|
||||
import DVMCard from "./dvm-feed/components/dvm-card";
|
||||
import { DVM_CONTENT_DISCOVERY_JOB_KIND } from "../../helpers/nostr/dvm";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
@ -10,8 +11,10 @@ import RequireCurrentAccount from "../../providers/route/require-current-account
|
||||
import { getEventCoordinate } from "../../helpers/nostr/event";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
|
||||
import Telescope from "../../components/icons/telescope";
|
||||
import HoverLinkOverlay from "../../components/hover-link-overlay";
|
||||
|
||||
function DVMFeedHomePage() {
|
||||
function DVMFeeds() {
|
||||
const readRelays = useReadRelays();
|
||||
const timeline = useTimelineLoader("content-discovery-dvms", readRelays, {
|
||||
kinds: [31990],
|
||||
@ -23,8 +26,10 @@ function DVMFeedHomePage() {
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<VerticalPageLayout>
|
||||
<Heading size="md">DVM Feeds</Heading>
|
||||
<>
|
||||
<Heading size="md" mt="4">
|
||||
DVM Feeds
|
||||
</Heading>
|
||||
<Text>
|
||||
Learn more about data vending machines here:{" "}
|
||||
<Link href="https://www.data-vending-machines.org/" isExternal color="blue.500">
|
||||
@ -34,18 +39,37 @@ function DVMFeedHomePage() {
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<SimpleGrid columns={{ base: 1, md: 1, lg: 2, xl: 3 }} spacing="2">
|
||||
{DMVs.map((appData) => (
|
||||
<DVMCard key={appData.id} appData={appData} to={`/dvm/${getEventCoordinate(appData)}`} />
|
||||
<DVMCard key={appData.id} appData={appData} to={`/discovery/dvm/${getEventCoordinate(appData)}`} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</IntersectionObserverProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DiscoveryHomePage() {
|
||||
return (
|
||||
<VerticalPageLayout>
|
||||
<Card as={LinkBox} display="block" p="4" maxW="lg">
|
||||
<Telescope boxSize={16} float="left" ml="2" my="2" mr="6" />
|
||||
<Flex direction="column">
|
||||
<Heading size="md">
|
||||
<HoverLinkOverlay as={RouterLink} to="/discovery/blindspot">
|
||||
Blind spots
|
||||
</HoverLinkOverlay>
|
||||
</Heading>
|
||||
<Text>What are other users seeing that you are not?</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
<DVMFeeds />
|
||||
</VerticalPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DVMFeedHomeView() {
|
||||
export default function DiscoveryHomeView() {
|
||||
return (
|
||||
<RequireCurrentAccount>
|
||||
<DVMFeedHomePage />
|
||||
<DiscoveryHomePage />
|
||||
</RequireCurrentAccount>
|
||||
);
|
||||
}
|
@ -14,14 +14,7 @@ import NoteFilterTypeButtons from "../../components/note-filter-type-buttons";
|
||||
import KindSelectionProvider, { useKindSelectionContext } from "../../providers/local/kind-selection-provider";
|
||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
|
||||
const defaultKinds = [
|
||||
kinds.ShortTextNote,
|
||||
kinds.Repost,
|
||||
kinds.GenericRepost,
|
||||
kinds.LongFormArticle,
|
||||
kinds.RecommendRelay,
|
||||
kinds.BadgeAward,
|
||||
];
|
||||
const defaultKinds = [kinds.ShortTextNote, kinds.Repost, kinds.GenericRepost, kinds.LongFormArticle];
|
||||
|
||||
function HomePage() {
|
||||
const showReplies = useDisclosure({ defaultIsOpen: localStorage.getItem("show-replies") === "true" });
|
||||
|
@ -24,6 +24,7 @@ import userMailboxesService from "../../../services/user-mailboxes";
|
||||
import { useContext } from "react";
|
||||
import { AppHandlerContext } from "../../../providers/route/app-handler-provider";
|
||||
import PubkeyAccount from "../../../classes/accounts/pubkey-account";
|
||||
import Telescope from "../../../components/icons/telescope";
|
||||
|
||||
export const UserProfileMenu = ({
|
||||
pubkey,
|
||||
@ -60,6 +61,13 @@ export const UserProfileMenu = ({
|
||||
<MenuItem icon={<DirectMessagesIcon fontSize="1.5em" />} as={RouterLink} to={`/dm/${nip19.npubEncode(pubkey)}`}>
|
||||
Direct messages
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<Telescope fontSize="1.5em" />}
|
||||
as={RouterLink}
|
||||
to={`/discovery/blindspot/${nip19.npubEncode(pubkey)}`}
|
||||
>
|
||||
Blind spot
|
||||
</MenuItem>
|
||||
<MenuItem icon={<SpyIcon fontSize="1.5em" />} onClick={() => loginAsUser()}>
|
||||
Login as user
|
||||
</MenuItem>
|
||||
|
Loading…
x
Reference in New Issue
Block a user