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

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

View File

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

View File

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

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