mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-29 04:52:59 +02:00
Add community create and edit modals
This commit is contained in:
5
.changeset/soft-eagles-smash.md
Normal file
5
.changeset/soft-eagles-smash.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add community create and edit modals
|
@@ -29,6 +29,9 @@ export function getCommunityDescription(community: NostrEvent) {
|
|||||||
export function getCommunityRules(community: NostrEvent) {
|
export function getCommunityRules(community: NostrEvent) {
|
||||||
return community.tags.find((t) => t[0] === "rules")?.[1];
|
return community.tags.find((t) => t[0] === "rules")?.[1];
|
||||||
}
|
}
|
||||||
|
export function getCommunityRanking(community: NostrEvent) {
|
||||||
|
return community.tags.find((t) => t[0] === "rank_mode")?.[1];
|
||||||
|
}
|
||||||
|
|
||||||
export function getPostSubject(event: NostrEvent) {
|
export function getPostSubject(event: NostrEvent) {
|
||||||
const subject = event.tags.find((t) => t[0] === "subject")?.[1];
|
const subject = event.tags.find((t) => t[0] === "subject")?.[1];
|
||||||
|
150
src/views/communities/components/community-create-modal.tsx
Normal file
150
src/views/communities/components/community-create-modal.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
FormControl,
|
||||||
|
FormErrorMessage,
|
||||||
|
FormHelperText,
|
||||||
|
FormLabel,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalProps,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
Stack,
|
||||||
|
Textarea,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||||
|
import UserAvatar from "../../../components/user-avatar";
|
||||||
|
import { UserLink } from "../../../components/user-link";
|
||||||
|
|
||||||
|
export type FormValues = {
|
||||||
|
name: string;
|
||||||
|
banner: string;
|
||||||
|
description: string;
|
||||||
|
rules: string;
|
||||||
|
mods: string[];
|
||||||
|
relays: string[];
|
||||||
|
ranking: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CommunityCreateModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
defaultValues,
|
||||||
|
isUpdate,
|
||||||
|
...props
|
||||||
|
}: Omit<ModalProps, "children"> & {
|
||||||
|
onSubmit: SubmitHandler<FormValues>;
|
||||||
|
defaultValues?: FormValues;
|
||||||
|
isUpdate?: boolean;
|
||||||
|
}) {
|
||||||
|
const account = useCurrentAccount();
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
handleSubmit,
|
||||||
|
watch,
|
||||||
|
getValues,
|
||||||
|
setValue,
|
||||||
|
} = useForm<FormValues>({
|
||||||
|
mode: "all",
|
||||||
|
defaultValues: defaultValues || {
|
||||||
|
name: "",
|
||||||
|
banner: "",
|
||||||
|
description: "",
|
||||||
|
rules: "",
|
||||||
|
mods: account ? [account.pubkey] : [],
|
||||||
|
relays: [],
|
||||||
|
ranking: "votes",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
watch("mods");
|
||||||
|
watch("ranking");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} size="2xl" {...props}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent as="form" onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<ModalHeader p="4">{isUpdate ? "Update Community" : "Create Community"}</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody px="4" py="0" gap="4" display="flex" flexDirection="column">
|
||||||
|
{!isUpdate && (
|
||||||
|
<FormControl isInvalid={!!errors.name}>
|
||||||
|
<FormLabel>Community Name</FormLabel>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
{...register("name", {
|
||||||
|
required: true,
|
||||||
|
validate: (v) => {
|
||||||
|
if (/\p{Z}/iu.test(v)) return "Must not have spaces";
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
isReadOnly={isUpdate}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
<FormHelperText>The name of your community (no-spaces)</FormHelperText>
|
||||||
|
{errors.name?.message && <FormErrorMessage>{errors.name?.message}</FormErrorMessage>}
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormControl isInvalid={!!errors.description}>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<Textarea {...register("description")} autoComplete="off" />
|
||||||
|
<FormHelperText>short description about your community</FormHelperText>
|
||||||
|
{errors.description?.message && <FormErrorMessage>{errors.description?.message}</FormErrorMessage>}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl isInvalid={!!errors.rules}>
|
||||||
|
<FormLabel>Rules and Guidelines</FormLabel>
|
||||||
|
<Textarea {...register("rules")} autoComplete="off" />
|
||||||
|
<FormHelperText>Rules and posting guidelines</FormHelperText>
|
||||||
|
{errors.rules?.message && <FormErrorMessage>{errors.rules?.message}</FormErrorMessage>}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl isInvalid={!!errors.mods}>
|
||||||
|
<FormLabel>Moderators</FormLabel>
|
||||||
|
{getValues().mods.map((pubkey) => (
|
||||||
|
<Flex gap="2" alignItems="center" key={pubkey}>
|
||||||
|
<UserAvatar pubkey={pubkey} size="sm" />
|
||||||
|
<UserLink pubkey={pubkey} fontWeight="bold" />
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl isInvalid={!!errors.mods}>
|
||||||
|
<FormLabel>Default Raking</FormLabel>
|
||||||
|
<RadioGroup
|
||||||
|
value={getValues().ranking}
|
||||||
|
onChange={(e) => setValue("ranking", e, { shouldDirty: true, shouldTouch: true })}
|
||||||
|
>
|
||||||
|
<Stack direction="row">
|
||||||
|
<Radio value="votes">Votes</Radio>
|
||||||
|
<Radio value="zaps">Zaps</Radio>
|
||||||
|
</Stack>
|
||||||
|
</RadioGroup>
|
||||||
|
<FormHelperText>The default by posts are ranked when viewing the community</FormHelperText>
|
||||||
|
{errors.rules?.message && <FormErrorMessage>{errors.rules?.message}</FormErrorMessage>}
|
||||||
|
</FormControl>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter p="4" display="flex" gap="2">
|
||||||
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
|
<Button colorScheme="primary" type="submit">
|
||||||
|
{isUpdate ? "Update Community" : "Create Community"}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,44 +1,101 @@
|
|||||||
import { Button, Center, Flex, Heading, Link, SimpleGrid, Text } from "@chakra-ui/react";
|
import { Button, Center, Flex, Heading, Link, SimpleGrid, Text, useDisclosure, useToast } from "@chakra-ui/react";
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink, useNavigate } from "react-router-dom";
|
||||||
import { Navigate } from "react-router-dom";
|
import { Navigate } from "react-router-dom";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
import { ErrorBoundary } from "../../components/error-boundary";
|
import { ErrorBoundary } from "../../components/error-boundary";
|
||||||
import useSubscribedCommunitiesList from "../../hooks/use-subscribed-communities-list";
|
import useSubscribedCommunitiesList from "../../hooks/use-subscribed-communities-list";
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
import { PointerCommunityCard } from "./components/community-card";
|
import { PointerCommunityCard } from "./components/community-card";
|
||||||
|
import CommunityCreateModal, { FormValues } from "./components/community-create-modal";
|
||||||
|
import { useSigningContext } from "../../providers/signing-provider";
|
||||||
|
import { DraftNostrEvent } from "../../types/nostr-event";
|
||||||
|
import { COMMUNITY_DEFINITION_KIND, getCommunityName } from "../../helpers/nostr/communities";
|
||||||
|
import NostrPublishAction from "../../classes/nostr-publish-action";
|
||||||
|
import { unique } from "../../helpers/array";
|
||||||
|
import clientRelaysService from "../../services/client-relays";
|
||||||
|
import replaceableEventLoaderService from "../../services/replaceable-event-requester";
|
||||||
|
|
||||||
function CommunitiesHomePage() {
|
function CommunitiesHomePage() {
|
||||||
|
const toast = useToast();
|
||||||
|
const { requestSignature } = useSigningContext();
|
||||||
|
const navigate = useNavigate();
|
||||||
const account = useCurrentAccount()!;
|
const account = useCurrentAccount()!;
|
||||||
|
const createModal = useDisclosure();
|
||||||
const { pointers: communities } = useSubscribedCommunitiesList(account.pubkey, { alwaysRequest: true });
|
const { pointers: communities } = useSubscribedCommunitiesList(account.pubkey, { alwaysRequest: true });
|
||||||
|
|
||||||
|
const createCommunity = async (values: FormValues) => {
|
||||||
|
try {
|
||||||
|
const draft: DraftNostrEvent = {
|
||||||
|
kind: COMMUNITY_DEFINITION_KIND,
|
||||||
|
created_at: dayjs().unix(),
|
||||||
|
content: "",
|
||||||
|
tags: [["d", values.name]],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const pubkey of values.mods) {
|
||||||
|
draft.tags.push(["p", pubkey, "moderator"]);
|
||||||
|
}
|
||||||
|
for (const url of values.relays) {
|
||||||
|
draft.tags.push(["relay", url]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.description) draft.tags.push(["description", values.description]);
|
||||||
|
if (values.banner) draft.tags.push(["image", values.banner]);
|
||||||
|
if (values.ranking) draft.tags.push(["rank_mode", values.ranking]);
|
||||||
|
|
||||||
|
const signed = await requestSignature(draft);
|
||||||
|
new NostrPublishAction(
|
||||||
|
"Create Community",
|
||||||
|
unique([...clientRelaysService.getWriteUrls(), ...values.relays]),
|
||||||
|
signed,
|
||||||
|
);
|
||||||
|
|
||||||
|
replaceableEventLoaderService.handleEvent(signed);
|
||||||
|
|
||||||
|
navigate(`/c/${getCommunityName(signed)}/${signed.pubkey}`);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalPageLayout>
|
<>
|
||||||
<Flex gap="2" alignItems="center" wrap="wrap">
|
<VerticalPageLayout>
|
||||||
<Button as={RouterLink} to="/communities/explore">
|
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||||
Explore Communities
|
<Button as={RouterLink} to="/communities/explore">
|
||||||
</Button>
|
Explore Communities
|
||||||
</Flex>
|
</Button>
|
||||||
{communities.length > 0 ? (
|
|
||||||
<SimpleGrid spacing="2" columns={{ base: 1, lg: 2 }}>
|
<Button ml="auto" onClick={createModal.onOpen}>
|
||||||
{communities.map((pointer) => (
|
Create Community
|
||||||
<ErrorBoundary key={pointer.kind + pointer.pubkey + pointer.identifier}>
|
</Button>
|
||||||
<PointerCommunityCard pointer={pointer} />
|
</Flex>
|
||||||
</ErrorBoundary>
|
{communities.length > 0 ? (
|
||||||
))}
|
<SimpleGrid spacing="2" columns={{ base: 1, lg: 2 }}>
|
||||||
</SimpleGrid>
|
{communities.map((pointer) => (
|
||||||
) : (
|
<ErrorBoundary key={pointer.kind + pointer.pubkey + pointer.identifier}>
|
||||||
<Center aspectRatio={3 / 4} flexDirection="column" gap="4">
|
<PointerCommunityCard pointer={pointer} />
|
||||||
<Heading size="md">No communities :(</Heading>
|
</ErrorBoundary>
|
||||||
<Text>
|
))}
|
||||||
go find a cool one to join.{" "}
|
</SimpleGrid>
|
||||||
<Link as={RouterLink} to="/communities/explore" color="blue.500">
|
) : (
|
||||||
Explore
|
<Center aspectRatio={3 / 4} flexDirection="column" gap="4">
|
||||||
</Link>
|
<Heading size="md">No communities :(</Heading>
|
||||||
</Text>
|
<Text>
|
||||||
</Center>
|
go find a cool one to join.{" "}
|
||||||
|
<Link as={RouterLink} to="/communities/explore" color="blue.500">
|
||||||
|
Explore
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</VerticalPageLayout>
|
||||||
|
{createModal.isOpen && (
|
||||||
|
<CommunityCreateModal isOpen={createModal.isOpen} onClose={createModal.onClose} onSubmit={createCommunity} />
|
||||||
)}
|
)}
|
||||||
</VerticalPageLayout>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Button, ButtonGroup, Divider, Flex, Heading, Text } from "@chakra-ui/react";
|
import { Button, ButtonGroup, Divider, Flex, Heading, Text, useDisclosure } from "@chakra-ui/react";
|
||||||
import { Outlet, Link as RouterLink, useLocation } from "react-router-dom";
|
import { Outlet, Link as RouterLink, useLocation } from "react-router-dom";
|
||||||
import { Kind, nip19 } from "nostr-tools";
|
import { Kind, nip19 } from "nostr-tools";
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ import { getEventCoordinate, getEventUID } from "../../helpers/nostr/events";
|
|||||||
import { WritingIcon } from "../../components/icons";
|
import { WritingIcon } from "../../components/icons";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||||
|
import CommunityEditModal from "./components/community-edit-modal";
|
||||||
|
|
||||||
function getCommunityPath(community: NostrEvent) {
|
function getCommunityPath(community: NostrEvent) {
|
||||||
return `/c/${encodeURIComponent(getCommunityName(community))}/${nip19.npubEncode(community.pubkey)}`;
|
return `/c/${encodeURIComponent(getCommunityName(community))}/${nip19.npubEncode(community.pubkey)}`;
|
||||||
@@ -35,6 +36,7 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
|
|||||||
const image = getCommunityImage(community);
|
const image = getCommunityImage(community);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { openModal } = useContext(PostModalContext);
|
const { openModal } = useContext(PostModalContext);
|
||||||
|
const editModal = useDisclosure();
|
||||||
const communityCoordinate = getEventCoordinate(community);
|
const communityCoordinate = getEventCoordinate(community);
|
||||||
|
|
||||||
const verticalLayout = useBreakpointValue({ base: true, xl: false });
|
const verticalLayout = useBreakpointValue({ base: true, xl: false });
|
||||||
@@ -50,77 +52,90 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
|
|||||||
if (location.pathname.endsWith("/pending")) active = "pending";
|
if (location.pathname.endsWith("/pending")) active = "pending";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdditionalRelayProvider relays={communityRelays}>
|
<>
|
||||||
<VerticalPageLayout pt={image && "0"}>
|
<AdditionalRelayProvider relays={communityRelays}>
|
||||||
<Flex
|
<VerticalPageLayout pt={image && "0"}>
|
||||||
backgroundImage={getCommunityImage(community)}
|
<Flex
|
||||||
backgroundRepeat="no-repeat"
|
backgroundImage={getCommunityImage(community)}
|
||||||
backgroundSize="cover"
|
backgroundRepeat="no-repeat"
|
||||||
backgroundPosition="center"
|
backgroundSize="cover"
|
||||||
aspectRatio={3 / 1}
|
backgroundPosition="center"
|
||||||
backgroundColor="rgba(0,0,0,0.2)"
|
aspectRatio={3 / 1}
|
||||||
p="4"
|
backgroundColor="rgba(0,0,0,0.2)"
|
||||||
gap="4"
|
p="4"
|
||||||
direction="column"
|
gap="4"
|
||||||
justifyContent="flex-end"
|
direction="column"
|
||||||
textShadow="2px 2px var(--chakra-blur-sm) var(--chakra-colors-blackAlpha-800)"
|
justifyContent="flex-end"
|
||||||
>
|
textShadow="2px 2px var(--chakra-blur-sm) var(--chakra-colors-blackAlpha-800)"
|
||||||
<Heading>{getCommunityName(community)}</Heading>
|
>
|
||||||
<Flex gap="2" alignItems="center">
|
<Heading>{getCommunityName(community)}</Heading>
|
||||||
<UserAvatarLink pubkey={community.pubkey} size="sm" />
|
<Flex gap="2" alignItems="center">
|
||||||
<Text>by</Text>
|
<UserAvatarLink pubkey={community.pubkey} size="sm" />
|
||||||
<UserLink pubkey={community.pubkey} />
|
<Text>by</Text>
|
||||||
</Flex>
|
<UserLink pubkey={community.pubkey} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{verticalLayout && <HorizontalCommunityDetails community={community} w="full" flexShrink={0} />}
|
|
||||||
|
|
||||||
<Flex gap="4" alignItems="flex-start" overflow="hidden">
|
|
||||||
<Flex direction="column" gap="4" flex={1} overflow="hidden">
|
|
||||||
<ButtonGroup size="sm">
|
|
||||||
<Button
|
|
||||||
colorScheme="primary"
|
|
||||||
leftIcon={<WritingIcon />}
|
|
||||||
onClick={() =>
|
|
||||||
openModal({
|
|
||||||
cacheFormKey: communityCoordinate + "-new-post",
|
|
||||||
initCommunity: communityCoordinate,
|
|
||||||
requireSubject: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
New Post
|
|
||||||
</Button>
|
|
||||||
<Divider orientation="vertical" h="2rem" />
|
|
||||||
<Button leftIcon={<TrendUp01 />} isDisabled>
|
|
||||||
Trending
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
leftIcon={<Clock />}
|
|
||||||
as={RouterLink}
|
|
||||||
to={getCommunityPath(community)}
|
|
||||||
replace
|
|
||||||
colorScheme={active === "new" ? "primary" : "gray"}
|
|
||||||
>
|
|
||||||
New
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
leftIcon={<Hourglass03 />}
|
|
||||||
as={RouterLink}
|
|
||||||
to={getCommunityPath(community) + "/pending"}
|
|
||||||
replace
|
|
||||||
colorScheme={active == "pending" ? "primary" : "gray"}
|
|
||||||
>
|
|
||||||
Pending
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
|
|
||||||
<Outlet context={{ community, timeline }} />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{!verticalLayout && <VerticalCommunityDetails community={community} w="full" maxW="xs" flexShrink={0} />}
|
{verticalLayout && (
|
||||||
</Flex>
|
<HorizontalCommunityDetails community={community} w="full" flexShrink={0} onEditClick={editModal.onOpen} />
|
||||||
</VerticalPageLayout>
|
)}
|
||||||
</AdditionalRelayProvider>
|
|
||||||
|
<Flex gap="4" alignItems="flex-start" overflow="hidden">
|
||||||
|
<Flex direction="column" gap="4" flex={1} overflow="hidden">
|
||||||
|
<ButtonGroup size="sm">
|
||||||
|
<Button
|
||||||
|
colorScheme="primary"
|
||||||
|
leftIcon={<WritingIcon />}
|
||||||
|
onClick={() =>
|
||||||
|
openModal({
|
||||||
|
cacheFormKey: communityCoordinate + "-new-post",
|
||||||
|
initCommunity: communityCoordinate,
|
||||||
|
requireSubject: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
New Post
|
||||||
|
</Button>
|
||||||
|
<Divider orientation="vertical" h="2rem" />
|
||||||
|
<Button leftIcon={<TrendUp01 />} isDisabled>
|
||||||
|
Trending
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
leftIcon={<Clock />}
|
||||||
|
as={RouterLink}
|
||||||
|
to={getCommunityPath(community)}
|
||||||
|
replace
|
||||||
|
colorScheme={active === "new" ? "primary" : "gray"}
|
||||||
|
>
|
||||||
|
New
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
leftIcon={<Hourglass03 />}
|
||||||
|
as={RouterLink}
|
||||||
|
to={getCommunityPath(community) + "/pending"}
|
||||||
|
replace
|
||||||
|
colorScheme={active == "pending" ? "primary" : "gray"}
|
||||||
|
>
|
||||||
|
Pending
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
|
<Outlet context={{ community, timeline }} />
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{!verticalLayout && (
|
||||||
|
<VerticalCommunityDetails
|
||||||
|
community={community}
|
||||||
|
w="full"
|
||||||
|
maxW="xs"
|
||||||
|
flexShrink={0}
|
||||||
|
onEditClick={editModal.onOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</VerticalPageLayout>
|
||||||
|
</AdditionalRelayProvider>
|
||||||
|
{editModal && <CommunityEditModal isOpen={editModal.isOpen} onClose={editModal.onClose} community={community} />}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
88
src/views/community/components/community-edit-modal.tsx
Normal file
88
src/views/community/components/community-edit-modal.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { ModalProps, useDisclosure, useToast } from "@chakra-ui/react";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
|
||||||
|
import { useSigningContext } from "../../../providers/signing-provider";
|
||||||
|
import {
|
||||||
|
COMMUNITY_DEFINITION_KIND,
|
||||||
|
getCommunityDescription,
|
||||||
|
getCommunityImage,
|
||||||
|
getCommunityMods,
|
||||||
|
getCommunityName,
|
||||||
|
getCommunityRanking,
|
||||||
|
getCommunityRelays,
|
||||||
|
getCommunityRules,
|
||||||
|
} from "../../../helpers/nostr/communities";
|
||||||
|
import CommunityCreateModal, { FormValues } from "../../communities/components/community-create-modal";
|
||||||
|
import NostrPublishAction from "../../../classes/nostr-publish-action";
|
||||||
|
import clientRelaysService from "../../../services/client-relays";
|
||||||
|
import { unique } from "../../../helpers/array";
|
||||||
|
import replaceableEventLoaderService from "../../../services/replaceable-event-requester";
|
||||||
|
|
||||||
|
export default function CommunityEditModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
community,
|
||||||
|
...props
|
||||||
|
}: Omit<ModalProps, "children"> & { community: NostrEvent }) {
|
||||||
|
const toast = useToast();
|
||||||
|
const { requestSignature } = useSigningContext();
|
||||||
|
|
||||||
|
const defaultValues = useMemo<FormValues>(
|
||||||
|
() => ({
|
||||||
|
name: getCommunityName(community),
|
||||||
|
description: getCommunityDescription(community) || "",
|
||||||
|
banner: getCommunityImage(community) || "",
|
||||||
|
rules: getCommunityRules(community) || "",
|
||||||
|
mods: getCommunityMods(community) || [],
|
||||||
|
relays: getCommunityRelays(community) || [],
|
||||||
|
ranking: getCommunityRanking(community) || "votes",
|
||||||
|
}),
|
||||||
|
[community],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateCommunity = async (values: FormValues) => {
|
||||||
|
try {
|
||||||
|
const draft: DraftNostrEvent = {
|
||||||
|
kind: COMMUNITY_DEFINITION_KIND,
|
||||||
|
created_at: dayjs().unix(),
|
||||||
|
content: "",
|
||||||
|
tags: [["d", getCommunityName(community)]],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const pubkey of values.mods) {
|
||||||
|
draft.tags.push(["p", pubkey, "moderator"]);
|
||||||
|
}
|
||||||
|
for (const url of values.relays) {
|
||||||
|
draft.tags.push(["relay", url]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.description) draft.tags.push(["description", values.description]);
|
||||||
|
if (values.banner) draft.tags.push(["image", values.banner]);
|
||||||
|
if (values.ranking) draft.tags.push(["rank_mode", values.ranking]);
|
||||||
|
|
||||||
|
const signed = await requestSignature(draft);
|
||||||
|
new NostrPublishAction(
|
||||||
|
"Update Community",
|
||||||
|
unique([...clientRelaysService.getWriteUrls(), ...values.relays]),
|
||||||
|
signed,
|
||||||
|
);
|
||||||
|
|
||||||
|
replaceableEventLoaderService.handleEvent(signed);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommunityCreateModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
onSubmit={updateCommunity}
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
isUpdate
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@@ -7,11 +7,15 @@ import { CodeIcon, ExternalLinkIcon, RepostIcon } from "../../../components/icon
|
|||||||
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
||||||
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
|
||||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||||
|
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||||
|
import PencilLine from "../../../components/icons/pencil-line";
|
||||||
|
|
||||||
export default function CommunityMenu({
|
export default function CommunityMenu({
|
||||||
community,
|
community,
|
||||||
|
onEditClick,
|
||||||
...props
|
...props
|
||||||
}: Omit<MenuIconButtonProps, "children"> & { community: NostrEvent }) {
|
}: Omit<MenuIconButtonProps, "children"> & { community: NostrEvent; onEditClick?: () => void }) {
|
||||||
|
const account = useCurrentAccount();
|
||||||
const debugModal = useDisclosure();
|
const debugModal = useDisclosure();
|
||||||
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
|
||||||
|
|
||||||
@@ -25,6 +29,11 @@ export default function CommunityMenu({
|
|||||||
View in app...
|
View in app...
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
{account?.pubkey === community.pubkey && onEditClick && (
|
||||||
|
<MenuItem onClick={onEditClick} icon={<PencilLine />}>
|
||||||
|
Edit Community
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
<MenuItem onClick={() => copyToClipboard("nostr:" + address)} icon={<RepostIcon />}>
|
<MenuItem onClick={() => copyToClipboard("nostr:" + address)} icon={<RepostIcon />}>
|
||||||
Copy Share Link
|
Copy Share Link
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@@ -30,8 +30,9 @@ import CommunityMembersModal from "./community-members-modal";
|
|||||||
|
|
||||||
export default function HorizontalCommunityDetails({
|
export default function HorizontalCommunityDetails({
|
||||||
community,
|
community,
|
||||||
|
onEditClick,
|
||||||
...props
|
...props
|
||||||
}: Omit<CardProps, "children"> & { community: NostrEvent }) {
|
}: Omit<CardProps, "children"> & { community: NostrEvent; onEditClick?: () => void }) {
|
||||||
const membersModal = useDisclosure();
|
const membersModal = useDisclosure();
|
||||||
const communityRelays = getCommunityRelays(community);
|
const communityRelays = getCommunityRelays(community);
|
||||||
const mods = getCommunityMods(community);
|
const mods = getCommunityMods(community);
|
||||||
@@ -47,7 +48,7 @@ export default function HorizontalCommunityDetails({
|
|||||||
<CardBody>
|
<CardBody>
|
||||||
<ButtonGroup float="right">
|
<ButtonGroup float="right">
|
||||||
<CommunityJoinButton community={community} />
|
<CommunityJoinButton community={community} />
|
||||||
<CommunityMenu community={community} aria-label="More" />
|
<CommunityMenu community={community} aria-label="More" onEditClick={onEditClick} />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
{description && (
|
{description && (
|
||||||
<>
|
<>
|
||||||
|
@@ -18,8 +18,9 @@ import { readablizeSats } from "../../../helpers/bolt11";
|
|||||||
|
|
||||||
export default function VerticalCommunityDetails({
|
export default function VerticalCommunityDetails({
|
||||||
community,
|
community,
|
||||||
|
onEditClick,
|
||||||
...props
|
...props
|
||||||
}: Omit<CardProps, "children"> & { community: NostrEvent }) {
|
}: Omit<CardProps, "children"> & { community: NostrEvent; onEditClick?: () => void }) {
|
||||||
const membersModal = useDisclosure();
|
const membersModal = useDisclosure();
|
||||||
const communityRelays = getCommunityRelays(community);
|
const communityRelays = getCommunityRelays(community);
|
||||||
const mods = getCommunityMods(community);
|
const mods = getCommunityMods(community);
|
||||||
@@ -41,7 +42,7 @@ export default function VerticalCommunityDetails({
|
|||||||
)}
|
)}
|
||||||
<ButtonGroup w="full">
|
<ButtonGroup w="full">
|
||||||
<CommunityJoinButton community={community} flex={1} />
|
<CommunityJoinButton community={community} flex={1} />
|
||||||
<CommunityMenu community={community} aria-label="More" />
|
<CommunityMenu community={community} aria-label="More" onEditClick={onEditClick} />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<Box>
|
<Box>
|
||||||
<Heading size="sm" mb="1">
|
<Heading size="sm" mb="1">
|
||||||
|
Reference in New Issue
Block a user