Skip to content

Commit 4550312

Browse files
committed
feat: Add support for local notifications
1 parent cc7df45 commit 4550312

File tree

14 files changed

+1364
-79
lines changed

14 files changed

+1364
-79
lines changed

internal/db/multiaccounts/settings_notifications/database.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,16 @@ func (ns *NotificationsSettings) SetMessagePreview(value int) error {
253253
return err
254254
}
255255

256+
// HasExemption returns true if an exemption row exists for the given chat/community id.
257+
func (ns *NotificationsSettings) HasExemption(id string) (bool, error) {
258+
var dummy int
259+
err := ns.db.QueryRow("SELECT 1 FROM notifications_settings WHERE id = ? AND exemption = 1 LIMIT 1", id).Scan(&dummy)
260+
if err == sql.ErrNoRows {
261+
return false, nil
262+
}
263+
return err == nil, err
264+
}
265+
256266
// Exemption settings
257267
func (ns *NotificationsSettings) GetExMuteAllMessages(id string) (result bool, err error) {
258268
err = ns.db.QueryRow(ns.buildSelectQuery(columnExMuteAllMessages), id).Scan(&result)

protocol/communities/community_description_encryption.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@ type DescriptionEncryptor interface {
1313
decryptCommunityDescription(keyIDSeqNo string, d []byte) (*DecryptCommunityResponse, error)
1414
}
1515

16+
// NoopDescriptionEncryptor implements DescriptionEncryptor with no-op behavior.
17+
// For use in tests when encryption is not needed (e.g. unencrypted communities).
18+
type NoopDescriptionEncryptor struct{}
19+
20+
func (*NoopDescriptionEncryptor) encryptCommunityDescription(*Community, *protobuf.CommunityDescription) (string, []byte, error) {
21+
return "", nil, nil
22+
}
23+
24+
func (*NoopDescriptionEncryptor) encryptCommunityDescriptionChannel(*Community, string, *protobuf.CommunityDescription) (string, []byte, error) {
25+
return "", nil, nil
26+
}
27+
28+
func (*NoopDescriptionEncryptor) decryptCommunityDescription(string, []byte) (*DecryptCommunityResponse, error) {
29+
return &DecryptCommunityResponse{}, nil
30+
}
31+
1632
// Encrypts members and chats
1733
func encryptDescription(encryptor DescriptionEncryptor, community *Community, description *protobuf.CommunityDescription) error {
1834
description.PrivateData = make(map[string][]byte)
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package protocol
2+
3+
import (
4+
"context"
5+
"encoding/hex"
6+
"errors"
7+
"testing"
8+
9+
"github.com/stretchr/testify/suite"
10+
11+
"github.com/status-im/status-go/internal/crypto"
12+
testutils2 "github.com/status-im/status-go/internal/testutils"
13+
)
14+
15+
func TestLocalNotificationSettingsSuite(t *testing.T) {
16+
suite.Run(t, new(LocalNotificationSettingsSuite))
17+
}
18+
19+
type LocalNotificationSettingsSuite struct {
20+
MessengerBaseTestSuite
21+
}
22+
23+
func (s *LocalNotificationSettingsSuite) SetupTest() {
24+
s.MessengerBaseTestSuite.setupMessaging()
25+
s.m = s.newMessenger()
26+
s.privateKey = s.m.identity
27+
}
28+
29+
// TestOneToOneChatsTurnOff verifies that when OneToOneChats is TurnOff,
30+
// no local notification is produced for 1:1 messages.
31+
func (s *LocalNotificationSettingsSuite) TestOneToOneChatsTurnOff() {
32+
bob := s.m
33+
alice := s.newMessenger()
34+
35+
// Bob disables 1:1 notifications
36+
s.Require().NoError(bob.settings.SetOneToOneChats(notifValueTurnOff))
37+
38+
// Create 1:1 chat from alice -> bob and send message
39+
pkString := hex.EncodeToString(crypto.FromECDSAPub(&bob.identity.PublicKey))
40+
chat := CreateOneToOneChat(pkString, &bob.identity.PublicKey, alice.getTimesource())
41+
s.Require().NoError(alice.SaveChat(chat))
42+
inputMessage := buildTestMessage(*chat)
43+
_, err := alice.SendChatMessage(context.Background(), inputMessage)
44+
s.Require().NoError(err)
45+
46+
// Sync until bob receives the message
47+
var bobResponse *MessengerResponse
48+
err = testutils2.RetryWithBackOff(func() error {
49+
_, err := alice.RetrieveAll()
50+
if err != nil {
51+
return err
52+
}
53+
bobResponse, err = bob.RetrieveAll()
54+
if err != nil {
55+
return err
56+
}
57+
if len(bobResponse.Messages()) == 0 {
58+
return errors.New("messages not received")
59+
}
60+
return nil
61+
})
62+
s.Require().NoError(err)
63+
s.Require().NotNil(bobResponse)
64+
65+
// Bob should have received the message but no local notification
66+
s.Require().NotEmpty(bobResponse.Messages())
67+
s.Require().Empty(bobResponse.Notifications(), "OneToOneChats=TurnOff should produce no local notifications")
68+
}
69+
70+
// TestOneToOneChatsSendAlerts verifies that when OneToOneChats is SendAlerts,
71+
// a local notification is produced for 1:1 messages.
72+
func (s *LocalNotificationSettingsSuite) TestOneToOneChatsSendAlerts() {
73+
bob := s.m
74+
alice := s.newMessenger()
75+
76+
// Ensure 1:1 notifications are on (default)
77+
s.Require().NoError(bob.settings.SetOneToOneChats(notifValueSendAlerts))
78+
79+
// Create 1:1 chat from alice -> bob and send message
80+
pkString := hex.EncodeToString(crypto.FromECDSAPub(&bob.identity.PublicKey))
81+
chat := CreateOneToOneChat(pkString, &bob.identity.PublicKey, alice.getTimesource())
82+
s.Require().NoError(alice.SaveChat(chat))
83+
inputMessage := buildTestMessage(*chat)
84+
_, err := alice.SendChatMessage(context.Background(), inputMessage)
85+
s.Require().NoError(err)
86+
87+
// Sync until bob receives the message
88+
var bobResponse *MessengerResponse
89+
err = testutils2.RetryWithBackOff(func() error {
90+
_, err := alice.RetrieveAll()
91+
if err != nil {
92+
return err
93+
}
94+
bobResponse, err = bob.RetrieveAll()
95+
if err != nil {
96+
return err
97+
}
98+
if len(bobResponse.Messages()) == 0 {
99+
return errors.New("messages not received")
100+
}
101+
return nil
102+
})
103+
s.Require().NoError(err)
104+
s.Require().NotNil(bobResponse)
105+
106+
// Bob should have a local notification
107+
s.Require().NotEmpty(bobResponse.Messages())
108+
s.Require().NotEmpty(bobResponse.Notifications(), "OneToOneChats=SendAlerts should produce a local notification")
109+
}
110+
111+
// TestGroupChatsTurnOff verifies that when GroupChats is TurnOff,
112+
// no local notification is produced for private group messages.
113+
func (s *LocalNotificationSettingsSuite) TestGroupChatsTurnOff() {
114+
bob := s.m
115+
alice := s.newMessenger()
116+
117+
// Bob disables group notifications
118+
s.Require().NoError(bob.settings.SetGroupChats(notifValueTurnOff))
119+
120+
// Create group chat with alice as creator, add bob as member
121+
response, err := alice.CreateGroupChatWithMembers(context.Background(), "test-group", []string{})
122+
s.Require().NoError(err)
123+
s.Require().Len(response.Chats(), 1)
124+
chat := response.Chats()[0]
125+
s.Require().NoError(alice.SaveChat(chat))
126+
127+
s.Require().NoError(makeMutualContact(alice, &bob.identity.PublicKey))
128+
members := []string{"0x" + hex.EncodeToString(crypto.FromECDSAPub(&bob.identity.PublicKey))}
129+
_, err = alice.AddMembersToGroupChat(context.Background(), chat.ID, members)
130+
s.Require().NoError(err)
131+
132+
// Bob receives invite and confirms
133+
_, err = WaitOnMessengerResponse(bob, func(r *MessengerResponse) bool { return len(r.Chats()) > 0 }, "chat invite")
134+
s.Require().NoError(err)
135+
_, err = bob.ConfirmJoiningGroup(context.Background(), chat.ID)
136+
s.Require().NoError(err)
137+
138+
// Wait for alice to see bob joined
139+
_, err = WaitOnMessengerResponse(alice, func(r *MessengerResponse) bool { return len(r.Chats()) > 0 }, "join event")
140+
s.Require().NoError(err)
141+
142+
// Alice sends message
143+
inputMessage := buildTestMessage(*chat)
144+
_, err = alice.SendChatMessage(context.Background(), inputMessage)
145+
s.Require().NoError(err)
146+
147+
var bobResponse *MessengerResponse
148+
err = testutils2.RetryWithBackOff(func() error {
149+
_, err := alice.RetrieveAll()
150+
if err != nil {
151+
return err
152+
}
153+
bobResponse, err = bob.RetrieveAll()
154+
if err != nil {
155+
return err
156+
}
157+
if len(bobResponse.Messages()) == 0 {
158+
return errors.New("messages not received")
159+
}
160+
return nil
161+
})
162+
s.Require().NoError(err)
163+
s.Require().NotNil(bobResponse)
164+
165+
s.Require().NotEmpty(bobResponse.Messages())
166+
s.Require().Empty(bobResponse.Notifications(), "GroupChats=TurnOff should produce no local notifications")
167+
}
168+
169+
// TestContactRequestsTurnOff_GroupInvite verifies that when ContactRequests is TurnOff,
170+
// no local notification is produced for private group invites.
171+
func (s *LocalNotificationSettingsSuite) TestContactRequestsTurnOff_GroupInvite() {
172+
bob := s.m
173+
alice := s.newMessenger()
174+
175+
// Bob disables contact-request notifications (group invites)
176+
s.Require().NoError(bob.settings.SetContactRequests(notifValueTurnOff))
177+
178+
// Alice creates group and invites Bob
179+
response, err := alice.CreateGroupChatWithMembers(context.Background(), "test-group", []string{})
180+
s.Require().NoError(err)
181+
s.Require().Len(response.Chats(), 1)
182+
chat := response.Chats()[0]
183+
s.Require().NoError(alice.SaveChat(chat))
184+
185+
s.Require().NoError(makeMutualContact(alice, &bob.identity.PublicKey))
186+
s.Require().NoError(makeMutualContact(bob, &alice.identity.PublicKey))
187+
members := []string{"0x" + hex.EncodeToString(crypto.FromECDSAPub(&bob.identity.PublicKey))}
188+
_, err = alice.AddMembersToGroupChat(context.Background(), chat.ID, members)
189+
s.Require().NoError(err)
190+
191+
// Bob receives the group invite
192+
inviteResponse, err := WaitOnMessengerResponse(bob, func(r *MessengerResponse) bool { return len(r.Chats()) > 0 }, "chat invite")
193+
s.Require().NoError(err)
194+
s.Require().NotNil(inviteResponse)
195+
196+
// Bob should have received the chat but no local notification for the invite
197+
s.Require().NotEmpty(inviteResponse.Chats())
198+
s.Require().Empty(inviteResponse.Notifications(), "ContactRequests=TurnOff should produce no local notification for group invite")
199+
}
200+
201+
// TestContactRequestsSendAlerts_GroupInvite verifies that when ContactRequests is SendAlerts,
202+
// a local notification is produced for private group invites.
203+
func (s *LocalNotificationSettingsSuite) TestContactRequestsSendAlerts_GroupInvite() {
204+
bob := s.m
205+
alice := s.newMessenger()
206+
207+
// Bob enables contact-request notifications (default)
208+
s.Require().NoError(bob.settings.SetContactRequests(notifValueSendAlerts))
209+
210+
// Alice creates group and invites Bob
211+
response, err := alice.CreateGroupChatWithMembers(context.Background(), "test-group", []string{})
212+
s.Require().NoError(err)
213+
s.Require().Len(response.Chats(), 1)
214+
chat := response.Chats()[0]
215+
s.Require().NoError(alice.SaveChat(chat))
216+
217+
s.Require().NoError(makeMutualContact(alice, &bob.identity.PublicKey))
218+
s.Require().NoError(makeMutualContact(bob, &alice.identity.PublicKey))
219+
members := []string{"0x" + hex.EncodeToString(crypto.FromECDSAPub(&bob.identity.PublicKey))}
220+
_, err = alice.AddMembersToGroupChat(context.Background(), chat.ID, members)
221+
s.Require().NoError(err)
222+
223+
// Bob receives the group invite
224+
inviteResponse, err := WaitOnMessengerResponse(bob, func(r *MessengerResponse) bool { return len(r.Chats()) > 0 }, "chat invite")
225+
s.Require().NoError(err)
226+
s.Require().NotNil(inviteResponse)
227+
228+
// Bob should have a local notification for the invite
229+
s.Require().NotEmpty(inviteResponse.Chats())
230+
s.Require().NotEmpty(inviteResponse.Notifications(), "ContactRequests=SendAlerts should produce a local notification for group invite")
231+
}

0 commit comments

Comments
 (0)