diff --git a/services/web/app/src/Features/Notifications/NotificationsBuilder.js b/services/web/app/src/Features/Notifications/NotificationsBuilder.js index 8823022e7a..30ef8b97d5 100644 --- a/services/web/app/src/Features/Notifications/NotificationsBuilder.js +++ b/services/web/app/src/Features/Notifications/NotificationsBuilder.js @@ -281,6 +281,9 @@ function personalAndGroupSubscriptions(userId) { } } +/** + * @param {string} [userId] + */ function ieeeCollabratecRetirement(userId) { return { key: 'notification-ieee-collabratec-retirement', @@ -295,8 +298,8 @@ function ieeeCollabratecRetirement(userId) { callback ) }, - read(callback) { - NotificationsHandler.markAsReadByKeyOnly(this.key, callback) + deleteAllUnread(callback) { + NotificationsHandler.markAsReadByKeyOnlyBulk(this.key, callback) }, } } diff --git a/services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx b/services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx index 7f46a0fbd6..2d8e5e9748 100644 --- a/services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx +++ b/services/web/frontend/js/features/project-list/components/notifications/groups/common.tsx @@ -13,6 +13,7 @@ import { } from '../../../../../../../types/project/dashboard/notification' import { User } from '../../../../../../../types/user' import GroupInvitationNotification from './group-invitation/group-invitation' +import IEEERetirementBanner from '../ieee-retirement-banner' import { debugConsole } from '@/utils/debugging' function Common() { @@ -305,21 +306,7 @@ function CommonNotification({ notification }: CommonNotificationProps) { ) : templateKey === 'notification_group_invitation' ? ( ) : templateKey === 'notification_ieee_collabratec_retirement' ? ( - id && handleDismiss(id)} - body={ - , - // eslint-disable-next-line jsx-a11y/anchor-has-content,react/jsx-key - , - ]} - /> - } - /> + ) : templateKey === 'notification_personal_and_group_subscriptions' ? ( { + eventTracking.sendMB('promo-dismiss', { + name: 'ieee - retirement', + }) + if (id) { + handleDismiss(id) + } + }, [id, handleDismiss]) + + const handleClickPlans = useCallback(() => { + eventTracking.sendMB('promo-click', { + name: 'ieee - retirement', + content: 'plans', + }) + }, []) + + const handleClickEmail = useCallback(() => { + eventTracking.sendMB('promo-click', { + name: 'ieee - retirement', + content: 'email', + }) + }, []) + + useEffect(() => { + if (!viewEventSent) { + eventTracking.sendMB('promo-prompt', { + name: 'ieee - retirement', + }) + viewEventSent = true + } + }, []) + + return ( + , + // eslint-disable-next-line jsx-a11y/anchor-has-content,react/jsx-key + , + ]} + /> + } + /> + ) +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index da7555964a..8d1602df20 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1212,7 +1212,7 @@ "note_experiments_under_development": "<0>Please note that experiments in this program are still being tested and actively developed. This means that they might <0>change, be <0>removed or <0>become part of a paid plan", "note_features_under_development": "<0>Please note that features in this program are still being tested and actively developed. This means that they might <0>change, be <0>removed or <0>become part of a premium plan", "notification_features_upgraded_by_affiliation": "Good news! Your affiliated organization __institutionName__ has an Overleaf subscription, and you now have access to all of Overleaf’s Professional features.", - "notification_ieee_collabratec_retirement_message": "From January 31, IEEE is no longer providing access to Overleaf premium features for Collabratec users. Please contact <0>authors@ieee.org with any questions. Need to upgrade? <1>View our plans", + "notification_ieee_collabratec_retirement_message": "As of April 2, 2024, free access to Overleaf Professional for Collabratec users or IEEE accounts is no longer provided. Please contact <0>authors@ieee.org with any questions. Need to upgrade? <1>View our plans", "notification_personal_and_group_subscriptions": "We’ve spotted that you’ve got <0>more than one active __appName__ subscription. To avoid paying more than you need to, <1>review your subscriptions.", "notification_personal_subscription_not_required_due_to_affiliation": " Good news! Your affiliated organization __institutionName__ has an Overleaf subscription, and you now have access to Overleaf’s Professional features through your affiliation. You can cancel your individual subscription without losing access to any features.", "notification_project_invite": "__userName__ would like you to join __projectName__ Join Project", diff --git a/services/web/locales/zh-CN.json b/services/web/locales/zh-CN.json index 198d7d2630..1077e0a67a 100644 --- a/services/web/locales/zh-CN.json +++ b/services/web/locales/zh-CN.json @@ -1200,7 +1200,6 @@ "note_experiments_under_development": "<0>请注意该计划中的实验仍在测试和快速开发中。 这意味着它们可能<0>发生改变、<0>被删除或<0>成为付费计划的一部分", "note_features_under_development": "<0>请注意此计划中的功能仍在测试和快速开发中。 这意味着它们可能<0>改变、<0>被删除或<0>成为高级计划的一部分", "notification_features_upgraded_by_affiliation": "好消息!您的组织__institutionName__已有 Overleaf 订阅,并且您现在可以访问 Overleaf 的所有专业功能。", - "notification_ieee_collabratec_retirement_message": "从 2024年 1 月 31日起, IEEE 不再提供 Overleaf premium 功能给协作者。有问题请联系 <0>authors@ieee.org。需要升级吗? <1>查看我们的订阅计划", "notification_personal_and_group_subscriptions": "我们发现您有<0>多个活跃的 __appName__ 订阅。 为避免支付超出您需要的费用,请<1>检查您的订阅。", "notification_personal_subscription_not_required_due_to_affiliation": " 好消息!您的组织 __institutionName__ 与 Overleaf 有合作关系。您可以取消您的个人订阅,而不会失去访问您的任何利益。", "notification_project_invite": "__userName__ 想让您加入 __projectName__ 加入项目", diff --git a/services/web/scripts/add_notification_ieee_collabratec_users.js b/services/web/scripts/add_notification_ieee_collabratec_users.js index 183e6217ae..09a23c6c27 100644 --- a/services/web/scripts/add_notification_ieee_collabratec_users.js +++ b/services/web/scripts/add_notification_ieee_collabratec_users.js @@ -1,11 +1,78 @@ +const path = require('path') +const fs = require('fs') const NotificationsBuilder = require('../app/src/Features/Notifications/NotificationsBuilder') const { waitForDb } = require('../app/src/infrastructure/mongodb') const { Subscription } = require('../app/src/models/Subscription') const minimist = require('minimist') +const { db } = require('../app/src/infrastructure/mongodb') +const { promiseMapWithLimit } = require('@overleaf/promise-utils') + +/** + * This script is used to notify some users in the IEEECollabratec group that + * they will lose access to Overleaf. + * + * Parameters: + * --filename: the filename of the JSON file containing emails of users that + * should **not** be notified. + * --commit: if present, the script will commit the changes to the database. + * + * Usage: + * - dry run: + * node add_notification_ieee_collabratec_users.js --filename=emails.json + * - commit: + * node add_notification_ieee_collabratec_users.js --filename=emails.json --commit + */ let COMMIT = false +let EMAILS_FILENAME + +/** + * The IEEE have provided us with a list of active users that should not be removed + * (and therefore not notified). This method retrives those users. + */ +function getActiveUserEmails(filename) { + const data = fs.readFileSync(path.join(__dirname, filename), 'utf8') + const emailsArray = JSON.parse(data) + const emailsSet = new Set(emailsArray) + console.log( + `Read ${emailsSet.size} (${emailsArray.length} in array) emails from ${filename}` + ) + return emailsSet +} + +async function getIEEEUsers() { + return await db.subscriptions + .aggregate([ + { $match: { teamName: 'IEEECollabratec' } }, + { $unwind: '$member_ids' }, + { + $lookup: { + from: 'users', + localField: 'member_ids', + foreignField: '_id', + as: 'member_details', + }, + }, + { + $project: { + _id: 1, + teamName: 1, + 'member_details._id': 1, + 'member_details.email': 1, + 'member_details.emails.email': 1, + }, + }, + ]) + .toArray() +} async function main() { + const start = performance.now() + + if (!EMAILS_FILENAME) { + throw new Error('No email filename provided') + } + await waitForDb() const subscription = await Subscription.findOne({ teamName: 'IEEECollabratec', @@ -16,35 +83,71 @@ async function main() { return } - const userIds = subscription.member_ids - - console.log(`Found ${userIds.length} users in IEEECollabratec group`) - - if (!COMMIT) { - console.log('Dry run enabled, quitting here') - return + // First we remove all existing Collabratec retirement notifications + if (COMMIT) { + await NotificationsBuilder.promises + .ieeeCollabratecRetirement() + .deleteAllUnread() } - if (userIds.length > 0) { - console.log(`Notifying ${userIds.length} users`) + let totalUsers = 0 + let totalUsersNotified = 0 - for (const id of userIds) { + const usersArray = await getIEEEUsers() + const activeUsers = getActiveUserEmails(EMAILS_FILENAME) + + const activeUsersFound = new Set() + + // Then go through each collabratec user to see if we need to notify them + await promiseMapWithLimit(10, usersArray, async member => { + if (totalUsers % 5000 === 0) + console.log( + `notified: ${totalUsersNotified} - progress: ${totalUsers} / ${usersArray.length}` + ) + + totalUsers = totalUsers + 1 + + const userDetails = member.member_details[0] + + for (const email of userDetails.emails) { + if (activeUsers.has(email.email)) { + activeUsersFound.add(email.email) + return + } + } + + if (COMMIT) { await NotificationsBuilder.promises - .ieeeCollabratecRetirement(id.toString()) + .ieeeCollabratecRetirement(userDetails._id.toString()) .create() } - console.log( - `Notification successfully added/updated for ${userIds.length} users` - ) - } else { - console.log('No users found') - } + totalUsersNotified += 1 + }) + + console.log(`Found ${totalUsers} users in IEEECollabratec group`) + + console.log( + `Found ${totalUsersNotified} users in IEEECollabratec group to notify` + ) + + console.log(`Found ${activeUsersFound.size} active users`) + + const activeUsersNotFound = Array.from(activeUsers).filter( + user => !activeUsersFound.has(user) + ) + + console.log(`${activeUsersNotFound.length} IEEE active users not found:`) + console.log(activeUsersNotFound) + + const end = performance.now() + console.log(`Took ${end - start} ms`) } const setup = () => { const argv = minimist(process.argv.slice(2)) COMMIT = argv.commit !== undefined + EMAILS_FILENAME = argv.filename if (!COMMIT) { console.warn('Doing dry run. Add --commit to commit changes') }