mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-20 13:01:07 +02:00
allow user to edit community moderators
This commit is contained in:
36
src/components/npub-autocomplete.tsx
Normal file
36
src/components/npub-autocomplete.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Input, InputProps } from "@chakra-ui/react";
|
||||||
|
import { forwardRef } from "react";
|
||||||
|
import { useAsync } from "react-use";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
|
||||||
|
import { useUserSearchDirectoryContext } from "../providers/user-directory-provider";
|
||||||
|
import userMetadataService from "../services/user-metadata";
|
||||||
|
import { getUserDisplayName } from "../helpers/user-metadata";
|
||||||
|
|
||||||
|
const NpubAutocomplete = forwardRef<HTMLInputElement, InputProps>(({ value, ...props }, ref) => {
|
||||||
|
const getDirectory = useUserSearchDirectoryContext();
|
||||||
|
|
||||||
|
const { value: users } = useAsync(async () => {
|
||||||
|
const dir = await getDirectory();
|
||||||
|
return dir.map(({ pubkey }) => ({ pubkey, metadata: userMetadataService.getSubject(pubkey).value }));
|
||||||
|
}, [getDirectory]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Input placeholder="npub..." list="users" value={value} {...props} ref={ref} />
|
||||||
|
{users && (
|
||||||
|
<datalist id="users">
|
||||||
|
{users
|
||||||
|
.filter((p) => !!p.metadata)
|
||||||
|
.map(({ metadata, pubkey }) => (
|
||||||
|
<option key={pubkey} value={nip19.npubEncode(pubkey)}>
|
||||||
|
{getUserDisplayName(metadata, pubkey)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default NpubAutocomplete;
|
@@ -2,7 +2,6 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
Heading,
|
Heading,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
|
||||||
NumberDecrementStepper,
|
NumberDecrementStepper,
|
||||||
NumberIncrementStepper,
|
NumberIncrementStepper,
|
||||||
NumberInput,
|
NumberInput,
|
||||||
@@ -12,18 +11,14 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { CloseIcon } from "@chakra-ui/icons";
|
import { CloseIcon } from "@chakra-ui/icons";
|
||||||
import { nip19 } from "nostr-tools";
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { EventSplit } from "../../helpers/nostr/zaps";
|
import { EventSplit } from "../../helpers/nostr/zaps";
|
||||||
import { AddIcon } from "../icons";
|
import { AddIcon } from "../icons";
|
||||||
import { useUserSearchDirectoryContext } from "../../providers/user-directory-provider";
|
|
||||||
import { useAsync } from "react-use";
|
|
||||||
import { getUserDisplayName } from "../../helpers/user-metadata";
|
|
||||||
import userMetadataService from "../../services/user-metadata";
|
|
||||||
import { normalizeToHex } from "../../helpers/nip19";
|
import { normalizeToHex } from "../../helpers/nip19";
|
||||||
import UserAvatar from "../user-avatar";
|
import UserAvatar from "../user-avatar";
|
||||||
import { UserLink } from "../user-link";
|
import { UserLink } from "../user-link";
|
||||||
|
import NpubAutocomplete from "../npub-autocomplete";
|
||||||
|
|
||||||
function getRemainingPercent(split: EventSplit) {
|
function getRemainingPercent(split: EventSplit) {
|
||||||
return Math.round((1 - split.reduce((v, p) => v + p.percent, 0)) * 100) / 100;
|
return Math.round((1 - split.reduce((v, p) => v + p.percent, 0)) * 100) / 100;
|
||||||
@@ -58,12 +53,6 @@ function AddUserForm({
|
|||||||
});
|
});
|
||||||
watch("percent");
|
watch("percent");
|
||||||
|
|
||||||
const getDirectory = useUserSearchDirectoryContext();
|
|
||||||
const { value: users } = useAsync(async () => {
|
|
||||||
const dir = await getDirectory();
|
|
||||||
return dir.map(({ pubkey }) => ({ pubkey, metadata: userMetadataService.getSubject(pubkey).value }));
|
|
||||||
}, [getDirectory]);
|
|
||||||
|
|
||||||
const submit = handleSubmit((values) => {
|
const submit = handleSubmit((values) => {
|
||||||
try {
|
try {
|
||||||
const pubkey = normalizeToHex(values.pubkey);
|
const pubkey = normalizeToHex(values.pubkey);
|
||||||
@@ -78,18 +67,7 @@ function AddUserForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex as="form" gap="2" onSubmit={submit}>
|
<Flex as="form" gap="2" onSubmit={submit}>
|
||||||
<Input placeholder="npub..." list="users" {...register("pubkey", { required: true, validate: validateNpub })} />
|
<NpubAutocomplete {...register("pubkey", { required: true, validate: validateNpub })} />
|
||||||
{users && (
|
|
||||||
<datalist id="users">
|
|
||||||
{users
|
|
||||||
.filter((p) => !!p.metadata)
|
|
||||||
.map(({ metadata, pubkey }) => (
|
|
||||||
<option key={pubkey} value={nip19.npubEncode(pubkey)}>
|
|
||||||
{getUserDisplayName(metadata, pubkey)}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</datalist>
|
|
||||||
)}
|
|
||||||
<NumberInput
|
<NumberInput
|
||||||
step={1}
|
step={1}
|
||||||
min={1}
|
min={1}
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -7,9 +8,8 @@ import {
|
|||||||
FormHelperText,
|
FormHelperText,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
IconButton,
|
IconButton,
|
||||||
Image,
|
IconButtonProps,
|
||||||
Input,
|
Input,
|
||||||
Link,
|
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
ModalCloseButton,
|
ModalCloseButton,
|
||||||
@@ -26,18 +26,23 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||||
import UserAvatar from "../../../components/user-avatar";
|
import UserAvatar from "../../../components/user-avatar";
|
||||||
import { UserLink } from "../../../components/user-link";
|
import { UserLink } from "../../../components/user-link";
|
||||||
import { TrashIcon, UploadImageIcon } from "../../../components/icons";
|
import { TrashIcon } from "../../../components/icons";
|
||||||
import Upload01 from "../../../components/icons/upload-01";
|
import Upload01 from "../../../components/icons/upload-01";
|
||||||
import Upload02 from "../../../components/icons/upload-02";
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { nostrBuildUploadImage } from "../../../helpers/nostr-build";
|
import { nostrBuildUploadImage } from "../../../helpers/nostr-build";
|
||||||
import { useSigningContext } from "../../../providers/signing-provider";
|
import { useSigningContext } from "../../../providers/signing-provider";
|
||||||
import { RelayUrlInput } from "../../../components/relay-url-input";
|
import { RelayUrlInput } from "../../../components/relay-url-input";
|
||||||
import { normalizeRelayUrl, safeRelayUrl } from "../../../helpers/url";
|
import { safeRelayUrl } from "../../../helpers/url";
|
||||||
import { RelayFavicon } from "../../../components/relay-favicon";
|
import { RelayFavicon } from "../../../components/relay-favicon";
|
||||||
|
import NpubAutocomplete from "../../../components/npub-autocomplete";
|
||||||
|
import { normalizeToHex } from "../../../helpers/nip19";
|
||||||
|
|
||||||
|
function RemoveButton({ ...props }: IconButtonProps) {
|
||||||
|
return <IconButton icon={<TrashIcon />} size="sm" colorScheme="red" variant="ghost" ml="auto" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
export type FormValues = {
|
export type FormValues = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -113,6 +118,22 @@ export default function CommunityCreateModal({
|
|||||||
[setValue, getValues, requestSignature, toast],
|
[setValue, getValues, requestSignature, toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [modInput, setModInput] = useState("");
|
||||||
|
const addMod = () => {
|
||||||
|
if (!modInput) return;
|
||||||
|
const pubkey = normalizeToHex(modInput);
|
||||||
|
if (pubkey) {
|
||||||
|
setValue("mods", getValues("mods").concat(pubkey));
|
||||||
|
}
|
||||||
|
setModInput("");
|
||||||
|
};
|
||||||
|
const removeMod = (pubkey: string) => {
|
||||||
|
setValue(
|
||||||
|
"mods",
|
||||||
|
getValues("mods").filter((p) => p !== pubkey),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const [relayInput, setRelayInput] = useState("");
|
const [relayInput, setRelayInput] = useState("");
|
||||||
const addRelay = () => {
|
const addRelay = () => {
|
||||||
if (!relayInput) return;
|
if (!relayInput) return;
|
||||||
@@ -216,12 +237,25 @@ export default function CommunityCreateModal({
|
|||||||
|
|
||||||
<FormControl isInvalid={!!errors.mods}>
|
<FormControl isInvalid={!!errors.mods}>
|
||||||
<FormLabel>Moderators</FormLabel>
|
<FormLabel>Moderators</FormLabel>
|
||||||
|
<Flex direction="column" gap="2" pb="2">
|
||||||
{getValues().mods.map((pubkey) => (
|
{getValues().mods.map((pubkey) => (
|
||||||
<Flex gap="2" alignItems="center" key={pubkey}>
|
<Flex gap="2" alignItems="center" key={pubkey}>
|
||||||
<UserAvatar pubkey={pubkey} size="sm" />
|
<UserAvatar pubkey={pubkey} size="sm" />
|
||||||
<UserLink pubkey={pubkey} fontWeight="bold" />
|
<UserLink pubkey={pubkey} fontWeight="bold" />
|
||||||
|
<RemoveButton
|
||||||
|
aria-label={`Remove moderator`}
|
||||||
|
title={`Remove moderator`}
|
||||||
|
onClick={() => removeMod(pubkey)}
|
||||||
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
))}
|
))}
|
||||||
|
</Flex>
|
||||||
|
<Flex gap="2">
|
||||||
|
<NpubAutocomplete value={modInput} onChange={(e) => setModInput(e.target.value)} />
|
||||||
|
<Button isDisabled={!modInput} onClick={addMod}>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormControl isInvalid={!!errors.mods}>
|
<FormControl isInvalid={!!errors.mods}>
|
||||||
@@ -249,16 +283,7 @@ export default function CommunityCreateModal({
|
|||||||
<Text fontWeight="bold" isTruncated>
|
<Text fontWeight="bold" isTruncated>
|
||||||
{url}
|
{url}
|
||||||
</Text>
|
</Text>
|
||||||
<IconButton
|
<RemoveButton aria-label={`Remove ${url}`} title={`Remove ${url}`} onClick={() => removeRelay(url)} />
|
||||||
icon={<TrashIcon />}
|
|
||||||
aria-label={`Remove ${url}`}
|
|
||||||
title={`Remove ${url}`}
|
|
||||||
onClick={() => removeRelay(url)}
|
|
||||||
size="sm"
|
|
||||||
colorScheme="red"
|
|
||||||
variant="ghost"
|
|
||||||
ml="auto"
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
Reference in New Issue
Block a user