new mention popup in composer

This commit is contained in:
Ren Amamiya 2023-09-03 08:43:08 +07:00
parent a4cf65e7c2
commit 39b7b34bb7
10 changed files with 67 additions and 187 deletions

View File

@ -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",

View File

@ -32,6 +32,8 @@
"all": false,
"removeFile": true,
"writeFile": true,
"readDir": true,
"readFile": true,
"scope": [
"$APPDATA/*",
"$DATA/*",

View File

@ -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()}

View File

@ -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';

View File

@ -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>

View File

@ -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';

View File

@ -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>
);
}

View File

@ -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();
},
};
},
};

View File

@ -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>

View File

@ -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">