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:
parent
74ae2538cb
commit
11fb45e527
1 changed files with 110 additions and 11 deletions
|
|
@ -23,12 +23,47 @@
|
|||
<!-- Peer List -->
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<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>
|
||||
<ScrollArea class="h-full">
|
||||
<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
|
||||
v-for="peer in peers"
|
||||
v-for="peer in filteredPeers"
|
||||
:key="peer.user_id"
|
||||
@click="selectPeer(peer)"
|
||||
:class="[
|
||||
|
|
@ -101,13 +136,13 @@
|
|||
>
|
||||
<div
|
||||
: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
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: '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">
|
||||
{{ formatTime(message.created_at) }}
|
||||
</p>
|
||||
|
|
@ -119,7 +154,7 @@
|
|||
</ScrollArea>
|
||||
|
||||
<!-- 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">
|
||||
<Input
|
||||
v-model="messageInput"
|
||||
|
|
@ -134,7 +169,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop: Split View -->
|
||||
<!-- Desktop: Full Layout -->
|
||||
<div v-else-if="!isMobile" class="flex flex-col h-full">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between p-4 border-b">
|
||||
|
|
@ -159,12 +194,47 @@
|
|||
<!-- Peer List -->
|
||||
<div class="w-80 border-r bg-muted/30">
|
||||
<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>
|
||||
<ScrollArea class="h-full">
|
||||
<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
|
||||
v-for="peer in peers"
|
||||
v-for="peer in filteredPeers"
|
||||
:key="peer.user_id"
|
||||
@click="selectPeer(peer)"
|
||||
:class="[
|
||||
|
|
@ -273,13 +343,14 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
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 { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { useNostrChat } from '@/composables/useNostrChat'
|
||||
import { useFuzzySearch } from '@/composables/useFuzzySearch'
|
||||
import { getAuthToken } from '@/lib/config/lnbits'
|
||||
import { config } from '@/lib/config'
|
||||
|
||||
|
|
@ -290,8 +361,6 @@ interface Peer {
|
|||
pubkey: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
// State
|
||||
const peers = ref<Peer[]>([])
|
||||
const selectedPeer = ref<Peer | null>(null)
|
||||
|
|
@ -306,6 +375,36 @@ const scrollTarget = ref<HTMLElement | null>(null)
|
|||
// Mobile detection
|
||||
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
|
||||
const checkMobile = () => {
|
||||
isMobile.value = window.innerWidth < 768 // md breakpoint
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue