mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-10-04 16:37:00 +02:00
Merge branch 'next'
This commit is contained in:
5
.changeset/red-carrots-dream.md
Normal file
5
.changeset/red-carrots-dream.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Replace nostrchat clink with blowater
|
5
.changeset/unlucky-keys-kneel.md
Normal file
5
.changeset/unlucky-keys-kneel.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Remove scroll-boxes and return to natural page scrolling
|
@@ -2,7 +2,7 @@ import React, { Suspense } from "react";
|
|||||||
import { createHashRouter, Outlet, RouterProvider, ScrollRestoration } from "react-router-dom";
|
import { createHashRouter, Outlet, RouterProvider, ScrollRestoration } from "react-router-dom";
|
||||||
import { Spinner } from "@chakra-ui/react";
|
import { Spinner } from "@chakra-ui/react";
|
||||||
import { ErrorBoundary } from "./components/error-boundary";
|
import { ErrorBoundary } from "./components/error-boundary";
|
||||||
import { Page } from "./components/page";
|
import Layout from "./components/layout";
|
||||||
|
|
||||||
import HomeView from "./views/home";
|
import HomeView from "./views/home";
|
||||||
import SettingsView from "./views/settings";
|
import SettingsView from "./views/settings";
|
||||||
@@ -43,12 +43,12 @@ const RootPage = () => {
|
|||||||
useSetColorMode();
|
useSetColorMode();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Layout>
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
<Suspense fallback={<Spinner />}>
|
<Suspense fallback={<Spinner />}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Page>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -13,7 +13,7 @@ export default function NoteDebugModal({ event, ...props }: { event: NostrEvent
|
|||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody overflow="auto" p="4">
|
<ModalBody p="4">
|
||||||
<Flex gap="2" direction="column">
|
<Flex gap="2" direction="column">
|
||||||
<RawValue heading="Event Id" value={event.id} />
|
<RawValue heading="Event Id" value={event.id} />
|
||||||
<RawValue heading="Encoded id (NIP-19)" value={hexToBech32(event.id, Bech32Prefix.Note) ?? "failed"} />
|
<RawValue heading="Encoded id (NIP-19)" value={hexToBech32(event.id, Bech32Prefix.Note) ?? "failed"} />
|
||||||
|
@@ -21,7 +21,7 @@ export default function UserDebugModal({ pubkey, ...props }: { pubkey: string }
|
|||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody overflow="auto" p="4">
|
<ModalBody p="4">
|
||||||
<Flex gap="2" direction="column">
|
<Flex gap="2" direction="column">
|
||||||
<RawValue heading="Hex pubkey" value={pubkey} />
|
<RawValue heading="Hex pubkey" value={pubkey} />
|
||||||
{npub && <RawValue heading="npub" value={npub} />}
|
{npub && <RawValue heading="npub" value={npub} />}
|
||||||
|
@@ -54,7 +54,7 @@ const videoExt = [".mp4", ".mkv", ".webm", ".mov"];
|
|||||||
export function renderVideoUrl(match: URL) {
|
export function renderVideoUrl(match: URL) {
|
||||||
if (!videoExt.some((ext) => match.pathname.endsWith(ext))) return null;
|
if (!videoExt.some((ext) => match.pathname.endsWith(ext))) return null;
|
||||||
|
|
||||||
return <video src={match.toString()} controls style={{ maxWidth: "30rem", maxHeight: "20rem" }} />;
|
return <video src={match.toString()} controls style={{ maxWidth: "30rem", maxHeight: "20rem", width: "100%" }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderGenericUrl(match: URL) {
|
export function renderGenericUrl(match: URL) {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { SettingsIcon } from "@chakra-ui/icons";
|
import { SettingsIcon } from "@chakra-ui/icons";
|
||||||
import { Avatar, Button, Flex, Heading, IconButton, LinkOverlay, Text, VStack } from "@chakra-ui/react";
|
import { Avatar, Button, Flex, FlexProps, Heading, IconButton, LinkOverlay, Text, VStack } from "@chakra-ui/react";
|
||||||
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
import accountService from "../../services/account";
|
import accountService from "../../services/account";
|
||||||
@@ -20,13 +20,13 @@ import AccountSwitcher from "./account-switcher";
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||||
|
|
||||||
export default function DesktopSideNav() {
|
export default function DesktopSideNav(props: Omit<FlexProps, "children">) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const { openModal } = useContext(PostModalContext);
|
const { openModal } = useContext(PostModalContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack width="15rem" pt="2" alignItems="stretch" flexShrink={0}>
|
<Flex {...props} gap="2" direction="column" width="15rem" pt="2" alignItems="stretch" flexShrink={0}>
|
||||||
<Flex gap="2" alignItems="center" position="relative">
|
<Flex gap="2" alignItems="center" position="relative">
|
||||||
<LinkOverlay as={RouterLink} to="/" />
|
<LinkOverlay as={RouterLink} to="/" />
|
||||||
<Avatar src="/apple-touch-icon.png" size="sm" />
|
<Avatar src="/apple-touch-icon.png" size="sm" />
|
||||||
@@ -81,6 +81,6 @@ export default function DesktopSideNav() {
|
|||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</VStack>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
34
src/components/layout/index.tsx
Normal file
34
src/components/layout/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Container, Flex } from "@chakra-ui/react";
|
||||||
|
import { ErrorBoundary } from "../error-boundary";
|
||||||
|
|
||||||
|
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||||
|
import { ReloadPrompt } from "../reload-prompt";
|
||||||
|
import DesktopSideNav from "./desktop-side-nav";
|
||||||
|
import MobileBottomNav from "./mobile-bottom-nav";
|
||||||
|
|
||||||
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ReloadPrompt mb="2" />
|
||||||
|
<Container size="lg" display="flex" padding="0" gap="4" alignItems="flex-start">
|
||||||
|
{!isMobile && <DesktopSideNav position="sticky" top="0" />}
|
||||||
|
<Flex flexGrow={1} direction="column" w="full" overflowX="hidden" pb={isMobile ? "14" : 0}>
|
||||||
|
<ErrorBoundary>{children}</ErrorBoundary>
|
||||||
|
</Flex>
|
||||||
|
{isMobile && (
|
||||||
|
<MobileBottomNav
|
||||||
|
position="fixed"
|
||||||
|
bottom="0"
|
||||||
|
left="0"
|
||||||
|
right="0"
|
||||||
|
backgroundColor="var(--chakra-colors-chakra-body-bg)"
|
||||||
|
zIndex={10}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import { Avatar, Flex, IconButton, useDisclosure } from "@chakra-ui/react";
|
import { Avatar, Flex, FlexProps, IconButton, useDisclosure } from "@chakra-ui/react";
|
||||||
import { useContext, useEffect } from "react";
|
import { useContext, useEffect } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
@@ -7,7 +7,7 @@ import { ChatIcon, FeedIcon, HomeIcon, NotificationIcon, PlusCircleIcon, SearchI
|
|||||||
import { UserAvatar } from "../user-avatar";
|
import { UserAvatar } from "../user-avatar";
|
||||||
import MobileSideDrawer from "./mobile-side-drawer";
|
import MobileSideDrawer from "./mobile-side-drawer";
|
||||||
|
|
||||||
export default function MobileBottomNav() {
|
export default function MobileBottomNav(props: Omit<FlexProps, "children">) {
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const { openModal } = useContext(PostModalContext);
|
const { openModal } = useContext(PostModalContext);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -18,7 +18,7 @@ export default function MobileBottomNav() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flex flexShrink={0} gap="2" padding="2" alignItems="center">
|
<Flex {...props} gap="2" padding="2" alignItems="center">
|
||||||
{account ? (
|
{account ? (
|
||||||
<UserAvatar pubkey={account.pubkey} size="sm" onClick={onOpen} noProxy />
|
<UserAvatar pubkey={account.pubkey} size="sm" onClick={onOpen} noProxy />
|
||||||
) : (
|
) : (
|
@@ -1,36 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Container, Flex } from "@chakra-ui/react";
|
|
||||||
import { ErrorBoundary } from "../error-boundary";
|
|
||||||
|
|
||||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
|
||||||
import { ReloadPrompt } from "../reload-prompt";
|
|
||||||
import { PostModalProvider } from "../../providers/post-modal-provider";
|
|
||||||
import DesktopSideNav from "./desktop-side-nav";
|
|
||||||
import MobileBottomNav from "./mobile-bottom-nav";
|
|
||||||
|
|
||||||
export const Page = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PostModalProvider>
|
|
||||||
<Container
|
|
||||||
size="lg"
|
|
||||||
display="flex"
|
|
||||||
flexDirection="column"
|
|
||||||
height="100%"
|
|
||||||
overflow="hidden"
|
|
||||||
position="relative"
|
|
||||||
padding="0"
|
|
||||||
>
|
|
||||||
<ReloadPrompt />
|
|
||||||
<Flex gap="4" grow={1} overflow="hidden">
|
|
||||||
{!isMobile && <DesktopSideNav />}
|
|
||||||
<Flex flexGrow={1} direction="column" overflow="hidden">
|
|
||||||
<ErrorBoundary>{children}</ErrorBoundary>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
{isMobile && <MobileBottomNav />}
|
|
||||||
</Container>
|
|
||||||
</PostModalProvider>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -1,10 +1,10 @@
|
|||||||
import { useRegisterSW } from "virtual:pwa-register/react";
|
import { useRegisterSW } from "virtual:pwa-register/react";
|
||||||
import { Alert, AlertIcon, AlertTitle, Button, CloseButton, useToast } from "@chakra-ui/react";
|
import { Alert, AlertIcon, AlertProps, AlertTitle, Button, CloseButton, useToast } from "@chakra-ui/react";
|
||||||
|
|
||||||
// check for updates every hour
|
// check for updates every hour
|
||||||
const intervalMS = 60 * 60 * 1000;
|
const intervalMS = 60 * 60 * 1000;
|
||||||
|
|
||||||
export const ReloadPrompt = () => {
|
export const ReloadPrompt = (props: Omit<AlertProps, "children" | "status">) => {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const {
|
const {
|
||||||
needRefresh: [needRefresh, setNeedRefresh],
|
needRefresh: [needRefresh, setNeedRefresh],
|
||||||
@@ -30,7 +30,7 @@ export const ReloadPrompt = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return needRefresh ? (
|
return needRefresh ? (
|
||||||
<Alert status="success" flexShrink={0}>
|
<Alert status="success" {...props}>
|
||||||
<AlertIcon />
|
<AlertIcon />
|
||||||
<AlertTitle>New update ready!</AlertTitle>
|
<AlertTitle>New update ready!</AlertTitle>
|
||||||
<Button size="sm" ml="auto" onClick={() => updateServiceWorker(true)}>
|
<Button size="sm" ml="auto" onClick={() => updateServiceWorker(true)}>
|
||||||
|
@@ -4,6 +4,8 @@ import { SigningProvider } from "./signing-provider";
|
|||||||
import createTheme from "../theme";
|
import createTheme from "../theme";
|
||||||
import useAppSettings from "../hooks/use-app-settings";
|
import useAppSettings from "../hooks/use-app-settings";
|
||||||
import { InvoiceModalProvider } from "./invoice-modal";
|
import { InvoiceModalProvider } from "./invoice-modal";
|
||||||
|
import NotificationTimelineProvider from "./notification-timeline";
|
||||||
|
import PostModalProvider from "./post-modal-provider";
|
||||||
|
|
||||||
export const Providers = ({ children }: { children: React.ReactNode }) => {
|
export const Providers = ({ children }: { children: React.ReactNode }) => {
|
||||||
const { primaryColor } = useAppSettings();
|
const { primaryColor } = useAppSettings();
|
||||||
@@ -12,7 +14,11 @@ export const Providers = ({ children }: { children: React.ReactNode }) => {
|
|||||||
return (
|
return (
|
||||||
<ChakraProvider theme={theme} colorModeManager={localStorageManager}>
|
<ChakraProvider theme={theme} colorModeManager={localStorageManager}>
|
||||||
<SigningProvider>
|
<SigningProvider>
|
||||||
<InvoiceModalProvider>{children}</InvoiceModalProvider>
|
<InvoiceModalProvider>
|
||||||
|
<NotificationTimelineProvider>
|
||||||
|
<PostModalProvider>{children}</PostModalProvider>
|
||||||
|
</NotificationTimelineProvider>
|
||||||
|
</InvoiceModalProvider>
|
||||||
</SigningProvider>
|
</SigningProvider>
|
||||||
</ChakraProvider>
|
</ChakraProvider>
|
||||||
);
|
);
|
||||||
|
50
src/providers/notification-timeline.tsx
Normal file
50
src/providers/notification-timeline.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { PropsWithChildren, createContext, useContext, useEffect, useMemo } from "react";
|
||||||
|
import { truncatedId } from "../helpers/nostr-event";
|
||||||
|
import { useReadRelayUrls } from "../hooks/use-client-relays";
|
||||||
|
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||||
|
import { TimelineLoader } from "../classes/timeline-loader";
|
||||||
|
import timelineCacheService from "../services/timeline-cache";
|
||||||
|
import { Kind } from "nostr-tools";
|
||||||
|
|
||||||
|
type NotificationTimelineContextType = {
|
||||||
|
timeline?: TimelineLoader;
|
||||||
|
};
|
||||||
|
const NotificationTimelineContext = createContext<NotificationTimelineContextType>({});
|
||||||
|
|
||||||
|
export function useNotificationTimeline() {
|
||||||
|
const context = useContext(NotificationTimelineContext);
|
||||||
|
|
||||||
|
if (!context?.timeline) throw new Error("No notification timeline");
|
||||||
|
|
||||||
|
return context.timeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NotificationTimelineProvider({ children }: PropsWithChildren) {
|
||||||
|
const account = useCurrentAccount();
|
||||||
|
const readRelays = useReadRelayUrls();
|
||||||
|
|
||||||
|
const timeline = useMemo(() => {
|
||||||
|
return account?.pubkey
|
||||||
|
? timelineCacheService.createTimeline(`${truncatedId(account?.pubkey ?? "anon")}-notification`)
|
||||||
|
: undefined;
|
||||||
|
}, [account?.pubkey]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (timeline && account) {
|
||||||
|
timeline.setQuery([{ "#p": [account?.pubkey], kinds: [Kind.Text, Kind.Repost, Kind.Reaction, Kind.Zap] }]);
|
||||||
|
}
|
||||||
|
}, [account, timeline]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
timeline?.setRelays(readRelays);
|
||||||
|
}, [readRelays.join("|")]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
timeline?.open();
|
||||||
|
return () => timeline?.close();
|
||||||
|
}, [timeline]);
|
||||||
|
|
||||||
|
const context = useMemo(() => ({ timeline }), [timeline]);
|
||||||
|
|
||||||
|
return <NotificationTimelineContext.Provider value={context}>{children}</NotificationTimelineContext.Provider>;
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
|
import React, { PropsWithChildren, useCallback, useMemo, useState } from "react";
|
||||||
import { useDisclosure } from "@chakra-ui/react";
|
import { useDisclosure } from "@chakra-ui/react";
|
||||||
import React, { useCallback, useMemo, useState } from "react";
|
|
||||||
import { ErrorBoundary } from "../components/error-boundary";
|
import { ErrorBoundary } from "../components/error-boundary";
|
||||||
import { PostModal } from "../components/post-modal";
|
import { PostModal } from "../components/post-modal";
|
||||||
import { DraftNostrEvent } from "../types/nostr-event";
|
import { DraftNostrEvent } from "../types/nostr-event";
|
||||||
@@ -12,7 +12,7 @@ export const PostModalContext = React.createContext<PostModalContextType>({
|
|||||||
openModal: () => {},
|
openModal: () => {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PostModalProvider = ({ children }: { children: React.ReactNode }) => {
|
export default function PostModalProvider({ children }: PropsWithChildren) {
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const [draft, setDraft] = useState<Partial<DraftNostrEvent> | undefined>(undefined);
|
const [draft, setDraft] = useState<Partial<DraftNostrEvent> | undefined>(undefined);
|
||||||
const openModal = useCallback(
|
const openModal = useCallback(
|
||||||
@@ -32,4 +32,4 @@ export const PostModalProvider = ({ children }: { children: React.ReactNode }) =
|
|||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</PostModalContext.Provider>
|
</PostModalContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
@@ -4,5 +4,4 @@ body,
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
@@ -98,12 +98,12 @@ function DirectMessagesPage() {
|
|||||||
<Alert status="info" flexShrink={0}>
|
<Alert status="info" flexShrink={0}>
|
||||||
<AlertIcon />
|
<AlertIcon />
|
||||||
<Flex direction={isMobile ? "column" : "row"}>
|
<Flex direction={isMobile ? "column" : "row"}>
|
||||||
<AlertTitle>Give NostrChat a try</AlertTitle>
|
<AlertTitle>Give Blowater a try</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Text>
|
<Text>
|
||||||
Its a much better chat app than what I can build inside of noStrudel.{" "}
|
Its a much better chat app than what I can build inside of noStrudel.{" "}
|
||||||
<Link href="https://www.nostrchat.io/" isExternal>
|
<Link href="https://blowater.deno.dev/" isExternal>
|
||||||
nostrchat.io <ExternalLinkIcon />
|
blowater.deno.dev <ExternalLinkIcon />
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
|
@@ -61,7 +61,7 @@ const NoteView = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="4" overflow="auto" flex={1} pb="4" pt="4" pl="1" pr="1">
|
<Flex direction="column" gap="4" flex={1} pb="4" pt="4" pl="1" pr="1">
|
||||||
{pageContent}
|
{pageContent}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@@ -1,65 +1,119 @@
|
|||||||
import { memo, useCallback, useRef } from "react";
|
import { memo, useCallback, useMemo, useRef } from "react";
|
||||||
import { Card, CardBody, CardHeader, Flex, Text } from "@chakra-ui/react";
|
import { Card, CardBody, CardHeader, Flex, Text } from "@chakra-ui/react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { UserAvatar } from "../../components/user-avatar";
|
import { UserAvatar } from "../../components/user-avatar";
|
||||||
import { UserLink } from "../../components/user-link";
|
import { UserLink } from "../../components/user-link";
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
|
||||||
import { NostrEvent } from "../../types/nostr-event";
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
import { NoteLink } from "../../components/note-link";
|
import { NoteLink } from "../../components/note-link";
|
||||||
import RequireCurrentAccount from "../../providers/require-current-account";
|
import RequireCurrentAccount from "../../providers/require-current-account";
|
||||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||||
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { truncatedId } from "../../helpers/nostr-event";
|
|
||||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||||
|
import { useNotificationTimeline } from "../../providers/notification-timeline";
|
||||||
|
import { Kind, getEventHash } from "nostr-tools";
|
||||||
|
import { parseZapEvent } from "../../helpers/zaps";
|
||||||
|
import { readablizeSats } from "../../helpers/bolt11";
|
||||||
|
import { getReferences } from "../../helpers/nostr-event";
|
||||||
|
|
||||||
const Kind1Notification = ({ event }: { event: NostrEvent }) => {
|
const Kind1Notification = ({ event }: { event: NostrEvent }) => (
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
<Card size="sm" variant="outline">
|
||||||
useRegisterIntersectionEntity(ref, event.id);
|
<CardHeader>
|
||||||
|
<Flex gap="2" alignItems="center">
|
||||||
|
<UserAvatar pubkey={event.pubkey} size="xs" />
|
||||||
|
<UserLink pubkey={event.pubkey} />
|
||||||
|
<Text>replied to your post</Text>
|
||||||
|
<NoteLink noteId={event.id} color="current" ml="auto">
|
||||||
|
{dayjs.unix(event.created_at).fromNow()}
|
||||||
|
</NoteLink>
|
||||||
|
</Flex>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody pt={0}>
|
||||||
|
<Text>{event.content.replace("\n", " ").slice(0, 64)}</Text>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ReactionNotification = ({ event }: { event: NostrEvent }) => {
|
||||||
|
const refs = getReferences(event);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card size="sm" variant="outline" ref={ref}>
|
<Flex gap="2" alignItems="center" px="2">
|
||||||
<CardHeader>
|
<UserAvatar pubkey={event.pubkey} size="xs" />
|
||||||
<Flex gap="4" alignItems="center">
|
<UserLink pubkey={event.pubkey} />
|
||||||
<UserAvatar pubkey={event.pubkey} size="sm" />
|
<Text>reacted {event.content} to your post</Text>
|
||||||
<UserLink pubkey={event.pubkey} />
|
<NoteLink noteId={refs.replyId || event.id} color="current" ml="auto">
|
||||||
<NoteLink noteId={event.id} color="current" ml="auto">
|
{dayjs.unix(event.created_at).fromNow()}
|
||||||
{dayjs.unix(event.created_at).fromNow()}
|
</NoteLink>
|
||||||
</NoteLink>
|
</Flex>
|
||||||
</Flex>
|
);
|
||||||
</CardHeader>
|
};
|
||||||
<CardBody pt={0}>
|
|
||||||
<Text>{event.content.replace("\n", " ").slice(0, 64)}</Text>
|
const ZapNotification = ({ event }: { event: NostrEvent }) => {
|
||||||
</CardBody>
|
const zap = useMemo(() => {
|
||||||
</Card>
|
try {
|
||||||
|
return parseZapEvent(event);
|
||||||
|
} catch (e) {}
|
||||||
|
}, [event]);
|
||||||
|
|
||||||
|
if (!zap || !zap.payment.amount) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
direction="row"
|
||||||
|
borderRadius="md"
|
||||||
|
borderColor="yellow.400"
|
||||||
|
borderWidth="1px"
|
||||||
|
p="2"
|
||||||
|
gap="2"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<UserAvatar pubkey={zap.request.pubkey} size="xs" />
|
||||||
|
<UserLink pubkey={zap.request.pubkey} />
|
||||||
|
<Text>
|
||||||
|
zapped {readablizeSats(zap.payment.amount / 1000)} sats
|
||||||
|
{zap.eventId && (
|
||||||
|
<span>
|
||||||
|
{" "}
|
||||||
|
on note: <NoteLink noteId={zap.eventId} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Text color="current" ml="auto">
|
||||||
|
{dayjs.unix(zap.request.created_at).fromNow()}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NotificationItem = memo(({ event }: { event: NostrEvent }) => {
|
const NotificationItem = memo(({ event }: { event: NostrEvent }) => {
|
||||||
if (event.kind === 1) {
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
return <Kind1Notification event={event} />;
|
useRegisterIntersectionEntity(ref, event.id);
|
||||||
|
|
||||||
|
let content = <Text>Unknown event type {event.kind}</Text>;
|
||||||
|
switch (event.kind) {
|
||||||
|
case Kind.Text:
|
||||||
|
content = <Kind1Notification event={event} />;
|
||||||
|
break;
|
||||||
|
case Kind.Reaction:
|
||||||
|
content = <ReactionNotification event={event} />;
|
||||||
|
break;
|
||||||
|
case Kind.Zap:
|
||||||
|
content = <ZapNotification event={event} />;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return <>Unknown event type {event.kind}</>;
|
|
||||||
|
return <div ref={ref}>{content}</div>;
|
||||||
});
|
});
|
||||||
|
|
||||||
function NotificationsPage() {
|
function NotificationsPage() {
|
||||||
const readRelays = useReadRelayUrls();
|
|
||||||
const account = useCurrentAccount()!;
|
const account = useCurrentAccount()!;
|
||||||
|
|
||||||
const eventFilter = useCallback((event: NostrEvent) => event.pubkey !== account.pubkey, [account]);
|
const eventFilter = useCallback((event: NostrEvent) => event.pubkey !== account.pubkey, [account]);
|
||||||
const timeline = useTimelineLoader(
|
const timeline = useNotificationTimeline();
|
||||||
`${truncatedId(account.pubkey)}-notifications`,
|
|
||||||
readRelays,
|
|
||||||
{
|
|
||||||
"#p": [account.pubkey],
|
|
||||||
kinds: [1],
|
|
||||||
},
|
|
||||||
{ eventFilter }
|
|
||||||
);
|
|
||||||
|
|
||||||
const events = useSubject(timeline.timeline);
|
const events = useSubject(timeline?.timeline) ?? [];
|
||||||
|
|
||||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
@@ -68,7 +68,7 @@ const MetadataForm = ({ defaultValues, onSubmit }: MetadataFormProps) => {
|
|||||||
}, [defaultValues]);
|
}, [defaultValues]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" pb="4" overflow="auto" px={isMobile ? "2" : 0}>
|
<Flex direction="column" pb="4" px={isMobile ? "2" : 0}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
<Flex direction="column" gap="2" pt="4">
|
<Flex direction="column" gap="2" pt="4">
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
|
@@ -74,7 +74,7 @@ function RelaysPage() {
|
|||||||
const hasPending = pendingAdd.length > 0 || pendingRemove.length > 0;
|
const hasPending = pendingAdd.length > 0 || pendingRemove.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" pt="2" pb="2" overflow="auto">
|
<Flex direction="column" pt="2" pb="2">
|
||||||
<TableContainer mb="4" overflowY="initial">
|
<TableContainer mb="4" overflowY="initial">
|
||||||
<Table variant="simple" size="sm">
|
<Table variant="simple" size="sm">
|
||||||
<Thead>
|
<Thead>
|
||||||
|
@@ -28,7 +28,7 @@ export default function SettingsView() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" pt="2" pb="2" overflow="auto">
|
<Flex direction="column" pt="2" pb="2">
|
||||||
<form onSubmit={saveSettings}>
|
<form onSubmit={saveSettings}>
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<Accordion defaultIndex={[0]} allowMultiple>
|
<Accordion defaultIndex={[0]} allowMultiple>
|
||||||
|
@@ -93,7 +93,7 @@ export default function StreamCard({ stream, ...props }: CardProps & { stream: P
|
|||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader>Raw event</ModalHeader>
|
<ModalHeader>Raw event</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody overflow="auto" p="4">
|
<ModalBody p="4">
|
||||||
<Flex gap="2" direction="column">
|
<Flex gap="2" direction="column">
|
||||||
<RawValue heading="Event Id" value={stream.event.id} />
|
<RawValue heading="Event Id" value={stream.event.id} />
|
||||||
<RawValue heading="naddr" value={naddr} />
|
<RawValue heading="naddr" value={naddr} />
|
||||||
|
@@ -101,13 +101,12 @@ const UserView = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AdditionalRelayProvider relays={unique([...userTopRelays, ...pointerRelays])}>
|
<AdditionalRelayProvider relays={unique([...userTopRelays, ...pointerRelays])}>
|
||||||
<Flex direction="column" alignItems="stretch" gap="2" overflow="hidden" h="full">
|
<Flex direction="column" alignItems="stretch" gap="2">
|
||||||
<Header pubkey={pubkey} showRelaySelectionModal={relayModal.onOpen} />
|
<Header pubkey={pubkey} showRelaySelectionModal={relayModal.onOpen} />
|
||||||
<Tabs
|
<Tabs
|
||||||
display="flex"
|
display="flex"
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
flexGrow="1"
|
flexGrow="1"
|
||||||
overflow="hidden"
|
|
||||||
isLazy
|
isLazy
|
||||||
index={activeTab}
|
index={activeTab}
|
||||||
onChange={(v) => navigate(tabs[v].path)}
|
onChange={(v) => navigate(tabs[v].path)}
|
||||||
@@ -119,9 +118,9 @@ const UserView = () => {
|
|||||||
))}
|
))}
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanels overflow="hidden" h="full">
|
<TabPanels>
|
||||||
{tabs.map(({ label }) => (
|
{tabs.map(({ label }) => (
|
||||||
<TabPanel key={label} p={0} h="full" overflow="hidden">
|
<TabPanel key={label} p={0}>
|
||||||
<Suspense fallback={<Spinner />}>
|
<Suspense fallback={<Spinner />}>
|
||||||
<Outlet context={{ pubkey, setRelayCount }} />
|
<Outlet context={{ pubkey, setRelayCount }} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
Reference in New Issue
Block a user