mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-09 20:29:17 +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,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@chakra-ui/react";
|
} 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 useEventReactions from "../../../hooks/use-event-reactions";
|
||||||
import { useSigningContext } from "../../../providers/signing-provider";
|
import { useSigningContext } from "../../../providers/signing-provider";
|
||||||
import clientRelaysService from "../../../services/client-relays";
|
import clientRelaysService from "../../../services/client-relays";
|
||||||
import eventReactionsService from "../../../services/event-reactions";
|
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 NostrPublishAction from "../../../classes/nostr-publish-action";
|
||||||
import { AddReactionIcon } from "../../icons";
|
import { AddReactionIcon } from "../../icons";
|
||||||
import ReactionPicker from "../../reaction-picker";
|
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 { DislikeIcon, LikeIcon } from "./icons";
|
||||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||||
import useReplaceableEvent from "../hooks/use-replaceable-event";
|
import useReplaceableEvent from "../hooks/use-replaceable-event";
|
||||||
import { getEmojisFromPack, getPackCordsFromFavorites, getPackName } from "../helpers/nostr/emoji-packs";
|
import { getEmojisFromPack, getPackCordsFromFavorites, getPackName } from "../helpers/nostr/emoji-packs";
|
||||||
import useFavoriteEmojiPacks from "../hooks/use-favorite-emoji-packs";
|
import useFavoriteEmojiPacks from "../hooks/use-favorite-emoji-packs";
|
||||||
|
import useAppSettings from "../hooks/use-app-settings";
|
||||||
|
|
||||||
export type ReactionPickerProps = {
|
export type ReactionPickerProps = {
|
||||||
onSelect: (emoji: string, url?: string) => void;
|
onSelect: (emoji: string, url?: string) => void;
|
||||||
@ -40,6 +41,7 @@ function EmojiPack({ cord, onSelect }: { cord: string; onSelect: ReactionPickerP
|
|||||||
export default function ReactionPicker({ onSelect }: ReactionPickerProps) {
|
export default function ReactionPicker({ onSelect }: ReactionPickerProps) {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
const favoritePacks = useFavoriteEmojiPacks(account?.pubkey);
|
const favoritePacks = useFavoriteEmojiPacks(account?.pubkey);
|
||||||
|
const { quickReactions } = useAppSettings();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
@ -52,26 +54,15 @@ export default function ReactionPicker({ onSelect }: ReactionPickerProps) {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onSelect("-")}
|
onClick={() => onSelect("-")}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
{quickReactions.map((emoji) => (
|
||||||
icon={<span>🤙</span>}
|
<IconButton
|
||||||
aria-label="Shaka"
|
icon={<span>{emoji}</span>}
|
||||||
variant="outline"
|
aria-label="Shaka"
|
||||||
size="sm"
|
variant="outline"
|
||||||
onClick={() => onSelect("🤙")}
|
size="sm"
|
||||||
/>
|
onClick={() => onSelect(emoji)}
|
||||||
<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>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
{favoritePacks &&
|
{favoritePacks &&
|
||||||
getPackCordsFromFavorites(favoritePacks).map((cord) => (
|
getPackCordsFromFavorites(favoritePacks).map((cord) => (
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { PropsWithChildren, createContext, useContext } from "react";
|
import { PropsWithChildren, createContext, useContext } from "react";
|
||||||
import { lib } from "emojilib";
|
import { lib } from "emojilib";
|
||||||
|
|
||||||
import useReplaceableEvents from "../hooks/use-replaceable-events";
|
import useReplaceableEvents from "../hooks/use-replaceable-events";
|
||||||
import { useCurrentAccount } from "../hooks/use-current-account";
|
import { useCurrentAccount } from "../hooks/use-current-account";
|
||||||
import { isEmojiTag } from "../types/nostr-event";
|
import { isEmojiTag } from "../types/nostr-event";
|
||||||
|
@ -31,6 +31,10 @@ export type AppSettingsV2 = Omit<AppSettingsV1, "version"> & {
|
|||||||
version: 2;
|
version: 2;
|
||||||
theme: string;
|
theme: string;
|
||||||
};
|
};
|
||||||
|
export type AppSettingsV3 = Omit<AppSettingsV2, "version"> & {
|
||||||
|
version: 3;
|
||||||
|
quickReactions: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export function isV0(settings: { version: number }): settings is AppSettingsV0 {
|
export function isV0(settings: { version: number }): settings is AppSettingsV0 {
|
||||||
return settings.version === undefined || settings.version === 0;
|
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 {
|
export function isV2(settings: { version: number }): settings is AppSettingsV2 {
|
||||||
return settings.version === 2;
|
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 = {
|
export const defaultSettings: AppSettings = {
|
||||||
version: 2,
|
version: 3,
|
||||||
theme: "default",
|
theme: "default",
|
||||||
colorMode: "system",
|
colorMode: "system",
|
||||||
maxPageWidth: "none",
|
maxPageWidth: "none",
|
||||||
@ -55,6 +62,8 @@ export const defaultSettings: AppSettings = {
|
|||||||
showReactions: true,
|
showReactions: true,
|
||||||
showSignatureVerification: false,
|
showSignatureVerification: false,
|
||||||
|
|
||||||
|
quickReactions: ["🤙", "❤️", "🤣", "😍", "🔥"],
|
||||||
|
|
||||||
autoPayWithWebLN: true,
|
autoPayWithWebLN: true,
|
||||||
customZapAmounts: "50,200,500,1000,2000,5000",
|
customZapAmounts: "50,200,500,1000,2000,5000",
|
||||||
|
|
||||||
@ -67,11 +76,11 @@ export const defaultSettings: AppSettings = {
|
|||||||
youtubeRedirect: undefined,
|
youtubeRedirect: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function upgradeSettings(settings: { version: number }): AppSettings | null {
|
export function upgradeSettings(settings: { version: number }): AppSettings {
|
||||||
if (isV0(settings)) return { ...settings, version: 2, maxPageWidth: "none", theme: "default" };
|
if (isV0(settings)) return { ...defaultSettings, ...settings, version: 3 };
|
||||||
if (isV1(settings)) return { ...settings, version: 2, theme: "default" };
|
if (isV1(settings)) return { ...defaultSettings, ...settings, version: 3 };
|
||||||
if (isV2(settings)) return settings;
|
if (isV2(settings)) return { ...defaultSettings, ...settings, version: 3 };
|
||||||
return null;
|
return settings as AppSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseAppSettings(event: NostrEvent): 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 {
|
import {
|
||||||
Flex,
|
Flex,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -13,12 +14,51 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
Textarea,
|
Textarea,
|
||||||
|
Divider,
|
||||||
|
Tag,
|
||||||
|
TagLabel,
|
||||||
|
TagCloseButton,
|
||||||
|
useDisclosure,
|
||||||
|
IconButton,
|
||||||
|
Button,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
|
import { matchSorter } from "match-sorter";
|
||||||
|
|
||||||
import { AppSettings } from "../../services/settings/migrations";
|
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() {
|
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 (
|
return (
|
||||||
<AccordionItem>
|
<AccordionItem>
|
||||||
@ -63,6 +103,51 @@ export default function DisplaySettings() {
|
|||||||
<span>The primary color of the theme</span>
|
<span>The primary color of the theme</span>
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormControl>
|
</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>
|
<FormControl>
|
||||||
<FormLabel htmlFor="maxPageWidth" mb="0">
|
<FormLabel htmlFor="maxPageWidth" mb="0">
|
||||||
Max Page width
|
Max Page width
|
||||||
|
Loading…
x
Reference in New Issue
Block a user