@@ -45,17 +45,13 @@ func messageThreadActivityUpdates(params MessageThreadActivityUpdate) map[string
4545 if params .Unarchive {
4646 updates ["is_archived" ] = false
4747 }
48- if params .MarkAsUnread {
49- updates ["is_read" ] = gorm .Expr (
50- "CASE WHEN last_read_at < ? THEN ? ELSE is_read END" ,
51- params .EventTimestamp ,
52- false ,
53- )
54- }
5548 return updates
5649}
5750
5851func messageThreadDeletedUpdates (params MessageThreadDeletedUpdate ) map [string ]any {
52+ if ! params .UpdateLastMessage {
53+ return map [string ]any {}
54+ }
5955 return map [string ]any {
6056 "last_message_id" : params .LastMessageID ,
6157 "last_message_content" : params .LastMessageContent ,
@@ -68,15 +64,65 @@ func messageThreadStatusUpdates(params MessageThreadStatusUpdate) map[string]any
6864 if params .IsArchived != nil {
6965 updates ["is_archived" ] = * params .IsArchived
7066 }
71- if params .IsRead != nil {
72- updates ["is_read" ] = * params .IsRead
73- if * params .IsRead {
74- updates ["last_read_at" ] = params .ReadAt
75- }
67+ if params .UnreadCount != nil {
68+ updates ["unread_count" ] = 0
69+ updates ["last_read_at" ] = params .ReadAt
7670 }
7771 return updates
7872}
7973
74+ func lockMessageThread (tx * gorm.DB , userID entities.UserID , threadID uuid.UUID ) (* entities.MessageThread , error ) {
75+ thread := new (entities.MessageThread )
76+ err := tx .
77+ Clauses (clause.Locking {Strength : "UPDATE" }).
78+ Where ("user_id = ?" , userID ).
79+ Where ("id = ?" , threadID ).
80+ First (thread ).
81+ Error
82+ if errors .Is (err , gorm .ErrRecordNotFound ) {
83+ return nil , stacktrace .PropagateWithCodef (
84+ err ,
85+ ErrCodeNotFound ,
86+ "message thread with ID [%s] for user [%s] does not exist" ,
87+ threadID ,
88+ userID ,
89+ )
90+ }
91+ if err != nil {
92+ return nil , stacktrace .Propagatef (err , "cannot lock message thread with ID [%s] for user [%s]" , threadID , userID )
93+ }
94+ return thread , nil
95+ }
96+
97+ func insertUnreadItem (tx * gorm.DB , item entities.MessageThreadUnreadItem ) (bool , error ) {
98+ result := tx .Clauses (clause.OnConflict {DoNothing : true }).Create (& item )
99+ if result .Error != nil {
100+ return false , stacktrace .Propagatef (
101+ result .Error ,
102+ "cannot insert unread ledger item for message [%s] in thread [%s]" ,
103+ item .MessageID ,
104+ item .MessageThreadID ,
105+ )
106+ }
107+ return result .RowsAffected == 1 , nil
108+ }
109+
110+ func deleteUnreadItem (tx * gorm.DB , messageID uuid.UUID , threadID uuid.UUID ) (bool , error ) {
111+ result := tx .
112+ Where ("message_id = ?" , messageID ).
113+ Where ("message_thread_id = ?" , threadID ).
114+ Delete (& entities.MessageThreadUnreadItem {})
115+ if result .Error != nil {
116+ return false , stacktrace .Propagatef (
117+ result .Error ,
118+ "cannot delete unread ledger item for message [%s] in thread [%s]" ,
119+ messageID ,
120+ threadID ,
121+ )
122+ }
123+ return result .RowsAffected == 1 , nil
124+ }
125+
80126func (repository * gormMessageThreadRepository ) DeleteAllForUser (ctx context.Context , userID entities.UserID ) error {
81127 ctx , span := repository .tracer .Start (ctx )
82128 defer span .End ()
@@ -106,39 +152,98 @@ func (repository *gormMessageThreadRepository) UpdateAfterDeletedMessage(ctx con
106152 ctx , span := repository .tracer .Start (ctx )
107153 defer span .End ()
108154
109- result := repository .db .WithContext (ctx ).
110- Model (& entities.MessageThread {}).
111- Where ("user_id = ?" , params .UserID ).
112- Where ("id = ?" , params .MessageThreadID ).
113- Updates (messageThreadDeletedUpdates (params ))
114- if result .Error != nil {
115- return repository .tracer .WrapErrorSpan (span , stacktrace .Propagatef (result .Error , "cannot update deleted-message metadata for thread [%s]" , params .MessageThreadID ))
155+ err := repository .db .WithContext (ctx ).Transaction (func (tx * gorm.DB ) error {
156+ thread , err := lockMessageThread (tx , params .UserID , params .MessageThreadID )
157+ if err != nil {
158+ return err
159+ }
160+
161+ deleted , err := deleteUnreadItem (tx , params .DeletedMessageID , params .MessageThreadID )
162+ if err != nil {
163+ return err
164+ }
165+ if deleted {
166+ if err := tx .
167+ Model (thread ).
168+ Where ("user_id = ?" , params .UserID ).
169+ Where ("id = ?" , params .MessageThreadID ).
170+ UpdateColumn ("unread_count" , gorm .Expr ("GREATEST(unread_count - 1, 0)" )).
171+ Error ; err != nil {
172+ return stacktrace .Propagatef (
173+ err ,
174+ "cannot decrement unread count for thread [%s] and user [%s]" ,
175+ params .MessageThreadID ,
176+ params .UserID ,
177+ )
178+ }
179+ if thread .UnreadCount > 0 {
180+ thread .UnreadCount --
181+ }
182+ }
183+
184+ if ! params .UpdateLastMessage {
185+ return nil
186+ }
187+ if err := tx .
188+ Model (thread ).
189+ Where ("user_id = ?" , params .UserID ).
190+ Where ("id = ?" , params .MessageThreadID ).
191+ Updates (messageThreadDeletedUpdates (params )).
192+ Error ; err != nil {
193+ return stacktrace .Propagatef (
194+ err ,
195+ "cannot update deleted-message metadata for thread [%s] and user [%s]" ,
196+ params .MessageThreadID ,
197+ params .UserID ,
198+ )
199+ }
200+ return nil
201+ })
202+ if err != nil {
203+ return repository .tracer .WrapErrorSpan (
204+ span ,
205+ stacktrace .Propagatef (
206+ err ,
207+ "cannot apply deleted message [%s] to thread [%s] for user [%s]" ,
208+ params .DeletedMessageID ,
209+ params .MessageThreadID ,
210+ params .UserID ,
211+ ),
212+ )
116213 }
117214
118215 return nil
119216}
120217
121218// Store a new entities.MessageThread
122- func (repository * gormMessageThreadRepository ) Store (ctx context.Context , thread * entities.MessageThread ) error {
219+ func (repository * gormMessageThreadRepository ) Store (ctx context.Context , thread * entities.MessageThread , unreadMessageID * uuid. UUID ) error {
123220 ctx , span := repository .tracer .Start (ctx )
124221 defer span .End ()
125222
126- isRead := thread .IsRead
127223 err := repository .db .WithContext (ctx ).Transaction (func (tx * gorm.DB ) error {
128224 result := tx .Clauses (clause.OnConflict {DoNothing : true }).Create (thread )
129- thread .IsRead = isRead
130225 if result .Error != nil {
131- return result .Error
226+ return stacktrace . Propagatef ( result .Error , "cannot insert message thread with ID [%s]" , thread . ID )
132227 }
133- if result .RowsAffected == 0 || isRead {
228+ if result .RowsAffected == 0 || unreadMessageID == nil {
134229 return nil
135230 }
136231
137- return tx .Model (& entities.MessageThread {}).
138- Where ("user_id = ?" , thread .UserID ).
139- Where ("id = ?" , thread .ID ).
140- UpdateColumn ("is_read" , false ).
141- Error
232+ inserted , err := insertUnreadItem (tx , entities.MessageThreadUnreadItem {
233+ MessageID : * unreadMessageID ,
234+ MessageThreadID : thread .ID ,
235+ })
236+ if err != nil {
237+ return err
238+ }
239+ if ! inserted {
240+ return stacktrace .NewErrorf (
241+ "unread ledger item for message [%s] was not inserted for new thread [%s]" ,
242+ * unreadMessageID ,
243+ thread .ID ,
244+ )
245+ }
246+ return nil
142247 })
143248 if err != nil {
144249 return repository .tracer .WrapErrorSpan (span , stacktrace .Propagatef (err , "cannot save message thread with ID [%s]" , thread .ID ))
@@ -152,16 +257,63 @@ func (repository *gormMessageThreadRepository) UpdateActivity(ctx context.Contex
152257 ctx , span := repository .tracer .Start (ctx )
153258 defer span .End ()
154259
155- result := repository .db .WithContext (ctx ).
156- Model (& entities.MessageThread {}).
157- Where ("user_id = ?" , params .UserID ).
158- Where ("id = ?" , params .MessageThreadID ).
159- Updates (messageThreadActivityUpdates (params ))
160- if result .Error != nil {
161- return repository .tracer .WrapErrorSpan (span , stacktrace .Propagatef (result .Error , "cannot update message activity for thread [%s]" , params .MessageThreadID ))
162- }
163- if result .RowsAffected == 0 {
164- return repository .tracer .WrapErrorSpan (span , stacktrace .PropagateWithCodef (gorm .ErrRecordNotFound , ErrCodeNotFound , "thread with id [%s] not found" , params .MessageThreadID ))
260+ err := repository .db .WithContext (ctx ).Transaction (func (tx * gorm.DB ) error {
261+ thread , err := lockMessageThread (tx , params .UserID , params .MessageThreadID )
262+ if err != nil {
263+ return err
264+ }
265+
266+ if err := tx .
267+ Model (thread ).
268+ Where ("user_id = ?" , params .UserID ).
269+ Where ("id = ?" , params .MessageThreadID ).
270+ Updates (messageThreadActivityUpdates (params )).
271+ Error ; err != nil {
272+ return stacktrace .Propagatef (
273+ err ,
274+ "cannot update message activity for thread [%s] and user [%s]" ,
275+ params .MessageThreadID ,
276+ params .UserID ,
277+ )
278+ }
279+ if ! params .CountAsUnread || ! params .EventTimestamp .After (thread .LastReadAt ) {
280+ return nil
281+ }
282+
283+ inserted , err := insertUnreadItem (tx , entities.MessageThreadUnreadItem {
284+ MessageID : params .MessageID ,
285+ MessageThreadID : params .MessageThreadID ,
286+ })
287+ if err != nil || ! inserted {
288+ return err
289+ }
290+
291+ if err := tx .
292+ Model (thread ).
293+ Where ("user_id = ?" , params .UserID ).
294+ Where ("id = ?" , params .MessageThreadID ).
295+ UpdateColumn ("unread_count" , gorm .Expr ("unread_count + ?" , 1 )).
296+ Error ; err != nil {
297+ return stacktrace .Propagatef (
298+ err ,
299+ "cannot increment unread count for thread [%s] and user [%s]" ,
300+ params .MessageThreadID ,
301+ params .UserID ,
302+ )
303+ }
304+ thread .UnreadCount ++
305+ return nil
306+ })
307+ if err != nil {
308+ return repository .tracer .WrapErrorSpan (
309+ span ,
310+ stacktrace .Propagatef (
311+ err ,
312+ "cannot update message activity for thread [%s] and user [%s]" ,
313+ params .MessageThreadID ,
314+ params .UserID ,
315+ ),
316+ )
165317 }
166318
167319 return nil
@@ -177,18 +329,70 @@ func (repository *gormMessageThreadRepository) UpdateStatus(
177329 ctx , span := repository .tracer .Start (ctx )
178330 defer span .End ()
179331
180- thread := new (entities.MessageThread )
181- result := repository .db .WithContext (ctx ).
182- Model (thread ).
183- Clauses (clause.Returning {}).
184- Where ("user_id = ?" , userID ).
185- Where ("id = ?" , messageThreadID ).
186- Updates (messageThreadStatusUpdates (params ))
187- if result .Error != nil {
188- return nil , repository .tracer .WrapErrorSpan (span , stacktrace .Propagatef (result .Error , "cannot update status for thread [%s] and user [%s]" , messageThreadID , userID ))
332+ if params .UnreadCount != nil && * params .UnreadCount != 0 {
333+ return nil , repository .tracer .WrapErrorSpan (
334+ span ,
335+ stacktrace .NewErrorf (
336+ "cannot set unread count to [%d] for thread [%s] and user [%s]: only zero is supported" ,
337+ * params .UnreadCount ,
338+ messageThreadID ,
339+ userID ,
340+ ),
341+ )
189342 }
190- if result .RowsAffected == 0 {
191- return nil , repository .tracer .WrapErrorSpan (span , stacktrace .PropagateWithCodef (gorm .ErrRecordNotFound , ErrCodeNotFound , "thread with id [%s] not found for user with ID [%s]" , messageThreadID , userID ))
343+
344+ var thread * entities.MessageThread
345+ err := repository .db .WithContext (ctx ).Transaction (func (tx * gorm.DB ) error {
346+ var err error
347+ thread , err = lockMessageThread (tx , userID , messageThreadID )
348+ if err != nil {
349+ return err
350+ }
351+
352+ updates := messageThreadStatusUpdates (params )
353+ if len (updates ) > 0 {
354+ if err := tx .
355+ Model (thread ).
356+ Clauses (clause.Returning {}).
357+ Where ("user_id = ?" , userID ).
358+ Where ("id = ?" , messageThreadID ).
359+ Updates (updates ).
360+ Error ; err != nil {
361+ return stacktrace .Propagatef (
362+ err ,
363+ "cannot update status for thread [%s] and user [%s]" ,
364+ messageThreadID ,
365+ userID ,
366+ )
367+ }
368+ }
369+ if params .IsArchived != nil {
370+ thread .IsArchived = * params .IsArchived
371+ }
372+ if params .UnreadCount == nil {
373+ return nil
374+ }
375+
376+ thread .UnreadCount = * params .UnreadCount
377+ thread .LastReadAt = params .ReadAt
378+ if err := tx .
379+ Where ("message_thread_id = ?" , messageThreadID ).
380+ Delete (& entities.MessageThreadUnreadItem {}).
381+ Error ; err != nil {
382+ return stacktrace .Propagatef (
383+ err ,
384+ "cannot clear unread ledger items for thread [%s] and user [%s]" ,
385+ messageThreadID ,
386+ userID ,
387+ )
388+ }
389+ return nil
390+ })
391+ if err != nil {
392+ return nil , repository .tracer .WrapErrorSpan (
393+ span ,
394+ stacktrace .Propagatef (err , "cannot update status for thread [%s] and user [%s]" , messageThreadID , userID ),
395+ )
192396 }
193397
194398 return thread , nil
0 commit comments