From 8faf3e42fd3236626a08601d806ddd0331f94e19 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Sun, 28 Apr 2024 12:13:27 -0500 Subject: [PATCH] add task manager modal --- .changeset/tasty-scissors-taste.md | 5 + src/app.tsx | 15 +- src/classes/nostr-publish-action.ts | 3 +- src/components/layout/desktop-side-nav.tsx | 5 +- src/components/layout/mobile-side-drawer.tsx | 13 +- src/components/layout/task-manager-button.tsx | 18 +++ src/components/post-modal/index.tsx | 6 +- src/components/publish-log.tsx | 96 ----------- src/providers/global/publish-provider.tsx | 14 +- src/views/task-manager/database/index.tsx | 5 + src/views/task-manager/layout.tsx | 44 +++++ src/views/task-manager/modal.tsx | 75 +++++++++ src/views/task-manager/network/index.tsx | 38 +++++ .../task-manager/network/inspect-relay.tsx | 5 + src/views/task-manager/provider.tsx | 152 ++++++++++++++++++ .../publish-log/action-status-tag.tsx | 40 +++++ src/views/task-manager/publish-log/index.tsx | 52 ++++++ .../publish-log}/publish-details.tsx | 10 +- 18 files changed, 466 insertions(+), 130 deletions(-) create mode 100644 .changeset/tasty-scissors-taste.md create mode 100644 src/components/layout/task-manager-button.tsx delete mode 100644 src/components/publish-log.tsx create mode 100644 src/views/task-manager/database/index.tsx create mode 100644 src/views/task-manager/layout.tsx create mode 100644 src/views/task-manager/modal.tsx create mode 100644 src/views/task-manager/network/index.tsx create mode 100644 src/views/task-manager/network/inspect-relay.tsx create mode 100644 src/views/task-manager/provider.tsx create mode 100644 src/views/task-manager/publish-log/action-status-tag.tsx create mode 100644 src/views/task-manager/publish-log/index.tsx rename src/{components => views/task-manager/publish-log}/publish-details.tsx (86%) diff --git a/.changeset/tasty-scissors-taste.md b/.changeset/tasty-scissors-taste.md new file mode 100644 index 000000000..e4bb2dabc --- /dev/null +++ b/.changeset/tasty-scissors-taste.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Add task manager modal diff --git a/src/app.tsx b/src/app.tsx index aeb2e3cac..c2a784139 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -89,6 +89,7 @@ import BookmarksView from "./views/bookmarks"; import LoginNostrAddressView from "./views/signin/address"; import LoginNostrAddressCreate from "./views/signin/address/create"; import DatabaseView from "./views/relays/cache/database"; +import TaskManagerProvider from "./views/task-manager/provider"; const TracksView = lazy(() => import("./views/tracks")); const UserTracksTab = lazy(() => import("./views/user/tracks")); const UserVideosTab = lazy(() => import("./views/user/videos")); @@ -451,11 +452,13 @@ const router = createHashRouter([ export const App = () => ( - - - }> - - - + + + + }> + + + + ); diff --git a/src/classes/nostr-publish-action.ts b/src/classes/nostr-publish-action.ts index cf1cc085a..3b9d62345 100644 --- a/src/classes/nostr-publish-action.ts +++ b/src/classes/nostr-publish-action.ts @@ -5,10 +5,11 @@ import relayPoolService from "../services/relay-pool"; import createDefer from "./deferred"; import { PersistentSubject } from "./subject"; import ControlledObservable from "./controlled-observable"; +import dayjs from "dayjs"; export type PublishResult = { relay: AbstractRelay; success: boolean; message: string }; -export default class NostrPublishAction { +export default class PublishAction { id = nanoid(); label: string; relays: string[]; diff --git a/src/components/layout/desktop-side-nav.tsx b/src/components/layout/desktop-side-nav.tsx index 2e326d78a..658e713e2 100644 --- a/src/components/layout/desktop-side-nav.tsx +++ b/src/components/layout/desktop-side-nav.tsx @@ -5,13 +5,14 @@ import { css } from "@emotion/react"; import useCurrentAccount from "../../hooks/use-current-account"; import AccountSwitcher from "./account-switcher"; -import PublishLog from "../publish-log"; import NavItems from "./nav-items"; import { PostModalContext } from "../../providers/route/post-modal-provider"; import { WritingIcon } from "../icons"; import useSubject from "../../hooks/use-subject"; import { offlineMode } from "../../services/offline-mode"; import WifiOff from "../icons/wifi-off"; +import { useTaskManagerContext } from "../../views/task-manager/provider"; +import TaskManagerButton from "./task-manager-button"; const hideScrollbar = css` -ms-overflow-style: none; @@ -88,7 +89,7 @@ export default function DesktopSideNav(props: Omit) { )} - + ); } diff --git a/src/components/layout/mobile-side-drawer.tsx b/src/components/layout/mobile-side-drawer.tsx index 6bd7d40d8..27a409db6 100644 --- a/src/components/layout/mobile-side-drawer.tsx +++ b/src/components/layout/mobile-side-drawer.tsx @@ -15,6 +15,7 @@ import { Link as RouterLink } from "react-router-dom"; import AccountSwitcher from "./account-switcher"; import useCurrentAccount from "../../hooks/use-current-account"; import NavItems from "./nav-items"; +import TaskManagerButton from "./task-manager-button"; export default function MobileSideDrawer({ ...props }: Omit) { const account = useCurrentAccount(); @@ -23,16 +24,7 @@ export default function MobileSideDrawer({ ...props }: Omit - + {account ? ( ) : ( @@ -48,6 +40,7 @@ export default function MobileSideDrawer({ ...props }: Omit )} + diff --git a/src/components/layout/task-manager-button.tsx b/src/components/layout/task-manager-button.tsx new file mode 100644 index 000000000..8cdf5bb87 --- /dev/null +++ b/src/components/layout/task-manager-button.tsx @@ -0,0 +1,18 @@ +import { useContext } from "react"; +import { Button, ButtonProps } from "@chakra-ui/react"; + +import { PublishContext } from "../../providers/global/publish-provider"; +import { useTaskManagerContext } from "../../views/task-manager/provider"; +import PublishActionStatusTag from "../../views/task-manager/publish-log/action-status-tag"; + +export default function TaskManagerButton({ ...props }: Omit) { + const { log } = useContext(PublishContext); + const { openTaskManager } = useTaskManagerContext(); + + return ( + + ); +} diff --git a/src/components/post-modal/index.tsx b/src/components/post-modal/index.tsx index ba81b8bb7..5c85b13eb 100644 --- a/src/components/post-modal/index.tsx +++ b/src/components/post-modal/index.tsx @@ -29,8 +29,8 @@ import { useForm } from "react-hook-form"; import { kinds } from "nostr-tools"; import { ChevronDownIcon, ChevronUpIcon, UploadImageIcon } from "../icons"; -import NostrPublishAction from "../../classes/nostr-publish-action"; -import { PublishDetails } from "../publish-details"; +import PublishAction from "../../classes/nostr-publish-action"; +import { PublishDetails } from "../../views/task-manager/publish-log/publish-details"; import { TrustProvider } from "../../providers/local/trust"; import { correctContentMentions, @@ -85,7 +85,7 @@ export default function PostModal({ const account = useCurrentAccount()!; const { noteDifficulty } = useAppSettings(); const [miningTarget, setMiningTarget] = useState(0); - const [publishAction, setPublishAction] = useState(); + const [publishAction, setPublishAction] = useState(); const emojis = useContextEmojis(); const moreOptions = useDisclosure(); diff --git a/src/components/publish-log.tsx b/src/components/publish-log.tsx deleted file mode 100644 index 1fbec4fcd..000000000 --- a/src/components/publish-log.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { useContext } from "react"; -import { - Flex, - FlexProps, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalHeader, - ModalOverlay, - Spinner, - Tag, - TagLabel, - TagProps, - Text, - useDisclosure, -} from "@chakra-ui/react"; - -import NostrPublishAction from "../classes/nostr-publish-action"; -import useSubject from "../hooks/use-subject"; -import { CheckIcon, ErrorIcon } from "./icons"; -import { PublishDetails } from "./publish-details"; -import { PublishContext } from "../providers/global/publish-provider"; - -export function PublishActionStatusTag({ pub, ...props }: { pub: NostrPublishAction } & Omit) { - const results = useSubject(pub.results); - - const successful = results.filter(({ success }) => success); - const failedWithMessage = results.filter(({ success, message }) => !success && !!message); - - let statusIcon = ; - let statusColor: TagProps["colorScheme"] = "blue"; - if (results.length !== pub.relays.length) { - statusColor = "blue"; - statusIcon = ; - } else if (successful.length === 0) { - statusColor = "red"; - statusIcon = ; - } else if (failedWithMessage.length > 0) { - statusColor = "orange"; - statusIcon = ; - } else { - statusColor = "green"; - statusIcon = ; - } - - return ( - - - {successful.length}/{pub.relays.length} - - {statusIcon} - - ); -} - -function PublishAction({ pub }: { pub: NostrPublishAction }) { - const details = useDisclosure(); - - return ( - <> - - {pub.label} - - - {details.isOpen && ( - - - - - {pub.label} - - - - - - - - )} - - ); -} - -export default function PublishLog({ ...props }: Omit) { - const { log } = useContext(PublishContext); - const reverseLog = Array.from(log).reverse(); - - return ( - - {reverseLog.length > 0 && Activity log:} - {reverseLog.map((pub) => ( - - ))} - - ); -} diff --git a/src/providers/global/publish-provider.tsx b/src/providers/global/publish-provider.tsx index 0afe8122f..d3af9bd84 100644 --- a/src/providers/global/publish-provider.tsx +++ b/src/providers/global/publish-provider.tsx @@ -4,7 +4,7 @@ import { EventTemplate, NostrEvent, kinds } from "nostr-tools"; import { useSigningContext } from "./signing-provider"; import { DraftNostrEvent } from "../../types/nostr-event"; -import NostrPublishAction from "../../classes/nostr-publish-action"; +import PublishAction from "../../classes/nostr-publish-action"; import clientRelaysService from "../../services/client-relays"; import RelaySet from "../../classes/relay-set"; import { getAllRelayHints, isReplaceable } from "../../helpers/nostr/event"; @@ -17,28 +17,28 @@ import deleteEventService from "../../services/delete-events"; import userMailboxesService from "../../services/user-mailboxes"; type PublishContextType = { - log: NostrPublishAction[]; + log: PublishAction[]; publishEvent( label: string, event: EventTemplate | NostrEvent, additionalRelays: Iterable | undefined, quite: false, onlyAdditionalRelays: false, - ): Promise; + ): Promise; publishEvent( label: string, event: EventTemplate | NostrEvent, additionalRelays: Iterable | undefined, quite: false, onlyAdditionalRelays?: boolean, - ): Promise; + ): Promise; publishEvent( label: string, event: EventTemplate | NostrEvent, additionalRelays?: Iterable | undefined, quite?: boolean, onlyAdditionalRelays?: boolean, - ): Promise; + ): Promise; }; export const PublishContext = createContext({ log: [], @@ -53,7 +53,7 @@ export function usePublishEvent() { export default function PublishProvider({ children }: PropsWithChildren) { const toast = useToast(); - const [log, setLog] = useState([]); + const [log, setLog] = useState([]); const { requestSignature } = useSigningContext(); const publishEvent = useCallback( @@ -84,7 +84,7 @@ export default function PublishProvider({ children }: PropsWithChildren) { signed = await requestSignature(draft); } else signed = event as NostrEvent; - const pub = new NostrPublishAction(label, relays, signed); + const pub = new PublishAction(label, relays, signed); setLog((arr) => arr.concat(pub)); pub.onResult.subscribe(({ relay, success }) => { diff --git a/src/views/task-manager/database/index.tsx b/src/views/task-manager/database/index.tsx new file mode 100644 index 000000000..6fbde1c32 --- /dev/null +++ b/src/views/task-manager/database/index.tsx @@ -0,0 +1,5 @@ +import DatabaseView from "../../relays/cache/database"; + +export default function TaskManagerDatabase() { + return ; +} diff --git a/src/views/task-manager/layout.tsx b/src/views/task-manager/layout.tsx new file mode 100644 index 000000000..230a126c9 --- /dev/null +++ b/src/views/task-manager/layout.tsx @@ -0,0 +1,44 @@ +import { Tab, TabIndicator, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react"; +import { Outlet, useLocation, useNavigate } from "react-router-dom"; + +const tabs = ["network", "publish-log", "database"]; + +export default function TaskManagerLayout() { + const location = useLocation(); + const navigate = useNavigate(); + + const index = tabs.indexOf(location.pathname.split("/")[1] || "network"); + + return ( + navigate("/" + tabs[i], { replace: true })} + > + + Network + Publish Log + Database + + + + + + + + + + + + + + + + ); +} diff --git a/src/views/task-manager/modal.tsx b/src/views/task-manager/modal.tsx new file mode 100644 index 000000000..a73ab3075 --- /dev/null +++ b/src/views/task-manager/modal.tsx @@ -0,0 +1,75 @@ +import { + Heading, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalOverlay, + ModalProps, + Spinner, + Tab, + TabIndicator, + TabList, + TabPanel, + TabPanels, + Tabs, +} from "@chakra-ui/react"; +import { RouterProvider, createMemoryRouter } from "react-router-dom"; + +import { PersistentSubject } from "../../classes/subject"; +import useSubject from "../../hooks/use-subject"; +import DatabaseView from "../relays/cache/database"; +import TaskManagerNetwork from "./network"; +import { Suspense } from "react"; + +type Router = ReturnType; + +export default function TaskManagerModal({ + router, + isOpen, + onClose, +}: { router: Router } & Omit) { + return ( + + + + + + Loading page + + } + > + + + {/* + + Network + Database + + + + + + + + + + + + */} + + + + + ); +} diff --git a/src/views/task-manager/network/index.tsx b/src/views/task-manager/network/index.tsx new file mode 100644 index 000000000..97d54220b --- /dev/null +++ b/src/views/task-manager/network/index.tsx @@ -0,0 +1,38 @@ +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Box, + Spacer, + Text, +} from "@chakra-ui/react"; +import relayPoolService from "../../../services/relay-pool"; +import { RelayFavicon } from "../../../components/relay-favicon"; +import { RelayStatus } from "../../../components/relay-status"; + +export default function TaskManagerNetwork() { + return ( + + {Array.from(relayPoolService.relays.values()).map((relay) => ( + +

+ + + {relay.url} + + + + +

+ + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex + ea commodo consequat. + +
+ ))} +
+ ); +} diff --git a/src/views/task-manager/network/inspect-relay.tsx b/src/views/task-manager/network/inspect-relay.tsx new file mode 100644 index 000000000..2b52d65d9 --- /dev/null +++ b/src/views/task-manager/network/inspect-relay.tsx @@ -0,0 +1,5 @@ +import VerticalPageLayout from "../../../components/vertical-page-layout"; + +export default function InspectRelayView() { + return ; +} diff --git a/src/views/task-manager/provider.tsx b/src/views/task-manager/provider.tsx new file mode 100644 index 000000000..bc1360244 --- /dev/null +++ b/src/views/task-manager/provider.tsx @@ -0,0 +1,152 @@ +import { PropsWithChildren, createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { Router, Location, To, createMemoryRouter, RouteObject } from "react-router-dom"; +import { useRouterMarker } from "../../providers/drawer-sub-view-provider"; +import { logger } from "../../helpers/debug"; +import { RouteProviders } from "../../providers/route"; +import InspectRelayView from "./network/inspect-relay"; + +import TaskManagerModal from "./modal"; +import TaskManagerLayout from "./layout"; +import TaskManagerNetwork from "./network"; +import TaskManagerDatabase from "./database"; +import PublishLogView from "./publish-log"; + +type Router = ReturnType; + +const log = logger.extend("TaskManagerProvider"); + +const TaskManagerContext = createContext<{ openTaskManager: (route: To) => void; closeTaskManager: () => void }>({ + openTaskManager() {}, + closeTaskManager() {}, +}); + +export function useTaskManagerContext() { + return useContext(TaskManagerContext); +} + +const routes: RouteObject[] = [ + { + path: "", + element: , + children: [ + { + path: "network", + element: , + children: [ + { + path: ":url", + element: ( + + + + ), + }, + ], + }, + { path: "publish-log", element: }, + { + path: "database", + element: , + }, + ], + }, +]; + +export default function TaskManagerProvider({ children, parentRouter }: PropsWithChildren & { parentRouter: Router }) { + const [router, setRouter] = useState(null); + + const openInParent = useCallback((to: To) => parentRouter.navigate(to), [parentRouter]); + + const direction = useRef<"up" | "down">(); + const marker = useRouterMarker(parentRouter); + + useEffect(() => { + return parentRouter.subscribe((event) => { + const location = event.location as Location<{ taskManagerRoute?: To | null } | null>; + const subRoute = location.state?.taskManagerRoute; + + if (subRoute) { + if (router) { + if (router.state.location.pathname !== subRoute && direction.current !== "up") { + log("Updating router from parent state"); + direction.current = "down"; + router.navigate(subRoute); + direction.current = undefined; + } + } else { + log("Create Router"); + + const newRouter = createMemoryRouter(routes, { initialEntries: [subRoute] }); + newRouter.subscribe((e) => { + if ( + e.errors && + e.errors["__shim-error-route__"].status === 404 && + e.errors["__shim-error-route__"].internal + ) { + openInParent(e.location); + } else if (direction.current !== "down") { + log("Updating parent state from Router"); + direction.current = "up"; + parentRouter.navigate(parentRouter.state.location, { + preventScrollReset: true, + state: { ...parentRouter.state.location.state, taskManagerRoute: e.location.pathname }, + }); + } + direction.current = undefined; + }); + + // use the parent routers createHref method so that users can open links in new tabs + newRouter.createHref = parentRouter.createHref; + + setRouter(newRouter); + } + } else if (router) { + log("Destroy Router"); + setRouter(null); + } + }); + }, [parentRouter, router, setRouter]); + + const openTaskManager = useCallback( + (to: To) => { + marker.set(); + parentRouter.navigate(parentRouter.state.location, { + preventScrollReset: true, + state: { ...parentRouter.state.location.state, taskManagerRoute: to }, + }); + }, + [parentRouter], + ); + + const closeTaskManager = useCallback(() => { + const i = marker.index.current; + if (i !== null && i > 0) { + log(`Navigating back ${i} entries to the point the task manager was opened`); + parentRouter.navigate(-i); + } else { + log(`Failed to navigate back, clearing state`); + parentRouter.navigate(parentRouter.state.location, { + preventScrollReset: true, + state: { ...parentRouter.state.location.state, taskManagerRoute: undefined }, + }); + } + + // reset marker + marker.reset(); + }, [parentRouter]); + + const context = useMemo( + () => ({ + openTaskManager, + closeTaskManager, + }), + [openTaskManager, closeTaskManager], + ); + + return ( + + {children} + {router && } + + ); +} diff --git a/src/views/task-manager/publish-log/action-status-tag.tsx b/src/views/task-manager/publish-log/action-status-tag.tsx new file mode 100644 index 000000000..5b13900b6 --- /dev/null +++ b/src/views/task-manager/publish-log/action-status-tag.tsx @@ -0,0 +1,40 @@ +import { Spinner, Tag, TagLabel, TagProps } from "@chakra-ui/react"; + +import PublishAction from "../../../classes/nostr-publish-action"; +import useSubject from "../../../hooks/use-subject"; +import { CheckIcon, ErrorIcon } from "../../../components/icons"; + +export default function PublishActionStatusTag({ + action, + ...props +}: { action: PublishAction } & Omit) { + const results = useSubject(action.results); + + const successful = results.filter(({ success }) => success); + const failedWithMessage = results.filter(({ success, message }) => !success && !!message); + + let statusIcon = ; + let statusColor: TagProps["colorScheme"] = "blue"; + if (results.length !== action.relays.length) { + statusColor = "blue"; + statusIcon = ; + } else if (successful.length === 0) { + statusColor = "red"; + statusIcon = ; + } else if (failedWithMessage.length > 0) { + statusColor = "orange"; + statusIcon = ; + } else { + statusColor = "green"; + statusIcon = ; + } + + return ( + + + {successful.length}/{action.relays.length} + + {statusIcon} + + ); +} diff --git a/src/views/task-manager/publish-log/index.tsx b/src/views/task-manager/publish-log/index.tsx new file mode 100644 index 000000000..27cf6479a --- /dev/null +++ b/src/views/task-manager/publish-log/index.tsx @@ -0,0 +1,52 @@ +import { useContext } from "react"; +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Heading, + Spacer, + Text, +} from "@chakra-ui/react"; + +import PublishAction from "../../../classes/nostr-publish-action"; +import PublishActionStatusTag from "./action-status-tag"; +import { PublishContext } from "../../../providers/global/publish-provider"; +import { PublishDetails } from "./publish-details"; + +function PublishLogAction({ action }: { action: PublishAction }) { + return ( + +

+ + {action.label} + + + + +

+ + + +
+ ); +} + +export default function PublishLogView() { + const { log } = useContext(PublishContext); + const reverseLog = Array.from(log).reverse(); + + return ( + + {reverseLog.length === 0 && ( + + No events published yet + + )} + {reverseLog.map((action) => ( + + ))} + + ); +} diff --git a/src/components/publish-details.tsx b/src/views/task-manager/publish-log/publish-details.tsx similarity index 86% rename from src/components/publish-details.tsx rename to src/views/task-manager/publish-log/publish-details.tsx index 2623a420d..826faded4 100644 --- a/src/components/publish-details.tsx +++ b/src/views/task-manager/publish-log/publish-details.tsx @@ -12,13 +12,13 @@ import { } from "@chakra-ui/react"; import { Link as RouterLink } from "react-router-dom"; -import NostrPublishAction, { PublishResult } from "../classes/nostr-publish-action"; -import useSubject from "../hooks/use-subject"; -import { RelayPaidTag } from "../views/relays/components/relay-card"; -import { EmbedEvent } from "./embed-event"; +import PublishAction, { PublishResult } from "../../../classes/nostr-publish-action"; +import useSubject from "../../../hooks/use-subject"; +import { RelayPaidTag } from "../../relays/components/relay-card"; +import { EmbedEvent } from "../../../components/embed-event"; export type PostResultsProps = { - pub: NostrPublishAction; + pub: PublishAction; }; function PublishResultRow({ result }: { result: PublishResult }) {