feat: Add fuzzy search functionality to peer list in ChatComponent

- Implement a search input for filtering peers by name or pubkey, enhancing user experience.
- Display search results count and a message when no peers match the search query.
- Update peer list rendering to utilize filtered results, improving performance and usability.
- Refactor layout for better responsiveness and clarity in the chat interface.
This commit is contained in:
padreug 2025-08-08 14:44:35 +02:00
parent 74ae2538cb
commit 11fb45e527

View file

@ -23,12 +23,47 @@
<!-- Peer List --> <!-- Peer List -->
<div class="flex-1 overflow-hidden"> <div class="flex-1 overflow-hidden">
<div class="p-4 border-b"> <div class="p-4 border-b">
<h3 class="font-medium">Peers ({{ peers.length }})</h3> <div class="flex items-center justify-between mb-2">
<h3 class="font-medium">Peers ({{ filteredPeers.length }})</h3>
<span v-if="isSearching" class="text-xs text-muted-foreground">
{{ resultCount }} found
</span>
</div>
<!-- Search Input -->
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search class="h-4 w-4 text-muted-foreground" />
</div>
<Input
v-model="searchQuery"
placeholder="Search peers by name or pubkey..."
class="pl-10 pr-10"
/>
<div v-if="searchQuery" class="absolute inset-y-0 right-0 pr-3 flex items-center">
<Button
variant="ghost"
size="sm"
@click="clearSearch"
class="h-6 w-6 p-0 hover:bg-muted"
>
<X class="h-3 w-3" />
<span class="sr-only">Clear search</span>
</Button>
</div>
</div>
</div> </div>
<ScrollArea class="h-full"> <ScrollArea class="h-full">
<div class="p-2 space-y-1"> <div class="p-2 space-y-1">
<!-- No results message -->
<div v-if="isSearching && filteredPeers.length === 0" class="text-center py-8 text-muted-foreground">
<Search class="h-8 w-8 mx-auto mb-2 opacity-50" />
<p class="text-sm">No peers found matching "{{ searchQuery }}"</p>
<p class="text-xs mt-1">Try searching by username or pubkey</p>
</div>
<!-- Peer list -->
<div <div
v-for="peer in peers" v-for="peer in filteredPeers"
:key="peer.user_id" :key="peer.user_id"
@click="selectPeer(peer)" @click="selectPeer(peer)"
:class="[ :class="[
@ -101,13 +136,13 @@
> >
<div <div
:class="[ :class="[
'max-w-[75%] px-4 py-2 rounded-lg', 'max-w-xs lg:max-w-md px-4 py-2 rounded-lg',
message.sent message.sent
? 'bg-primary text-primary-foreground' ? 'bg-primary text-primary-foreground'
: 'bg-muted' : 'bg-muted'
]" ]"
> >
<p class="text-sm break-words">{{ message.content }}</p> <p class="text-sm">{{ message.content }}</p>
<p class="text-xs opacity-70 mt-1"> <p class="text-xs opacity-70 mt-1">
{{ formatTime(message.created_at) }} {{ formatTime(message.created_at) }}
</p> </p>
@ -119,7 +154,7 @@
</ScrollArea> </ScrollArea>
<!-- Message Input --> <!-- Message Input -->
<div class="p-4 border-t bg-background"> <div class="p-4 border-t">
<form @submit.prevent="sendMessage" class="flex space-x-2"> <form @submit.prevent="sendMessage" class="flex space-x-2">
<Input <Input
v-model="messageInput" v-model="messageInput"
@ -134,7 +169,7 @@
</div> </div>
</div> </div>
<!-- Desktop: Split View --> <!-- Desktop: Full Layout -->
<div v-else-if="!isMobile" class="flex flex-col h-full"> <div v-else-if="!isMobile" class="flex flex-col h-full">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between p-4 border-b"> <div class="flex items-center justify-between p-4 border-b">
@ -159,12 +194,47 @@
<!-- Peer List --> <!-- Peer List -->
<div class="w-80 border-r bg-muted/30"> <div class="w-80 border-r bg-muted/30">
<div class="p-4 border-b"> <div class="p-4 border-b">
<h3 class="font-medium">Peers ({{ peers.length }})</h3> <div class="flex items-center justify-between mb-2">
<h3 class="font-medium">Peers ({{ filteredPeers.length }})</h3>
<span v-if="isSearching" class="text-xs text-muted-foreground">
{{ resultCount }} found
</span>
</div>
<!-- Search Input -->
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Search class="h-4 w-4 text-muted-foreground" />
</div>
<Input
v-model="searchQuery"
placeholder="Search peers by name or pubkey..."
class="pl-10 pr-10"
/>
<div v-if="searchQuery" class="absolute inset-y-0 right-0 pr-3 flex items-center">
<Button
variant="ghost"
size="sm"
@click="clearSearch"
class="h-6 w-6 p-0 hover:bg-muted"
>
<X class="h-3 w-3" />
<span class="sr-only">Clear search</span>
</Button>
</div>
</div>
</div> </div>
<ScrollArea class="h-full"> <ScrollArea class="h-full">
<div class="p-2 space-y-1"> <div class="p-2 space-y-1">
<!-- No results message -->
<div v-if="isSearching && filteredPeers.length === 0" class="text-center py-8 text-muted-foreground">
<Search class="h-8 w-8 mx-auto mb-2 opacity-50" />
<p class="text-sm">No peers found matching "{{ searchQuery }}"</p>
<p class="text-xs mt-1">Try searching by username or pubkey</p>
</div>
<!-- Peer list -->
<div <div
v-for="peer in peers" v-for="peer in filteredPeers"
:key="peer.user_id" :key="peer.user_id"
@click="selectPeer(peer)" @click="selectPeer(peer)"
:class="[ :class="[
@ -273,13 +343,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue' import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'
import { Send, RefreshCw, MessageSquare, ArrowLeft } from 'lucide-vue-next' import { Send, RefreshCw, MessageSquare, ArrowLeft, Search, X } from 'lucide-vue-next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar' import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
import { useNostrChat } from '@/composables/useNostrChat' import { useNostrChat } from '@/composables/useNostrChat'
import { useFuzzySearch } from '@/composables/useFuzzySearch'
import { getAuthToken } from '@/lib/config/lnbits' import { getAuthToken } from '@/lib/config/lnbits'
import { config } from '@/lib/config' import { config } from '@/lib/config'
@ -290,8 +361,6 @@ interface Peer {
pubkey: string pubkey: string
} }
// State // State
const peers = ref<Peer[]>([]) const peers = ref<Peer[]>([])
const selectedPeer = ref<Peer | null>(null) const selectedPeer = ref<Peer | null>(null)
@ -306,6 +375,36 @@ const scrollTarget = ref<HTMLElement | null>(null)
// Mobile detection // Mobile detection
const isMobile = ref(false) const isMobile = ref(false)
// Fuzzy search for peers
// This integrates the useFuzzySearch composable to provide intelligent search functionality
// for finding peers by username or pubkey with typo tolerance and scoring
const {
searchQuery,
filteredItems: filteredPeers,
isSearching,
resultCount,
clearSearch
} = useFuzzySearch(peers, {
fuseOptions: {
keys: [
{ name: 'username', weight: 0.7 }, // Username has higher weight for better UX
{ name: 'pubkey', weight: 0.3 } // Pubkey has lower weight but still searchable
],
threshold: 0.3, // Fuzzy matching threshold (0.0 = perfect, 1.0 = match anything)
distance: 100, // Maximum distance for fuzzy matching
ignoreLocation: true, // Ignore location for better performance
useExtendedSearch: false, // Don't use extended search syntax
minMatchCharLength: 1, // Minimum characters to match
shouldSort: true, // Sort results by relevance
findAllMatches: false, // Don't find all matches for performance
location: 0, // Start search from beginning
isCaseSensitive: false, // Case insensitive search
},
resultLimit: 50, // Limit results for performance
matchAllWhenSearchEmpty: true, // Show all peers when search is empty
minSearchLength: 1, // Start searching after 1 character
})
// Check if device is mobile // Check if device is mobile
const checkMobile = () => { const checkMobile = () => {
isMobile.value = window.innerWidth < 768 // md breakpoint isMobile.value = window.innerWidth < 768 // md breakpoint