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 -->
|
<!-- 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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue