Revert "feat: pubkey completion"

This reverts commit 9ad7e34ae3.
This commit is contained in:
Shusui MOYATANI
2024-02-25 17:55:06 +09:00
parent 9ad7e34ae3
commit 57893898e9
3 changed files with 88 additions and 199 deletions

View File

@@ -1,38 +1,24 @@
import { import { createSignal, createMemo, Show, For, type Component, type JSX } from 'solid-js';
createSignal,
createMemo,
Show,
For,
type Component,
type JSX,
createEffect,
onCleanup,
} from 'solid-js';
import { createMutation } from '@tanstack/solid-query'; 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 ExclamationTriangle from 'heroicons/24/outline/exclamation-triangle.svg';
import FaceSmile from 'heroicons/24/outline/face-smile.svg'; import FaceSmile from 'heroicons/24/outline/face-smile.svg';
import Photo from 'heroicons/24/outline/photo.svg'; import Photo from 'heroicons/24/outline/photo.svg';
import XMark from 'heroicons/24/outline/x-mark.svg'; import XMark from 'heroicons/24/outline/x-mark.svg';
import PaperAirplane from 'heroicons/24/solid/paper-airplane.svg'; import PaperAirplane from 'heroicons/24/solid/paper-airplane.svg';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import * as nip19 from 'nostr-tools/nip19';
import { Event as NostrEvent } from 'nostr-tools/pure'; import { Event as NostrEvent } from 'nostr-tools/pure';
import useEmojiPicker, { EmojiData } from '@/components/useEmojiPicker'; import useEmojiPicker, { EmojiData } from '@/components/useEmojiPicker';
import UserNameDisplay from '@/components/UserDisplayName'; 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 { useTranslation } from '@/i18n/useTranslation';
import { type CreateTextNoteParams } from '@/nostr/builder/createTextNote'; import { type CreateTextNoteParams } from '@/nostr/builder/createTextNote';
import { textNote } from '@/nostr/event'; import { textNote } from '@/nostr/event';
import parseTextNote, { ParsedTextNote } from '@/nostr/parseTextNote'; import parseTextNote, { ParsedTextNote } from '@/nostr/parseTextNote';
import useCommands from '@/nostr/useCommands'; import useCommands from '@/nostr/useCommands';
import useFollowings from '@/nostr/useFollowings';
import { UseProfile, useProfiles } from '@/nostr/useProfile';
import usePubkey from '@/nostr/usePubkey'; import usePubkey from '@/nostr/usePubkey';
import ensureNonNull from '@/utils/ensureNonNull';
import { uploadFiles, uploadNostrBuild } from '@/utils/imageUpload'; import { uploadFiles, uploadNostrBuild } from '@/utils/imageUpload';
// import usePersistStatus from '@/hooks/usePersistStatus'; // import usePersistStatus from '@/hooks/usePersistStatus';
@@ -92,87 +78,6 @@ const format = (parsed: ParsedTextNote) => {
return content.join(''); 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<HTMLTextAreaElement | undefined>();
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 = (
<div class="flex gap-1 border-b border-border px-2 py-1">
<img class="h-6 max-w-[3rem]" src={config.url} alt={config.shortcode} />
<div>{config.shortcode}</div>
</div>
) 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 = (
<div class="flex gap-1 border-b border-border px-2 py-1">
<Show when={profile.profile()?.picture} fallback={<div class="h-6" />} keyed>
{(url) => <img class="h-6 max-w-[3rem]" src={url} alt="icon" />}
</Show>
{profile.profile()?.name}
</div>
) 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<NotePostFormProps> = (props) => { const NotePostForm: Component<NotePostFormProps> = (props) => {
const i18n = useTranslation(); const i18n = useTranslation();
@@ -180,7 +85,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
let contentWarningReasonRef: HTMLInputElement | undefined; let contentWarningReasonRef: HTMLInputElement | undefined;
let fileInputRef: HTMLInputElement | undefined; let fileInputRef: HTMLInputElement | undefined;
const { elementRef: completeTextAreaRef } = useComplete(); const { elementRef: emojiTextAreaRef } = useEmojiComplete();
const [text, setText] = createSignal<string>(''); const [text, setText] = createSignal<string>('');
const [contentWarning, setContentWarning] = createSignal(false); const [contentWarning, setContentWarning] = createSignal(false);
const [contentWarningReason, setContentWarningReason] = createSignal(''); const [contentWarningReason, setContentWarningReason] = createSignal('');
@@ -476,7 +381,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
ref={(el) => { ref={(el) => {
textAreaRef = el; textAreaRef = el;
props.textAreaRef?.(el); props.textAreaRef?.(el);
completeTextAreaRef(el); emojiTextAreaRef(el);
}} }}
name="text" 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" 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"

View File

@@ -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<HTMLTextAreaElement | undefined>();
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 = (
<div class="flex gap-1 border-b border-border px-2 py-1">
<img class="h-6 max-w-[3rem]" src={config.url} alt={config.shortcode} />
<div>{config.shortcode}</div>
</div>
) 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;

View File

@@ -1,12 +1,6 @@
import { createMemo } from 'solid-js'; import { createMemo } from 'solid-js';
import { import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/solid-query';
createQuery,
useQueryClient,
type CreateQueryResult,
QueryClient,
createQueries,
} from '@tanstack/solid-query';
import { Event as NostrEvent } from 'nostr-tools/pure'; import { Event as NostrEvent } from 'nostr-tools/pure';
import { Profile, ProfileWithOtherProperties, safeParseProfile } from '@/nostr/event/Profile'; import { Profile, ProfileWithOtherProperties, safeParseProfile } from '@/nostr/event/Profile';
@@ -19,7 +13,6 @@ export type UseProfileProps = {
export type UseProfile = { export type UseProfile = {
profile: () => ProfileWithOtherProperties | null; profile: () => ProfileWithOtherProperties | null;
pubkey: () => string | undefined;
event: () => NostrEvent | null | undefined; event: () => NostrEvent | null | undefined;
lud06: () => string | undefined; lud06: () => string | undefined;
lud16: () => string | undefined; lud16: () => string | undefined;
@@ -37,17 +30,29 @@ export type UseProfiles = {
queries: CreateQueryResult<NostrEvent | null>[]; queries: CreateQueryResult<NostrEvent | null>[];
}; };
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 buildMethod = ({ const query = createQuery(() => ({
props, queryKey: genQueryKey(),
query, queryFn: latestEventQuery<ReturnType<typeof genQueryKey>>({
taskProvider: ([, currentProps]) => {
if (currentProps == null) return null;
const { pubkey } = currentProps;
return new BatchedEventsTask<ProfileTask>({ type: 'Profile', pubkey });
},
queryClient, queryClient,
}: { }),
props: () => UseProfileProps | null; // Profiles are updated occasionally, so a short staleTime is used here.
query: CreateQueryResult<NostrEvent | null>; // gcTime is long so that the user see profiles instantly.
queryClient: QueryClient; staleTime: 5 * 60 * 1000, // 5 min
}): UseProfile => { gcTime: 3 * 24 * 60 * 60 * 1000, // 3 days
refetchInterval: 5 * 60 * 1000, // 5 min
refetchOnWindowFocus: false,
}));
const event = () => query.data; const event = () => query.data;
const profile = createMemo((): Profile | null => { const profile = createMemo((): Profile | null => {
@@ -71,88 +76,9 @@ const buildMethod = ({
const isZapConfigured = (): boolean => lud06() != null || lud16() != null; const isZapConfigured = (): boolean => lud06() != null || lud16() != null;
const invalidateProfile = (): Promise<void> => const invalidateProfile = (): Promise<void> =>
queryClient.invalidateQueries({ queryKey: genQueryKey(props()) }); queryClient.invalidateQueries({ queryKey: genQueryKey() });
return { return { profile, lud06, lud16, event, isZapConfigured, invalidateProfile, query };
profile,
pubkey: () => props()?.pubkey,
lud06,
lud16,
event,
isZapConfigured,
invalidateProfile,
query,
};
};
const queryOptions = ({
props,
queryClient,
}: {
props: () => UseProfileProps | null;
queryClient: QueryClient;
}) => ({
queryKey: genQueryKey(props()),
queryFn: latestEventQuery<ReturnType<typeof genQueryKey>>({
taskProvider: ([, currentProps]) => {
if (currentProps == null) return null;
const { pubkey } = currentProps;
return new BatchedEventsTask<ProfileTask>({ 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,
};
}; };
export default useProfile; export default useProfile;