Просмотр исходного кода

Merge pull request #17508 from overleaf/dp-ac-ieee-deprecation-notifications

Update notifications for IEEE Retirement

GitOrigin-RevId: f4e02e5fd838c2b1a6227c86f48bb12dd6bdb9a3
David 2 лет назад
Родитель
Сommit
3ca09c07a1

+ 5 - 2
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)
     },
   }
 }

+ 2 - 15
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' ? (
         <GroupInvitationNotification notification={notification} />
       ) : templateKey === 'notification_ieee_collabratec_retirement' ? (
-        <Notification
-          bsStyle="warning"
-          onDismiss={() => id && handleDismiss(id)}
-          body={
-            <Trans
-              i18nKey="notification_ieee_collabratec_retirement_message"
-              components={[
-                // eslint-disable-next-line jsx-a11y/anchor-has-content,react/jsx-key
-                <a href="mailto:authors@ieee.org" />,
-                // eslint-disable-next-line jsx-a11y/anchor-has-content,react/jsx-key
-                <a href="/user/subscription" />,
-              ]}
-            />
-          }
-        />
+        <IEEERetirementBanner id={id} />
       ) : templateKey === 'notification_personal_and_group_subscriptions' ? (
         <Notification
           bsStyle="warning"

+ 68 - 0
services/web/frontend/js/features/project-list/components/notifications/ieee-retirement-banner.tsx

@@ -0,0 +1,68 @@
+import { useCallback, useEffect } from 'react'
+import Notification from './notification'
+import { Trans } from 'react-i18next'
+import * as eventTracking from '../../../../infrastructure/event-tracking'
+import useAsyncDismiss from './hooks/useAsyncDismiss'
+
+let viewEventSent = false
+
+type IEEERetirementBannerProps = {
+  id: number | undefined
+}
+
+export default function IEEERetirementBanner({
+  id,
+}: IEEERetirementBannerProps) {
+  const { handleDismiss } = useAsyncDismiss()
+
+  const handleClose = useCallback(() => {
+    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 (
+    <Notification
+      bsStyle="warning"
+      onDismiss={handleClose}
+      newNotificationStyle
+      body={
+        <Trans
+          i18nKey="notification_ieee_collabratec_retirement_message"
+          components={[
+            // eslint-disable-next-line jsx-a11y/anchor-has-content,react/jsx-key
+            <a href="mailto:authors@ieee.org" onClick={handleClickEmail} />,
+            // eslint-disable-next-line jsx-a11y/anchor-has-content,react/jsx-key
+            <a href="/user/subscription" onClick={handleClickPlans} />,
+          ]}
+        />
+      }
+    />
+  )
+}

+ 1 - 1
services/web/locales/en.json

@@ -1212,7 +1212,7 @@
   "note_experiments_under_development": "<0>Please note</0> that experiments in this program are still being tested and actively developed. This means that they might <0>change</0>, be <0>removed</0> or <0>become part of a paid plan</0>",
   "note_features_under_development": "<0>Please note</0> that features in this program are still being tested and actively developed. This means that they might <0>change</0>, be <0>removed</0> or <0>become part of a premium plan</0>",
   "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</0> with any questions. Need to upgrade? <1>View our plans</1>",
+  "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</0> with any questions. Need to upgrade? <1>View our plans</1>",
   "notification_personal_and_group_subscriptions": "We’ve spotted that you’ve got <0>more than one active __appName__ subscription</0>. To avoid paying more than you need to, <1>review your subscriptions</1>.",
   "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": "<b>__userName__</b> would like you to join <b>__projectName__</b> <a class=\"btn btn-sm btn-info pull-right\" href=\"/project/__projectId__/invite/token/__token__\">Join Project</a>",

+ 0 - 1
services/web/locales/zh-CN.json

@@ -1200,7 +1200,6 @@
   "note_experiments_under_development": "<0>请注意</0>该计划中的实验仍在测试和快速开发中。 这意味着它们可能<0>发生改变</0>、<0>被删除</0>或<0>成为付费计划的一部分</0>",
   "note_features_under_development": "<0>请注意</0>此计划中的功能仍在测试和快速开发中。 这意味着它们可能<0>改变</0>、<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</0>。需要升级吗? <1>查看我们的订阅计划</1>",
   "notification_personal_and_group_subscriptions": "我们发现您有<0>多个活跃的 __appName__ 订阅</0>。 为避免支付超出您需要的费用,请<1>检查您的订阅</1>。",
   "notification_personal_subscription_not_required_due_to_affiliation": " 好消息!您的组织 __institutionName__ 与 Overleaf 有合作关系。您可以取消您的个人订阅,而不会失去访问您的任何利益。",
   "notification_project_invite": "<b>__userName__</b> 想让您加入 <b>__projectName__</b> <a class=\"btn btn-sm btn-info pull-right\" href=\"’/project/__projectId__/invite/token/__token__’\">加入项目</a>",

+ 119 - 16
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
+  // First we remove all existing Collabratec retirement notifications
+  if (COMMIT) {
+    await NotificationsBuilder.promises
+      .ieeeCollabratecRetirement()
+      .deleteAllUnread()
+  }
 
-  console.log(`Found ${userIds.length} users in IEEECollabratec group`)
+  let totalUsers = 0
+  let totalUsersNotified = 0
 
-  if (!COMMIT) {
-    console.log('Dry run enabled, quitting here')
-    return
-  }
+  const usersArray = await getIEEEUsers()
+  const activeUsers = getActiveUserEmails(EMAILS_FILENAME)
+
+  const activeUsersFound = new Set()
 
-  if (userIds.length > 0) {
-    console.log(`Notifying ${userIds.length} users`)
+  // 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}`
+      )
 
-    for (const id of userIds) {
+    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')
   }