@@ -6,8 +6,11 @@ import (
66 "fmt"
77 "regexp"
88 "runtime"
9+ "strconv"
10+ "sync"
911 "time"
1012
13+ lru "github.com/hashicorp/golang-lru"
1114 "github.com/keybase/client/go/chat"
1215 "github.com/keybase/client/go/chat/globals"
1316 "github.com/keybase/client/go/chat/storage"
@@ -20,6 +23,42 @@ import (
2023 "github.com/kyokomi/emoji"
2124)
2225
26+ var (
27+ seenNotificationsMtx sync.Mutex
28+ seenNotifications , _ = lru .New (100 )
29+
30+ multipleAccountsMtx sync.Mutex
31+ multipleAccountsCached * bool
32+ )
33+
34+ // accountCacheLogoutHook implements libkb.LogoutHook. It clears the cached
35+ // result of hasMultipleLoggedInAccounts so that the next background
36+ // notification recomputes it against the post-logout account list.
37+ type accountCacheLogoutHook struct {}
38+
39+ func (accountCacheLogoutHook ) OnLogout (_ libkb.MetaContext ) error {
40+ multipleAccountsMtx .Lock ()
41+ multipleAccountsCached = nil
42+ multipleAccountsMtx .Unlock ()
43+ return nil
44+ }
45+
46+ func hasMultipleLoggedInAccounts (ctx context.Context ) bool {
47+ multipleAccountsMtx .Lock ()
48+ defer multipleAccountsMtx .Unlock ()
49+ if multipleAccountsCached != nil {
50+ return * multipleAccountsCached
51+ }
52+ users , err := kbCtx .GetUsersWithStoredSecrets (ctx )
53+ if err != nil {
54+ // Don't cache on error; retry next time.
55+ return false
56+ }
57+ result := len (users ) > 1
58+ multipleAccountsCached = & result
59+ return result
60+ }
61+
2362type Person struct {
2463 KeybaseUsername string
2564 KeybaseAvatar string
@@ -47,6 +86,8 @@ type ChatNotification struct {
4786 IsPlaintext bool
4887 SoundName string
4988 BadgeCount int
89+ // Title is the notification title, e.g. "username@keybase"
90+ Title string
5091}
5192
5293func HandlePostTextReply (strConvID , tlfName string , intMessageID int , body string ) (err error ) {
@@ -106,6 +147,23 @@ func HandleBackgroundNotification(strConvID, body, serverMessageBody, sender str
106147 return libkb.LoginRequiredError {}
107148 }
108149 mp := chat .NewMobilePush (gc )
150+ // Dedupe by convID||msgID
151+ dupKey := strConvID + "||" + strconv .Itoa (intMessageID )
152+ // Optimistic early-exit: check under the mutex so that if another goroutine
153+ // is currently in the display+add critical section below, we wait for it to
154+ // finish and then see the cache entry rather than proceeding with redundant work.
155+ seenNotificationsMtx .Lock ()
156+ _ , isDup := seenNotifications .Get (dupKey )
157+ seenNotificationsMtx .Unlock ()
158+ if isDup {
159+ // Cancel any duplicate visible notifications
160+ if len (pushID ) > 0 {
161+ mp .AckNotificationSuccess (ctx , []string {pushID })
162+ }
163+ kbCtx .Log .CDebugf (ctx , "HandleBackgroundNotification: duplicate notification convID=%s msgID=%d" , strConvID , intMessageID )
164+ // Return nil (not an error) so Android does not treat this as failure and show a fallback notification.
165+ return nil
166+ }
109167 uid := gregor1 .UID (kbCtx .Env .GetUID ().ToBytes ())
110168 convID , err := chat1 .MakeConvID (strConvID )
111169 if err != nil {
@@ -119,6 +177,11 @@ func HandleBackgroundNotification(strConvID, body, serverMessageBody, sender str
119177 return err
120178 }
121179
180+ currentUsername := string (kbCtx .Env .GetUsername ())
181+ title := "Keybase"
182+ if hasMultipleLoggedInAccounts (ctx ) {
183+ title = fmt .Sprintf ("%s@keybase" , currentUsername )
184+ }
122185 chatNotification := ChatNotification {
123186 IsPlaintext : displayPlaintext ,
124187 Message : & Message {
@@ -131,10 +194,12 @@ func HandleBackgroundNotification(strConvID, body, serverMessageBody, sender str
131194 TopicName : conv .Info .TopicName ,
132195 TlfName : conv .Info .TlfName ,
133196 IsGroupConversation : len (conv .Info .Participants ) > 2 ,
134- ConversationName : utils .FormatConversationName (conv .Info , string ( kbCtx . Env . GetUsername ()) ),
197+ ConversationName : utils .FormatConversationName (conv .Info , currentUsername ),
135198 SoundName : soundName ,
136199 BadgeCount : badgeCount ,
200+ Title : title ,
137201 }
202+ kbCtx .Log .CDebugf (ctx , "HandleBackgroundNotification: title=%s" , chatNotification .Title )
138203
139204 msgUnboxed , err := mp .UnboxPushNotification (ctx , uid , convID , membersType , body )
140205 if err == nil && msgUnboxed .IsValid () {
@@ -195,6 +260,22 @@ func HandleBackgroundNotification(strConvID, body, serverMessageBody, sender str
195260
196261 // only display and ack this notification if we actually have something to display
197262 if pusher != nil && (len (chatNotification .Message .Plaintext ) > 0 || len (chatNotification .Message .ServerMessage ) > 0 ) {
263+ // Lock and check if we've already processed this notification.
264+ seenNotificationsMtx .Lock ()
265+ defer seenNotificationsMtx .Unlock ()
266+ if _ , ok := seenNotifications .Get (dupKey ); ok {
267+ // Cancel any duplicate visible notifications
268+ if len (pushID ) > 0 {
269+ mp .AckNotificationSuccess (ctx , []string {pushID })
270+ }
271+ kbCtx .Log .CDebugf (ctx , "HandleBackgroundNotification: duplicate notification convID=%s msgID=%d" , strConvID , intMessageID )
272+ // Return nil (not an error) so Android does not treat this as failure and show a fallback notification.
273+ return nil
274+ }
275+ // Add to cache before displaying so that any concurrent goroutine that
276+ // reaches the second check while DisplayChatNotification is running will
277+ // see the entry and bail out rather than displaying a duplicate.
278+ seenNotifications .Add (dupKey , struct {}{})
198279 pusher .DisplayChatNotification (& chatNotification )
199280 if len (pushID ) > 0 {
200281 mp .AckNotificationSuccess (ctx , []string {pushID })
0 commit comments