Add email notifications settings for redesigned editor

GitOrigin-RevId: d31f13bbc668d790564618e6b4a8a83fdf2d8780
This commit is contained in:
Domagoj Kriskovic
2025-12-04 11:33:25 +01:00
committed by Copybot
parent b9224ea11d
commit 3f6e96f58e
10 changed files with 200 additions and 3 deletions

View File

@@ -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 =>

View File

@@ -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": "",

View File

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

View File

@@ -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>
</>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
</>
)

View File

@@ -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(() => {

View File

@@ -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);
}

View File

@@ -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": "Youll 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 wont 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": "Youll 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. Youll 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.",