This one contains data that is viewable whenever a user has certain right, it is like a real-time status.
For example, when a Live is held currently but not ended inside a channel, we notify all people there is an ONGOING_LIVE status.
We don't treat this as IndividualUserNotification because it needs a userEmail, but the status ONGOING_LIVE can be viewed even if a user join the channel much later than the Live is held.
This notification should not have target user as the notification target is uncertain at any time .
Therefore each channel-notification must have a parent projectId.
Simiarly, each session-notification must have a channelId and projectId.
We prototype the frontend notification data as follows:
1export type IndividualType={2channel:"NEW_CHANNEL_TO_JOIN"3session:"NEW_ISSUE"4|"NEW_LLM_REPLY"5|"NEW_DRAFT"6|"SESSION_NEW_LIVE"7|"SESSION_NEW_LIVE_REPLY"8|"ASSIGNED_AS_FOLLOWER"9|"WATCH"10}1112export type GlobalNotificationType="ONGOING_LIVE"1314export type InappNotification={15 global?:{16channels:{17[projectId in string]?:{18[channelId in string]?:{19[type inGlobalNotificationType]?: number
20}21}22}23},24 individual?:{25channels:{26[projectId in string]?:{27[channelId in string]?:{28[type inIndividualType["channel"]]?: number
29}30}31},32sessions:{33[projectId in string]?:{34[channelId in string]?:{35[sessionId in string]?:{36[type inIndividualType["session"]]?: number
37}38}39}40}41}42}
2.2.
How Frontend Consume the Data?
We store the data inside state.inappNotification of some slice.
1...2const notification =useAppSelector(s=> s.chat3.inappNotification4?.individual
5?.sessions
6?.[selectedProjectId]7?.[selectedChannelId]8?.[roomId]9);1011const numOfNotification =(()=>{12if(type ==="LIVE"){13if(isLiveEnded){14return notification?.NEW_LLM_REPLY;15}else{16if(notification?.SESSION_NEW_LIVE){17// this specialCount is -1, indicating NEW18returnNotificationSpecialCount.NEW;19}20else{21return notification?.SESSION_NEW_LIVE_REPLY22}23}24}else{25if(notification?.NEW_ISSUE){26returnNotificationSpecialCount.NEW;27}else{28return notification?.NEW_LLM_REPLY29}30}31})();32...
2.3.
Backend Handler that Responses Desired Prototype
2.3.1.
General Idea
The general idea is to form an object that contains all the notification.
Since we want to get all data in one single query. If it is hard to read, it is suggested to separate individual notifications into two separate queries for channels and for sessions respectively.
1constgetInappNotifications=async(req:Request,res:Response)=>{2const userEmail = req.user?.email ||"";34// this is a fixed array of notification types that is suppose to be "session notification"5constindividualSession:IndividualUserNotification["type"][]=[6"ASSIGNED_AS_FOLLOWER",7"NEW_DRAFT",8"NEW_ISSUE",9"NEW_LLM_REPLY",10"SESSION_NEW_LIVE",11"SESSION_NEW_LIVE_REPLY",12"WATCH"13];14// that to be "channel notification"15constindividualChannel:IndividualUserNotification["type"][]=[16"NEW_CHANNEL_TO_JOIN"17];1819const[globalChannelsResult, notificationBySessionsResult]=awaitPromise.all([20 db.selectFrom("GlobalNotification")21.leftJoin("UserToChannel","UserToChannel.channelId","GlobalNotification.channelId")22.leftJoin("Channel","Channel.id","GlobalNotification.channelId")23.leftJoin("Project","Project.id","Channel.projectId")24.select([25"GlobalNotification.type",26"GlobalNotification.channelId",27"Project.id as projectId"28])29.where("UserToChannel.userEmail","=", userEmail)30.execute(),
In the follwoing query we make the following aliases:
NotificationChannel = channel being notified
NotificationSessionChannel = the channel of the session being notified
NotificationProject = project being notified
NotificationSessionProject = project of session being notified (forget to add Notification at the prefix)
This will introduce sparsities (nulls) to each selected row.
Just recall that project contains many channel, channel contains many messagesSession, then the nullity check will make sense
In frontend each session, channel and project will calculate what notification to show based on these informations
31 db.selectFrom("IndividualUserNotification")32.leftJoin("MessagesSession","MessagesSession.id","IndividualUserNotification.sessionId")33.leftJoin("Channel as NotificationChannel","NotificationChannel.id","IndividualUserNotification.channelId")34.leftJoin("Channel as NotificationSessionChannel","NotificationSessionChannel.id","MessagesSession.channelId")35.leftJoin("Project as NotificationChannelProject","NotificationChannelProject.id","NotificationChannel.projectId")36.leftJoin("Project as NotificationSessionProject","NotificationSessionProject.id","NotificationSessionChannel.projectId")37.select([38"IndividualUserNotification.sessionId as sessionId",39"IndividualUserNotification.type as notificationType",40"NotificationSessionChannel.id as notificationSessionChannelId",41"NotificationChannel.id as notificationChannelId",42"NotificationChannelProject.id as notificationChannelProjectId",43"NotificationSessionProject.id as notificationSessionProjectId"44])45.where("IndividualUserNotification.userEmail","=", userEmail)46.execute()47])48constglobalChannels:{49[projectId in string]?:{50[channelId in string]?:{51[type inGlobalNotification["type"]]?: number
52}53}54}={};5556constnotificationByChannels:{57[projectId in string]?:{58[channelId in string]?:{59[type inIndividualUserNotification["type"]]?: number
60}61}62}={};6364constnotificationBySessions:{65[sectionId in string]?:{66[projectId in string]?:{67[channelId in string]?:{68[sessionId in string]?:{[type inIndividualUserNotification["type"]]?: number }69}70}71}72}={};7374// set global channels notifications75for(const result of globalChannelsResult){76const{ channelId ="", type ="", projectId =""}= result;77if(projectId && channelId && type){78const key =`[${projectId}][${channelId}][${type}]`;79const count = lodash.get(globalChannels, key,0)as number;80 lodash.set(globalChannels, key, count +1);81}82}8384// set individual sessions and channels notifications85for(const result of notificationBySessionsResult){86const{87 notificationChannelId ="",88 notificationSessionChannelId ="",89 notificationType,90 notificationChannelProjectId ="",91 notificationSessionProjectId ="",92 sessionId =""93}= result;9495if(!notificationType){96continue;97}98if(individualChannel.includes(notificationType)){99if(notificationChannelProjectId && notificationChannelId && notificationType){100const key =`[${notificationChannelProjectId}][${notificationChannelId}][${notificationType}]`;101const count = lodash.get(notificationByChannels, key,0)as number;102 lodash.set(notificationByChannels, key, count +1);103}104}105if(individualSession.includes(notificationType)){106if(notificationSessionProjectId && notificationSessionChannelId && sessionId && notificationType){107const key =`[${notificationSessionProjectId}][${notificationSessionChannelId}][${sessionId}][${notificationType}]`;108const count = lodash.get(notificationBySessions, key,0)as number;109 lodash.set(notificationBySessions, key, count +1);110}111}112}113114 res.json({115success:true,116result:{117global:{118channels: globalChannels
119},120individual:{121channels: notificationByChannels,122sessions: notificationBySessions
123}124}125})126}