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

Add email notifications settings for redesigned editor

GitOrigin-RevId: d31f13bbc668d790564618e6b4a8a83fdf2d8780
Domagoj Kriskovic 8 месяцев назад
Родитель
Сommit
3f6e96f58e

+ 1 - 0
services/web/app/src/Features/Project/ProjectController.mjs

@@ -460,6 +460,7 @@ const _ProjectController = {
       'writefull-asymetric-queue-size-per-model',
       'pdf-dark-mode',
       'editor-redesign-opt-out',
+      'email-notifications',
     ].filter(Boolean)
 
     const getUserValues = async userId =>

+ 8 - 0
services/web/frontend/extracted-translations.json

@@ -131,6 +131,8 @@
   "all_logs": "",
   "all_premium_features": "",
   "all_premium_features_including": "",
+  "all_project_activity": "",
+  "all_project_activity_description": "",
   "all_projects": "",
   "all_projects_will_be_transferred_immediately": "",
   "all_these_experiments_are_available_exclusively": "",
@@ -541,6 +543,7 @@
   "email_link_expired": "",
   "email_managed_by_group_is": "",
   "email_must_be_linked_to_institution": "",
+  "email_notifications_are_currently_in_beta": "",
   "email_or_password_wrong_try_again": "",
   "email_remove_by_date": "",
   "emails_and_affiliations_explanation": "",
@@ -1158,6 +1161,7 @@
   "no_pdf_error_reason_unrecoverable_error": "",
   "no_pdf_error_title": "",
   "no_preview_available": "",
+  "no_project_notifications_description": "",
   "no_projects": "",
   "no_resolved_comments": "",
   "no_search_results": "",
@@ -1361,6 +1365,7 @@
   "project_linked_to": "",
   "project_name": "",
   "project_not_linked_to_github": "",
+  "project_notifications": "",
   "project_ownership_transfer_confirmation_1": "",
   "project_ownership_transfer_confirmation_2": "",
   "project_renamed_or_deleted": "",
@@ -1480,6 +1485,8 @@
   "replace_from_computer": "",
   "replace_from_project_files": "",
   "replace_from_url": "",
+  "replies_to_your_activity_only": "",
+  "replies_to_your_activity_only_description": "",
   "reply": "",
   "repository_name": "",
   "repository_visibility": "",
@@ -1868,6 +1875,7 @@
   "then_x_price_per_year": "",
   "there_are_lots_of_options_to_edit_and_customize_your_figures": "",
   "there_was_a_problem_restoring_the_project_please_try_again_in_a_few_moments_or_contact_us": "",
+  "these_settings_might_change_in_the_future": "",
   "they_lose_access_to_account": "",
   "they_will_be_removed_from_the_group": "",
   "they_will_continue_to_have_access_to_any_projects_shared_with_them": "",

BIN
services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2


+ 1 - 0
services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs

