mirror of
https://github.com/luminous-devs/lume.git
synced 2024-10-02 18:00:47 +00:00
add @ suggestion popup
This commit is contained in:
parent
dc229f40cb
commit
31a53b9c48
@ -2,3 +2,4 @@ export * from './articleCoverUploader';
|
||||
export * from './mediaUploader';
|
||||
export * from './mentionPopup';
|
||||
export * from './mentionPopupItem';
|
||||
export * from './mentionList';
|
||||
|
104
src/app/new/components/mentionList.tsx
Normal file
104
src/app/new/components/mentionList.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import * as Avatar from '@radix-ui/react-avatar';
|
||||
import { minidenticon } from 'minidenticons';
|
||||
import { Ref, forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { NDKCacheUserProfile } from '@utils/types';
|
||||
|
||||
type MentionListRef = {
|
||||
onKeyDown: (props: { event: Event }) => boolean;
|
||||
};
|
||||
|
||||
const List = (
|
||||
props: {
|
||||
items: NDKCacheUserProfile[];
|
||||
command: (arg0: { id: string }) => void;
|
||||
},
|
||||
ref: Ref<unknown>
|
||||
) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const selectItem = (index) => {
|
||||
const item = props.items[index];
|
||||
if (item) {
|
||||
props.command({ id: item.pubkey });
|
||||
}
|
||||
};
|
||||
|
||||
const upHandler = () => {
|
||||
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
|
||||
};
|
||||
|
||||
const downHandler = () => {
|
||||
setSelectedIndex((selectedIndex + 1) % props.items.length);
|
||||
};
|
||||
|
||||
const enterHandler = () => {
|
||||
selectItem(selectedIndex);
|
||||
};
|
||||
|
||||
useEffect(() => setSelectedIndex(0), [props.items]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onKeyDown: ({ event }) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
upHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
downHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
enterHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="flex w-[200px] flex-col overflow-y-auto rounded-lg border border-neutral-200 bg-neutral-50 p-2 shadow-lg shadow-neutral-500/20 dark:border-neutral-800 dark:bg-neutral-950 dark:shadow-neutral-300/50">
|
||||
{props.items.length ? (
|
||||
props.items.map((item, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
className={twMerge(
|
||||
'inline-flex h-11 items-center gap-2 rounded-md px-2',
|
||||
index === selectedIndex ? 'bg-neutral-100 dark:bg-neutral-900' : ''
|
||||
)}
|
||||
>
|
||||
<Avatar.Root className="h-8 w-8 shrink-0">
|
||||
<Avatar.Image
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-8 w-8 rounded-md"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={150}>
|
||||
<img
|
||||
src={
|
||||
'data:image/svg+xml;utf8,' +
|
||||
encodeURIComponent(minidenticon(item.name, 90, 50))
|
||||
}
|
||||
alt={item.name}
|
||||
className="h-8 w-8 rounded-md bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<h5 className="max-w-[150px] truncate text-sm font-medium">{item.name}</h5>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center text-sm font-medium">No result</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MentionList = forwardRef<MentionListRef>(List);
|
@ -1,10 +1,12 @@
|
||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import CharacterCount from '@tiptap/extension-character-count';
|
||||
import Image from '@tiptap/extension-image';
|
||||
import Mention from '@tiptap/extension-mention';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import { EditorContent, useEditor } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { convert } from 'html-to-text';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
@ -18,11 +20,13 @@ import { MentionNote } from '@shared/notes';
|
||||
|
||||
import { WIDGET_KIND } from '@stores/constants';
|
||||
|
||||
import { useSuggestion } from '@utils/hooks/useSuggestion';
|
||||
import { useWidget } from '@utils/hooks/useWidget';
|
||||
|
||||
export function NewPostScreen() {
|
||||
const { ndk } = useNDK();
|
||||
const { addWidget } = useWidget();
|
||||
const { suggestion } = useSuggestion();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [height, setHeight] = useState(0);
|
||||
@ -41,6 +45,14 @@ export function NewPostScreen() {
|
||||
},
|
||||
}),
|
||||
CharacterCount.configure(),
|
||||
Mention.configure({
|
||||
suggestion,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
renderLabel({ options, node }) {
|
||||
const npub = nip19.npubEncode(node.attrs.id);
|
||||
return `nostr:${npub}`;
|
||||
},
|
||||
}),
|
||||
],
|
||||
content: JSON.parse(localStorage.getItem('editor-post') || '{}'),
|
||||
editorProps: {
|
||||
|
@ -12,6 +12,7 @@ import type {
|
||||
NDKCacheEvent,
|
||||
NDKCacheEventTag,
|
||||
NDKCacheUser,
|
||||
NDKCacheUserProfile,
|
||||
Relays,
|
||||
Widget,
|
||||
} from '@utils/types';
|
||||
@ -52,6 +53,20 @@ export class LumeStorage {
|
||||
return await invoke('secure_remove', { key });
|
||||
}
|
||||
|
||||
public async getAllCacheUsers() {
|
||||
const results: Array<NDKCacheUser> = await this.db.select(
|
||||
'SELECT * FROM ndk_users ORDER BY createdAt DESC;'
|
||||
);
|
||||
|
||||
if (!results.length) return [];
|
||||
|
||||
const users: NDKCacheUserProfile[] = results.map((item) => ({
|
||||
pubkey: item.pubkey,
|
||||
...JSON.parse(item.profile as string),
|
||||
}));
|
||||
return users;
|
||||
}
|
||||
|
||||
public async getCacheUser(pubkey: string) {
|
||||
const results: Array<NDKCacheUser> = await this.db.select(
|
||||
'SELECT * FROM ndk_users WHERE pubkey = $1 ORDER BY pubkey DESC LIMIT 1;',
|
||||
|
76
src/utils/hooks/useSuggestion.ts
Normal file
76
src/utils/hooks/useSuggestion.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { MentionOptions } from '@tiptap/extension-mention';
|
||||
import { ReactRenderer } from '@tiptap/react';
|
||||
import tippy from 'tippy.js';
|
||||
|
||||
import { MentionList } from '@app/new/components';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
export function useSuggestion() {
|
||||
const { db } = useStorage();
|
||||
|
||||
const suggestion: MentionOptions['suggestion'] = {
|
||||
items: async ({ query }) => {
|
||||
const users = await db.getAllCacheUsers();
|
||||
return users
|
||||
.filter((item) => item.name.toLowerCase().startsWith(query.toLowerCase()))
|
||||
.slice(0, 5);
|
||||
},
|
||||
render: () => {
|
||||
let component;
|
||||
let popup;
|
||||
|
||||
return {
|
||||
onStart: (props) => {
|
||||
component = new ReactRenderer(MentionList, {
|
||||
props,
|
||||
editor: props.editor,
|
||||
});
|
||||
|
||||
if (!props.clientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'bottom-start',
|
||||
});
|
||||
},
|
||||
|
||||
onUpdate(props) {
|
||||
component.updateProps(props);
|
||||
|
||||
if (!props.clientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
});
|
||||
},
|
||||
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
popup[0].hide();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return component.ref?.onKeyDown(props);
|
||||
},
|
||||
|
||||
onExit() {
|
||||
popup[0].destroy();
|
||||
component.destroy();
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return { suggestion };
|
||||
}
|
4
src/utils/types.d.ts
vendored
4
src/utils/types.d.ts
vendored
@ -122,6 +122,10 @@ export interface NDKCacheUser {
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface NDKCacheUserProfile extends NDKUserProfile {
|
||||
pubkey: string;
|
||||
}
|
||||
|
||||
export interface NDKCacheEvent {
|
||||
id: string;
|
||||
pubkey: string;
|
||||
|
Loading…
Reference in New Issue
Block a user