diff --git a/src/components/NotePostForm.tsx b/src/components/NotePostForm.tsx index a736411..4b8e752 100644 --- a/src/components/NotePostForm.tsx +++ b/src/components/NotePostForm.tsx @@ -1,38 +1,24 @@ -import { - createSignal, - createMemo, - Show, - For, - type Component, - type JSX, - createEffect, - onCleanup, -} from 'solid-js'; +import { createSignal, createMemo, Show, For, type Component, type JSX } from 'solid-js'; import { createMutation } from '@tanstack/solid-query'; -import { Textcomplete } from '@textcomplete/core'; -import { TextareaEditor } from '@textcomplete/textarea'; import ExclamationTriangle from 'heroicons/24/outline/exclamation-triangle.svg'; import FaceSmile from 'heroicons/24/outline/face-smile.svg'; import Photo from 'heroicons/24/outline/photo.svg'; import XMark from 'heroicons/24/outline/x-mark.svg'; import PaperAirplane from 'heroicons/24/solid/paper-airplane.svg'; import uniq from 'lodash/uniq'; -import * as nip19 from 'nostr-tools/nip19'; import { Event as NostrEvent } from 'nostr-tools/pure'; import useEmojiPicker, { EmojiData } from '@/components/useEmojiPicker'; import UserNameDisplay from '@/components/UserDisplayName'; -import useConfig, { CustomEmojiConfig } from '@/core/useConfig'; +import useConfig from '@/core/useConfig'; +import useEmojiComplete from '@/hooks/useEmojiComplete'; import { useTranslation } from '@/i18n/useTranslation'; import { type CreateTextNoteParams } from '@/nostr/builder/createTextNote'; import { textNote } from '@/nostr/event'; import parseTextNote, { ParsedTextNote } from '@/nostr/parseTextNote'; import useCommands from '@/nostr/useCommands'; -import useFollowings from '@/nostr/useFollowings'; -import { UseProfile, useProfiles } from '@/nostr/useProfile'; import usePubkey from '@/nostr/usePubkey'; -import ensureNonNull from '@/utils/ensureNonNull'; import { uploadFiles, uploadNostrBuild } from '@/utils/imageUpload'; // import usePersistStatus from '@/hooks/usePersistStatus'; @@ -92,87 +78,6 @@ const format = (parsed: ParsedTextNote) => { return content.join(''); }; -const useComplete = () => { - const { searchEmojis } = useConfig(); - - const pubkey = usePubkey(); - const { followingPubkeys } = useFollowings(() => - ensureNonNull([pubkey()] as const)(([pubkeyNonNull]) => ({ pubkey: pubkeyNonNull })), - ); - const { searchProfiles } = useProfiles(() => ({ - pubkeys: followingPubkeys(), - })); - - const [elementRef, setElementRef] = createSignal(); - - createEffect(() => { - const el = elementRef(); - if (el == null) return; - - const editor = new TextareaEditor(el); - const textcomplete = new Textcomplete( - editor, - [ - { - id: 'customEmoji', - match: /\B:(\w+)$/, - search: (term, callback) => { - callback(searchEmojis(term)); - }, - template: (config: CustomEmojiConfig) => { - const e = ( -
- {config.shortcode} -
{config.shortcode}
-
- ) as HTMLElement; - return e.outerHTML; - }, - replace: (result: CustomEmojiConfig) => `:${result.shortcode}: `, - }, - { - id: 'profiles', - match: /\B@(.+)$/, - search: (term, callback) => { - callback(searchProfiles(term)); - }, - template: (profile: UseProfile) => { - const e = ( -
- } keyed> - {(url) => icon} - - {profile.profile()?.name} -
- ) as HTMLElement; - return e.outerHTML; - }, - replace: (result: UseProfile) => { - const selectedPubkey = result.pubkey(); - if (selectedPubkey == null) return ''; - return nip19.npubEncode(selectedPubkey); - }, - }, - ], - { - dropdown: { - className: 'bg-bg shadow rounded', - item: { - className: 'cursor-pointer', - activeClassName: 'bg-bg-tertiary cursor-pointer', - }, - }, - }, - ); - - onCleanup(() => { - textcomplete.destroy(); - }); - }); - - return { elementRef: setElementRef }; -}; - const NotePostForm: Component = (props) => { const i18n = useTranslation(); @@ -180,7 +85,7 @@ const NotePostForm: Component = (props) => { let contentWarningReasonRef: HTMLInputElement | undefined; let fileInputRef: HTMLInputElement | undefined; - const { elementRef: completeTextAreaRef } = useComplete(); + const { elementRef: emojiTextAreaRef } = useEmojiComplete(); const [text, setText] = createSignal(''); const [contentWarning, setContentWarning] = createSignal(false); const [contentWarningReason, setContentWarningReason] = createSignal(''); @@ -476,7 +381,7 @@ const NotePostForm: Component = (props) => { ref={(el) => { textAreaRef = el; props.textAreaRef?.(el); - completeTextAreaRef(el); + emojiTextAreaRef(el); }} name="text" class="scrollbar max-h-[40vh] min-h-[4rem] overflow-y-auto rounded-md border border-border bg-bg ring-border placeholder:text-fg-secondary focus:border-border focus:ring-primary" diff --git a/src/hooks/useEmojiComplete.tsx b/src/hooks/useEmojiComplete.tsx new file mode 100644 index 0000000..b5090f1 --- /dev/null +++ b/src/hooks/useEmojiComplete.tsx @@ -0,0 +1,58 @@ +import { createEffect, createSignal, onCleanup } from 'solid-js'; + +import { Textcomplete } from '@textcomplete/core'; +import { TextareaEditor } from '@textcomplete/textarea'; + +import useConfig, { CustomEmojiConfig } from '@/core/useConfig'; + +const useEmojiComplete = () => { + const { searchEmojis } = useConfig(); + + const [elementRef, setElementRef] = createSignal(); + + createEffect(() => { + const el = elementRef(); + if (el == null) return; + + const editor = new TextareaEditor(el); + const textcomplete = new Textcomplete( + editor, + [ + { + id: 'customEmoji', + match: /\B:(\w+)$/, + search: (term, callback) => { + callback(searchEmojis(term)); + }, + template: (config: CustomEmojiConfig) => { + const e = ( +
+ {config.shortcode} +
{config.shortcode}
+
+ ) as HTMLElement; + return e.outerHTML; + }, + replace: (result: CustomEmojiConfig) => `:${result.shortcode}: `, + }, + ], + { + dropdown: { + className: 'bg-bg shadow rounded', + item: { + className: 'cursor-pointer', + activeClassName: 'bg-bg-tertiary cursor-pointer', + }, + }, + }, + ); + + onCleanup(() => { + textcomplete.destroy(); + }); + }); + + return { elementRef: setElementRef }; +}; + +export default useEmojiComplete; diff --git a/src/nostr/useProfile.ts b/src/nostr/useProfile.ts index d1e460a..3a9ac6c 100644 --- a/src/nostr/useProfile.ts +++ b/src/nostr/useProfile.ts @@ -1,12 +1,6 @@ import { createMemo } from 'solid-js'; -import { - createQuery, - useQueryClient, - type CreateQueryResult, - QueryClient, - createQueries, -} from '@tanstack/solid-query'; +import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/solid-query'; import { Event as NostrEvent } from 'nostr-tools/pure'; import { Profile, ProfileWithOtherProperties, safeParseProfile } from '@/nostr/event/Profile'; @@ -19,7 +13,6 @@ export type UseProfileProps = { export type UseProfile = { profile: () => ProfileWithOtherProperties | null; - pubkey: () => string | undefined; event: () => NostrEvent | null | undefined; lud06: () => string | undefined; lud16: () => string | undefined; @@ -37,17 +30,29 @@ export type UseProfiles = { queries: CreateQueryResult[]; }; -const genQueryKey = (props: UseProfileProps | null) => ['useProfile', props] as const; +const useProfile = (propsProvider: () => UseProfileProps | null): UseProfile => { + const queryClient = useQueryClient(); + const props = createMemo(propsProvider); + const genQueryKey = createMemo(() => ['useProfile', props()] as const); + + const query = createQuery(() => ({ + queryKey: genQueryKey(), + queryFn: latestEventQuery>({ + taskProvider: ([, currentProps]) => { + if (currentProps == null) return null; + const { pubkey } = currentProps; + return new BatchedEventsTask({ type: 'Profile', pubkey }); + }, + queryClient, + }), + // Profiles are updated occasionally, so a short staleTime is used here. + // gcTime is long so that the user see profiles instantly. + staleTime: 5 * 60 * 1000, // 5 min + gcTime: 3 * 24 * 60 * 60 * 1000, // 3 days + refetchInterval: 5 * 60 * 1000, // 5 min + refetchOnWindowFocus: false, + })); -const buildMethod = ({ - props, - query, - queryClient, -}: { - props: () => UseProfileProps | null; - query: CreateQueryResult; - queryClient: QueryClient; -}): UseProfile => { const event = () => query.data; const profile = createMemo((): Profile | null => { @@ -71,88 +76,9 @@ const buildMethod = ({ const isZapConfigured = (): boolean => lud06() != null || lud16() != null; const invalidateProfile = (): Promise => - queryClient.invalidateQueries({ queryKey: genQueryKey(props()) }); + queryClient.invalidateQueries({ queryKey: genQueryKey() }); - return { - profile, - pubkey: () => props()?.pubkey, - lud06, - lud16, - event, - isZapConfigured, - invalidateProfile, - query, - }; -}; - -const queryOptions = ({ - props, - queryClient, -}: { - props: () => UseProfileProps | null; - queryClient: QueryClient; -}) => ({ - queryKey: genQueryKey(props()), - queryFn: latestEventQuery>({ - taskProvider: ([, currentProps]) => { - if (currentProps == null) return null; - const { pubkey } = currentProps; - return new BatchedEventsTask({ type: 'Profile', pubkey }); - }, - queryClient, - }), - // Profiles are updated occasionally, so a short staleTime is used here. - // gcTime is long so that the user see profiles instantly. - staleTime: 5 * 60 * 1000, // 5 min - gcTime: 3 * 24 * 60 * 60 * 1000, // 3 days - refetchInterval: 5 * 60 * 1000, // 5 min - refetchOnWindowFocus: false, -}); - -const useProfile = (propsProvider: () => UseProfileProps | null): UseProfile => { - const queryClient = useQueryClient(); - const props = createMemo(propsProvider); - const query = createQuery(() => queryOptions({ props, queryClient })); - - return buildMethod({ props, query, queryClient }); -}; - -export const useProfiles = (propsProvider: () => UseProfilesProps) => { - const queryClient = useQueryClient(); - const props = createMemo(propsProvider); - - const queries = createQueries(() => ({ - queries: props().pubkeys.map((pubkey) => - queryOptions({ - props: () => ({ pubkey }), - queryClient, - }), - ), - })); - - const profiles = createMemo((): UseProfile[] => - queries.map((query, i) => - buildMethod({ - props: () => ({ pubkey: props().pubkeys[i] }), - query, - queryClient, - }), - ), - ); - - const searchProfiles = (query: string) => - profiles().filter( - (profile) => - profile.profile()?.name?.includes(query) || - profile.profile()?.display_name?.includes(query) || - profile.profile()?.nip05?.includes(query), - ); - - return { - profiles, - searchProfiles, - queries, - }; + return { profile, lud06, lud16, event, isZapConfigured, invalidateProfile, query }; }; export default useProfile;