diff --git a/.changeset/shaggy-carrots-destroy.md b/.changeset/shaggy-carrots-destroy.md
new file mode 100644
index 000000000..9cdf0d7da
--- /dev/null
+++ b/.changeset/shaggy-carrots-destroy.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Add option to customize quick reactions
diff --git a/src/components/note/components/reaction-button.tsx b/src/components/note/components/reaction-button.tsx
index 006796686..98b83ed55 100644
--- a/src/components/note/components/reaction-button.tsx
+++ b/src/components/note/components/reaction-button.tsx
@@ -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";
diff --git a/src/components/reaction-picker.tsx b/src/components/reaction-picker.tsx
index e70b2e89d..9218f709f 100644
--- a/src/components/reaction-picker.tsx
+++ b/src/components/reaction-picker.tsx
@@ -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 (
@@ -52,26 +54,15 @@ export default function ReactionPicker({ onSelect }: ReactionPickerProps) {
size="sm"
onClick={() => onSelect("-")}
/>
- 🤙}
- aria-label="Shaka"
- variant="outline"
- size="sm"
- onClick={() => onSelect("🤙")}
- />
- 🫂}
- aria-label="Hug"
- variant="outline"
- size="sm"
- onClick={() => onSelect("🫂")}
- />
-
-
-
-
+ {quickReactions.map((emoji) => (
+ {emoji}}
+ aria-label="Shaka"
+ variant="outline"
+ size="sm"
+ onClick={() => onSelect(emoji)}
+ />
+ ))}
{favoritePacks &&
getPackCordsFromFavorites(favoritePacks).map((cord) => (
diff --git a/src/providers/emoji-provider.tsx b/src/providers/emoji-provider.tsx
index 8eed4bd63..0f51988b1 100644
--- a/src/providers/emoji-provider.tsx
+++ b/src/providers/emoji-provider.tsx
@@ -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";
diff --git a/src/services/settings/migrations.ts b/src/services/settings/migrations.ts
index d09e55eda..97a6880d1 100644
--- a/src/services/settings/migrations.ts
+++ b/src/services/settings/migrations.ts
@@ -31,6 +31,10 @@ export type AppSettingsV2 = Omit & {
version: 2;
theme: string;
};
+export type AppSettingsV3 = Omit & {
+ 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 {
diff --git a/src/views/settings/display-settings.tsx b/src/views/settings/display-settings.tsx
index 2f834ea60..33b166bd6 100644
--- a/src/views/settings/display-settings.tsx
+++ b/src/views/settings/display-settings.tsx
@@ -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();
+ const { register, setValue, getValues, watch } = useFormContext();
+ 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 (
@@ -63,6 +103,51 @@ export default function DisplaySettings() {
The primary color of the theme
+
+
+ Quick Reactions
+
+
+ {getValues().quickReactions.map((char, i) => (
+
+ {char}
+ {emojiPicker.isOpen && removeEmoji(char)} />}
+
+ ))}
+ {!emojiPicker.isOpen && (
+ }>
+ Customize
+
+ )}
+
+ {emojiPicker.isOpen && (
+ <>
+
+ setEmojiSearch(e.target.value)}
+ mb="2"
+ />
+
+ {filteredEmojis.map((emoji) => (
+ {emoji.char}}
+ aria-label={`Add ${emoji.name}`}
+ title={`Add ${emoji.name}`}
+ variant="outline"
+ size="sm"
+ fontSize="lg"
+ onClick={() => addEmoji(emoji.char)}
+ />
+ ))}
+
+ >
+ )}
+
Max Page width