@@ -42,6 +42,7 @@ export default /** @type {const} */ ([
   'more_vert',
   'neurology',
   'note_add',
+  'notifications',
   'open_in_new',
   'password',
   'picture_as_pdf',

+ 51 - 0
services/web/frontend/js/features/ide-redesign/components/settings/editor-settings/project-notifications-setting.tsx

@@ -0,0 +1,51 @@
+import { useTranslation } from 'react-i18next'
+import RadioButtonSetting, { RadioOption } from '../radio-button-setting'
+import { useState } from 'react'
+
+type NotificationLevel = 'all' | 'replies' | 'off'
+
+export default function ProjectNotificationsSetting() {
+  const { t } = useTranslation()
+  // TODO: Connect to project settings context when backend support is added
+  const [notificationLevel, setNotificationLevel] =
+    useState<NotificationLevel>('all')
+
+  const options: Array<RadioOption<NotificationLevel>> = [
+    {
+      value: 'all',
+      label: t('all_project_activity'),
+      description: t('all_project_activity_description'),
+    },
+    {
+      value: 'replies',
+      label: t('replies_to_your_activity_only'),
+      description: t('replies_to_your_activity_only_description'),
+    },
+    {
+      value: 'off',
+      label: t('off'),
+      description: t('no_project_notifications_description'),
+    },
+  ]
+
+  return (
+    <>
+      <RadioButtonSetting
+        id="projectNotifications"
+        options={options}
+        value={notificationLevel}
+        onChange={setNotificationLevel}
+      />
+      <div className="project-notifications-beta-note">
+        <span className="beta-note-text">
+          {t('email_notifications_are_currently_in_beta')}{' '}
+          {t('these_settings_might_change_in_the_future')}{' '}
+        </span>
+        {/* TODO: update forms link */}
+        <a target="_blank" rel="noopener noreferrer" href="https://forms.gle/">
+          {t('give_feedback')}
+        </a>
+      </div>
+    </>
+  )
+}

+ 51 - 0
services/web/frontend/js/features/ide-redesign/components/settings/radio-button-setting.tsx

@@ -0,0 +1,51 @@
+import React from 'react'
+
+export type RadioOption<T extends string = string> = {
+  value: T
+  label: string
+  description?: string
+}
+
+type RadioButtonSettingProps<T extends string = string> = {
+  id: string
+  options: Array<RadioOption<T>>
+  value: T | undefined
+  onChange: (value: T) => void
+}
+
+export default function RadioButtonSetting<T extends string = string>({
+  id,
+  options,
+  value,
+  onChange,
+}: RadioButtonSettingProps<T>) {
+  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    onChange(event.target.value as T)
+  }
+
+  return (
+    <div className="ide-radio-setting-options">
+      {options.map(option => (
+        <label key={`${id}-${option.value}`} className="ide-radio-option">
+          <input
+            type="radio"
+            id={`${id}-${option.value}`}
+            name={id}
+            value={option.value}
+            checked={value === option.value}
+            onChange={handleChange}
+            className="ide-radio-input"
+          />
+          <div className="ide-radio-text">
+            <span className="ide-setting-title">{option.label}</span>
+            {option.description && (
+              <span className="ide-setting-description">
+                {option.description}
+              </span>
+            )}
+          </div>
+        </label>
+      ))}
+    </div>
+  )
+}

+ 17 - 0
services/web/frontend/js/features/ide-redesign/components/settings/settings-modal-body.tsx

@@ -3,6 +3,9 @@ import MaterialIcon from '@/shared/components/material-icon'
 import { Nav, NavLink, TabContainer, TabContent } from 'react-bootstrap'
 import { SettingsEntry } from '../../contexts/settings-modal-context'
 import SettingsTabPane from './settings-tab-pane'
+import BetaBadgeIcon from '@/shared/components/beta-badge-icon'
+import OLTooltip from '@/shared/components/ol/ol-tooltip'
+import { useTranslation } from 'react-i18next'
 
 export const SettingsModalBody = ({
   activeTab,
@@ -42,6 +45,8 @@ export const SettingsModalBody = ({
 }
 
 const SettingsNavLink = ({ entry }: { entry: SettingsEntry }) => {
+  const { t } = useTranslation()
+
   if ('href' in entry) {
     return (
       <a
@@ -77,6 +82,18 @@ const SettingsNavLink = ({ entry }: { entry: SettingsEntry }) => {
             unfilled
           />
           <span>{entry.title}</span>
+          <div className="flex-grow-1" />
+          {entry.key === 'project_notifications' && (
+            <OLTooltip
+              id="project-notifications-beta-badge"
+              description={t('email_notifications_are_currently_in_beta')}
+              overlayProps={{ placement: 'right', delay: 100 }}
+            >
+              <span>
+                <BetaBadgeIcon />
+              </span>
+            </OLTooltip>
+          )}
         </NavLink>
       </>
     )

+ 30 - 2
services/web/frontend/js/features/ide-redesign/contexts/settings-modal-context.tsx

@@ -28,6 +28,7 @@ import NewEditorSetting from '../components/settings/editor-settings/new-editor-
 import DarkModePdfSetting from '../components/settings/appearance-settings/dark-mode-pdf-setting'
 import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
 import { useFeatureFlag } from '@/shared/context/split-test-context'
+import ProjectNotificationsSetting from '../components/settings/editor-settings/project-notifications-setting'
 
 const [referenceSearchSettingModule] = importOverleafModules(
   'referenceSearchSetting'
@@ -51,6 +52,7 @@ export type SettingsTab = {
   icon: AvailableUnfilledIcon
   sections: SettingsSection[]
   title: string
+  hidden?: boolean
 }
 
 type SettingsLink = {
@@ -58,6 +60,7 @@ type SettingsLink = {
   icon: AvailableUnfilledIcon
   href: string
   title: string
+  hidden?: boolean
 }
 
 export type SettingsEntry = SettingsLink | SettingsTab
@@ -85,7 +88,8 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
   const { leftMenuShown, setLeftMenuShown } = useLayoutContext()
 
   const hasDarkModePdf = useFeatureFlag('pdf-dark-mode')
-  const settingsTabs: SettingsEntry[] = useMemo(
+  const hasEmailNotifications = useFeatureFlag('email-notifications')
+  const allSettingsTabs: SettingsEntry[] = useMemo(
     () => [
       {
         key: 'editor',
@@ -229,6 +233,25 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
           },
         ],
       },
+
+      {
+        key: 'project_notifications',
+        title: t('project_notifications'),
+        icon: 'notifications' as const,
+        sections: [
+          {
+            key: 'general',
+            settings: [
+              {
+                key: 'projectNotifications',
+                component: <ProjectNotificationsSetting />,
+              },
+            ],
+          },
+        ],
+        hidden: !hasEmailNotifications,
+      },
+
       {
         key: 'account_settings',
         title: t('account_settings'),
@@ -242,7 +265,12 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
         href: '/user/subscription',
       },
     ],
-    [t, overallTheme, hasDarkModePdf]
+    [t, overallTheme, hasDarkModePdf, hasEmailNotifications]
+  )
+
+  const settingsTabs = useMemo(
+    () => allSettingsTabs.filter(tab => !tab.hidden),
+    [allSettingsTabs]
   )
 
   const settingToTabMap = useMemo(() => {

+ 33 - 1
services/web/frontend/stylesheets/pages/editor/settings.scss

@@ -1,5 +1,5 @@
 .ide-settings-tab-nav.nav {
-  width: 240px;
+  width: 300px;
   border-right: var(--bs-modal-header-border-width) solid
     var(--bs-modal-header-border-color);
   padding: var(--spacing-02);
@@ -98,3 +98,35 @@
   justify-content: flex-end;
   margin-left: var(--spacing-06);
 }
+
+.ide-radio-setting-options {
+  display: flex;
+  flex-direction: column;
+  gap: var(--spacing-06);
+}
+
+.ide-radio-option {
+  display: flex;
+  align-items: flex-start;
+  gap: var(--spacing-04);
+  cursor: pointer;
+}
+
+.ide-radio-input {
+  margin-top: var(--spacing-02);
+}
+
+.ide-radio-text {
+  display: flex;
+  flex-direction: column;
+}
+
+.project-notifications-beta-note {
+  margin-top: var(--spacing-06);
+  padding: var(--spacing-04);
+  font-size: var(--font-size-02);
+}
+
+.beta-note-text {
+  color: var(--content-secondary);
+}

+ 8 - 0
services/web/locales/en.json

@@ -157,6 +157,8 @@
   "all_premium_features": "All premium features",
   "all_premium_features_including": "All premium features, including:",
   "all_prices_displayed_are_in_currency": "All prices displayed are in __recommendedCurrency__.",
+  "all_project_activity": "All project activity",
+  "all_project_activity_description": "You’ll be notified about all comments and track changes in this project.",
   "all_projects": "All projects",
   "all_projects_will_be_transferred_immediately": "All projects will be transferred to the new owner immediately.",
   "all_templates": "All templates",
@@ -686,6 +688,7 @@
   "email_link_expired": "Email link expired, please request a new one.",
   "email_managed_by_group_is": "The email address that will be managed by your group is <0>__email__</0>.",
   "email_must_be_linked_to_institution": "As a member of __institutionName__, this email address can only be added via single sign-on on your <0>account settings</0> page. Please add a different recovery email address.",
+  "email_notifications_are_currently_in_beta": "Email notifications are currently in beta.",
   "email_or_password_wrong_try_again": "Your email or password is incorrect. Please try again.",
   "email_or_password_wrong_try_again_or_reset": "Your email or password is incorrect. Please try again, or <0>set or reset your password</0>.",
   "email_remove_by_date": "If this is not done by __date__, it will be removed from the account.",
@@ -1498,6 +1501,7 @@
   "no_pdf_error_title": "No PDF",
   "no_planned_maintenance": "There is currently no planned maintenance",
   "no_preview_available": "Sorry, no preview is available.",
+  "no_project_notifications_description": "You won’t be notified about this project.",
   "no_projects": "No projects",
   "no_resolved_comments": "No resolved comments",
   "no_search_results": "No Search Results",
@@ -1776,6 +1780,7 @@
   "project_linked_to": "This project is linked to",
   "project_name": "Project name",
   "project_not_linked_to_github": "This project is not linked to a GitHub repository. You can create a repository for it in GitHub:",
+  "project_notifications": "Project notifications",
   "project_ownership_transfer_confirmation_1": "Are you sure you want to make <0>__user__</0> the owner of <1>__project__</1>?",
   "project_ownership_transfer_confirmation_2": "This action cannot be undone. The new owner will be notified and will be able to change project access settings (including removing your own access).",
   "project_renamed_or_deleted": "Project Renamed or Deleted",
@@ -1914,6 +1919,8 @@
   "replace_from_computer": "Replace from computer",
   "replace_from_project_files": "Replace from project files",
   "replace_from_url": "Replace from URL",
+  "replies_to_your_activity_only": "Replies to your activity only",
+  "replies_to_your_activity_only_description": "You’ll be notified about direct replies and activity on your track changes only.",
   "reply": "Reply",
   "repository_name": "Repository Name",
   "repository_visibility": "Repository visibility",
@@ -2388,6 +2395,7 @@
   "there_are_lots_of_options_to_edit_and_customize_your_figures": "There are lots of options to edit and customize your figures, such as wrapping text around the figure, rotating the image, or including multiple images in a single figure. You’ll need to edit the LaTeX code to do this. <0>Find out how</0>",
   "there_was_a_problem_restoring_the_project_please_try_again_in_a_few_moments_or_contact_us": "There was a problem restoring the project. Please try again in a few moments. Contact us if the problem persists.",
   "there_was_an_error_opening_your_content": "There was an error creating your project",
+  "these_settings_might_change_in_the_future": "These settings might change in the future.",
   "thesis": "Thesis",
   "they_lose_access_to_account": "They lose all access to this Overleaf account immediately",
   "they_will_be_removed_from_the_group": "They will be removed from the group.",