add quick reaction option

This commit is contained in:
hzrd149 2023-10-17 06:08:57 -05:00
parent 44985aeb83
commit 9569281b6e
6 changed files with 123 additions and 35 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add option to customize quick reactions

View File

@ -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";

View File

@ -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) => (

View File

@ -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";

View File

@ -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 {

View File

@ -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