mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-29 16:30:55 +00:00
new mention popup in composer
This commit is contained in:
parent
a4cf65e7c2
commit
39b7b34bb7
@ -16,7 +16,7 @@ tauri-build = { version = "1.4.0", features = [] }
|
||||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.4.0", features = [
|
||||
tauri = { version = "1.4.0", features = [ "fs-read-dir", "fs-read-file",
|
||||
"window-start-dragging",
|
||||
"path-all",
|
||||
"http-all",
|
||||
|
@ -32,6 +32,8 @@
|
||||
"all": false,
|
||||
"removeFile": true,
|
||||
"writeFile": true,
|
||||
"readDir": true,
|
||||
"readFile": true,
|
||||
"scope": [
|
||||
"$APPDATA/*",
|
||||
"$DATA/*",
|
||||
|
@ -1,16 +1,14 @@
|
||||
import { message } from '@tauri-apps/api/dialog';
|
||||
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 { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { Suggestion } from '@shared/composer';
|
||||
import { CancelIcon, LoaderIcon, MediaIcon, MentionIcon } from '@shared/icons';
|
||||
import { MentionPopup } from '@shared/composer';
|
||||
import { CancelIcon, LoaderIcon, MediaIcon } from '@shared/icons';
|
||||
import { MentionNote } from '@shared/notes';
|
||||
|
||||
import { useComposer } from '@stores/composer';
|
||||
@ -33,12 +31,6 @@ export function Composer() {
|
||||
},
|
||||
}),
|
||||
Placeholder.configure({ placeholder: 'Type something...' }),
|
||||
Mention.configure({
|
||||
suggestion: Suggestion,
|
||||
renderLabel({ node }) {
|
||||
return `nostr:${nip19.npubEncode(node.attrs.id.pubkey)} `;
|
||||
},
|
||||
}),
|
||||
Image.configure({
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
@ -164,13 +156,7 @@ export function Composer() {
|
||||
<MediaIcon className="h-5 w-5 text-white/80" />
|
||||
Add media
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => uploadImage()}
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-lg hover:bg-white/10 hover:backdrop-blur-xl"
|
||||
>
|
||||
<MentionIcon className="h-5 w-5 text-white/80" />
|
||||
</button>
|
||||
<MentionPopup editor={editor} />
|
||||
</div>
|
||||
<button
|
||||
onClick={() => submit()}
|
||||
|
@ -1,6 +1,5 @@
|
||||
export * from './user';
|
||||
export * from './modal';
|
||||
export * from './composer';
|
||||
export * from './mention/list';
|
||||
export * from './mention/item';
|
||||
export * from './mention/suggestion';
|
||||
export * from './mention/popup';
|
@ -1,22 +1,36 @@
|
||||
import { Image } from '@shared/image';
|
||||
|
||||
import { useProfile } from '@utils/hooks/useProfile';
|
||||
import { displayNpub } from '@utils/shortenKey';
|
||||
import { Profile } from '@utils/types';
|
||||
|
||||
export function MentionItem({ profile }: { profile: Profile }) {
|
||||
export function MentionItem({ pubkey }: { pubkey: string }) {
|
||||
const { status, user } = useProfile(pubkey);
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative h-8 w-8 shrink-0 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
||||
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
|
||||
<span className="h-4 w-1/2 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
||||
<span className="h-3 w-1/3 animate-pulse rounded bg-white/10 backdrop-blur-xl" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-11 items-center justify-start gap-2.5 px-2 hover:bg-white/10">
|
||||
<Image
|
||||
src={profile.picture || profile.image}
|
||||
alt={profile.pubkey}
|
||||
src={user.picture || user.image}
|
||||
alt={pubkey}
|
||||
className="shirnk-0 h-8 w-8 rounded-md object-cover"
|
||||
/>
|
||||
<div className="flex flex-col gap-px">
|
||||
<h5 className="max-w-[15rem] text-sm font-medium leading-none text-white">
|
||||
{profile.ident}
|
||||
<div className="flex flex-col items-start gap-px">
|
||||
<h5 className="max-w-[10rem] truncate text-sm font-medium leading-none text-white">
|
||||
{user.display_name || user.displayName || user.name}
|
||||
</h5>
|
||||
<span className="text-sm leading-none text-white/50">
|
||||
{displayNpub(profile.pubkey, 16)}
|
||||
{displayNpub(pubkey, 16)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,84 +0,0 @@
|
||||
import { NDKUserProfile } from '@nostr-dev-kit/ndk';
|
||||
import { type SuggestionProps } from '@tiptap/suggestion';
|
||||
import {
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { MentionItem } from '@shared/composer';
|
||||
|
||||
export const MentionList = forwardRef(
|
||||
(props: SuggestionProps, ref: ForwardedRef<unknown>) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const selectItem = (index) => {
|
||||
const item = props.items[index];
|
||||
|
||||
if (item) {
|
||||
props.command({ id: item });
|
||||
}
|
||||
};
|
||||
|
||||
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-[250px] flex-col rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
|
||||
{props.items.length ? (
|
||||
props.items.map((item: NDKUserProfile, index: number) => (
|
||||
<button
|
||||
className={twMerge(
|
||||
'h-11 w-full rounded-lg px-2 text-start text-sm font-medium hover:bg-white/10',
|
||||
`${index === selectedIndex ? 'is-selected' : ''}`
|
||||
)}
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
<MentionItem profile={item} />
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div>No result</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
MentionList.displayName = 'MentionList';
|
@ -1,7 +1,38 @@
|
||||
export function MentionPopup() {
|
||||
import * as Popover from '@radix-ui/react-popover';
|
||||
import { Editor } from '@tiptap/react';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { MentionItem } from '@shared/composer';
|
||||
import { MentionIcon } from '@shared/icons';
|
||||
|
||||
export function MentionPopup({ editor }: { editor: Editor }) {
|
||||
const { db } = useStorage();
|
||||
|
||||
const insertMention = (pubkey: string) => {
|
||||
editor.commands.insertContent(`nostr:${nip19.npubEncode(pubkey)}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>TODO</p>
|
||||
</div>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-lg hover:bg-white/10 hover:backdrop-blur-xl"
|
||||
>
|
||||
<MentionIcon className="h-5 w-5 text-white/80" />
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content className="h-full max-h-[200px] w-[250px] overflow-hidden overflow-y-auto rounded-lg bg-white/10 backdrop-blur-xl focus:outline-none">
|
||||
<div className="flex flex-col gap-1 py-1">
|
||||
{db.account.follows.map((item) => (
|
||||
<button key={item} type="button" onClick={() => insertMention(item)}>
|
||||
<MentionItem pubkey={item} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
);
|
||||
}
|
||||
|
@ -1,68 +0,0 @@
|
||||
import { ReactRenderer } from '@tiptap/react';
|
||||
import tippy from 'tippy.js';
|
||||
|
||||
import { MentionList } from '@shared/composer';
|
||||
|
||||
export const Suggestion = {
|
||||
items: async ({ query }) => {
|
||||
const users = [];
|
||||
return users
|
||||
.filter((item) => item.ident.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();
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
@ -50,7 +50,7 @@ export function NoteActions({
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content className="-left-10 select-none rounded-md bg-black px-3.5 py-1.5 text-sm leading-none text-white will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
|
||||
Open thread
|
||||
Focus
|
||||
<Tooltip.Arrow className="fill-black" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
|
@ -88,7 +88,7 @@ export function User({
|
||||
</div>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
className="w-[300px] overflow-hidden rounded-md bg-white/10 backdrop-blur-3xl focus:outline-none"
|
||||
className="w-[300px] overflow-hidden rounded-lg bg-white/10 backdrop-blur-3xl focus:outline-none"
|
||||
sideOffset={5}
|
||||
>
|
||||
<div className="flex gap-2.5 border-b border-white/5 px-3 py-3">
|
||||
|
Loading…
Reference in New Issue
Block a user