Skip to content

Commit

Permalink
fix: handle new message for group list
Browse files Browse the repository at this point in the history
  • Loading branch information
aseerkt committed Jul 12, 2024
1 parent 8f13867 commit 67c6bb1
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 94 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ pnpm --filter web test
- [x] infinite scroll cursor pagination (messages/groups/members)
- [x] tanstack react-query integration
- [x] add members
- [x] realtime unread count
- [ ] leave group, transfer ownership, delete group
- [ ] alert component
- [ ] confirm dialog
- [ ] delete group
Expand Down
7 changes: 2 additions & 5 deletions server/src/modules/groups/groups.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,9 +167,6 @@ export const listUserGroups: RequestHandler = async (req, res, next) => {
lastActivity: groupsWithLastMessage.lastActivity,
id: groupsWithLastMessage.id,
name: groupsWithLastMessage.name,
ownerId: groupsWithLastMessage.ownerId,
createdAt: groupsWithLastMessage.createdAt,
updatedAt: groupsWithLastMessage.updatedAt,
unreadCount: sql<number>`COALESCE (${unreadCounts.unreadCount}, 0)`
.mapWith(Number)
.as('unread_count'),
Expand Down Expand Up @@ -222,9 +219,9 @@ export const addGroupMembers: RequestHandler = async (req, res, next) => {

const io = req.app.get('io') as TypedIOServer

io.to(getGroupRoomId(req.params.groupId)).emit('newMembers', newMembers)
// let existing members know new member is joined

// let existing members know new member is joined in member list
io.to(getGroupRoomId(req.params.groupId)).emit('newMembers', newMembers)

res.json(newMembers)
} catch (error) {
Expand Down
17 changes: 7 additions & 10 deletions server/src/modules/members/members.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,7 @@ export const addMembers = async (

setMemberRolesForAGroup(group.id, memberRoles)

const userSockets = await getMultipleUserSockets(memberIds)

userSockets.forEach(socketId => {
const socket = io.sockets.sockets.get(socketId)
console.log('member socket', socket)
socket?.join(group.id.toString())
})
const userSockets = await joinMultiSocketRooms(io, memberIds, [group.id])

io.to(userSockets).emit('newGroup', group)

Expand All @@ -80,7 +74,10 @@ export const joinMultiSocketRooms = async (
) => {
const userSockets = await getMultipleUserSockets(userIds)

userSockets.forEach(socketId => {
io.sockets.sockets.get(socketId)?.join(groupIds.map(String))
})
for (const socketId of userSockets) {
const socket = io.sockets.sockets.get(socketId)
socket?.join(groupIds.map(String))
}

return userSockets
}
70 changes: 69 additions & 1 deletion server/src/modules/messages/messages.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { db } from '@/database'
import { getUserSockets } from '@/redis/handlers'
import { and, eq, isNull } from 'drizzle-orm'
import { groups } from '../groups/groups.schema'
import { checkPermission } from '../members/members.service'
import { messageRecipients, messages } from './messages.schema'

Expand All @@ -11,6 +14,10 @@ export const insertMessage = async (
if (!isAllowed) {
throw new Error('createMessage: Not authorized')
}
const [group] = await db
.select({ groupName: groups.name })
.from(groups)
.where(eq(groups.id, groupId))
const [message] = await db
.insert(messages)
.values({
Expand All @@ -19,15 +26,76 @@ export const insertMessage = async (
senderId,
})
.returning()
return message
return { ...message, groupName: group.groupName }
}

export const markMessageAsRead = async (
messageId: number,
recipientId: number,
) => {
const [message] = await db
.select({ senderId: messages.senderId, groupId: messages.groupId })
.from(messages)
.where(eq(messages.id, messageId))
.limit(1)
if (!message?.groupId) {
throw new Error('markMessageAsRead: message does not belongs to a group')
}

const { isAllowed } = await checkPermission(
message.groupId,
recipientId,
'member',
)

if (!isAllowed) {
throw new Error(
"markMessageAsRead: you don't have permission to mark the message as read",
)
}

await db.insert(messageRecipients).values({
messageId,
recipientId,
})

return getUserSockets(message.senderId)
}

export const markGroupMessagesAsRead = async (
groupId: number,
recipientId: number,
) => {
const { isAllowed } = await checkPermission(groupId, recipientId, 'member')

if (!isAllowed) {
throw new Error(
"markGroupMessagesAsRead: you don't have permission to mark the message as read",
)
}

const unreadMessages = await db
.select({ messageId: messages.id, senderId: messages.senderId })
.from(messages)
.leftJoin(
messageRecipients,
and(
eq(messageRecipients.messageId, messages.id),
eq(messageRecipients.recipientId, recipientId),
),
)
.where(
and(eq(messages.groupId, groupId), isNull(messageRecipients.messageId)),
)

if (unreadMessages.length) {
await db.insert(messageRecipients).values(
unreadMessages.map(message => ({
messageId: message.messageId,
recipientId,
})),
)

return getUserSockets(recipientId)
}
}
74 changes: 10 additions & 64 deletions server/src/socket/events.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import { db } from '@/database'
import { groups } from '@/modules/groups/groups.schema'
import { members } from '@/modules/members/members.schema'
import { checkPermission } from '@/modules/members/members.service'
import { messageRecipients, messages } from '@/modules/messages/messages.schema'
import {
insertMessage,
markGroupMessagesAsRead,
markMessageAsRead,
} from '@/modules/messages/messages.service'
import {
addUserSocket,
getMultipleUserSockets,
getTypingUsers,
getUserSockets,
markUserOffline,
markUserOnline,
removeTypingUser,
removeUserSocket,
setTypingUser,
} from '@/redis/handlers'
import { and, eq, isNull } from 'drizzle-orm'
import { eq } from 'drizzle-orm'
import { config } from '../config'
import { getGroupRoomId } from './helpers'
import { TypedIOServer, TypedSocket } from './socket.interface'
Expand Down Expand Up @@ -82,75 +79,24 @@ export const registerSocketEvents = (io: TypedIOServer) => {
})

socket.on('markMessageAsRead', async messageId => {
const [message] = await db
.select({ senderId: messages.senderId, groupId: messages.groupId })
.from(messages)
.where(eq(messages.id, messageId))
.limit(1)
if (!message?.groupId) {
throw new Error(
'markMessageAsRead: message does not belongs to a group',
)
}

const { isAllowed } = await checkPermission(
message.groupId,
const messageSenderSocketIds = await markMessageAsRead(
messageId,
socket.data.user.id,
'member',
)

if (!isAllowed) {
throw new Error(
"markMessageAsRead: you don't have permission to mark the message as read",
)
}

await markMessageAsRead(messageId, socket.data.user.id)
const senderSocketIds = await getMultipleUserSockets([message.senderId])
io.to(senderSocketIds).emit('messageRead', messageId)
// let message sender know that his message is read by the current socket user
io.to(messageSenderSocketIds).emit('messageRead', messageId)
})

socket.on('markGroupMessagesAsRead', async groupId => {
const { isAllowed } = await checkPermission(
const socketIds = await markGroupMessagesAsRead(
groupId,
socket.data.user.id,
'member',
)

if (!isAllowed) {
throw new Error(
"markGroupMessagesAsRead: you don't have permission to mark the message as read",
)
}

const unreadMessages = await db
.select({ messageId: messages.id, senderId: messages.senderId })
.from(messages)
.leftJoin(
messageRecipients,
and(
eq(messageRecipients.messageId, messages.id),
eq(messageRecipients.recipientId, socket.data.user.id),
),
)
.where(
and(
eq(messages.groupId, groupId),
isNull(messageRecipients.messageId),
),
)

if (unreadMessages.length) {
await db.insert(messageRecipients).values(
unreadMessages.map(message => ({
messageId: message.messageId,
recipientId: socket.data.user.id,
})),
)

const socketIds = await getUserSockets(socket.data.user.id)

if (socketIds?.length) {
// let the current user know that the unread messages of the group is marked as read
io.to(socketIds).emit('groupMarkedAsRead', groupId)
// TODO: let the message senders know their message is read
}
})

Expand Down
4 changes: 3 additions & 1 deletion server/src/socket/socket.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { Server, Socket } from 'socket.io'
export interface ServerToClientEvents {
userOnline: (userId: number) => void
userOffline: (userId: number) => void
newMessage: (message: Message & { username: string }) => void
newMessage: (
message: Message & { username: string; groupName: string },
) => void
newMember: (member: Member & { username: string }) => void
newMembers: (member: Member[]) => void
newGroup: (group: Group) => void
Expand Down
26 changes: 15 additions & 11 deletions web/src/features/group/components/UserGroupList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const UserGroupList = () => {
)
}

function handleNewMessage(message: IMessage) {
function handleNewMessage(message: IMessage & { groupName: string }) {
queryClient.setQueryData<IPaginatedInfiniteGroups>(
['userGroups', auth],
data => {
Expand All @@ -80,20 +80,24 @@ export const UserGroupList = () => {
}
})

if (messageGroup) {
messageGroup.lastMessage = {
const group: IGroupWithLastMessage = {
id: messageGroup?.id || message.groupId,
name: messageGroup?.name || message.groupName,
lastActivity: message.createdAt,
unreadCount: messageGroup?.unreadCount || 0,
lastMessage: {
id: message.id,
content: message.content,
senderId: message.senderId,
}
messageGroup.lastActivity = message.createdAt
if (params.groupId === message.groupId.toString()) {
socket.emit('markMessageAsRead', message.id)
} else {
messageGroup.unreadCount++
}
draft.pages[0].data.unshift(messageGroup)
},
}

if (params.groupId === message.groupId.toString()) {
socket.emit('markMessageAsRead', message.id)
} else {
group.unreadCount++
}
draft.pages[0].data.unshift(group)
})
return updatedData
},
Expand Down
3 changes: 2 additions & 1 deletion web/src/features/group/group.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export interface IGroup {
createdAt: string
}

export interface IGroupWithLastMessage extends IGroup {
export interface IGroupWithLastMessage
extends Omit<IGroup, 'createdAt' | 'ownerId'> {
lastMessage?: {
id: number
content: string
Expand Down
2 changes: 1 addition & 1 deletion web/src/interfaces/socket.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { IMessage } from '../features/message/message.interface'
export interface ServerToClientEvents {
userOnline: (userId: number) => void
userOffline: (userId: number) => void
newMessage: (message: IMessage) => void
newMessage: (message: IMessage & { groupName: string }) => void
newMember: (member: IMember) => void
newMembers: (member: IMember[]) => void
newGroup: (group: IGroup) => void
Expand Down

0 comments on commit 67c6bb1

Please sign in to comment.