fix: close mentions dropdown on mobile

This commit is contained in:
codytseng
2025-02-27 14:36:18 +08:00
parent 3c23a7f9f8
commit d4fa40900b
2 changed files with 87 additions and 69 deletions

View File

@@ -1,17 +1,10 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { extractMentions } from '@/lib/event' import { extractMentions } from '@/lib/event'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { Check } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useEffect, useState } from 'react' import { HTMLAttributes, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SimpleUserAvatar } from '../UserAvatar' import { SimpleUserAvatar } from '../UserAvatar'
import { SimpleUsername } from '../Username' import { SimpleUsername } from '../Username'
@@ -70,8 +63,8 @@ export default function Mentions({
}, [pubkeys, relatedPubkeys, parentEventPubkey, addedPubkeys, removedPubkeys]) }, [pubkeys, relatedPubkeys, parentEventPubkey, addedPubkeys, removedPubkeys])
return ( return (
<DropdownMenu> <Popover>
<DropdownMenuTrigger asChild> <PopoverTrigger asChild>
<Button <Button
className="px-3" className="px-3"
variant="ghost" variant="ghost"
@@ -80,25 +73,22 @@ export default function Mentions({
> >
{t('Mentions')} {mentions.length > 0 && `(${mentions.length})`} {t('Mentions')} {mentions.length > 0 && `(${mentions.length})`}
</Button> </Button>
</DropdownMenuTrigger> </PopoverTrigger>
<DropdownMenuContent className="w-48"> <PopoverContent className="w-52 p-0 py-1">
<div className="space-y-2"> <div className="space-y-1">
<DropdownMenuLabel>{t('Mentions')}:</DropdownMenuLabel>
{parentEventPubkey && ( {parentEventPubkey && (
<DropdownMenuCheckboxItem className="flex gap-1 items-center" checked disabled> <PopoverCheckboxItem checked disabled>
<SimpleUserAvatar userId={parentEventPubkey} size="small" /> <SimpleUserAvatar userId={parentEventPubkey} size="small" />
<SimpleUsername <SimpleUsername
userId={parentEventPubkey} userId={parentEventPubkey}
className="font-semibold text-sm truncate" className="font-semibold text-sm truncate"
skeletonClassName="h-3" skeletonClassName="h-3"
/> />
</DropdownMenuCheckboxItem> </PopoverCheckboxItem>
)} )}
{(pubkeys.length > 0 || relatedPubkeys.length > 0) && <DropdownMenuSeparator />}
{pubkeys.concat(relatedPubkeys).map((pubkey, index) => ( {pubkeys.concat(relatedPubkeys).map((pubkey, index) => (
<DropdownMenuCheckboxItem <PopoverCheckboxItem
key={`${pubkey}-${index}`} key={`${pubkey}-${index}`}
className="flex gap-1 items-center cursor-pointer"
checked={mentions.includes(pubkey)} checked={mentions.includes(pubkey)}
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
if (checked) { if (checked) {
@@ -116,23 +106,37 @@ export default function Mentions({
className="font-semibold text-sm truncate" className="font-semibold text-sm truncate"
skeletonClassName="h-3" skeletonClassName="h-3"
/> />
</DropdownMenuCheckboxItem> </PopoverCheckboxItem>
))} ))}
{(relatedPubkeys.length > 0 || pubkeys.length > 0) && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => {
setAddedPubkeys([...relatedPubkeys])
setRemovedPubkeys([])
}}
>
{t('Select all')}
</DropdownMenuItem>
</>
)}
</div> </div>
</DropdownMenuContent> </PopoverContent>
</DropdownMenu> </Popover>
)
}
function PopoverCheckboxItem({
children,
checked,
onCheckedChange,
disabled,
...props
}: HTMLAttributes<HTMLButtonElement> & {
disabled?: boolean
checked: boolean
onCheckedChange?: (checked: boolean) => void
}) {
return (
<div className="px-1">
<Button
variant="ghost"
className="w-full rounded-md justify-start px-2"
onClick={() => onCheckedChange?.(!checked)}
disabled={disabled}
{...props}
>
{checked ? <Check className="shrink-0" /> : <div className="w-4 shrink-0" />}
{children}
</Button>
</div>
) )
} }

View File

@@ -171,25 +171,30 @@ export function getProfileFromProfileEvent(event: Event) {
} }
export async function extractMentions(content: string, parentEvent?: Event) { export async function extractMentions(content: string, parentEvent?: Event) {
let parentEventPubkey: string | undefined const parentEventPubkey = parentEvent ? parentEvent.pubkey : undefined
const pubkeySet = new Set<string>() const pubkeys: string[] = []
const relatedPubkeySet = new Set<string>() const relatedPubkeys: string[] = []
const matches = content.match( const matches = content.match(
/nostr:(npub1[a-z0-9]{58}|nprofile1[a-z0-9]+|note1[a-z0-9]{58}|nevent1[a-z0-9]+)/g /nostr:(npub1[a-z0-9]{58}|nprofile1[a-z0-9]+|note1[a-z0-9]{58}|nevent1[a-z0-9]+)/g
) )
const addToSet = (arr: string[], pubkey: string) => {
if (pubkey === parentEventPubkey) return
if (!arr.includes(pubkey)) arr.push(pubkey)
}
for (const m of matches || []) { for (const m of matches || []) {
try { try {
const id = m.split(':')[1] const id = m.split(':')[1]
const { type, data } = nip19.decode(id) const { type, data } = nip19.decode(id)
if (type === 'nprofile') { if (type === 'nprofile') {
pubkeySet.add(data.pubkey) addToSet(pubkeys, data.pubkey)
} else if (type === 'npub') { } else if (type === 'npub') {
pubkeySet.add(data) addToSet(pubkeys, data)
} else if (['nevent', 'note', 'naddr'].includes(type)) { } else if (['nevent', 'note'].includes(type)) {
const event = await client.fetchEvent(id) const event = await client.fetchEvent(id)
if (event) { if (event) {
pubkeySet.add(event.pubkey) addToSet(pubkeys, event.pubkey)
} }
} }
} catch (e) { } catch (e) {
@@ -198,41 +203,44 @@ export async function extractMentions(content: string, parentEvent?: Event) {
} }
if (parentEvent) { if (parentEvent) {
parentEventPubkey = parentEvent.pubkey
parentEvent.tags.forEach(([tagName, tagValue]) => { parentEvent.tags.forEach(([tagName, tagValue]) => {
if (['p', 'P'].includes(tagName) && !!tagValue) { if (['p', 'P'].includes(tagName) && !!tagValue) {
relatedPubkeySet.add(tagValue) addToSet(relatedPubkeys, tagValue)
} }
}) })
} }
if (parentEventPubkey) {
pubkeySet.delete(parentEventPubkey)
relatedPubkeySet.delete(parentEventPubkey)
}
return { return {
pubkeys: Array.from(pubkeySet), pubkeys,
relatedPubkeys: Array.from(relatedPubkeySet).filter((p) => !pubkeySet.has(p)), relatedPubkeys: relatedPubkeys.filter((p) => !pubkeys.includes(p)),
parentEventPubkey parentEventPubkey
} }
} }
export async function extractRelatedEventIds(content: string, parentEvent?: Event) { export async function extractRelatedEventIds(content: string, parentEvent?: Event) {
const relatedEventIdSet = new Set<string>() const relatedEventIds: string[] = []
const quoteEventIdSet = new Set<string>() const quoteEventIds: string[] = []
let rootEventId: string | undefined let rootEventId: string | undefined
let parentEventId: string | undefined let parentEventId: string | undefined
const matches = content.match(/nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+)/g) const matches = content.match(/nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+)/g)
const addToSet = (arr: string[], item: string) => {
if (!arr.includes(item)) arr.push(item)
}
const removeFromSet = (arr: string[], item: string) => {
const index = arr.indexOf(item)
if (index !== -1) arr.splice(index, 1)
}
for (const m of matches || []) { for (const m of matches || []) {
try { try {
const id = m.split(':')[1] const id = m.split(':')[1]
const { type, data } = nip19.decode(id) const { type, data } = nip19.decode(id)
if (type === 'nevent') { if (type === 'nevent') {
quoteEventIdSet.add(data.id) addToSet(quoteEventIds, data.id)
} else if (type === 'note') { } else if (type === 'note') {
quoteEventIdSet.add(data) addToSet(quoteEventIds, data)
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@@ -240,12 +248,12 @@ export async function extractRelatedEventIds(content: string, parentEvent?: Even
} }
if (parentEvent) { if (parentEvent) {
relatedEventIdSet.add(parentEvent.id) addToSet(relatedEventIds, parentEvent.id)
parentEvent.tags.forEach((tag) => { parentEvent.tags.forEach((tag) => {
if (isRootETag(tag)) { if (isRootETag(tag)) {
rootEventId = tag[1] rootEventId = tag[1]
} else if (tagNameEquals('e')(tag)) { } else if (tagNameEquals('e')(tag)) {
relatedEventIdSet.add(tag[1]) addToSet(relatedEventIds, tag[1])
} }
}) })
if (rootEventId || isReplyNoteEvent(parentEvent)) { if (rootEventId || isReplyNoteEvent(parentEvent)) {
@@ -255,19 +263,22 @@ export async function extractRelatedEventIds(content: string, parentEvent?: Even
} }
} }
if (rootEventId) relatedEventIdSet.delete(rootEventId) if (rootEventId) {
if (parentEventId) relatedEventIdSet.delete(parentEventId) removeFromSet(relatedEventIds, rootEventId)
}
if (parentEventId) {
removeFromSet(relatedEventIds, parentEventId)
}
return { return {
otherRelatedEventIds: Array.from(relatedEventIdSet), otherRelatedEventIds: relatedEventIds,
quoteEventIds: Array.from(quoteEventIdSet), quoteEventIds,
rootEventId, rootEventId,
parentEventId parentEventId
} }
} }
export async function extractCommentMentions(content: string, parentEvent: Event) { export async function extractCommentMentions(content: string, parentEvent: Event) {
const quoteEventIdSet = new Set<string>() const quoteEventIds: string[] = []
const rootEventId = parentEvent.tags.find(tagNameEquals('E'))?.[1] ?? parentEvent.id const rootEventId = parentEvent.tags.find(tagNameEquals('E'))?.[1] ?? parentEvent.id
const rootEventKind = parentEvent.tags.find(tagNameEquals('K'))?.[1] ?? parentEvent.kind const rootEventKind = parentEvent.tags.find(tagNameEquals('K'))?.[1] ?? parentEvent.kind
const rootEventPubkey = parentEvent.tags.find(tagNameEquals('P'))?.[1] ?? parentEvent.pubkey const rootEventPubkey = parentEvent.tags.find(tagNameEquals('P'))?.[1] ?? parentEvent.pubkey
@@ -275,16 +286,19 @@ export async function extractCommentMentions(content: string, parentEvent: Event
const parentEventKind = parentEvent.kind const parentEventKind = parentEvent.kind
const parentEventPubkey = parentEvent.pubkey const parentEventPubkey = parentEvent.pubkey
const matches = content.match(/nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+)/g) const addToSet = (arr: string[], item: string) => {
if (!arr.includes(item)) arr.push(item)
}
const matches = content.match(/nostr:(note1[a-z0-9]{58}|nevent1[a-z0-9]+)/g)
for (const m of matches || []) { for (const m of matches || []) {
try { try {
const id = m.split(':')[1] const id = m.split(':')[1]
const { type, data } = nip19.decode(id) const { type, data } = nip19.decode(id)
if (type === 'nevent') { if (type === 'nevent') {
quoteEventIdSet.add(data.id) addToSet(quoteEventIds, data.id)
} else if (type === 'note') { } else if (type === 'note') {
quoteEventIdSet.add(data) addToSet(quoteEventIds, data)
} }
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@@ -292,7 +306,7 @@ export async function extractCommentMentions(content: string, parentEvent: Event
} }
return { return {
quoteEventIds: Array.from(quoteEventIdSet), quoteEventIds,
rootEventId, rootEventId,
rootEventKind, rootEventKind,
rootEventPubkey, rootEventPubkey,