add mention to composer

This commit is contained in:
Ren Amamiya
2023-07-21 18:07:17 +07:00
parent 64cd17389d
commit 17d2a8cb56
11 changed files with 250 additions and 183 deletions

View File

@@ -2,11 +2,13 @@ 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 { nip19 } from 'nostr-tools';
import { useState } from 'react';
import { twMerge } from 'tailwind-merge';
import { Button } from '@shared/button';
import { Suggestion } from '@shared/composer';
import { CancelIcon } from '@shared/icons';
import { CancelIcon, LoaderIcon } from '@shared/icons';
import { MentionNote } from '@shared/notes';
import { useComposer } from '@stores/composer';
@@ -15,6 +17,7 @@ import { FULL_RELAYS } from '@stores/constants';
import { usePublish } from '@utils/hooks/usePublish';
export function Composer() {
const [loading, setLoading] = useState(false);
const [reply, clearReply, toggle] = useComposer((state) => [
state.reply,
state.clearReply,
@@ -27,17 +30,17 @@ export function Composer() {
StarterKit,
Placeholder.configure({ placeholder: "What's on your mind?" }),
Mention.configure({
HTMLAttributes: {
class: 'mention',
},
suggestion: Suggestion,
renderLabel({ node }) {
return `nostr:${nip19.npubEncode(node.attrs.id.pubkey)} `;
},
}),
],
content: '',
editorProps: {
attributes: {
class: twMerge(
'markdown max-h-[500px] overflow-y-auto outline-none',
'markdown break-all max-h-[500px] overflow-y-auto outline-none',
`${reply.id ? '!min-h-42' : '!min-h-[86px]'}`
),
},
@@ -45,25 +48,40 @@ export function Composer() {
});
const submit = async () => {
let tags: string[][] = [];
setLoading(true);
try {
let tags: string[][] = [];
if (reply.id && reply.pubkey) {
tags = [
['e', reply.id, FULL_RELAYS[0], 'reply'],
['p', reply.pubkey],
];
} else {
tags = [];
if (reply.id && reply.pubkey) {
if (reply.root) {
tags = [
['e', reply.root, FULL_RELAYS[0], 'root'],
['e', reply.id, FULL_RELAYS[0], 'reply'],
['p', reply.pubkey],
];
} else {
tags = [
['e', reply.id, FULL_RELAYS[0], 'reply'],
['p', reply.pubkey],
];
}
} else {
tags = [];
}
// get plaintext content
const serializedContent = editor.getText();
// publish message
await publish({ content: serializedContent, kind: 1, tags });
// close modal
setLoading(false);
toggle(false);
} catch {
setLoading(false);
console.log('failed to publish');
}
// serialize content
const serializedContent = editor.getText();
// publish message
// await publish({ content: serializedContent, kind: 1, tags });
// close modal
toggle(false);
};
return (
@@ -91,7 +109,11 @@ export function Composer() {
<div className="mt-4 flex items-center justify-between">
<div />
<Button onClick={() => submit()} preset="publish">
Publish
{loading ? (
<LoaderIcon className="h-4 w-4 animate-spin text-zinc-100" />
) : (
'Publish'
)}
</Button>
</div>
</div>

View File

@@ -2,4 +2,5 @@ export * from './user';
export * from './modal';
export * from './composer';
export * from './mention/list';
export * from './mention/item';
export * from './mention/suggestion';

View File

@@ -0,0 +1,31 @@
import { Image } from '@shared/image';
import { DEFAULT_AVATAR } from '@stores/constants';
import { displayNpub } from '@utils/shortenKey';
import { Profile } from '@utils/types';
export function MentionItem({ profile }: { profile: Profile }) {
return (
<div className="flex items-center gap-2">
<div className="h-8 w-8 shrink-0 overflow-hidden rounded-md bg-zinc-900">
<Image
src={profile.picture || profile.image}
fallback={DEFAULT_AVATAR}
alt={profile.pubkey}
className="h-8 w-8 object-cover"
/>
</div>
<div className="flex flex-col gap-px">
<h5 className="max-w-[15rem] text-sm font-medium leading-none text-zinc-100">
{profile.ident || (
<div className="h-3 w-20 animate-pulse rounded-sm bg-zinc-700" />
)}
</h5>
<span className="text-sm leading-none text-zinc-400">
{profile.nip05 || profile.username || displayNpub(profile.pubkey, 16)}
</span>
</div>
</div>
);
}

View File

@@ -1,4 +1,8 @@
import { NDKUserProfile } from '@nostr-dev-kit/ndk';
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { twMerge } from 'tailwind-merge';
import { MentionItem } from '@shared/composer';
export const MentionList = forwardRef((props: any, ref: any) => {
const [selectedIndex, setSelectedIndex] = useState(0);
@@ -47,19 +51,22 @@ export const MentionList = forwardRef((props: any, ref: any) => {
}));
return (
<div className="items">
<div className="flex w-[250px] flex-col rounded-xl border-t border-zinc-700/50 bg-zinc-800 px-3 py-3">
{props.items.length ? (
props.items.map((item, index) => (
props.items.map((item: NDKUserProfile, index: number) => (
<button
className={`item ${index === selectedIndex ? 'is-selected' : ''}`}
className={twMerge(
'h-11 w-full rounded-lg px-2 text-start text-sm font-medium hover:bg-zinc-700',
`${index === selectedIndex ? 'is-selected' : ''}`
)}
key={index}
onClick={() => selectItem(index)}
>
{item}
<MentionItem profile={item} />
</button>
))
) : (
<div className="item">No result</div>
<div>No result</div>
)}
</div>
);

View File

@@ -1,38 +1,16 @@
import { ReactRenderer } from '@tiptap/react';
import tippy from 'tippy.js';
import { getAllMetadata } from '@libs/storage';
import { MentionList } from '@shared/composer';
const users = await getAllMetadata();
export const Suggestion = {
items: ({ query }) => {
return [
'Lea Thompson',
'Cyndi Lauper',
'Tom Cruise',
'Madonna',
'Jerry Hall',
'Joan Collins',
'Winona Ryder',
'Christina Applegate',
'Alyssa Milano',
'Molly Ringwald',
'Ally Sheedy',
'Debbie Harry',
'Olivia Newton-John',
'Elton John',
'Michael J. Fox',
'Axl Rose',
'Emilio Estevez',
'Ralph Macchio',
'Rob Lowe',
'Jennifer Grey',
'Mickey Rourke',
'John Cusack',
'Matthew Broderick',
'Justine Bateman',
'Lisa Bonet',
]
.filter((item) => item.toLowerCase().startsWith(query.toLowerCase()))
return users
.filter((item) => item.ident.toLowerCase().startsWith(query.toLowerCase()))
.slice(0, 5);
},

View File

@@ -71,15 +71,13 @@ export function ComposerModal() {
<ChevronDownIcon width={14} height={14} />
</div>
</div>
<div
<button
onClick={closeModal}
onKeyDown={closeModal}
role="button"
tabIndex={0}
type="button"
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-800"
>
<CancelIcon width={16} height={16} className="text-zinc-500" />
</div>
</button>
</div>
<Composer />
</Dialog.Panel>