mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 13:21:44 +01:00
Add option to favorite apps
This commit is contained in:
parent
0b6e8e9947
commit
1045c26472
5
.changeset/giant-llamas-reply.md
Normal file
5
.changeset/giant-llamas-reply.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add option to favorite apps
|
22
package.json
22
package.json
@ -100,8 +100,8 @@
|
||||
"react-diff-viewer-continued": "^3.4.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-force-graph-2d": "^1.26.2",
|
||||
"react-force-graph-3d": "^1.25.2",
|
||||
"react-force-graph-2d": "^1.27.0",
|
||||
"react-force-graph-3d": "^1.26.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-mosaic-component": "^6.1.1",
|
||||
@ -133,14 +133,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor-community/http": "^1.4.1",
|
||||
"@capacitor-mlkit/barcode-scanning": "^6.1.0",
|
||||
"@capacitor/android": "^6.1.1",
|
||||
"@capacitor/app": "^6.0.0",
|
||||
"@capacitor-mlkit/barcode-scanning": "^6.2.0",
|
||||
"@capacitor/android": "^6.2.0",
|
||||
"@capacitor/app": "^6.0.2",
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
"@capacitor/cli": "^6.1.1",
|
||||
"@capacitor/core": "^6.1.1",
|
||||
"@capacitor/ios": "^6.1.1",
|
||||
"@capacitor/preferences": "^6.0.2",
|
||||
"@capacitor/cli": "^6.2.0",
|
||||
"@capacitor/core": "^6.2.0",
|
||||
"@capacitor/ios": "^6.2.0",
|
||||
"@capacitor/preferences": "^6.0.3",
|
||||
"@changesets/cli": "^2.27.11",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/chroma-js": "^2.4.5",
|
||||
@ -153,8 +153,8 @@
|
||||
"@types/leaflet.locatecontrol": "^0.74.6",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/ngeohash": "^0.6.8",
|
||||
"@types/react": "^19.0.7",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@types/react": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/three": "^0.160.0",
|
||||
"@types/webscopeio__react-textarea-autocomplete": "^4.7.5",
|
||||
|
493
pnpm-lock.yaml
generated
493
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,74 +1,52 @@
|
||||
import { useMemo } from "react";
|
||||
import { Spacer } from "@chakra-ui/react";
|
||||
import { useLocation } from "react-router";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { Divider, Spacer } from "@chakra-ui/react";
|
||||
|
||||
import {
|
||||
DirectMessagesIcon,
|
||||
NotificationsIcon,
|
||||
ProfileIcon,
|
||||
RelayIcon,
|
||||
SearchIcon,
|
||||
NotesIcon,
|
||||
LightningIcon,
|
||||
SettingsIcon,
|
||||
} from "../../icons";
|
||||
import { LightningIcon, SettingsIcon } from "../../icons";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import PuzzlePiece01 from "../../icons/puzzle-piece-01";
|
||||
import Package from "../../icons/package";
|
||||
import Rocket02 from "../../icons/rocket-02";
|
||||
import useRecentIds from "../../../hooks/use-recent-ids";
|
||||
import { internalApps, internalTools } from "../../../views/other-stuff/apps";
|
||||
import { App } from "../../../views/other-stuff/component/app-card";
|
||||
import { defaultFavoriteApps, internalApps, internalTools } from "../../navigation/apps";
|
||||
import NavItem from "./nav-item";
|
||||
import { QuestionIcon } from "@chakra-ui/icons";
|
||||
import Plus from "../../icons/plus";
|
||||
import useFavoriteInternalIds from "../../../hooks/use-favorite-internal-ids";
|
||||
|
||||
export default function NavItems() {
|
||||
const location = useLocation();
|
||||
const account = useCurrentAccount();
|
||||
|
||||
const { recent: recentApps } = useRecentIds("apps");
|
||||
const otherStuff = useMemo(() => {
|
||||
const { ids: favorites = defaultFavoriteApps } = useFavoriteInternalIds("apps", "app");
|
||||
const { recent } = useRecentIds("apps", 3);
|
||||
|
||||
const favoriteApps = useMemo(() => {
|
||||
const internal = [...internalApps, ...internalTools];
|
||||
const apps = recentApps.map((id) => internal.find((app) => app.id === id)).filter(Boolean) as App[];
|
||||
if (apps.length > 3) {
|
||||
apps.length = 3;
|
||||
} else {
|
||||
if (apps.length < 3 && !apps.some((a) => a.id === "streams")) {
|
||||
apps.push(internal.find((app) => app.id === "streams")!);
|
||||
}
|
||||
if (apps.length < 3 && !apps.some((a) => a.id === "articles")) {
|
||||
apps.push(internal.find((app) => app.id === "articles")!);
|
||||
}
|
||||
if (apps.length < 3 && !apps.some((a) => a.id === "channels")) {
|
||||
apps.push(internal.find((app) => app.id === "channels")!);
|
||||
}
|
||||
}
|
||||
return apps;
|
||||
}, [recentApps]);
|
||||
return favorites.map((id) => internal.find((app) => app.id === id)).filter((a) => !!a);
|
||||
}, [favorites]);
|
||||
|
||||
const recentApps = useMemo(() => {
|
||||
const internal = [...internalApps, ...internalTools];
|
||||
return recent
|
||||
.filter((id) => !favorites.includes(id))
|
||||
.map((id) => internal.find((app) => app.id === id))
|
||||
.filter((a) => !!a);
|
||||
}, [recent, favorites]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{account && !account.readonly && (
|
||||
<NavItem icon={Plus} label="Create new" colorScheme="primary" to="/new" variant="solid" />
|
||||
)}
|
||||
<NavItem to="/launchpad" icon={Rocket02} label="Launchpad" />
|
||||
<NavItem to="/" icon={NotesIcon} colorScheme={location.pathname === "/" ? "primary" : "gray"} label="Notes" />
|
||||
<NavItem label="Discover" to="/discovery" icon={PuzzlePiece01} />
|
||||
{account && (
|
||||
<>
|
||||
<NavItem to="/notifications" icon={NotificationsIcon} label="Notifications" />
|
||||
<NavItem to="/messages" icon={DirectMessagesIcon} label="Messages" />
|
||||
</>
|
||||
)}
|
||||
<NavItem to="/search" icon={SearchIcon} label="Search" />
|
||||
{account?.pubkey && <NavItem to={"/u/" + nip19.npubEncode(account.pubkey)} icon={ProfileIcon} label="Profile" />}
|
||||
<NavItem to="/relays" icon={RelayIcon} label="Relays" />
|
||||
{otherStuff.map((app) => (
|
||||
{favoriteApps.map((app) => (
|
||||
<NavItem key={app.id} to={app.to} icon={app.icon || QuestionIcon} label={app.title} />
|
||||
))}
|
||||
<NavItem to="/other-stuff" icon={Package} label="More" />
|
||||
<NavItem to="/other-stuff" icon={Package} label="All Apps" />
|
||||
{recentApps.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
{recentApps.map((app) => (
|
||||
<NavItem key={app.id} to={app.to} icon={app.icon || QuestionIcon} label={app.title} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
<Spacer />
|
||||
<NavItem to="/support" icon={LightningIcon} label="Support" />
|
||||
<NavItem label="Settings" icon={SettingsIcon} to="/settings" />
|
||||
|
45
src/components/layout/presets/contained-parent-view.tsx
Normal file
45
src/components/layout/presets/contained-parent-view.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { PropsWithChildren, Suspense } from "react";
|
||||
import { Outlet, useMatch } from "react-router";
|
||||
import { Box, Flex, FlexProps, Spinner } from "@chakra-ui/react";
|
||||
|
||||
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
|
||||
import SimpleHeader from "./simple-header";
|
||||
import { ErrorBoundary } from "../../error-boundary";
|
||||
|
||||
export default function ContainedParentView({
|
||||
children,
|
||||
path,
|
||||
title,
|
||||
width = "xs",
|
||||
}: PropsWithChildren<{ path: string; title?: string; width?: FlexProps["w"]; contain?: boolean }>) {
|
||||
const match = useMatch(path);
|
||||
const isMobile = useBreakpointValue({ base: true, lg: false });
|
||||
const showMenu = !isMobile || !!match;
|
||||
|
||||
if (showMenu)
|
||||
return (
|
||||
<Flex flex={1} overflow="hidden" h="full">
|
||||
<Flex width={width} direction="column">
|
||||
{title && <SimpleHeader title={title} position="initial" />}
|
||||
<Flex direction="column" p="2" gap="2" overflowY="auto" overflowX="hidden">
|
||||
{children}
|
||||
</Flex>
|
||||
</Flex>
|
||||
{!isMobile && (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<ErrorBoundary>
|
||||
<Outlet />
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<ErrorBoundary>
|
||||
<Outlet />
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
@ -15,7 +15,7 @@ export default function SimpleHeader({ children, title, ...props }: Omit<FlexPro
|
||||
top="var(--safe-top)"
|
||||
mt="var(--safe-top)"
|
||||
backgroundColor="var(--chakra-colors-chakra-body-bg)"
|
||||
zIndex="popover"
|
||||
zIndex="modal"
|
||||
{...props}
|
||||
>
|
||||
<BackIconButton hideFrom="lg" />
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PropsWithChildren, Suspense } from "react";
|
||||
import { Outlet, useMatch } from "react-router";
|
||||
import { Box, Flex, Spinner } from "@chakra-ui/react";
|
||||
import { Box, Flex, FlexProps, Spinner } from "@chakra-ui/react";
|
||||
|
||||
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
|
||||
import SimpleHeader from "./simple-header";
|
||||
@ -10,7 +10,9 @@ export default function SimpleParentView({
|
||||
children,
|
||||
path,
|
||||
title,
|
||||
}: PropsWithChildren<{ path: string; title?: string }>) {
|
||||
width = "xs",
|
||||
contain,
|
||||
}: PropsWithChildren<{ path: string; title?: string; width?: FlexProps["w"]; contain?: boolean }>) {
|
||||
const match = useMatch(path);
|
||||
const isMobile = useBreakpointValue({ base: true, lg: false });
|
||||
const showMenu = !isMobile || !!match;
|
||||
@ -20,9 +22,9 @@ export default function SimpleParentView({
|
||||
if (showMenu)
|
||||
return (
|
||||
<Flex flex={1} direction={floating ? "row" : "column"}>
|
||||
<Box w={floating ? "xs" : 0} flexGrow={0} flexShrink={0} />
|
||||
<Box w={floating ? width : 0} flexGrow={0} flexShrink={0} />
|
||||
<Flex
|
||||
minW="xs"
|
||||
width={width}
|
||||
direction="column"
|
||||
position={floating ? "fixed" : "initial"}
|
||||
top="var(--safe-top)"
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Flex, FlexProps } from "@chakra-ui/react";
|
||||
|
||||
import SimpleHeader from "./simple-header";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export default function SimpleView({
|
||||
children,
|
||||
@ -20,8 +20,6 @@ export default function SimpleView({
|
||||
|
||||
<Flex
|
||||
direction="column"
|
||||
overflowY="auto"
|
||||
overflowX="hidden"
|
||||
px={flush ? 0 : "4"}
|
||||
pt={flush ? 0 : "4"}
|
||||
pb={flush ? 0 : "max(1rem, var(--safe-bottom))"}
|
||||
|
53
src/components/navigation/app-favorite-button.tsx
Normal file
53
src/components/navigation/app-favorite-button.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { useState } from "react";
|
||||
import { IconButton, IconButtonProps } from "@chakra-ui/react";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { modifyEventTags, unixNow } from "applesauce-core/helpers";
|
||||
import { Operations } from "applesauce-lists/helpers";
|
||||
|
||||
import { App, defaultFavoriteApps } from "./apps";
|
||||
import useFavoriteInternalIds from "../../hooks/use-favorite-internal-ids";
|
||||
import { useSigningContext } from "../../providers/global/signing-provider";
|
||||
import { usePublishEvent } from "../../providers/global/publish-provider";
|
||||
import { StarEmptyIcon, StarFullIcon } from "../icons";
|
||||
|
||||
export default function AppFavoriteButton({
|
||||
app,
|
||||
...props
|
||||
}: { app: App } & Omit<IconButtonProps, "children" | "aria-label" | "isLoading" | "onClick">) {
|
||||
const publish = usePublishEvent();
|
||||
const { finalizeDraft } = useSigningContext();
|
||||
const { favorites } = useFavoriteInternalIds("apps", "app");
|
||||
const isFavorite = favorites?.tags.some((t) => t[0] === "app" && t[1] === app.id);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleClick = async () => {
|
||||
const prev =
|
||||
favorites ||
|
||||
(await finalizeDraft({
|
||||
kind: kinds.Application,
|
||||
created_at: unixNow(),
|
||||
content: "",
|
||||
tags: [["d", "nostrudel-favorite-apps"], ...defaultFavoriteApps.map((id) => ["app", id])],
|
||||
}));
|
||||
|
||||
setLoading(true);
|
||||
const tag = ["app", app.id];
|
||||
const draft = await modifyEventTags(prev, {
|
||||
public: isFavorite ? Operations.removeNameValueTag(tag) : Operations.addNameValueTag(tag),
|
||||
});
|
||||
await publish(isFavorite ? "Unfavorite app" : "Favorite app", draft);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
icon={isFavorite ? <StarFullIcon boxSize="1.1em" /> : <StarEmptyIcon boxSize="1.1em" />}
|
||||
aria-label={isFavorite ? "Unfavorite app" : "Favorite app"}
|
||||
title={isFavorite ? "Unfavorite app" : "Favorite app"}
|
||||
onClick={handleClick}
|
||||
isLoading={loading}
|
||||
color={isFavorite ? "yellow.400" : undefined}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { ComponentWithAs, IconProps } from "@chakra-ui/react";
|
||||
|
||||
import {
|
||||
ArticleIcon,
|
||||
BadgeIcon,
|
||||
@ -11,21 +13,52 @@ import {
|
||||
MapIcon,
|
||||
MediaIcon,
|
||||
MuteIcon,
|
||||
NotesIcon,
|
||||
NotificationsIcon,
|
||||
SearchIcon,
|
||||
TorrentIcon,
|
||||
TrackIcon,
|
||||
VideoIcon,
|
||||
WikiIcon,
|
||||
} from "../../components/icons";
|
||||
import { App } from "./component/app-card";
|
||||
import ShieldOff from "../../components/icons/shield-off";
|
||||
import MessageQuestionSquare from "../../components/icons/message-question-square";
|
||||
import UploadCloud01 from "../../components/icons/upload-cloud-01";
|
||||
import Edit04 from "../../components/icons/edit-04";
|
||||
import Users03 from "../../components/icons/users-03";
|
||||
import FileAttachment01 from "../../components/icons/file-attachment-01";
|
||||
} from "../icons";
|
||||
import ShieldOff from "../icons/shield-off";
|
||||
import MessageQuestionSquare from "../icons/message-question-square";
|
||||
import UploadCloud01 from "../icons/upload-cloud-01";
|
||||
import Edit04 from "../icons/edit-04";
|
||||
import Users03 from "../icons/users-03";
|
||||
import FileAttachment01 from "../icons/file-attachment-01";
|
||||
import Rocket02 from "../icons/rocket-02";
|
||||
import PuzzlePiece01 from "../icons/puzzle-piece-01";
|
||||
|
||||
export type App = {
|
||||
icon?: ComponentWithAs<"svg", IconProps>;
|
||||
image?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
id: string;
|
||||
isExternal?: boolean;
|
||||
to: string;
|
||||
};
|
||||
|
||||
export const internalApps: App[] = [
|
||||
{ title: "Notes", description: "Short text posts from your friends", icon: NotesIcon, id: "notes", to: "/notes" },
|
||||
{ title: "Launchpad", description: "Quick account overview", icon: Rocket02, id: "launchpad", to: "/launchpad" },
|
||||
{ title: "Discover", description: "Discover new feeds", icon: PuzzlePiece01, id: "discover", to: "/discovery" },
|
||||
{
|
||||
title: "Notifications",
|
||||
description: "Notifications feed",
|
||||
icon: NotificationsIcon,
|
||||
id: "notifications",
|
||||
to: "/notifications",
|
||||
},
|
||||
{
|
||||
title: "Messages",
|
||||
description: "Direct Messages",
|
||||
icon: DirectMessagesIcon,
|
||||
id: "messages",
|
||||
to: "/messages",
|
||||
},
|
||||
{ title: "Search", description: "Search for users and notes", icon: SearchIcon, id: "search", to: "/search" },
|
||||
{
|
||||
title: "Streams",
|
||||
description: "Watch live streams",
|
||||
@ -219,4 +252,6 @@ export const externalTools: App[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultFavoriteApps = ["launchpad", "notes", "discover", "notifications", "messages", "search"];
|
||||
|
||||
export const allApps = [...internalApps, ...internalTools, ...externalTools];
|
@ -2,7 +2,7 @@ import { ComponentWithAs, Flex, FlexProps } from "@chakra-ui/react";
|
||||
|
||||
const VerticalPageLayout: ComponentWithAs<"div", FlexProps> = ({ children, ...props }: FlexProps) => {
|
||||
return (
|
||||
<Flex direction="column" pt="2" pb="12" gap="2" px="2" overflowX="hidden" w="full" {...props}>
|
||||
<Flex direction="column" pt="2" pb="12" gap="2" px="2" w="full" {...props}>
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
|
16
src/hooks/use-favorite-internal-ids.ts
Normal file
16
src/hooks/use-favorite-internal-ids.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { kinds } from "nostr-tools";
|
||||
|
||||
import useReplaceableEvent from "./use-replaceable-event";
|
||||
import useCurrentAccount from "./use-current-account";
|
||||
|
||||
export default function useFavoriteInternalIds(identifier: string, tagName = "id", pubkey?: string) {
|
||||
const account = useCurrentAccount();
|
||||
pubkey = pubkey || account?.pubkey;
|
||||
|
||||
const favorites = useReplaceableEvent(
|
||||
pubkey ? { kind: kinds.Application, pubkey, identifier: `nostrudel-favorite-${identifier}` } : undefined,
|
||||
);
|
||||
const ids = favorites?.tags.filter((t) => t[0] === tagName && t[1]).map((t) => t[1]);
|
||||
|
||||
return { ids, favorites };
|
||||
}
|
@ -32,4 +32,5 @@ body,
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { Button, Card, CardBody, CardHeader, CardProps, Heading, Input, Link, Si
|
||||
import { Link as RouterLink } from "react-router";
|
||||
|
||||
import useRecentIds from "../../../hooks/use-recent-ids";
|
||||
import { allApps } from "../../other-stuff/apps";
|
||||
import { allApps } from "../../../components/navigation/apps";
|
||||
import AppCard from "../../other-stuff/component/app-card";
|
||||
|
||||
export default function ToolsCard({ ...props }: Omit<CardProps, "children">) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { memo, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { Button, ButtonGroup, Card, Flex, IconButton } from "@chakra-ui/react";
|
||||
import { Button, ButtonGroup, Flex, IconButton } from "@chakra-ui/react";
|
||||
import { UNSAFE_DataRouterContext, useLocation, useNavigate } from "react-router";
|
||||
import { NostrEvent, kinds } from "nostr-tools";
|
||||
|
||||
@ -12,7 +12,6 @@ import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
|
||||
import UserDnsIdentity from "../../components/user/user-dns-identity";
|
||||
import SendMessageForm from "./components/send-message-form";
|
||||
import { groupMessages } from "../../helpers/nostr/dms";
|
||||
import ThreadDrawer from "./components/thread-drawer";
|
||||
@ -24,8 +23,10 @@ import RelaySet from "../../classes/relay-set";
|
||||
import useAppSettings from "../../hooks/use-user-app-settings";
|
||||
import { truncateId } from "../../helpers/string";
|
||||
import useRouterMarker from "../../hooks/use-router-marker";
|
||||
import { BackIconButton } from "../../components/router/back-button";
|
||||
import decryptionCacheService from "../../services/decryption-cache";
|
||||
import SimpleView from "../../components/layout/presets/simple-view";
|
||||
import UserDnsIdentityIcon from "../../components/user/user-dns-identity-icon";
|
||||
import SimpleHeader from "../../components/layout/presets/simple-header";
|
||||
|
||||
/** This is broken out from DirectMessageChatPage for performance reasons. Don't use outside of file */
|
||||
const ChatLog = memo(({ messages }: { messages: NostrEvent[] }) => {
|
||||
@ -113,14 +114,17 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
||||
|
||||
return (
|
||||
<ThreadsProvider timeline={loader}>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<Card size="sm" flexShrink={0} p="2" flexDirection="row">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<BackIconButton />
|
||||
<UserAvatar pubkey={pubkey} size="sm" />
|
||||
<UserLink pubkey={pubkey} fontWeight="bold" />
|
||||
<UserDnsIdentity pubkey={pubkey} onlyIcon />
|
||||
</Flex>
|
||||
<Flex flex={1} direction="column" pr="var(--safe-right)" pl="var(--safe-left)" h="full" overflow="hidden">
|
||||
<SimpleHeader
|
||||
position="initial"
|
||||
title={
|
||||
<Flex gap="2" alignItems="center">
|
||||
<UserAvatar pubkey={pubkey} size="sm" />
|
||||
<UserLink pubkey={pubkey} fontWeight="bold" />
|
||||
<UserDnsIdentityIcon pubkey={pubkey} />
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<ButtonGroup ml="auto">
|
||||
{!autoDecryptDMs && (
|
||||
<Button onClick={decryptAll} isLoading={loading}>
|
||||
@ -134,16 +138,19 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
||||
onClick={openDrawerList}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</Card>
|
||||
<Flex h="0" flex={1} overflowX="hidden" overflowY="scroll" direction="column-reverse" gap="2" py="4" px="2">
|
||||
<ChatLog messages={messages} />
|
||||
<TimelineActionAndStatus timeline={loader} />
|
||||
</Flex>
|
||||
<SendMessageForm flexShrink={0} pubkey={pubkey} />
|
||||
{location.state?.thread && (
|
||||
<ThreadDrawer isOpen onClose={closeDrawer} threadId={location.state.thread} pubkey={pubkey} />
|
||||
)}
|
||||
</IntersectionObserverProvider>
|
||||
</SimpleHeader>
|
||||
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<Flex h="0" flex={1} overflowX="hidden" overflowY="auto" direction="column-reverse" gap="2" py="4" px="2">
|
||||
<ChatLog messages={messages} />
|
||||
<TimelineActionAndStatus timeline={loader} />
|
||||
</Flex>
|
||||
<SendMessageForm flexShrink={0} pubkey={pubkey} px="2" pb="2" />
|
||||
{location.state?.thread && (
|
||||
<ThreadDrawer isOpen onClose={closeDrawer} threadId={location.state.thread} pubkey={pubkey} />
|
||||
)}
|
||||
</IntersectionObserverProvider>
|
||||
</Flex>
|
||||
</ThreadsProvider>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { Card, CardBody, Flex, LinkBox, LinkOverlay, Text } from "@chakra-ui/react";
|
||||
import { Outlet, Link as RouterLink, useLocation, useParams } from "react-router";
|
||||
import { Link as RouterLink, useLocation } from "react-router";
|
||||
import { useObservable } from "applesauce-react/hooks";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
@ -21,6 +21,8 @@ import { CheckIcon } from "../../components/icons";
|
||||
import UserDnsIdentity from "../../components/user/user-dns-identity";
|
||||
import useEventIntersectionRef from "../../hooks/use-event-intersection-ref";
|
||||
import { useKind4Decrypt } from "../../hooks/use-kind4-decryption";
|
||||
import SimpleParentView from "../../components/layout/presets/simple-parent-view";
|
||||
import ContainedParentView from "../../components/layout/presets/contained-parent-view";
|
||||
|
||||
function MessagePreview({ message, pubkey }: { message: NostrEvent; pubkey: string }) {
|
||||
const ref = useEventIntersectionRef(message);
|
||||
@ -59,8 +61,7 @@ function ConversationCard({ conversation }: { conversation: KnownConversation })
|
||||
);
|
||||
}
|
||||
|
||||
function DirectMessagesPage() {
|
||||
const params = useParams();
|
||||
function MessagesHomePage() {
|
||||
const { people } = usePeopleListContext();
|
||||
|
||||
const account = useCurrentAccount()!;
|
||||
@ -76,44 +77,28 @@ function DirectMessagesPage() {
|
||||
return filtered.sort((a, b) => b.messages[0].created_at - a.messages[0].created_at);
|
||||
}, [messages, people, account.pubkey]);
|
||||
|
||||
const isChatOpen = !!params.pubkey;
|
||||
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<Flex gap="4" h={{ base: "calc(100vh - 3.5rem)", md: "100vh" }} overflow="hidden">
|
||||
<Flex
|
||||
gap="2"
|
||||
direction="column"
|
||||
w={!isChatOpen ? { base: "full", lg: "sm" } : "sm"}
|
||||
overflowX="hidden"
|
||||
overflowY="auto"
|
||||
py="2"
|
||||
px={{ base: "2", lg: 0 }}
|
||||
hideBelow={!isChatOpen ? undefined : "xl"}
|
||||
>
|
||||
<Flex gap="2">
|
||||
<PeopleListSelection flexShrink={0} />
|
||||
</Flex>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
{conversations.map((conversation) => (
|
||||
<ConversationCard key={conversation.pubkeys.join("-")} conversation={conversation} />
|
||||
))}
|
||||
</IntersectionObserverProvider>
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
<ContainedParentView path="/messages" width="md">
|
||||
<Flex gap="2">
|
||||
<PeopleListSelection flexShrink={0} size="sm" />
|
||||
</Flex>
|
||||
<Flex gap="2" direction="column" flex={1} hideBelow={!isChatOpen ? "xl" : undefined} overflow="hidden">
|
||||
<Outlet />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
{conversations.map((conversation) => (
|
||||
<ConversationCard key={conversation.pubkeys.join("-")} conversation={conversation} />
|
||||
))}
|
||||
</IntersectionObserverProvider>
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</ContainedParentView>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DirectMessagesView() {
|
||||
export default function MessagesHomeView() {
|
||||
return (
|
||||
<RequireCurrentAccount>
|
||||
<PeopleListProvider initList="global">
|
||||
<DirectMessagesPage />
|
||||
<MessagesHomePage />
|
||||
</PeopleListProvider>
|
||||
</RequireCurrentAccount>
|
||||
);
|
||||
|
@ -1,17 +1,9 @@
|
||||
import { Link as RouterLink, To } from "react-router";
|
||||
import { Card, ComponentWithAs, Flex, Heading, IconProps, Image, LinkBox, Text } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router";
|
||||
import { Card, Flex, Heading, Image, LinkBox, Text } from "@chakra-ui/react";
|
||||
|
||||
import HoverLinkOverlay from "../../../components/hover-link-overlay";
|
||||
|
||||
export type App = {
|
||||
icon?: ComponentWithAs<"svg", IconProps>;
|
||||
image?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
id: string;
|
||||
isExternal?: boolean;
|
||||
to: string;
|
||||
};
|
||||
import { App } from "../../../components/navigation/apps";
|
||||
import AppFavoriteButton from "../../../components/navigation/app-favorite-button";
|
||||
|
||||
export function AppIcon({ app, size }: { app: App; size: string }) {
|
||||
if (app.icon) {
|
||||
@ -21,7 +13,15 @@ export function AppIcon({ app, size }: { app: App; size: string }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function AppCard({ app, onClick }: { app: App; onClick?: () => void }) {
|
||||
export default function AppCard({
|
||||
app,
|
||||
canFavorite = true,
|
||||
onClick,
|
||||
}: {
|
||||
app: App;
|
||||
onClick?: () => void;
|
||||
canFavorite?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Flex as={LinkBox} gap="4" alignItems="flex-start">
|
||||
<Card p="3" borderRadius="lg">
|
||||
@ -41,6 +41,8 @@ export default function AppCard({ app, onClick }: { app: App; onClick?: () => vo
|
||||
</Heading>
|
||||
<Text>{app.description}</Text>
|
||||
</Flex>
|
||||
|
||||
{canFavorite && <AppFavoriteButton app={app} variant="ghost" ms="auto" my="2" mr="2" />}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@ -2,10 +2,10 @@ import { useState } from "react";
|
||||
import { Heading, Input, SimpleGrid, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
|
||||
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import AppCard, { App } from "./component/app-card";
|
||||
import AppCard from "./component/app-card";
|
||||
import useRouteSearchValue from "../../hooks/use-route-search-value";
|
||||
import useRecentIds from "../../hooks/use-recent-ids";
|
||||
import { allApps, externalTools, internalTools } from "./apps";
|
||||
import { allApps, App, externalTools, internalTools } from "../../components/navigation/apps";
|
||||
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
|
||||
|
||||
const tabs = ["all", "tools", "3rd-party-tools"];
|
||||
@ -51,7 +51,14 @@ export default function OtherStuffView() {
|
||||
<SimpleGrid spacing="2" columns={columns}>
|
||||
{recentApps.slice(0, 6).map((id) => {
|
||||
const app = allApps.find((a) => a.id === id);
|
||||
return app ? <AppCard key={app.id} app={app} onClick={() => useApp(app.id)} /> : null;
|
||||
return app ? (
|
||||
<AppCard
|
||||
key={app.id}
|
||||
app={app}
|
||||
onClick={() => useApp(app.id)}
|
||||
canFavorite={!externalTools.includes(app)}
|
||||
/>
|
||||
) : null;
|
||||
})}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
@ -72,7 +79,12 @@ export default function OtherStuffView() {
|
||||
<TabPanels>
|
||||
<TabPanel as={SimpleGrid} spacing="2" columns={columns} px="0" py="4">
|
||||
{allApps.sort(sortByName).map((app) => (
|
||||
<AppCard key={app.id} app={app} onClick={() => useApp(app.id)} />
|
||||
<AppCard
|
||||
key={app.id}
|
||||
app={app}
|
||||
onClick={() => useApp(app.id)}
|
||||
canFavorite={!externalTools.includes(app)}
|
||||
/>
|
||||
))}
|
||||
</TabPanel>
|
||||
<TabPanel as={SimpleGrid} spacing="2" columns={columns} px="0" py="4">
|
||||
@ -82,7 +94,7 @@ export default function OtherStuffView() {
|
||||
</TabPanel>
|
||||
<TabPanel as={SimpleGrid} spacing="2" columns={columns} px="0" py="4">
|
||||
{externalTools.sort(sortByName).map((app) => (
|
||||
<AppCard key={app.id} app={app} onClick={() => useApp(app.id)} />
|
||||
<AppCard key={app.id} app={app} onClick={() => useApp(app.id)} canFavorite={false} />
|
||||
))}
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
|
Loading…
x
Reference in New Issue
Block a user