mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-07 03:18:02 +02:00
add quick reaction option
This commit is contained in:
parent
44985aeb83
commit
9569281b6e
5
.changeset/shaggy-carrots-destroy.md
Normal file
5
.changeset/shaggy-carrots-destroy.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add option to customize quick reactions
|
@ -7,15 +7,12 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@chakra-ui/react";
|
||||
import dayjs from "dayjs";
|
||||
import { Kind } from "nostr-tools";
|
||||
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import useEventReactions from "../../../hooks/use-event-reactions";
|
||||
import { useSigningContext } from "../../../providers/signing-provider";
|
||||
import clientRelaysService from "../../../services/client-relays";
|
||||
import eventReactionsService from "../../../services/event-reactions";
|
||||
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import NostrPublishAction from "../../../classes/nostr-publish-action";
|
||||
import { AddReactionIcon } from "../../icons";
|
||||
import ReactionPicker from "../../reaction-picker";
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { Button, Divider, Flex, IconButton, Image, Input, Text } from "@chakra-ui/react";
|
||||
import { Divider, Flex, IconButton, Image, Text } from "@chakra-ui/react";
|
||||
|
||||
import { DislikeIcon, LikeIcon } from "./icons";
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
import useReplaceableEvent from "../hooks/use-replaceable-event";
|
||||
import { getEmojisFromPack, getPackCordsFromFavorites, getPackName } from "../helpers/nostr/emoji-packs";
|
||||
import useFavoriteEmojiPacks from "../hooks/use-favorite-emoji-packs";
|
||||
import useAppSettings from "../hooks/use-app-settings";
|
||||
|
||||
export type ReactionPickerProps = {
|
||||
onSelect: (emoji: string, url?: string) => void;
|
||||
@ -40,6 +41,7 @@ function EmojiPack({ cord, onSelect }: { cord: string; onSelect: ReactionPickerP
|
||||
export default function ReactionPicker({ onSelect }: ReactionPickerProps) {
|
||||
const account = useCurrentAccount();
|
||||
const favoritePacks = useFavoriteEmojiPacks(account?.pubkey);
|
||||
const { quickReactions } = useAppSettings();
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
@ -52,26 +54,15 @@ export default function ReactionPicker({ onSelect }: ReactionPickerProps) {
|
||||
size="sm"
|
||||
onClick={() => onSelect("-")}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<span>🤙</span>}
|
||||
aria-label="Shaka"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onSelect("🤙")}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<span>🫂</span>}
|
||||
aria-label="Hug"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onSelect("🫂")}
|
||||
/>
|
||||
<Flex>
|
||||
<Input placeholder="🔥" display="inline" size="sm" minW="2rem" w="5rem" />
|
||||
<Button variant="solid" colorScheme="primary" size="sm">
|
||||
Add
|
||||
</Button>
|
||||
</Flex>
|
||||
{quickReactions.map((emoji) => (
|
||||
<IconButton
|
||||
icon={<span>{emoji}</span>}
|
||||
aria-label="Shaka"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onSelect(emoji)}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
{favoritePacks &&
|
||||
getPackCordsFromFavorites(favoritePacks).map((cord) => (
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { PropsWithChildren, createContext, useContext } from "react";
|
||||
import { lib } from "emojilib";
|
||||
|
||||
import useReplaceableEvents from "../hooks/use-replaceable-events";
|
||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||
import { isEmojiTag } from "../types/nostr-event";
|
||||
|
@ -31,6 +31,10 @@ export type AppSettingsV2 = Omit<AppSettingsV1, "version"> & {
|
||||
version: 2;
|
||||
theme: string;
|
||||
};
|
||||
export type AppSettingsV3 = Omit<AppSettingsV2, "version"> & {
|
||||
version: 3;
|
||||
quickReactions: string[];
|
||||
};
|
||||
|
||||
export function isV0(settings: { version: number }): settings is AppSettingsV0 {
|
||||
return settings.version === undefined || settings.version === 0;
|
||||
@ -41,11 +45,14 @@ export function isV1(settings: { version: number }): settings is AppSettingsV1 {
|
||||
export function isV2(settings: { version: number }): settings is AppSettingsV2 {
|
||||
return settings.version === 2;
|
||||
}
|
||||
export function isV3(settings: { version: number }): settings is AppSettingsV3 {
|
||||
return settings.version === 3;
|
||||
}
|
||||
|
||||
export type AppSettings = AppSettingsV2;
|
||||
export type AppSettings = AppSettingsV3;
|
||||
|
||||
export const defaultSettings: AppSettings = {
|
||||
version: 2,
|
||||
version: 3,
|
||||
theme: "default",
|
||||
colorMode: "system",
|
||||
maxPageWidth: "none",
|
||||
@ -55,6 +62,8 @@ export const defaultSettings: AppSettings = {
|
||||
showReactions: true,
|
||||
showSignatureVerification: false,
|
||||
|
||||
quickReactions: ["🤙", "❤️", "🤣", "😍", "🔥"],
|
||||
|
||||
autoPayWithWebLN: true,
|
||||
customZapAmounts: "50,200,500,1000,2000,5000",
|
||||
|
||||
@ -67,11 +76,11 @@ export const defaultSettings: AppSettings = {
|
||||
youtubeRedirect: undefined,
|
||||
};
|
||||
|
||||
export function upgradeSettings(settings: { version: number }): AppSettings | null {
|
||||
if (isV0(settings)) return { ...settings, version: 2, maxPageWidth: "none", theme: "default" };
|
||||
if (isV1(settings)) return { ...settings, version: 2, theme: "default" };
|
||||
if (isV2(settings)) return settings;
|
||||
return null;
|
||||
export function upgradeSettings(settings: { version: number }): AppSettings {
|
||||
if (isV0(settings)) return { ...defaultSettings, ...settings, version: 3 };
|
||||
if (isV1(settings)) return { ...defaultSettings, ...settings, version: 3 };
|
||||
if (isV2(settings)) return { ...defaultSettings, ...settings, version: 3 };
|
||||
return settings as AppSettings;
|
||||
}
|
||||
|
||||
export function parseAppSettings(event: NostrEvent): AppSettings {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useMemo, useState } from "react";
|
||||
import { UseControllerProps, useController, useFormContext } from "react-hook-form";
|
||||
import {
|
||||
Flex,
|
||||
FormControl,
|
||||
@ -13,12 +14,51 @@ import {
|
||||
Input,
|
||||
Select,
|
||||
Textarea,
|
||||
Divider,
|
||||
Tag,
|
||||
TagLabel,
|
||||
TagCloseButton,
|
||||
useDisclosure,
|
||||
IconButton,
|
||||
Button,
|
||||
} from "@chakra-ui/react";
|
||||
import { matchSorter } from "match-sorter";
|
||||
|
||||
import { AppSettings } from "../../services/settings/migrations";
|
||||
import { AppearanceIcon } from "../../components/icons";
|
||||
import { AppearanceIcon, EditIcon } from "../../components/icons";
|
||||
import { useContextEmojis } from "../../providers/emoji-provider";
|
||||
|
||||
export default function DisplaySettings() {
|
||||
const { register } = useFormContext<AppSettings>();
|
||||
const { register, setValue, getValues, watch } = useFormContext<AppSettings>();
|
||||
const emojiPicker = useDisclosure();
|
||||
|
||||
const emojis = useContextEmojis();
|
||||
const [emojiSearch, setEmojiSearch] = useState("");
|
||||
|
||||
watch("quickReactions");
|
||||
const filteredEmojis = useMemo(() => {
|
||||
const values = getValues();
|
||||
if (emojiSearch.trim()) {
|
||||
const noCustom = emojis.filter((e) => e.char && !e.url && !values.quickReactions.includes(e.char));
|
||||
return matchSorter(noCustom, emojiSearch.trim(), { keys: ["keywords"] }).slice(0, 10);
|
||||
}
|
||||
return [];
|
||||
}, [emojiSearch, getValues().quickReactions]);
|
||||
|
||||
const addEmoji = (char: string) => {
|
||||
const values = getValues();
|
||||
if (values.quickReactions.includes(char)) return;
|
||||
setValue("quickReactions", values.quickReactions.concat(char), { shouldTouch: true, shouldDirty: true });
|
||||
};
|
||||
const removeEmoji = (char: string) => {
|
||||
const values = getValues();
|
||||
if (!values.quickReactions.includes(char)) return;
|
||||
setValue(
|
||||
"quickReactions",
|
||||
values.quickReactions.filter((e) => e !== char),
|
||||
{ shouldTouch: true, shouldDirty: true },
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AccordionItem>
|
||||
@ -63,6 +103,51 @@ export default function DisplaySettings() {
|
||||
<span>The primary color of the theme</span>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="quickReactions" mb="0">
|
||||
Quick Reactions
|
||||
</FormLabel>
|
||||
<Flex gap="2" wrap="wrap">
|
||||
{getValues().quickReactions.map((char, i) => (
|
||||
<Tag key={char + i}>
|
||||
<TagLabel>{char}</TagLabel>
|
||||
{emojiPicker.isOpen && <TagCloseButton onClick={() => removeEmoji(char)} />}
|
||||
</Tag>
|
||||
))}
|
||||
{!emojiPicker.isOpen && (
|
||||
<Button size="sm" onClick={emojiPicker.onOpen} leftIcon={<EditIcon />}>
|
||||
Customize
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
{emojiPicker.isOpen && (
|
||||
<>
|
||||
<Divider my="2" />
|
||||
<Input
|
||||
type="search"
|
||||
w="sm"
|
||||
h="8"
|
||||
value={emojiSearch}
|
||||
onChange={(e) => setEmojiSearch(e.target.value)}
|
||||
mb="2"
|
||||
/>
|
||||
<Flex gap="2" wrap="wrap">
|
||||
{filteredEmojis.map((emoji) => (
|
||||
<IconButton
|
||||
key={emoji.char}
|
||||
icon={<span>{emoji.char}</span>}
|
||||
aria-label={`Add ${emoji.name}`}
|
||||
title={`Add ${emoji.name}`}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
fontSize="lg"
|
||||
onClick={() => addEmoji(emoji.char)}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="maxPageWidth" mb="0">
|
||||
Max Page width
|
||||
|
Loading…
x
Reference in New Issue
Block a user