Skip to content

Commit

Permalink
add real time active statuses
Browse files Browse the repository at this point in the history
  • Loading branch information
ngquyduc committed Jul 23, 2023
1 parent 0d4a83c commit 14f6994
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 11 deletions.
11 changes: 11 additions & 0 deletions app/components/ActiveStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client'

import useActiveChannel from '../hooks/useActiveChannel'

const ActiveStatus = () => {
useActiveChannel()

return null
}

export default ActiveStatus
11 changes: 10 additions & 1 deletion app/components/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

import { User } from '@prisma/client'
import Image from 'next/image'
import useActiveList from '../hooks/useActiveList'

interface AvatarProps {
user?: User
}

const Avatar: React.FC<AvatarProps> = ({ user }) => {
const { members } = useActiveList()
const isActive = members.indexOf(user?.email!) !== -1

return (
<div className="relative">
<div className="relative inline-block rounded-full overflow-hidden h-9 w-9 md:h-11 md:w-11">
Expand All @@ -17,7 +21,12 @@ const Avatar: React.FC<AvatarProps> = ({ user }) => {
fill
/>
</div>
<span className="absolute block rounded-full bg-green-500 ring-2 ring-white top-0 right-0 h-2 w-2 md:h-3 md:w-3" />
{isActive && (
<span
className="absolute block rounded-full bg-green-500 ring-2
ring-white top-0 right-0 h-2 w-2 md:h-3 md:w-3"
/>
)}
</div>
)
}
Expand Down
6 changes: 5 additions & 1 deletion app/conversations/[conversationId]/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import Avatar from '@/app/components/Avatar'
import AvatarGroup from '@/app/components/AvatarGroup'
import useActiveList from '@/app/hooks/useActiveList'
import useOtherUser from '@/app/hooks/useOtherUser'
import { Conversation, User } from '@prisma/client'
import Link from 'next/link'
Expand All @@ -19,12 +20,15 @@ const Header: React.FC<HeaderProps> = ({ conversation }) => {
const otherUser = useOtherUser(conversation)
const [drawerOpen, setDrawerOpen] = useState(false)

const { members } = useActiveList()
const isActive = members.indexOf(otherUser?.email!) !== -1

const statusText = useMemo(() => {
if (conversation.isGroup) {
return `${conversation.users.length} members`
}

return 'Active'
return isActive ? 'Active' : 'Offline'
}, [conversation])

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import Avatar from '@/app/components/Avatar'
import AvatarGroup from '@/app/components/AvatarGroup'
import ConfirmModal from '@/app/components/ConfirmModal'
import Modal from '@/app/components/Modal'
import useActiveList from '@/app/hooks/useActiveList'
import useOtherUser from '@/app/hooks/useOtherUser'
import { Dialog, Transition } from '@headlessui/react'
import { Conversation, User } from '@prisma/client'
import { format } from 'date-fns'
import { Fragment, useMemo, useState } from 'react'
import { IoClose, IoTrash } from 'react-icons/io5'

interface ProfileDrawerProps {
isOpen: boolean
onClose: () => void
Expand All @@ -27,6 +27,9 @@ const ProfileDrawer: React.FC<ProfileDrawerProps> = ({
const otherUser = useOtherUser(data)
const [confirmOpen, setConfirmOpen] = useState(false)

const { members } = useActiveList()
const isActive = members.indexOf(otherUser?.email!) !== -1

const joinedDate = useMemo(() => {
return format(new Date(otherUser.createdAt), 'PP')
}, [])
Expand All @@ -39,8 +42,8 @@ const ProfileDrawer: React.FC<ProfileDrawerProps> = ({
if (data.isGroup) {
return `${data.users.length} members`
}
return 'Active'
}, [data])
return isActive ? 'Active' : 'Offline'
}, [data, isActive])

return (
<>
Expand Down
44 changes: 44 additions & 0 deletions app/hooks/useActiveChannel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Channel, Members } from 'pusher-js'
import { useEffect, useState } from 'react'
import { pusherClient } from '../libs/pusher'
import useActiveList from './useActiveList'

const useActiveChannel = () => {
const { set, add, remove } = useActiveList()
const [activeChannel, setActiveChannel] = useState<Channel | null>(null)

useEffect(() => {
let channel = activeChannel

if (!channel) {
channel = pusherClient.subscribe('presence-messenger')
setActiveChannel(channel)
}

channel.bind('pusher:subscription_succeeded', (members: Members) => {
const initialMembers: string[] = []

members.each((member: Record<string, any>) =>
initialMembers.push(member.id)
)
set(initialMembers)
})

channel.bind('pusher:member_added', (member: Record<string, any>) => {
add(member.id)
})

channel.bind('pusher:member_removed', (member: Record<string, any>) => {
remove(member.id)
})

return () => {
if (activeChannel) {
pusherClient.unsubscribe('presence-messenger')
setActiveChannel(null)
}
}
}, [activeChannel, set, add, remove])
}

export default useActiveChannel
20 changes: 20 additions & 0 deletions app/hooks/useActiveList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { create } from 'zustand'

interface ActiveListStore {
members: string[]
add: (id: string) => void
remove: (id: string) => void
set: (ids: string[]) => void
}

const useActiveList = create<ActiveListStore>((set) => ({
members: [],
add: (id) => set((state) => ({ members: [...state.members, id] })),
remove: (id) =>
set((state) => ({
members: state.members.filter((memberId) => memberId !== id),
})),
set: (ids) => set({ members: ids }),
}))

export default useActiveList
2 changes: 2 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ActiveStatus from './components/ActiveStatus'
import AuthContext from './context/AuthContext'
import ToasterContext from './context/ToasterContext'

Expand All @@ -22,6 +23,7 @@ export default function RootLayout({
<body className={inter.className}>
<AuthContext>
<ToasterContext />
<ActiveStatus />
{children}
</AuthContext>
</body>
Expand Down
8 changes: 4 additions & 4 deletions app/libs/pusher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ export const pusherServer = new PusherServer({
export const pusherClient = new PusherClient(
process.env.NEXT_PUBLIC_PUSHER_APP_KEY!,
{
// channelAuthorization: {
// endpoint: '/api/pusher/auth',
// transport: 'ajax',
// },
channelAuthorization: {
endpoint: '/api/pusher/auth',
transport: 'ajax',
},
cluster: 'ap1',
}
)
34 changes: 33 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"react-select": "^5.7.4",
"react-spinners": "^0.13.8",
"tailwindcss": "3.3.2",
"typescript": "5.1.3"
"typescript": "5.1.3",
"zustand": "^4.3.9"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
Expand Down

0 comments on commit 14f6994

Please sign in to comment.