add blindspots feed

This commit is contained in:
hzrd149 2024-08-16 11:41:24 -05:00
parent 6d33b0d19d
commit 5add2819a9
21 changed files with 375 additions and 74 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add blindspots discovery feed

View File

@ -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 /> },

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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) {

View File

@ -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

View File

@ -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);

View 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>
);
}

View 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>
);
}

View File

@ -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 (

View File

@ -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;

View File

@ -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,

View File

@ -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,

View File

@ -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({

View File

@ -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();

View File

@ -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);

View File

@ -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} />

View File

@ -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>
);
}

View File

@ -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" });

View File

@ -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>