feat: [wip] search panel

This commit is contained in:
Shibo Lyu 2024-09-10 04:53:51 +08:00
parent 1a1ac5cd43
commit 3ee0156d19
12 changed files with 179 additions and 43 deletions

View file

@ -1,16 +1,21 @@
<script>
<script lang="ts">
import { browser } from '$app/environment';
import { useChatList } from '$lib/chatList';
import { chatServerConnectionPool } from '$lib/chatServers';
import { scale } from 'svelte/transition';
import ChatListHeader from './ChatListHeader.svelte';
import ChatListItem from './ChatListItem.svelte';
import SearchPanel from './SearchPanel.svelte';
const chatList = browser ? useChatList(chatServerConnectionPool.chatList) : null;
let isSearchFocused: boolean;
let searchQuery: string;
</script>
<div class="flex h-[100dvh] flex-col justify-stretch">
<ChatListHeader />
<div class="min-h-0 flex-1 touch-pan-y overflow-y-auto">
<ChatListHeader bind:isSearchFocused bind:searchQuery />
<div class="relative min-h-0 flex-1 touch-pan-y overflow-y-auto">
<ul>
{#if $chatList}
{#each $chatList as chat}
@ -18,5 +23,13 @@
{/each}
{/if}
</ul>
{#if isSearchFocused}
<div
class="absolute inset-0 size-full origin-top touch-pan-y overflow-y-auto bg-sb-primary"
transition:scale={{ start: 0.9 }}
>
<SearchPanel {searchQuery} />
</div>
{/if}
</div>
</div>

View file

@ -1,45 +1,51 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import InputFrame from '$lib/components/InputFrame.svelte';
import { Icon, MagnifyingGlass, PencilSquare, XCircle } from 'svelte-hero-icons';
import IdentityMenu from './IdentityMenu.svelte';
import { tw } from '$lib/tw';
import { tick } from 'svelte';
export let searchQuery: string = '';
export let isSearchFocused: boolean;
let inputElement: HTMLInputElement;
function onTapX() {
searchQuery = '';
inputElement.blur();
}
</script>
<header class="flex items-center justify-stretch gap-2 border-b border-ss-secondary p-2 shadow-sm">
<IdentityMenu />
<InputFrame class="flex-1">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-5 text-slate-400"
>
<path
fill-rule="evenodd"
d="M10.5 3.75a6.75 6.75 0 1 0 0 13.5 6.75 6.75 0 0 0 0-13.5ZM2.25 10.5a8.25 8.25 0 1 1 14.59 5.28l4.69 4.69a.75.75 0 1 1-1.06 1.06l-4.69-4.69A8.25 8.25 0 0 1 2.25 10.5Z"
clip-rule="evenodd"
/>
</svg>
<IdentityMenu class={tw('transition-opacity duration-200', isSearchFocused && 'opacity-0')} />
<InputFrame class={tw('z-10 flex-1 transition-all duration-200', isSearchFocused && '-mx-10')}>
<Icon src={MagnifyingGlass} class="size-5 text-slate-400" />
<input
type="text"
placeholder="Search"
class="w-full flex-1 bg-transparent text-sm leading-4 text-slate-900 focus:outline-none"
bind:value={searchQuery}
bind:this={inputElement}
on:focus={() => (isSearchFocused = true)}
on:blur={async () => {
await tick();
isSearchFocused = false;
}}
/>
</InputFrame>
<Button class="size-8">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-5"
<button
class={tw(
'size-4 cursor-default text-slate-300 opacity-0 transition-opacity dark:text-slate-500',
isSearchFocused && 'opacity-100'
)}
on:click={onTapX}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
/>
</svg>
<Icon src={XCircle} mini />
<span class="sr-only">Clear</span>
</button>
</InputFrame>
<Button class={tw('size-8 transition-opacity duration-200', isSearchFocused && 'opacity-0')}>
<Icon src={PencilSquare} class="size-5" />
<span class="sr-only">Compose</span>
</Button>
</header>

View file

@ -4,6 +4,9 @@
import { keyStore, currentKeyIndex, currentKeyPair } from '$lib/keystore';
import { BlahKeyPair, generateName } from '$lib/blah/crypto';
let className: string = '';
export { className as class };
let currentKeyId: string | undefined;
let currentKeyName: string | null;
$: {
@ -24,10 +27,10 @@
</script>
<DropdownMenu.Root closeOnItemClick={false}>
<DropdownMenu.Trigger>
<DropdownMenu.Trigger class={className}>
{#if currentKeyId}
{#key currentKeyId}
<AvatarBeam size={30} name={currentKeyId} />
<AvatarBeam size={32} name={currentKeyId} />
{/key}
<span class="sr-only">Using identity {currentKeyName}</span>
{:else}

View file

@ -0,0 +1,21 @@
<script lang="ts">
import type { Chat } from '$lib/types';
import ChatListItem from './ChatListItem.svelte';
export let name: string;
export let results: Chat[];
</script>
<li>
<h3
class="ms-2 border-b-[0.5px] border-ss-secondary pb-1 pe-2 pt-2 text-xs font-semibold uppercase text-sf-secondary"
>
{name}
</h3>
<ul>
{#each results as chat}
<ChatListItem {chat} />
{/each}
</ul>
</li>

View file

@ -0,0 +1,34 @@
<script lang="ts">
import { chatServerConnectionPool } from '$lib/chatServers';
import { ChatBubbleLeftRight, Icon } from 'svelte-hero-icons';
import ChatListItem from './ChatListItem.svelte';
import SearchChatResultSection from './SearchChatResultSection.svelte';
export let searchQuery: string;
async function search(query: string) {
const results = await chatServerConnectionPool.searchManager.searchChats(query);
return results;
}
</script>
{#await search(searchQuery)}
<div class="flex size-full items-center justify-center">
<Icon src={ChatBubbleLeftRight} solid class="w-1/3 animate-pulse fill-sf-tertiary" />
</div>
{:then results}
{#if results.joined.length === 0 && results.public.length === 0}
<div class="flex size-full items-center justify-center">
<p class="text-sf-tertiary">No results found</p>
</div>
{:else}
<ul>
{#if results.joined.length > 0}
<SearchChatResultSection name="Recents" results={results.joined} />
{/if}
{#if results.public.length > 0}
<SearchChatResultSection name="Discover" results={results.public} />
{/if}
</ul>
{/if}
{/await}