Skip to content

Commit 3ed06eb

Browse files
authored
Merge pull request #218 from abhinavxd/feat-notifications
feat: user app notification system with bell icon in the sidebar
2 parents 96559fb + 91445d7 commit 3ed06eb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1957
-283
lines changed

cmd/conversation.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,28 @@ func handleUpdateConversationAssigneeLastSeen(r *fastglue.Request) error {
353353
return r.SendEnvelope(true)
354354
}
355355

356+
// handleMarkConversationAsUnread marks a conversation as unread for the current user.
357+
func handleMarkConversationAsUnread(r *fastglue.Request) error {
358+
var (
359+
app = r.Context.(*App)
360+
uuid = r.RequestCtx.UserValue("uuid").(string)
361+
auser = r.RequestCtx.UserValue("user").(amodels.User)
362+
)
363+
user, err := app.user.GetAgent(auser.ID, "")
364+
if err != nil {
365+
return sendErrorEnvelope(r, err)
366+
}
367+
_, err = enforceConversationAccess(app, uuid, user)
368+
if err != nil {
369+
return sendErrorEnvelope(r, err)
370+
}
371+
372+
if err = app.conversation.MarkAsUnread(uuid, auser.ID); err != nil {
373+
return sendErrorEnvelope(r, err)
374+
}
375+
return r.SendEnvelope(true)
376+
}
377+
356378
// handleGetConversationParticipants retrieves participants of a conversation.
357379
func handleGetConversationParticipants(r *fastglue.Request) error {
358380
var (

cmd/handlers.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
5959
g.PUT("/api/v1/conversations/{uuid}/priority", perm(handleUpdateConversationPriority, "conversations:update_priority"))
6060
g.PUT("/api/v1/conversations/{uuid}/status", perm(handleUpdateConversationStatus, "conversations:update_status"))
6161
g.PUT("/api/v1/conversations/{uuid}/last-seen", perm(handleUpdateConversationAssigneeLastSeen, "conversations:read"))
62+
g.PUT("/api/v1/conversations/{uuid}/mark-unread", perm(handleMarkConversationAsUnread, "conversations:read"))
6263
g.POST("/api/v1/conversations/{uuid}/tags", perm(handleUpdateConversationtags, "conversations:update_tags"))
6364
g.GET("/api/v1/conversations/{cuuid}/messages/{uuid}", perm(handleGetMessage, "messages:read"))
6465
g.GET("/api/v1/conversations/{uuid}/messages", perm(handleGetMessages, "messages:read"))
@@ -231,6 +232,14 @@ func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) {
231232
// Actvity logs.
232233
g.GET("/api/v1/activity-logs", perm(handleGetActivityLogs, "activity_logs:manage"))
233234

235+
// User notifications.
236+
g.GET("/api/v1/notifications", auth(handleGetUserNotifications))
237+
g.GET("/api/v1/notifications/stats", auth(handleGetUserNotificationStats))
238+
g.PUT("/api/v1/notifications/{id}/read", auth(handleMarkNotificationAsRead))
239+
g.PUT("/api/v1/notifications/read-all", auth(handleMarkAllNotificationsAsRead))
240+
g.DELETE("/api/v1/notifications/{id}", auth(handleDeleteNotification))
241+
g.DELETE("/api/v1/notifications", auth(handleDeleteAllNotifications))
242+
234243
// WebSocket.
235244
g.GET("/ws", auth(func(r *fastglue.Request) error {
236245
return handleWS(r, hub)

cmd/init.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,6 @@ func initConversations(
238238
status *status.Manager,
239239
priority *priority.Manager,
240240
hub *ws.Hub,
241-
notif *notifier.Service,
242241
db *sqlx.DB,
243242
inboxStore *inbox.Manager,
244243
userStore *user.Manager,
@@ -249,8 +248,9 @@ func initConversations(
249248
automationEngine *automation.Engine,
250249
template *tmpl.Manager,
251250
webhook *webhook.Manager,
251+
dispatcher *notifier.Dispatcher,
252252
) *conversation.Manager {
253-
c, err := conversation.New(hub, i18n, notif, sla, status, priority, inboxStore, userStore, teamStore, mediaStore, settings, csat, automationEngine, template, webhook, conversation.Opts{
253+
c, err := conversation.New(hub, i18n, sla, status, priority, inboxStore, userStore, teamStore, mediaStore, settings, csat, automationEngine, template, webhook, dispatcher, conversation.Opts{
254254
DB: db,
255255
Lo: initLogger("conversation_manager"),
256256
OutgoingMessageQueueSize: ko.MustInt("message.outgoing_queue_size"),
@@ -319,13 +319,13 @@ func initBusinessHours(db *sqlx.DB, i18n *i18n.I18n) *businesshours.Manager {
319319
}
320320

321321
// initSLA inits SLA manager.
322-
func initSLA(db *sqlx.DB, teamManager *team.Manager, settings *setting.Manager, businessHours *businesshours.Manager, notifier *notifier.Service, template *tmpl.Manager, userManager *user.Manager, i18n *i18n.I18n) *sla.Manager {
322+
func initSLA(db *sqlx.DB, teamManager *team.Manager, settings *setting.Manager, businessHours *businesshours.Manager, template *tmpl.Manager, userManager *user.Manager, i18n *i18n.I18n, dispatcher *notifier.Dispatcher) *sla.Manager {
323323
var lo = initLogger("sla")
324324
m, err := sla.New(sla.Opts{
325325
DB: db,
326326
Lo: lo,
327327
I18n: i18n,
328-
}, teamManager, settings, businessHours, notifier, template, userManager)
328+
}, teamManager, settings, businessHours, template, userManager, dispatcher)
329329
if err != nil {
330330
log.Fatalf("error initializing SLA manager: %v", err)
331331
}
@@ -940,6 +940,30 @@ func initWebhook(db *sqlx.DB, i18n *i18n.I18n) *webhook.Manager {
940940
return m
941941
}
942942

943+
// initUserNotification inits user notification manager.
944+
func initUserNotification(db *sqlx.DB, i18n *i18n.I18n) *notifier.UserNotificationManager {
945+
var lo = initLogger("user-notification")
946+
m, err := notifier.NewUserNotificationManager(notifier.UserNotificationOpts{
947+
DB: db,
948+
Lo: lo,
949+
I18n: i18n,
950+
})
951+
if err != nil {
952+
log.Fatalf("error initializing user notification manager: %v", err)
953+
}
954+
return m
955+
}
956+
957+
// initNotifDispatcher initializes the notification dispatcher.
958+
func initNotifDispatcher(userNotification *notifier.UserNotificationManager, outbound *notifier.Service, wsHub *ws.Hub) *notifier.Dispatcher {
959+
return notifier.NewDispatcher(notifier.DispatcherOpts{
960+
InApp: userNotification,
961+
Outbound: outbound,
962+
WSHub: wsHub,
963+
Lo: initLogger("notification-dispatcher"),
964+
})
965+
}
966+
943967
// initLogger initializes a logf logger.
944968
func initLogger(src string) *logf.Logger {
945969
lvl, env := ko.MustString("app.log_level"), ko.MustString("app.env")

cmd/main.go

Lines changed: 71 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -69,38 +69,39 @@ const (
6969

7070
// App is the global app context which is passed and injected in the http handlers.
7171
type App struct {
72-
redis *redis.Client
73-
fs stuffbin.FileSystem
74-
consts atomic.Value
75-
auth *auth_.Auth
76-
authz *authz.Enforcer
77-
i18n *i18n.I18n
78-
lo *logf.Logger
79-
oidc *oidc.Manager
80-
media *media.Manager
81-
setting *setting.Manager
82-
role *role.Manager
83-
user *user.Manager
84-
team *team.Manager
85-
status *status.Manager
86-
priority *priority.Manager
87-
tag *tag.Manager
88-
inbox *inbox.Manager
89-
tmpl *template.Manager
90-
macro *macro.Manager
91-
conversation *conversation.Manager
92-
automation *automation.Engine
93-
businessHours *businesshours.Manager
94-
sla *sla.Manager
95-
csat *csat.Manager
96-
view *view.Manager
97-
ai *ai.Manager
98-
search *search.Manager
99-
activityLog *activitylog.Manager
100-
notifier *notifier.Service
101-
customAttribute *customAttribute.Manager
102-
report *report.Manager
103-
webhook *webhook.Manager
72+
redis *redis.Client
73+
fs stuffbin.FileSystem
74+
consts atomic.Value
75+
auth *auth_.Auth
76+
authz *authz.Enforcer
77+
i18n *i18n.I18n
78+
lo *logf.Logger
79+
oidc *oidc.Manager
80+
media *media.Manager
81+
setting *setting.Manager
82+
role *role.Manager
83+
user *user.Manager
84+
team *team.Manager
85+
status *status.Manager
86+
priority *priority.Manager
87+
tag *tag.Manager
88+
inbox *inbox.Manager
89+
tmpl *template.Manager
90+
macro *macro.Manager
91+
conversation *conversation.Manager
92+
automation *automation.Engine
93+
businessHours *businesshours.Manager
94+
sla *sla.Manager
95+
csat *csat.Manager
96+
view *view.Manager
97+
ai *ai.Manager
98+
search *search.Manager
99+
activityLog *activitylog.Manager
100+
notifier *notifier.Service
101+
userNotification *notifier.UserNotificationManager
102+
customAttribute *customAttribute.Manager
103+
report *report.Manager
104+
webhook *webhook.Manager
104105

105106
// Global state that stores data on an available app update.
106107
update *AppUpdate
@@ -210,9 +211,11 @@ func main() {
210211
user = initUser(i18n, db)
211212
wsHub = initWS(user)
212213
notifier = initNotifier()
214+
userNotification = initUserNotification(db, i18n)
215+
notifDispatcher = initNotifDispatcher(userNotification, notifier, wsHub)
213216
automation = initAutomationEngine(db, i18n)
214-
sla = initSLA(db, team, settings, businessHours, notifier, template, user, i18n)
215-
conversation = initConversations(i18n, sla, status, priority, wsHub, notifier, db, inbox, user, team, media, settings, csat, automation, template, webhook)
217+
sla = initSLA(db, team, settings, businessHours, template, user, i18n, notifDispatcher)
218+
conversation = initConversations(i18n, sla, status, priority, wsHub, db, inbox, user, team, media, settings, csat, automation, template, webhook, notifDispatcher)
216219
autoassigner = initAutoAssigner(team, user, conversation)
217220
)
218221
automation.SetConversationStore(conversation)
@@ -229,40 +232,42 @@ func main() {
229232
go media.DeleteUnlinkedMedia(ctx)
230233
go user.MonitorAgentAvailability(ctx)
231234
go conversation.RunDraftCleaner(ctx, draftRetentionDuration)
235+
go userNotification.RunNotificationCleaner(ctx)
232236

233237
var app = &App{
234-
lo: lo,
235-
redis: rdb,
236-
fs: fs,
237-
sla: sla,
238-
oidc: oidc,
239-
i18n: i18n,
240-
auth: auth,
241-
media: media,
242-
setting: settings,
243-
inbox: inbox,
244-
user: user,
245-
team: team,
246-
status: status,
247-
priority: priority,
248-
tmpl: template,
249-
notifier: notifier,
250-
consts: atomic.Value{},
251-
conversation: conversation,
252-
automation: automation,
253-
businessHours: businessHours,
254-
activityLog: initActivityLog(db, i18n),
255-
customAttribute: initCustomAttribute(db, i18n),
256-
authz: initAuthz(i18n),
257-
view: initView(db, i18n),
258-
report: initReport(db, i18n),
259-
csat: initCSAT(db, i18n),
260-
search: initSearch(db, i18n),
261-
role: initRole(db, i18n),
262-
tag: initTag(db, i18n),
263-
macro: initMacro(db, i18n),
264-
ai: initAI(db, i18n),
265-
webhook: webhook,
238+
lo: lo,
239+
redis: rdb,
240+
fs: fs,
241+
sla: sla,
242+
oidc: oidc,
243+
i18n: i18n,
244+
auth: auth,
245+
media: media,
246+
setting: settings,
247+
inbox: inbox,
248+
user: user,
249+
team: team,
250+
status: status,
251+
priority: priority,
252+
tmpl: template,
253+
notifier: notifier,
254+
userNotification: userNotification,
255+
consts: atomic.Value{},
256+
conversation: conversation,
257+
automation: automation,
258+
businessHours: businessHours,
259+
activityLog: initActivityLog(db, i18n),
260+
customAttribute: initCustomAttribute(db, i18n),
261+
authz: initAuthz(i18n),
262+
view: initView(db, i18n),
263+
report: initReport(db, i18n),
264+
csat: initCSAT(db, i18n),
265+
search: initSearch(db, i18n),
266+
role: initRole(db, i18n),
267+
tag: initTag(db, i18n),
268+
macro: initMacro(db, i18n),
269+
ai: initAI(db, i18n),
270+
webhook: webhook,
266271
}
267272
app.consts.Store(constants)
268273

cmd/user_notifications.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package main
2+
3+
import (
4+
"strconv"
5+
6+
amodels "github.com/abhinavxd/libredesk/internal/auth/models"
7+
"github.com/abhinavxd/libredesk/internal/envelope"
8+
"github.com/valyala/fasthttp"
9+
"github.com/zerodha/fastglue"
10+
)
11+
12+
func handleGetUserNotifications(r *fastglue.Request) error {
13+
var (
14+
app = r.Context.(*App)
15+
auser = r.RequestCtx.UserValue("user").(amodels.User)
16+
limit = 20
17+
offset = 0
18+
)
19+
if l := r.RequestCtx.QueryArgs().GetUintOrZero("limit"); l > 0 && l <= 100 {
20+
limit = l
21+
}
22+
if o := r.RequestCtx.QueryArgs().GetUintOrZero("offset"); o > 0 {
23+
offset = o
24+
}
25+
notifications, err := app.userNotification.GetAll(auser.ID, limit, offset)
26+
if err != nil {
27+
return sendErrorEnvelope(r, err)
28+
}
29+
return r.SendEnvelope(notifications)
30+
}
31+
32+
func handleGetUserNotificationStats(r *fastglue.Request) error {
33+
var (
34+
app = r.Context.(*App)
35+
auser = r.RequestCtx.UserValue("user").(amodels.User)
36+
)
37+
stats, err := app.userNotification.GetStats(auser.ID)
38+
if err != nil {
39+
return sendErrorEnvelope(r, err)
40+
}
41+
return r.SendEnvelope(stats)
42+
}
43+
44+
func handleMarkNotificationAsRead(r *fastglue.Request) error {
45+
var (
46+
app = r.Context.(*App)
47+
auser = r.RequestCtx.UserValue("user").(amodels.User)
48+
id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
49+
)
50+
if id <= 0 {
51+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest,
52+
app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
53+
}
54+
if err := app.userNotification.MarkAsRead(id, auser.ID); err != nil {
55+
return sendErrorEnvelope(r, err)
56+
}
57+
return r.SendEnvelope(true)
58+
}
59+
60+
func handleMarkAllNotificationsAsRead(r *fastglue.Request) error {
61+
var (
62+
app = r.Context.(*App)
63+
auser = r.RequestCtx.UserValue("user").(amodels.User)
64+
)
65+
66+
if err := app.userNotification.MarkAllAsRead(auser.ID); err != nil {
67+
return sendErrorEnvelope(r, err)
68+
}
69+
return r.SendEnvelope(true)
70+
}
71+
72+
func handleDeleteNotification(r *fastglue.Request) error {
73+
var (
74+
app = r.Context.(*App)
75+
auser = r.RequestCtx.UserValue("user").(amodels.User)
76+
id, _ = strconv.Atoi(r.RequestCtx.UserValue("id").(string))
77+
)
78+
if id <= 0 {
79+
return r.SendErrorEnvelope(fasthttp.StatusBadRequest,
80+
app.i18n.Ts("globals.messages.invalid", "name", "`id`"), nil, envelope.InputError)
81+
}
82+
83+
if err := app.userNotification.Delete(id, auser.ID); err != nil {
84+
return sendErrorEnvelope(r, err)
85+
}
86+
return r.SendEnvelope(true)
87+
}
88+
89+
func handleDeleteAllNotifications(r *fastglue.Request) error {
90+
var (
91+
app = r.Context.(*App)
92+
auser = r.RequestCtx.UserValue("user").(amodels.User)
93+
)
94+
95+
if err := app.userNotification.DeleteAll(auser.ID); err != nil {
96+
return sendErrorEnvelope(r, err)
97+
}
98+
return r.SendEnvelope(true)
99+
}

cmd/users.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,6 @@ const (
2626
maxAvatarSizeMB = 2
2727
)
2828

29-
type updateAvailabilityRequest struct {
30-
Status string `json:"status"`
31-
}
32-
3329
type resetPasswordRequest struct {
3430
Email string `json:"email"`
3531
}

0 commit comments

Comments
 (0)