mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 21:31:43 +01:00
added WIP media post view
This commit is contained in:
parent
b3f91e8751
commit
e8b7ecafe3
@ -53,8 +53,7 @@
|
||||
"applesauce-react": "next",
|
||||
"applesauce-signer": "next",
|
||||
"bech32": "^2.0.0",
|
||||
"blossom-client-sdk": "^2.1.1",
|
||||
"blossom-drive-sdk": "^0.4.1",
|
||||
"blossom-client-sdk": "next",
|
||||
"blurhash": "^2.0.5",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"chart.js": "^4.4.7",
|
||||
|
86
pnpm-lock.yaml
generated
86
pnpm-lock.yaml
generated
@ -124,11 +124,8 @@ importers:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
blossom-client-sdk:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
blossom-drive-sdk:
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1(typescript@5.7.2)
|
||||
specifier: next
|
||||
version: 0.0.0-next-20250104165538
|
||||
blurhash:
|
||||
specifier: ^2.0.5
|
||||
version: 2.0.5
|
||||
@ -2021,16 +2018,10 @@ packages:
|
||||
bezier-js@6.1.4:
|
||||
resolution: {integrity: sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==}
|
||||
|
||||
blossom-client-sdk@0.7.0:
|
||||
resolution: {integrity: sha512-xG0HiuhFcK6UpmYjJ4vRPm3APMrRf+MQDfZWlNRTxs2gEETfqbhYm5pCl2hPfLjpEcFSDXgr3sLCh6C77ABKgg==}
|
||||
|
||||
blossom-client-sdk@2.1.1:
|
||||
resolution: {integrity: sha512-a95eZV7W5/QPN30p0s2K8ZxX0vwMXsnl2JIJDXaOu0nDVxikfKINa/7mhTRtA2i3dzjQ378FPET8vwn0GjtFzg==}
|
||||
blossom-client-sdk@0.0.0-next-20250104165538:
|
||||
resolution: {integrity: sha512-JOVhS5lIr/2+JB1XlTywrYmuhhdYvehn4brVmoraHdAq1SGx1z0TXW9QgybNhDU2suf56bTYS0pE3D/XvAmlVA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
blossom-drive-sdk@0.4.1:
|
||||
resolution: {integrity: sha512-X4OIA4+X4zpZy11zP6ekE61LHFIOCvS+H+McqsyDVtGrwFeFSuqfZVyKpg1QuQTlxSVIlJVX0jt9+gHEfdGqWA==}
|
||||
|
||||
blurhash@2.0.5:
|
||||
resolution: {integrity: sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==}
|
||||
|
||||
@ -2203,9 +2194,6 @@ packages:
|
||||
crelt@1.0.6:
|
||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||
|
||||
cross-fetch@4.1.0:
|
||||
resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
@ -3292,11 +3280,6 @@ packages:
|
||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
mime@4.0.6:
|
||||
resolution: {integrity: sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
@ -3359,15 +3342,6 @@ packages:
|
||||
ngraph.random@1.1.0:
|
||||
resolution: {integrity: sha512-h25UdUN/g8U7y29TzQtRm/GvGr70lK37yQPvPKXXuVfs7gCm82WipYFZcksQfeKumtOemAzBIcT7lzzyK/edLw==}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
|
||||
node-releases@2.0.19:
|
||||
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
|
||||
|
||||
@ -4206,9 +4180,6 @@ packages:
|
||||
toggle-selection@1.0.6:
|
||||
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
|
||||
|
||||
tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
tr46@1.0.1:
|
||||
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
|
||||
|
||||
@ -4418,9 +4389,6 @@ packages:
|
||||
w3c-keyname@2.2.8:
|
||||
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
webidl-conversions@4.0.2:
|
||||
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
||||
|
||||
@ -4435,9 +4403,6 @@ packages:
|
||||
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
whatwg-url@7.1.0:
|
||||
resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
|
||||
|
||||
@ -6616,31 +6581,11 @@ snapshots:
|
||||
|
||||
bezier-js@6.1.4: {}
|
||||
|
||||
blossom-client-sdk@0.7.0:
|
||||
dependencies:
|
||||
'@noble/hashes': 1.7.0
|
||||
cross-fetch: 4.1.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
blossom-client-sdk@2.1.1:
|
||||
blossom-client-sdk@0.0.0-next-20250104165538:
|
||||
dependencies:
|
||||
'@cashu/cashu-ts': 2.1.0
|
||||
'@noble/hashes': 1.7.0
|
||||
|
||||
blossom-drive-sdk@0.4.1(typescript@5.7.2):
|
||||
dependencies:
|
||||
'@noble/hashes': 1.7.0
|
||||
'@scure/base': 1.2.1
|
||||
blossom-client-sdk: 0.7.0
|
||||
eventemitter3: 5.0.1
|
||||
mime: 4.0.6
|
||||
nanoid: 5.0.9
|
||||
nostr-tools: 2.10.4(typescript@5.7.2)
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- typescript
|
||||
|
||||
blurhash@2.0.5: {}
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
@ -6845,12 +6790,6 @@ snapshots:
|
||||
|
||||
crelt@1.0.6: {}
|
||||
|
||||
cross-fetch@4.1.0:
|
||||
dependencies:
|
||||
node-fetch: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
@ -8237,8 +8176,6 @@ snapshots:
|
||||
braces: 3.0.3
|
||||
picomatch: 2.3.1
|
||||
|
||||
mime@4.0.6: {}
|
||||
|
||||
minimatch@3.1.2:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.11
|
||||
@ -8299,10 +8236,6 @@ snapshots:
|
||||
|
||||
ngraph.random@1.1.0: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
node-releases@2.0.19: {}
|
||||
|
||||
nostr-idb@2.2.0(typescript@5.7.2):
|
||||
@ -9278,8 +9211,6 @@ snapshots:
|
||||
|
||||
toggle-selection@1.0.6: {}
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
tr46@1.0.1:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
@ -9466,8 +9397,6 @@ snapshots:
|
||||
|
||||
w3c-keyname@2.2.8: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
webidl-conversions@4.0.2: {}
|
||||
|
||||
webln@0.3.2:
|
||||
@ -9480,11 +9409,6 @@ snapshots:
|
||||
|
||||
whatwg-mimetype@4.0.0: {}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
|
||||
whatwg-url@7.1.0:
|
||||
dependencies:
|
||||
lodash.sortby: 4.7.0
|
||||
|
11
src/app.tsx
11
src/app.tsx
@ -109,6 +109,9 @@ import ArticleView from "./views/articles/article";
|
||||
import WalletView from "./views/wallet";
|
||||
import SupportView from "./views/support";
|
||||
import UserMediaPostsTab from "./views/user/media-posts";
|
||||
import NewView from "./views/new";
|
||||
import NewNoteView from "./views/new/note";
|
||||
import NewMediaPostView from "./views/new/media";
|
||||
const TracksView = lazy(() => import("./views/tracks"));
|
||||
const UserTracksTab = lazy(() => import("./views/user/tracks"));
|
||||
const UserVideosTab = lazy(() => import("./views/user/videos"));
|
||||
@ -253,6 +256,14 @@ const router = createHashRouter([
|
||||
path: "/",
|
||||
element: <RootPage />,
|
||||
children: [
|
||||
{
|
||||
path: "new",
|
||||
children: [
|
||||
{ path: "", element: <NewView /> },
|
||||
{ path: "note", element: <NewNoteView /> },
|
||||
{ path: "media", element: <NewMediaPostView /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/u/:pubkey",
|
||||
element: <UserView />,
|
||||
|
@ -70,6 +70,7 @@ import Modem02 from "./icons/modem-02";
|
||||
import BookOpen01 from "./icons/book-open-01";
|
||||
import Edit04 from "./icons/edit-04";
|
||||
import Film02 from "./icons/film-02";
|
||||
import Camera01 from "./icons/camera-01";
|
||||
|
||||
const defaultProps: IconProps = { boxSize: 4 };
|
||||
|
||||
@ -253,3 +254,5 @@ export const WikiIcon = BookOpen01;
|
||||
|
||||
export const ArticleIcon = Edit04;
|
||||
export const VideoIcon = Film02;
|
||||
|
||||
export const MediaIcon = Camera01
|
||||
|
@ -61,7 +61,6 @@ export default function AccountSwitcher() {
|
||||
borderWidth={1}
|
||||
display="flex"
|
||||
gap="2"
|
||||
mb="2"
|
||||
alignItems="center"
|
||||
flexGrow={1}
|
||||
onClick={onToggle}
|
||||
|
@ -4,11 +4,11 @@ import { Link as RouterLink } from "react-router-dom";
|
||||
import { css } from "@emotion/react";
|
||||
import { useObservable } from "applesauce-react/hooks";
|
||||
|
||||
import Plus from "../icons/plus";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import AccountSwitcher from "./account-switcher";
|
||||
import NavItems from "./nav-items";
|
||||
import { PostModalContext } from "../../providers/route/post-modal-provider";
|
||||
import { WritingIcon } from "../icons";
|
||||
import { offlineMode } from "../../services/offline-mode";
|
||||
import WifiOff from "../icons/wifi-off";
|
||||
import TaskManagerButtons from "./task-manager-buttons";
|
||||
@ -65,15 +65,15 @@ export default function DesktopSideNav(props: Omit<FlexProps, "children">) {
|
||||
<>
|
||||
<AccountSwitcher />
|
||||
<Button
|
||||
leftIcon={<WritingIcon boxSize={6} />}
|
||||
aria-label="Write Note"
|
||||
title="Write Note"
|
||||
onClick={() => openModal()}
|
||||
as={RouterLink}
|
||||
leftIcon={<Plus boxSize={6} />}
|
||||
aria-label="Create new"
|
||||
title="Create new"
|
||||
colorScheme="primary"
|
||||
size="lg"
|
||||
isDisabled={account.readonly}
|
||||
to="/new"
|
||||
>
|
||||
Write Note
|
||||
New
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { Avatar, Flex, FlexProps, IconButton, useDisclosure } from "@chakra-ui/react";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import { useLocation, Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import { PostModalContext } from "../../providers/route/post-modal-provider";
|
||||
import { DirectMessagesIcon, NotesIcon, NotificationsIcon, PlusCircleIcon, SearchIcon } from "../icons";
|
||||
import UserAvatar from "../user/user-avatar";
|
||||
import MobileSideDrawer from "./mobile-side-drawer";
|
||||
@ -11,8 +10,6 @@ import Rocket02 from "../icons/rocket-02";
|
||||
|
||||
export default function MobileBottomNav(props: Omit<FlexProps, "children">) {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const { openModal } = useContext(PostModalContext);
|
||||
const navigate = useNavigate();
|
||||
const account = useCurrentAccount();
|
||||
|
||||
const location = useLocation();
|
||||
@ -26,50 +23,41 @@ export default function MobileBottomNav(props: Omit<FlexProps, "children">) {
|
||||
) : (
|
||||
<Avatar size="sm" src="/apple-touch-icon.png" onClick={onOpen} cursor="pointer" />
|
||||
)}
|
||||
<IconButton as={RouterLink} icon={<NotesIcon boxSize={6} />} aria-label="Home" flexGrow="1" size="md" to="/" />
|
||||
<IconButton
|
||||
icon={<NotesIcon boxSize={6} />}
|
||||
aria-label="Home"
|
||||
onClick={() => navigate("/")}
|
||||
flexGrow="1"
|
||||
size="md"
|
||||
/>
|
||||
<IconButton
|
||||
as={RouterLink}
|
||||
icon={<SearchIcon boxSize={6} />}
|
||||
aria-label="Search"
|
||||
onClick={() => navigate(`/search`)}
|
||||
flexGrow="1"
|
||||
size="md"
|
||||
to="/search"
|
||||
/>
|
||||
<IconButton
|
||||
as={RouterLink}
|
||||
icon={<PlusCircleIcon boxSize={6} />}
|
||||
aria-label="New Note"
|
||||
onClick={() => {
|
||||
openModal();
|
||||
}}
|
||||
aria-label="Create new"
|
||||
title="Create new"
|
||||
variant="solid"
|
||||
colorScheme="primary"
|
||||
isDisabled={account?.readonly ?? true}
|
||||
to="/new"
|
||||
/>
|
||||
<IconButton
|
||||
as={RouterLink}
|
||||
icon={<DirectMessagesIcon boxSize={6} />}
|
||||
aria-label="Messages"
|
||||
onClick={() => navigate(`/dm`)}
|
||||
flexGrow="1"
|
||||
size="md"
|
||||
to="/dm"
|
||||
/>
|
||||
<IconButton
|
||||
as={RouterLink}
|
||||
icon={<NotificationsIcon boxSize={6} />}
|
||||
aria-label="Notifications"
|
||||
onClick={() => navigate("/notifications")}
|
||||
flexGrow="1"
|
||||
size="md"
|
||||
to="/notifications"
|
||||
/>
|
||||
<IconButton
|
||||
icon={<Rocket02 boxSize={6} />}
|
||||
aria-label="Launchpad"
|
||||
onClick={() => navigate("/launchpad")}
|
||||
isDisabled={account?.readonly ?? true}
|
||||
/>
|
||||
<IconButton as={RouterLink} icon={<Rocket02 boxSize={6} />} aria-label="Launchpad" to="/launchpad" />
|
||||
</Flex>
|
||||
<MobileSideDrawer isOpen={isOpen} onClose={onClose} />
|
||||
</>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
@ -27,7 +27,7 @@ import {
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { EventTemplate, UnsignedEvent } from "nostr-tools";
|
||||
import { UnsignedEvent } from "nostr-tools";
|
||||
import { useAsync, useThrottle } from "react-use";
|
||||
import { useEventFactory, useObservable } from "applesauce-react/hooks";
|
||||
import { Emoji, ZapSplit } from "applesauce-core/helpers";
|
||||
@ -39,7 +39,7 @@ import { TrustProvider } from "../../providers/local/trust-provider";
|
||||
import MagicTextArea, { RefType } from "../magic-textarea";
|
||||
import { useContextEmojis } from "../../providers/global/emoji-provider";
|
||||
import CommunitySelect from "./community-select";
|
||||
import ZapSplitCreator from "./zap-split-creator";
|
||||
import ZapSplitCreator from "../../views/new/note/zap-split-creator";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import useCacheForm from "../../hooks/use-cache-form";
|
||||
import useTextAreaUploadFile, { useTextAreaInsertTextWithForm } from "../../hooks/use-textarea-upload-file";
|
||||
@ -51,7 +51,7 @@ import { TextNoteContents } from "../note/timeline-note/text-note-contents";
|
||||
import localSettings from "../../services/local-settings";
|
||||
import useLocalStorageDisclosure from "../../hooks/use-localstorage-disclosure";
|
||||
import InsertGifButton from "../gif/insert-gif-button";
|
||||
import InsertImageButton from "./insert-image-button";
|
||||
import InsertImageButton from "../../views/new/note/insert-image-button";
|
||||
|
||||
type FormValues = {
|
||||
subject: string;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { BlobDescriptor, createUploadAuth, ServerType, Signer } from "blossom-client-sdk";
|
||||
import { multiServerUpload, MultiServerUploadOptions } from "blossom-client-sdk/actions/upload";
|
||||
import { multiServerUpload, MultiServerUploadOptions } from "blossom-client-sdk/actions/multi-server";
|
||||
|
||||
export async function simpleMultiServerUpload<T extends ServerType = ServerType>(
|
||||
servers: T[],
|
||||
@ -8,8 +8,11 @@ export async function simpleMultiServerUpload<T extends ServerType = ServerType>
|
||||
opts?: MultiServerUploadOptions<T, File>,
|
||||
): Promise<BlobDescriptor> {
|
||||
const results = await multiServerUpload(servers, file, {
|
||||
isMedia: file.type.startsWith("image/") || file.type.startsWith("video/"),
|
||||
mediaUploadBehavior: "any",
|
||||
mediaUploadFallback: true,
|
||||
...opts,
|
||||
onAuth: (_server, blob) => createUploadAuth(signer, blob),
|
||||
onAuth: (_server, blob, type) => createUploadAuth(signer, blob, { type }),
|
||||
});
|
||||
|
||||
let blob: BlobDescriptor | null = null;
|
||||
|
@ -12,7 +12,7 @@ import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { useContextEmojis } from "../../../providers/global/emoji-provider";
|
||||
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
import InsertGifButton from "../../../components/gif/insert-gif-button";
|
||||
import InsertImageButton from "../../../components/post-modal/insert-image-button";
|
||||
import InsertImageButton from "../../new/note/insert-image-button";
|
||||
|
||||
export default function ChannelMessageForm({
|
||||
channel,
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { useContext } from "react";
|
||||
import { Button, Container, Flex, IconButton } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import RequireCurrentAccount from "../../providers/route/require-current-account";
|
||||
import { PostModalContext } from "../../providers/route/post-modal-provider";
|
||||
import AccountSwitcher from "../../components/layout/account-switcher";
|
||||
import { SettingsIcon } from "../../components/icons";
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
@ -15,31 +13,36 @@ import DMsCard from "./components/dms-card";
|
||||
import NotificationsCard from "./components/notifications-card";
|
||||
import ToolsCard from "./components/tools-card";
|
||||
import StreamsCard from "./components/streams-card";
|
||||
import Plus from "../../components/icons/plus";
|
||||
|
||||
function LaunchpadPage() {
|
||||
const { openModal } = useContext(PostModalContext);
|
||||
|
||||
return (
|
||||
<VerticalPageLayout gap="4" direction="row" wrap="wrap">
|
||||
<Flex justifyContent="space-between" w="full">
|
||||
<AccountSwitcher />
|
||||
<Flex gap="2">
|
||||
<AccountSwitcher />
|
||||
<Button
|
||||
as={RouterLink}
|
||||
colorScheme="primary"
|
||||
size="lg"
|
||||
to="/new"
|
||||
variant="outline"
|
||||
leftIcon={<Plus boxSize={6} />}
|
||||
>
|
||||
New
|
||||
<KeyboardShortcut letter="n" ml="2" />
|
||||
</Button>
|
||||
</Flex>
|
||||
<IconButton
|
||||
as={RouterLink}
|
||||
icon={<SettingsIcon boxSize={6} />}
|
||||
aria-label="Settings"
|
||||
title="Settings"
|
||||
size="lg"
|
||||
borderRadius="50%"
|
||||
to="/settings"
|
||||
/>
|
||||
</Flex>
|
||||
<Flex gap="4" w="full">
|
||||
<Button colorScheme="primary" size="lg" onClick={() => openModal()} variant="outline">
|
||||
New Note
|
||||
<KeyboardShortcut letter="n" ml="2" onPress={(e) => openModal()} />
|
||||
</Button>
|
||||
<SearchForm flex={1} />
|
||||
</Flex>
|
||||
<SearchForm flex={1} />
|
||||
|
||||
<ErrorBoundary>
|
||||
<FeedsCard w="full" />
|
||||
|
37
src/views/new/index.tsx
Normal file
37
src/views/new/index.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Card, CardBody, ComponentWithAs, Flex, Heading, IconProps, LinkBox, SimpleGrid, Text } from "@chakra-ui/react";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import { MediaIcon, NotesIcon } from "../../components/icons";
|
||||
import HoverLinkOverlay from "../../components/hover-link-overlay";
|
||||
|
||||
const NEW_TYPES: { title: string; path: string; summary?: string; icon: ComponentWithAs<"svg", IconProps> }[] = [
|
||||
{ title: "Text Note", path: "/new/note", summary: "A short text post with media", icon: NotesIcon },
|
||||
{ title: "Media Post", path: "/new/media", summary: "Image and video posts", icon: MediaIcon },
|
||||
];
|
||||
|
||||
export default function NewView() {
|
||||
return (
|
||||
<VerticalPageLayout>
|
||||
<Heading size="lg">Create new...</Heading>
|
||||
<SimpleGrid columns={{ base: 1, lg: 2 }} gap="2">
|
||||
{NEW_TYPES.map(({ title, path, icon: Icon, summary }) => (
|
||||
<Card as={LinkBox}>
|
||||
<CardBody display="flex" gap="4">
|
||||
<Icon boxSize={10} />
|
||||
<Flex gap="2" flexDirection="column">
|
||||
<Heading size="md">
|
||||
<HoverLinkOverlay as={RouterLink} to={path}>
|
||||
{title}
|
||||
</HoverLinkOverlay>
|
||||
</Heading>
|
||||
|
||||
{summary && <Text whiteSpace="pre-line">{summary}</Text>}
|
||||
</Flex>
|
||||
</CardBody>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</VerticalPageLayout>
|
||||
);
|
||||
}
|
169
src/views/new/media/index.tsx
Normal file
169
src/views/new/media/index.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
Heading,
|
||||
Input,
|
||||
Spacer,
|
||||
Switch,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import { nanoid } from "nanoid";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import VerticalPageLayout from "../../../components/vertical-page-layout";
|
||||
import BackButton from "../../../components/router/back-button";
|
||||
import useCacheForm from "../../../hooks/use-cache-form";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import { useEventFactory } from "applesauce-react/hooks";
|
||||
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
import MagicTextArea from "../../../components/magic-textarea";
|
||||
import NewMediaSlide from "./new-media-slide";
|
||||
import MediaSlide from "./media-slide";
|
||||
import { ErrorBoundary } from "../../../components/error-boundary";
|
||||
import ZapSplitCreator, { Split } from "../note/zap-split-creator";
|
||||
|
||||
type FormValues = {
|
||||
content: string;
|
||||
nsfw: boolean;
|
||||
nsfwReason: string;
|
||||
media: { id: string; alt?: string; file: File }[];
|
||||
split: Split[];
|
||||
};
|
||||
|
||||
const setOptions = { shouldDirty: true, shouldTouch: true };
|
||||
|
||||
export default function NewMediaPostView() {
|
||||
const toast = useToast();
|
||||
const account = useCurrentAccount()!;
|
||||
const factory = useEventFactory();
|
||||
const publish = usePublishEvent();
|
||||
|
||||
const advanced = useDisclosure();
|
||||
|
||||
const { getValues, reset, formState, handleSubmit, setValue, watch, register } = useForm<FormValues>({
|
||||
mode: "all",
|
||||
defaultValues: { media: [], content: "", nsfw: false, nsfwReason: "", split: [] },
|
||||
});
|
||||
|
||||
watch("content");
|
||||
watch("media");
|
||||
watch("nsfw");
|
||||
watch("split");
|
||||
|
||||
// TODO: cache for needs to save File and Blobs
|
||||
const clearFormCache = useCacheForm(
|
||||
"new-media-post",
|
||||
// @ts-expect-error
|
||||
getValues,
|
||||
reset,
|
||||
formState,
|
||||
);
|
||||
|
||||
const submit = handleSubmit(async (values) => {
|
||||
try {
|
||||
// do something
|
||||
|
||||
clearFormCache();
|
||||
} catch (error) {
|
||||
if (error instanceof Error) toast({ status: "error", description: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
const showAdvanced = advanced.isOpen || getValues("nsfw") || getValues("split").length > 0;
|
||||
|
||||
return (
|
||||
<VerticalPageLayout as="form" onSubmit={submit} mx="auto" maxW="4xl" w="full">
|
||||
<Flex gap="2">
|
||||
<BackButton />
|
||||
<Heading>Media post</Heading>
|
||||
</Flex>
|
||||
|
||||
<Alert status="info" flexShrink={0}>
|
||||
<AlertIcon />
|
||||
Work in progress, this view does not work yet
|
||||
</Alert>
|
||||
|
||||
<Flex overflowY="hidden" overflowX="scroll" position="relative" h="md" flexShrink={0} gap="2" pb="2">
|
||||
{getValues("media")
|
||||
.filter((m) => m.file instanceof File)
|
||||
.map((media) => (
|
||||
<ErrorBoundary key={media.id}>
|
||||
<MediaSlide
|
||||
alt={media.alt}
|
||||
file={media.file}
|
||||
onChange={(alt) =>
|
||||
setValue(
|
||||
"media",
|
||||
getValues("media").map((m) => (m.id === media.id ? { ...media, alt } : m)),
|
||||
setOptions,
|
||||
)
|
||||
}
|
||||
onRemove={() =>
|
||||
setValue(
|
||||
"media",
|
||||
getValues("media").filter((m) => m.id !== media.id),
|
||||
setOptions,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
<NewMediaSlide
|
||||
onSelect={(files) =>
|
||||
setValue("media", [...files.map((file) => ({ id: nanoid(6), file })), ...getValues("media")], setOptions)
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<MagicTextArea
|
||||
value={getValues().content}
|
||||
onChange={(e) => setValue("content", e.target.value, setOptions)}
|
||||
rows={3}
|
||||
placeholder="Add a caption"
|
||||
/>
|
||||
|
||||
{showAdvanced && (
|
||||
<>
|
||||
<Flex gap="2" alignItems="center" mt="2">
|
||||
<Divider />
|
||||
<Heading size="sm">Advanced</Heading>
|
||||
<Divider />
|
||||
</Flex>
|
||||
<Flex gap="2" direction="column">
|
||||
<Switch {...register("nsfw")}>NSFW</Switch>
|
||||
{getValues().nsfw && (
|
||||
<Input {...register("nsfwReason", { required: true })} placeholder="NSFW Reason" isRequired />
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<ZapSplitCreator
|
||||
splits={getValues("split")}
|
||||
onChange={(splits) => setValue("split", splits, setOptions)}
|
||||
authorPubkey={account?.pubkey}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Flex gap="2">
|
||||
{!showAdvanced && (
|
||||
<Button variant="link" p="2" onClick={advanced.onOpen}>
|
||||
Show advanced
|
||||
</Button>
|
||||
)}
|
||||
<Spacer />
|
||||
{formState.isDirty && (
|
||||
<Button variant="ghost" onClick={() => confirm("Clear draft?") && reset()}>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
<Button type="submit" colorScheme="primary">
|
||||
Preview
|
||||
</Button>
|
||||
</Flex>
|
||||
</VerticalPageLayout>
|
||||
);
|
||||
}
|
58
src/views/new/media/media-slide.tsx
Normal file
58
src/views/new/media/media-slide.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Box, Button, Flex, FlexProps, IconButton, Image, Input } from "@chakra-ui/react";
|
||||
|
||||
import useObjectURL from "../../../hooks/use-object-url";
|
||||
import { CloseIcon } from "@chakra-ui/icons";
|
||||
|
||||
function ImagePreview({ file }: { file: File }) {
|
||||
const url = useObjectURL(file);
|
||||
|
||||
return <Image src={url} w="auto" h="auto" maxW="full" maxH="full" />;
|
||||
}
|
||||
function VideoPreview({ file }: { file: File }) {
|
||||
const url = useObjectURL(file);
|
||||
|
||||
// TODO: make better
|
||||
return <Box as="video" src={url} w="auto" h="auto" maxW="full" maxH="full" />;
|
||||
}
|
||||
|
||||
export type MediaSlideProps = Omit<FlexProps, "children" | "onChange"> & {
|
||||
file: File;
|
||||
alt?: string;
|
||||
onChange?: (alt: string) => void;
|
||||
onRemove?: () => void;
|
||||
};
|
||||
|
||||
export default function MediaSlide({ file, onChange, alt, onRemove, ...props }: MediaSlideProps) {
|
||||
let preview: ReactNode;
|
||||
if (file.type.includes("image/")) preview = <ImagePreview file={file} />;
|
||||
else if (file.type.includes("video/")) preview = <VideoPreview file={file} />;
|
||||
else preview = null;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
h="full"
|
||||
maxW="min(100%, var(--chakra-sizes-lg))"
|
||||
direction="column"
|
||||
gap="2"
|
||||
flexShrink={0}
|
||||
overflow="hidden"
|
||||
alignItems="center"
|
||||
{...props}
|
||||
>
|
||||
<Flex overflow="hidden" alignItems="center" justifyContent="center" position="relative" w="max-content">
|
||||
{preview}
|
||||
</Flex>
|
||||
<Flex gap="2" w="full" justifyContent="flex-end">
|
||||
{alt !== undefined ? (
|
||||
<Input value={alt} onChange={(e) => onChange?.(e.target.value)} placeholder="Alt text for media" p="2" />
|
||||
) : (
|
||||
<Button variant="link" onClick={() => onChange?.("")}>
|
||||
add alt text
|
||||
</Button>
|
||||
)}
|
||||
<IconButton aria-label="Remove" icon={<CloseIcon />} colorScheme="red" variant="ghost" onClick={onRemove} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
50
src/views/new/media/new-media-slide.tsx
Normal file
50
src/views/new/media/new-media-slide.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { Flex, Text, VisuallyHiddenInput } from "@chakra-ui/react";
|
||||
import { ChangeEventHandler, useMemo, useRef } from "react";
|
||||
import Plus from "../../../components/icons/plus";
|
||||
|
||||
export default function NewMediaSlide({ onSelect }: { onSelect: (files: File[]) => void }) {
|
||||
const input = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const files = event.target.files;
|
||||
if (files && files.length > 0) {
|
||||
onSelect(Array.from(files));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
as="label"
|
||||
htmlFor="select-media"
|
||||
direction="column"
|
||||
gap="2"
|
||||
w="full"
|
||||
flexShrink={0}
|
||||
h="full"
|
||||
maxW="md"
|
||||
borderWidth="1px"
|
||||
rounded="md"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
cursor="pointer"
|
||||
>
|
||||
<Plus boxSize={10} />
|
||||
<Text fontWeight="bold" fontSize="xl">
|
||||
Add media
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<VisuallyHiddenInput
|
||||
id="select-media"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
ref={input}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
17
src/views/new/note/index.tsx
Normal file
17
src/views/new/note/index.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { Flex, Heading } from "@chakra-ui/react";
|
||||
import VerticalPageLayout from "../../../components/vertical-page-layout";
|
||||
import ShortTextNoteForm from "./short-text-form";
|
||||
import BackButton from "../../../components/router/back-button";
|
||||
|
||||
export default function NewNoteView() {
|
||||
return (
|
||||
<VerticalPageLayout mx="auto" maxW="4xl" w="full">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<BackButton />
|
||||
<Heading>New note</Heading>
|
||||
</Flex>
|
||||
|
||||
<ShortTextNoteForm />
|
||||
</VerticalPageLayout>
|
||||
);
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { useRef } from "react";
|
||||
import { IconButton, IconButtonProps, VisuallyHiddenInput } from "@chakra-ui/react";
|
||||
|
||||
import { UploadImageIcon } from "../icons";
|
||||
import useTextAreaUploadFile from "../../hooks/use-textarea-upload-file";
|
||||
import { UploadImageIcon } from "../../../components/icons";
|
||||
import useTextAreaUploadFile from "../../../hooks/use-textarea-upload-file";
|
||||
|
||||
export default function InsertImageButton({
|
||||
onUploaded,
|
310
src/views/new/note/short-text-form.tsx
Normal file
310
src/views/new/note/short-text-form.tsx
Normal file
@ -0,0 +1,310 @@
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
Flex,
|
||||
Button,
|
||||
Box,
|
||||
Heading,
|
||||
useDisclosure,
|
||||
Input,
|
||||
Switch,
|
||||
ModalProps,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Link,
|
||||
Slider,
|
||||
SliderTrack,
|
||||
SliderFilledTrack,
|
||||
SliderThumb,
|
||||
ModalCloseButton,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
ButtonGroup,
|
||||
Text,
|
||||
FlexProps,
|
||||
} from "@chakra-ui/react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { UnsignedEvent } from "nostr-tools";
|
||||
import { useAsync, useThrottle } from "react-use";
|
||||
import { useEventFactory, useObservable } from "applesauce-react/hooks";
|
||||
import { Emoji, ZapSplit } from "applesauce-core/helpers";
|
||||
|
||||
import { useFinalizeDraft, usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
||||
import useAppSettings from "../../../hooks/use-app-settings";
|
||||
import localSettings from "../../../services/local-settings";
|
||||
import useLocalStorageDisclosure from "../../../hooks/use-localstorage-disclosure";
|
||||
import PublishAction from "../../../classes/nostr-publish-action";
|
||||
import { useContextEmojis } from "../../../providers/global/emoji-provider";
|
||||
import useCacheForm from "../../../hooks/use-cache-form";
|
||||
import MagicTextArea, { RefType } from "../../../components/magic-textarea";
|
||||
import useTextAreaUploadFile, { useTextAreaInsertTextWithForm } from "../../../hooks/use-textarea-upload-file";
|
||||
import { ErrorBoundary } from "../../../components/error-boundary";
|
||||
import { TrustProvider } from "../../../providers/local/trust-provider";
|
||||
import TextNoteContents from "../../../components/note/timeline-note/text-note-contents";
|
||||
import InsertImageButton from "./insert-image-button";
|
||||
import InsertGifButton from "../../../components/gif/insert-gif-button";
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "../../../components/icons";
|
||||
import ZapSplitCreator, { Split } from "./zap-split-creator";
|
||||
import MinePOW from "../../../components/pow/mine-pow";
|
||||
import { PublishDetails } from "../../task-manager/publish-log/publish-details";
|
||||
import CommunitySelect from "../../../components/post-modal/community-select";
|
||||
|
||||
type FormValues = {
|
||||
content: string;
|
||||
nsfw: boolean;
|
||||
nsfwReason: string;
|
||||
community: string;
|
||||
split: Split[];
|
||||
difficulty: number;
|
||||
};
|
||||
|
||||
export type ShortTextNoteFormProps = {
|
||||
cacheFormKey?: string | null;
|
||||
initContent?: string;
|
||||
initCommunity?: string;
|
||||
};
|
||||
|
||||
export default function ShortTextNoteForm({
|
||||
cacheFormKey = "new-note",
|
||||
initContent = "",
|
||||
initCommunity = "",
|
||||
}: Omit<FlexProps, "children"> & ShortTextNoteFormProps) {
|
||||
const publish = usePublishEvent();
|
||||
const finalizeDraft = useFinalizeDraft();
|
||||
const account = useCurrentAccount()!;
|
||||
const { noteDifficulty } = useAppSettings();
|
||||
const addClientTag = useObservable(localSettings.addClientTag);
|
||||
const promptAddClientTag = useLocalStorageDisclosure("prompt-add-client-tag", true);
|
||||
const [miningTarget, setMiningTarget] = useState(0);
|
||||
const [publishAction, setPublishAction] = useState<PublishAction>();
|
||||
const emojis = useContextEmojis();
|
||||
const advanced = useDisclosure();
|
||||
|
||||
const factory = useEventFactory();
|
||||
const [draft, setDraft] = useState<UnsignedEvent>();
|
||||
const { getValues, setValue, watch, register, handleSubmit, formState, reset } = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
content: initContent,
|
||||
nsfw: false,
|
||||
nsfwReason: "",
|
||||
community: initCommunity,
|
||||
split: [] as Split[],
|
||||
difficulty: noteDifficulty || 0,
|
||||
},
|
||||
mode: "all",
|
||||
});
|
||||
|
||||
// watch form state
|
||||
formState.isDirty;
|
||||
watch("content");
|
||||
watch("nsfw");
|
||||
watch("nsfwReason");
|
||||
watch("split");
|
||||
watch("difficulty");
|
||||
|
||||
// cache form to localStorage
|
||||
useCacheForm<FormValues>(cacheFormKey, getValues, reset, formState);
|
||||
|
||||
const getDraft = async (values = getValues()) => {
|
||||
// build draft using factory
|
||||
let draft = await factory.note(values.content, {
|
||||
emojis: emojis.filter((e) => !!e.url) as Emoji[],
|
||||
contentWarning: values.nsfw ? values.nsfwReason || values.nsfw : false,
|
||||
splits: values.split,
|
||||
});
|
||||
|
||||
// TODO: remove when NIP-72 communities are removed
|
||||
if (values.community) draft.tags.push(["a", values.community]);
|
||||
|
||||
const unsigned = await finalizeDraft(draft);
|
||||
|
||||
setDraft(unsigned);
|
||||
return unsigned;
|
||||
};
|
||||
|
||||
// throttle update the draft every 500ms
|
||||
const throttleValues = useThrottle(getValues(), 500);
|
||||
const { value: preview } = useAsync(() => getDraft(), [throttleValues]);
|
||||
|
||||
const textAreaRef = useRef<RefType | null>(null);
|
||||
const insertText = useTextAreaInsertTextWithForm(textAreaRef, getValues, setValue);
|
||||
const { onPaste } = useTextAreaUploadFile(insertText);
|
||||
|
||||
const publishPost = async (unsigned?: UnsignedEvent) => {
|
||||
unsigned = unsigned || draft || (await getDraft());
|
||||
|
||||
const pub = await publish("Post", unsigned);
|
||||
if (pub) setPublishAction(pub);
|
||||
};
|
||||
const submit = handleSubmit(async (values) => {
|
||||
if (values.difficulty > 0) {
|
||||
setMiningTarget(values.difficulty);
|
||||
} else {
|
||||
const unsigned = await getDraft(values);
|
||||
publishPost(unsigned);
|
||||
}
|
||||
});
|
||||
|
||||
const canSubmit = getValues().content.length > 0;
|
||||
|
||||
const renderBody = () => {
|
||||
if (publishAction) {
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<PublishDetails pub={publishAction} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
if (miningTarget && draft) {
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<MinePOW
|
||||
draft={draft}
|
||||
targetPOW={miningTarget}
|
||||
onCancel={() => setMiningTarget(0)}
|
||||
onSkip={publishPost}
|
||||
onComplete={publishPost}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
const showAdvanced =
|
||||
advanced.isOpen || formState.dirtyFields.difficulty || formState.dirtyFields.nsfw || formState.dirtyFields.split;
|
||||
|
||||
// TODO: wrap this in a form
|
||||
return (
|
||||
<>
|
||||
<Flex direction="column" gap="2">
|
||||
<MagicTextArea
|
||||
autoFocus
|
||||
mb="2"
|
||||
value={getValues().content}
|
||||
onChange={(e) => setValue("content", e.target.value, { shouldDirty: true, shouldTouch: true })}
|
||||
rows={8}
|
||||
isRequired
|
||||
instanceRef={(inst) => (textAreaRef.current = inst)}
|
||||
onPaste={onPaste}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") submit();
|
||||
}}
|
||||
/>
|
||||
{preview && preview.content.length > 0 && (
|
||||
<Box>
|
||||
<Heading size="sm">Preview:</Heading>
|
||||
<Box borderWidth={1} borderRadius="md" p="2">
|
||||
<ErrorBoundary>
|
||||
<TrustProvider trust>
|
||||
<TextNoteContents event={preview} />
|
||||
</TrustProvider>
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<Flex gap="2" alignItems="center" justifyContent="flex-end">
|
||||
<Flex mr="auto" gap="2">
|
||||
<InsertImageButton onUploaded={insertText} aria-label="Upload image" />
|
||||
<InsertGifButton onSelectURL={insertText} aria-label="Add gif" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex gap="2" alignItems="center" justifyContent="space-between">
|
||||
<Button
|
||||
variant="link"
|
||||
rightIcon={advanced.isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||
onClick={advanced.onToggle}
|
||||
>
|
||||
More Options
|
||||
</Button>
|
||||
{formState.isDirty && (
|
||||
<Button variant="ghost" onClick={() => confirm("Clear draft?") && reset()} ms="auto">
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
type="submit"
|
||||
isLoading={formState.isSubmitting}
|
||||
onClick={submit}
|
||||
isDisabled={!canSubmit}
|
||||
>
|
||||
Post
|
||||
</Button>
|
||||
</Flex>
|
||||
{showAdvanced && (
|
||||
<Flex direction={{ base: "column", lg: "row" }} gap="4">
|
||||
<Flex direction="column" gap="2" flex={1}>
|
||||
<FormControl>
|
||||
<FormLabel>Post to community</FormLabel>
|
||||
<CommunitySelect {...register("community")} />
|
||||
</FormControl>
|
||||
<Flex gap="2" direction="column">
|
||||
<Switch {...register("nsfw")}>NSFW</Switch>
|
||||
{getValues().nsfw && (
|
||||
<Input {...register("nsfwReason", { required: true })} placeholder="Reason" isRequired />
|
||||
)}
|
||||
</Flex>
|
||||
<FormControl>
|
||||
<FormLabel>POW Difficulty ({getValues("difficulty")})</FormLabel>
|
||||
<Slider
|
||||
aria-label="difficulty"
|
||||
value={getValues("difficulty")}
|
||||
onChange={(v) => setValue("difficulty", v, { shouldDirty: true, shouldTouch: true })}
|
||||
min={0}
|
||||
max={40}
|
||||
step={1}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb />
|
||||
</Slider>
|
||||
<FormHelperText>
|
||||
The number of leading 0's in the event id. see{" "}
|
||||
<Link href="https://github.com/nostr-protocol/nips/blob/master/13.md" isExternal>
|
||||
NIP-13
|
||||
</Link>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
<Flex direction="column" gap="2" flex={1}>
|
||||
<ZapSplitCreator
|
||||
splits={getValues().split}
|
||||
onChange={(splits) => setValue("split", splits, { shouldDirty: true, shouldTouch: true })}
|
||||
authorPubkey={account?.pubkey}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{!addClientTag && promptAddClientTag.isOpen && (
|
||||
<Alert status="info" whiteSpace="pre-wrap" flexDirection={{ base: "column", lg: "row" }}>
|
||||
<AlertIcon hideBelow="lg" />
|
||||
<Text>
|
||||
Enable{" "}
|
||||
<Link isExternal href="https://github.com/nostr-protocol/nips/blob/master/89.md#client-tag">
|
||||
NIP-89
|
||||
</Link>{" "}
|
||||
client tags and let other users know what app you're using to write notes
|
||||
</Text>
|
||||
<ButtonGroup ml="auto" size="sm" variant="ghost">
|
||||
<Button onClick={promptAddClientTag.onClose}>Close</Button>
|
||||
<Button colorScheme="primary" onClick={() => localSettings.addClientTag.next(true)}>
|
||||
Enable
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{publishAction && <ModalCloseButton />}
|
||||
{renderBody()}
|
||||
</>
|
||||
);
|
||||
}
|
@ -12,13 +12,13 @@ import {
|
||||
import { CloseIcon } from "@chakra-ui/icons";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { AddIcon } from "../icons";
|
||||
import { normalizeToHexPubkey } from "../../helpers/nip19";
|
||||
import UserAvatar from "../user/user-avatar";
|
||||
import UserLink from "../user/user-link";
|
||||
import UserAutocomplete from "../user-autocomplete";
|
||||
import { AddIcon } from "../../../components/icons";
|
||||
import { normalizeToHexPubkey } from "../../../helpers/nip19";
|
||||
import UserAvatar from "../../../components/user/user-avatar";
|
||||
import UserLink from "../../../components/user/user-link";
|
||||
import UserAutocomplete from "../../../components/user-autocomplete";
|
||||
|
||||
type Split = { pubkey: string; weight: number };
|
||||
export type Split = { pubkey: string; weight: number };
|
||||
|
||||
function validateNpub(input: string) {
|
||||
const pubkey = normalizeToHexPubkey(input);
|
||||
@ -48,9 +48,12 @@ function AddUserForm({ onSubmit }: { onSubmit: (values: Split) => void }) {
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex as="form" gap="2" onSubmit={submit}>
|
||||
<UserAutocomplete {...register("pubkey", { required: true, validate: validateNpub })} />
|
||||
<IconButton icon={<AddIcon boxSize={5} />} aria-label="Add" type="submit" />
|
||||
<Flex gap="2">
|
||||
<UserAutocomplete
|
||||
placeholder="Search users"
|
||||
{...register("pubkey", { required: true, validate: validateNpub })}
|
||||
/>
|
||||
<IconButton icon={<AddIcon boxSize={5} />} aria-label="Add" onClick={submit} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
@ -10,6 +10,7 @@ import {
|
||||
ListsIcon,
|
||||
LiveStreamIcon,
|
||||
MapIcon,
|
||||
MediaIcon,
|
||||
MuteIcon,
|
||||
SearchIcon,
|
||||
TorrentIcon,
|
||||
@ -23,7 +24,6 @@ import MessageQuestionSquare from "../../components/icons/message-question-squar
|
||||
import UploadCloud01 from "../../components/icons/upload-cloud-01";
|
||||
import Edit04 from "../../components/icons/edit-04";
|
||||
import Users03 from "../../components/icons/users-03";
|
||||
import Camera01 from "../../components/icons/camera-01";
|
||||
|
||||
export const internalApps: App[] = [
|
||||
{
|
||||
@ -36,7 +36,7 @@ export const internalApps: App[] = [
|
||||
{
|
||||
title: "Media",
|
||||
description: "Browser media posts",
|
||||
icon: Camera01,
|
||||
icon: MediaIcon,
|
||||
id: "media",
|
||||
to: "/media",
|
||||
},
|
||||
|
@ -7,7 +7,7 @@ import { unixNow } from "applesauce-core/helpers";
|
||||
import MagicTextArea, { RefType } from "../../../components/magic-textarea";
|
||||
import useTextAreaUploadFile, { useTextAreaInsertTextWithForm } from "../../../hooks/use-textarea-upload-file";
|
||||
import { LightningIcon } from "../../../components/icons";
|
||||
import InsertImageButton from "../../../components/post-modal/insert-image-button";
|
||||
import InsertImageButton from "../../new/note/insert-image-button";
|
||||
import InsertGifButton from "../../../components/gif/insert-gif-button";
|
||||
import TextNoteContents from "../../../components/note/timeline-note/text-note-contents";
|
||||
import { TrustProvider } from "../../../providers/local/trust-provider";
|
||||
|
@ -16,7 +16,7 @@ import { TextNoteContents } from "../../../components/note/timeline-note/text-no
|
||||
import useCacheForm from "../../../hooks/use-cache-form";
|
||||
import useTextAreaUploadFile, { useTextAreaInsertTextWithForm } from "../../../hooks/use-textarea-upload-file";
|
||||
import InsertGifButton from "../../../components/gif/insert-gif-button";
|
||||
import InsertImageButton from "../../../components/post-modal/insert-image-button";
|
||||
import InsertImageButton from "../../new/note/insert-image-button";
|
||||
|
||||
export type ReplyFormProps = {
|
||||
item: ThreadItem;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { VisuallyHidden } from "@chakra-ui/react";
|
||||
import SimpleMDE, { SimpleMDEReactProps } from "react-simplemde-editor";
|
||||
import { multiServerUpload } from "blossom-client-sdk/actions/upload";
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
import { Global, css } from "@emotion/react";
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user