Skip to content

Commit dfc56b6

Browse files
feat: notificationSettings (#2954)
1 parent 247bb76 commit dfc56b6

27 files changed

+1544
-223
lines changed

__tests__/__snapshots__/users.ts.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ Object {
5050
"infoConfirmed": false,
5151
"language": null,
5252
"name": "Ido",
53-
"notificationEmail": true,
5453
"permalink": "http://localhost:5002/aaa1",
5554
"timezone": "Europe/London",
5655
"twitter": null,

__tests__/boot.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,9 @@ const LOGGED_IN_BODY = {
129129
timezone: DEFAULT_TIMEZONE,
130130
reputation: 10,
131131
portfolio: null,
132-
notificationEmail: true,
133132
acceptedMarketing: false,
134133
company: null,
135134
experienceLevel: null,
136-
followNotifications: true,
137-
followingEmail: true,
138-
awardEmail: true,
139-
awardNotifications: true,
140135
isTeamMember: false,
141136
bluesky: null,
142137
roadmap: null,

__tests__/common/mailing.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,8 @@ import {
99
UserPersonalizedDigestType,
1010
} from '../../src/entity';
1111
import { usersFixture } from '../fixture/user';
12-
import {
13-
CioUnsubscribeTopic,
14-
syncSubscription,
15-
updateFlagsStatement,
16-
} from '../../src/common';
12+
import { syncSubscription, updateFlagsStatement } from '../../src/common';
13+
import { CioUnsubscribeTopic } from '../../src/cio';
1714

1815
let con: DataSource;
1916

__tests__/notifications.ts

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
NotificationPreference,
2121
NotificationAttachmentType,
2222
} from '../src/entity';
23+
import type { UserNotificationFlags } from '../src/entity/user/User';
2324
import { DataSource } from 'typeorm';
2425
import createOrGetConnection from '../src/db';
2526
import { usersFixture } from './fixture/user';
@@ -33,6 +34,8 @@ import {
3334
NotificationPreferenceType,
3435
NotificationType,
3536
saveNotificationPreference,
37+
streamNotificationUsers,
38+
NotificationChannel,
3639
} from '../src/notifications/common';
3740
import { postsFixture, sharedPostsFixture } from './fixture/post';
3841
import { sourcesFixture } from './fixture/source';
@@ -1227,3 +1230,238 @@ describe('mutation subscribeNotificationPreference', () => {
12271230
);
12281231
});
12291232
});
1233+
1234+
describe('streamNotificationUsers', () => {
1235+
const setupNotificationAndUsers = async (
1236+
users: {
1237+
id: string;
1238+
name: string;
1239+
email: string;
1240+
notificationFlags?: UserNotificationFlags;
1241+
}[],
1242+
) => {
1243+
await con.getRepository(User).save(users);
1244+
1245+
const notif = await con.getRepository(NotificationV2).save({
1246+
...notificationV2Fixture,
1247+
type: NotificationType.ArticleNewComment,
1248+
});
1249+
1250+
const userNotifications = users.map((user) => ({
1251+
userId: user.id,
1252+
notificationId: notif.id,
1253+
public: true,
1254+
createdAt: notificationV2Fixture.createdAt,
1255+
}));
1256+
1257+
await con.getRepository(UserNotification).insert(userNotifications);
1258+
1259+
return notif.id;
1260+
};
1261+
1262+
const streamToArray = async (
1263+
stream: NodeJS.ReadableStream,
1264+
): Promise<{ userId: string }[]> => {
1265+
const results: { userId: string }[] = [];
1266+
return new Promise((res) => {
1267+
stream.on('data', (data: { userId: string }) => {
1268+
results.push({ userId: data.userId });
1269+
});
1270+
stream.on('end', () => res(results));
1271+
});
1272+
};
1273+
1274+
it('should return users for inApp channel when no preferences set', async () => {
1275+
const users = [
1276+
{ id: 'user1', name: 'User 1', email: '[email protected]' },
1277+
{ id: 'user2', name: 'User 2', email: '[email protected]' },
1278+
];
1279+
1280+
const notifId = await setupNotificationAndUsers(users);
1281+
const stream = await streamNotificationUsers(
1282+
con,
1283+
notifId,
1284+
NotificationChannel.InApp,
1285+
);
1286+
const results = await streamToArray(stream);
1287+
1288+
expect(results).toHaveLength(2);
1289+
expect(results.map((r) => r.userId).sort()).toEqual(['user1', 'user2']);
1290+
});
1291+
1292+
it('should return users for email channel when no preferences set', async () => {
1293+
const users = [
1294+
{ id: 'user3', name: 'User 3', email: '[email protected]' },
1295+
{ id: 'user4', name: 'User 4', email: '[email protected]' },
1296+
];
1297+
1298+
const notifId = await setupNotificationAndUsers(users);
1299+
const stream = await streamNotificationUsers(
1300+
con,
1301+
notifId,
1302+
NotificationChannel.Email,
1303+
);
1304+
const results = await streamToArray(stream);
1305+
1306+
expect(results).toHaveLength(2);
1307+
expect(results.map((r) => r.userId).sort()).toEqual(['user3', 'user4']);
1308+
});
1309+
1310+
it('should exclude users with global inApp mute preference', async () => {
1311+
const users = [
1312+
{
1313+
id: 'user5',
1314+
name: 'User 5',
1315+
1316+
notificationFlags: {
1317+
[NotificationType.ArticleNewComment]: {
1318+
inApp: NotificationPreferenceStatus.Muted,
1319+
},
1320+
},
1321+
},
1322+
{
1323+
id: 'user6',
1324+
name: 'User 6',
1325+
1326+
notificationFlags: {
1327+
[NotificationType.ArticleNewComment]: {
1328+
inApp: NotificationPreferenceStatus.Subscribed,
1329+
},
1330+
},
1331+
},
1332+
];
1333+
1334+
const notifId = await setupNotificationAndUsers(users);
1335+
const stream = await streamNotificationUsers(
1336+
con,
1337+
notifId,
1338+
NotificationChannel.InApp,
1339+
);
1340+
const results = await streamToArray(stream);
1341+
1342+
expect(results).toHaveLength(1);
1343+
expect(results[0].userId).toBe('user6');
1344+
});
1345+
1346+
it('should exclude users with global email mute preference', async () => {
1347+
const users = [
1348+
{
1349+
id: 'user7',
1350+
name: 'User 7',
1351+
1352+
notificationFlags: {
1353+
[NotificationType.ArticleNewComment]: {
1354+
email: NotificationPreferenceStatus.Muted,
1355+
},
1356+
},
1357+
},
1358+
{
1359+
id: 'user8',
1360+
name: 'User 8',
1361+
1362+
notificationFlags: {
1363+
[NotificationType.ArticleNewComment]: {
1364+
email: NotificationPreferenceStatus.Subscribed,
1365+
},
1366+
},
1367+
},
1368+
];
1369+
1370+
const notifId = await setupNotificationAndUsers(users);
1371+
const stream = await streamNotificationUsers(
1372+
con,
1373+
notifId,
1374+
NotificationChannel.Email,
1375+
);
1376+
const results = await streamToArray(stream);
1377+
1378+
expect(results).toHaveLength(1);
1379+
expect(results[0].userId).toBe('user8');
1380+
});
1381+
1382+
it('should not send notification to user with global inApp mute preference and email subscribed', async () => {
1383+
const users = [
1384+
{
1385+
id: 'user9',
1386+
name: 'User 9',
1387+
1388+
notificationFlags: {
1389+
[NotificationType.ArticleNewComment]: {
1390+
inApp: NotificationPreferenceStatus.Muted,
1391+
email: NotificationPreferenceStatus.Subscribed,
1392+
},
1393+
},
1394+
},
1395+
];
1396+
1397+
const notifId = await setupNotificationAndUsers(users);
1398+
1399+
const inAppStream = await streamNotificationUsers(
1400+
con,
1401+
notifId,
1402+
NotificationChannel.InApp,
1403+
);
1404+
const inAppResults = await streamToArray(inAppStream);
1405+
expect(inAppResults).toHaveLength(0);
1406+
1407+
const emailStream = await streamNotificationUsers(
1408+
con,
1409+
notifId,
1410+
NotificationChannel.Email,
1411+
);
1412+
const emailResults = await streamToArray(emailStream);
1413+
expect(emailResults).toHaveLength(1);
1414+
expect(emailResults[0].userId).toBe('user9');
1415+
});
1416+
1417+
it('should exclude globally muted users even if they have entity-level subscriptions', async () => {
1418+
const users = [
1419+
{
1420+
id: 'user14',
1421+
name: 'User 14',
1422+
1423+
notificationFlags: {
1424+
[NotificationType.ArticleNewComment]: {
1425+
inApp: NotificationPreferenceStatus.Muted,
1426+
},
1427+
},
1428+
},
1429+
];
1430+
1431+
await con.getRepository(User).save(users);
1432+
await saveFixtures(con, Source, sourcesFixture);
1433+
await saveFixtures(con, Post, postsFixture);
1434+
1435+
await con.getRepository(NotificationPreferencePost).save({
1436+
userId: 'user14',
1437+
postId: postsFixture[0].id,
1438+
referenceId: postsFixture[0].id,
1439+
notificationType: NotificationType.ArticleNewComment,
1440+
status: NotificationPreferenceStatus.Subscribed,
1441+
type: NotificationPreferenceType.Post,
1442+
});
1443+
1444+
const notif = await con.getRepository(NotificationV2).save({
1445+
...notificationV2Fixture,
1446+
type: NotificationType.ArticleNewComment,
1447+
});
1448+
1449+
await con.getRepository(UserNotification).insert([
1450+
{
1451+
userId: 'user14',
1452+
notificationId: notif.id,
1453+
public: true,
1454+
createdAt: notificationV2Fixture.createdAt,
1455+
},
1456+
]);
1457+
1458+
const stream = await streamNotificationUsers(
1459+
con,
1460+
notif.id,
1461+
NotificationChannel.InApp,
1462+
);
1463+
const results = await streamToArray(stream);
1464+
1465+
expect(results).toHaveLength(0);
1466+
});
1467+
});

0 commit comments

Comments
 (0)