generated from warmachine028/github-super-starter-kit
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: new chat dialog and new chat selected user bubbles
- Loading branch information
1 parent
a985a9c
commit d765a2a
Showing
6 changed files
with
201 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
'use client' | ||
|
||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' | ||
import { useSession, useToast, useDebounce } from '@/hooks' | ||
import { useQuery } from '@tanstack/react-query' | ||
import { Check, Loader2, Search, X } from 'lucide-react' | ||
import { useState } from 'react' | ||
import type { UserResponse } from 'stream-chat' | ||
import { type DefaultStreamChatGenerics, useChatContext } from 'stream-chat-react' | ||
import { Input } from '../ui/input' | ||
import { Button } from '../ui/button' | ||
import Avatar from '../Avatar' | ||
|
||
interface NewChatDialogProps { | ||
onOpenChange: (open: boolean) => void | ||
onChatCreated: () => void | ||
} | ||
|
||
const NewChatDialog = ({ onOpenChange, onChatCreated }: NewChatDialogProps) => { | ||
const { client, setActiveChannel } = useChatContext() | ||
const { toast } = useToast() | ||
const { user } = useSession() | ||
const [searchInput, setSearchInput] = useState('') | ||
const debouncedSearchInput = useDebounce(searchInput) | ||
|
||
const [selectedUsers, setSelectedUsers] = useState<UserResponse<DefaultStreamChatGenerics>[]>([]) | ||
|
||
const { data, isFetching, isError, isSuccess, error } = useQuery({ | ||
queryKey: ['stream-users', debouncedSearchInput], | ||
queryFn: async () => | ||
client.queryUsers( | ||
{ | ||
id: { $ne: user?.id }, | ||
role: { $ne: 'admin' }, | ||
...(debouncedSearchInput && { | ||
$or: [ | ||
{ name: { $autocomplete: debouncedSearchInput } }, | ||
{ username: { $autocomplete: debouncedSearchInput } } | ||
] | ||
}) | ||
}, | ||
{ name: 1, username: 1 }, | ||
{ limit: 15 } | ||
) | ||
}) | ||
return ( | ||
<Dialog open onOpenChange={onOpenChange}> | ||
<DialogContent className="bg-card p-0"> | ||
<DialogHeader className="px-6 pt-6"> | ||
<DialogTitle>New Chat</DialogTitle> | ||
</DialogHeader> | ||
<div className="flex flex-col gap-4 px-6 pb-6"> | ||
<div className="group relative"> | ||
<Search className="absolute left-5 top-1/2 size-5 -translate-y-1/2 transform text-muted-foreground group-focus-within:text-primary" /> | ||
<Input | ||
placeholder="Search users..." | ||
className="h-12 w-full px-14 pe-4 focus:outline-none" | ||
value={searchInput} | ||
onChange={(e) => setSearchInput(e.target.value)} | ||
/> | ||
</div> | ||
{!!selectedUsers.length && ( | ||
<div className="mt-4 flex flex-wrap gap-2 p-2"> | ||
{selectedUsers.map((user) => ( | ||
<SelectedUserTag | ||
key={user.id} | ||
user={user} | ||
onRemove={() => setSelectedUsers((prev) => prev.filter((u) => u.id !== user.id))} | ||
/> | ||
))} | ||
</div> | ||
)} | ||
<hr /> | ||
<div className="h-96 overflow-y-auto"> | ||
{isSuccess && | ||
data.users.map((user) => ( | ||
<UserResult | ||
key={user.id} | ||
user={user} | ||
selected={selectedUsers.some((u) => u.id === user.id)} | ||
onClick={() => { | ||
setSelectedUsers((prev) => | ||
prev.some((u) => u.id === user.id) ? | ||
prev.filter((u) => u.id !== user.id) | ||
: [...prev, user] | ||
) | ||
}} | ||
/> | ||
))} | ||
{isSuccess && !data.users.length && ( | ||
<p className="my-3 text-center text-muted-foreground"> | ||
No users found. Try a different name. | ||
</p> | ||
)} | ||
{isFetching && <Loader2 className="mx-auto my-3 animate-spin" />} | ||
{isError && ( | ||
<p className="my-3 text-center text-destructive"> | ||
An error ocurred while loading users. {error.message} | ||
</p> | ||
)} | ||
</div> | ||
</div> | ||
</DialogContent> | ||
</Dialog> | ||
) | ||
} | ||
|
||
interface UserResultProps { | ||
user: UserResponse<DefaultStreamChatGenerics> | ||
selected: boolean | ||
onClick: () => void | ||
} | ||
|
||
const UserResult = ({ user, selected, onClick }: UserResultProps) => { | ||
return ( | ||
<Button | ||
className="flex min-h-16 w-full items-center justify-between px-4 py-2.5 transition-colors hover:bg-muted/50" | ||
onClick={onClick} | ||
variant="ghost" | ||
> | ||
<div className="flex items-center gap-2"> | ||
<Avatar url={user.image} /> | ||
<div className="flex flex-col text-start"> | ||
<p className="font-bold">{user.name}</p> | ||
<p className="text-muted-foreground">@{user.username}</p> | ||
</div> | ||
</div> | ||
{selected && <Check className="size-5 text-success" />} | ||
</Button> | ||
) | ||
} | ||
|
||
interface SelectedUserTagProps { | ||
user: UserResponse<DefaultStreamChatGenerics> | ||
onRemove: () => void | ||
} | ||
|
||
const SelectedUserTag = ({ user, onRemove }: SelectedUserTagProps) => { | ||
return ( | ||
<Button | ||
className="flex items-center gap-2 rounded-full border p-1 hover:bg-muted/50" | ||
onClick={onRemove} | ||
variant="ghost" | ||
> | ||
<Avatar url={user.image} size={24} /> | ||
<p className="font-bold">{user.name}</p> | ||
<X className="mx-2 size-5 text-muted-foreground" /> | ||
</Button> | ||
) | ||
} | ||
|
||
export default NewChatDialog |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export { default as Chat } from './Chat' | ||
export { default as Sidebar } from './Sidebar' | ||
export { default as Channel } from './Channel' | ||
export { default as NewChatDialog } from './NewChatDialog' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { useEffect, useState } from 'react' | ||
|
||
const useDebounce = <T>(value: T, delay: number = 250): T => { | ||
const [debouncedValue, setDebouncedValue] = useState<T>(value) | ||
|
||
useEffect(() => { | ||
const handler = setTimeout(() => { | ||
setDebouncedValue(value) | ||
}, delay) | ||
|
||
return () => clearTimeout(handler) | ||
}, [value, delay]) | ||
|
||
return debouncedValue | ||
} | ||
|
||
export default useDebounce |
d765a2a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
next-book – ./
nextife.vercel.app
next-boook.vercel.app
next-book-15.vercel.app
nextifly.vercel.app
next-book-pritam.vercel.app
next-book-git-main-pritam-kundu.vercel.app
next-book-pritam-kundu.vercel.app