Skip to content

Commit

Permalink
feat: new chat dialog and new chat selected user bubbles
Browse files Browse the repository at this point in the history
  • Loading branch information
warmachine028 committed Nov 25, 2024
1 parent a985a9c commit d765a2a
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 11 deletions.
152 changes: 152 additions & 0 deletions client/src/components/chats/NewChatDialog.tsx
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
40 changes: 29 additions & 11 deletions client/src/components/chats/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
'use client'

import { useSession } from '@/hooks'
import { ChannelList, ChannelPreviewMessenger, ChannelPreviewUIComponentProps } from 'stream-chat-react'
import {
ChannelList, //
ChannelPreviewMessenger,
ChannelPreviewUIComponentProps
} from 'stream-chat-react'
import { Button } from '../ui/button'
import { MailPlus, X } from 'lucide-react'
import { cn } from '@/lib/utils'
import { useCallback } from 'react'
import { useCallback, useState } from 'react'
import { NewChatDialog } from '.'

interface ChatSidebarProps {
open: boolean
Expand Down Expand Up @@ -52,18 +57,31 @@ interface MenuHeaderProps {
}

const MenuHeader = ({ onClose }: MenuHeaderProps) => {
const [showNewChatDialog, setShowNewChatDialog] = useState(false)

return (
<div className="flex items-center gap-3 bg-card p-2">
<div className="h-full md:hidden">
<Button size="icon" variant="ghost" onClick={onClose}>
<X className="size-5" />
<>
<div className="flex items-center gap-3 bg-card p-2">
<div className="h-full md:hidden">
<Button size="icon" variant="ghost" onClick={onClose}>
<X className="size-5" />
</Button>
</div>
<h1 className="me-auto text-xl font-bold md:ms-2">Messages</h1>
<Button size="icon" variant="ghost" title="Start new chat" onClick={() => setShowNewChatDialog(true)}>
<MailPlus className="size-5" />
</Button>
</div>
<h1 className="me-auto text-xl font-bold md:ms-2">Messages</h1>
<Button size="icon" variant="ghost" title="Start new chat">
<MailPlus className="size-5" />
</Button>
</div>
{showNewChatDialog && (
<NewChatDialog
onOpenChange={setShowNewChatDialog}
onChatCreated={() => {
setShowNewChatDialog(false)
onClose()
}}
/>
)}
</>
)
}

Expand Down
1 change: 1 addition & 0 deletions client/src/components/chats/index.ts
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'
1 change: 1 addition & 0 deletions client/src/components/posts/editor/PostEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ const AddAttachmentButton = ({ onFilesSelected, disabled }: AddAttachmentButtonP
className="text-primary hover:text-primary"
disabled={disabled}
onClick={() => fileInputRef.current?.click()}
title='attachment'
>
<ImageIcon size={20} />
</Button>
Expand Down
1 change: 1 addition & 0 deletions client/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { useUploadThing } from '@/lib/uploadthing'
export { default as useDeleteCommentMutation } from './useDeleteCommentMutation'
export { default as useDeleteNotificationMutation } from './useDeleteNotificationMutation'
export { default as useInitializeChatClient } from './useInitializeChatClient'
export { default as useDebounce } from './useDebounce'
17 changes: 17 additions & 0 deletions client/src/hooks/useDebounce.ts
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

1 comment on commit d765a2a

@vercel
Copy link

@vercel vercel bot commented on d765a2a Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.