diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs index 9297fe08a9..1d47042304 100644 --- a/services/web/app/src/Features/Project/ProjectController.mjs +++ b/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 => diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 5f7b913308..4b6fa33f01 100644 --- a/services/web/frontend/extracted-translations.json +++ b/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": "", diff --git a/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 b/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 index da383987f3..0687bc0fcb 100644 Binary files a/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 and b/services/web/frontend/fonts/material-symbols/MaterialSymbolsRoundedUnfilledPartialSlice.woff2 differ diff --git a/services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs b/services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs index 078c2c9bae..209b03d649 100644 --- a/services/web/frontend/fonts/material-symbols/unfilled-symbols.mjs +++ b/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', diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/editor-settings/project-notifications-setting.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/editor-settings/project-notifications-setting.tsx new file mode 100644 index 0000000000..4ba6b2176e --- /dev/null +++ b/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('all') + + const options: Array> = [ + { + 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 ( + <> + +
+ + {t('email_notifications_are_currently_in_beta')}{' '} + {t('these_settings_might_change_in_the_future')}{' '} + + {/* TODO: update forms link */} + + {t('give_feedback')} + +
+ + ) +} diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/radio-button-setting.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/radio-button-setting.tsx new file mode 100644 index 0000000000..bfbea7cc85 --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/components/settings/radio-button-setting.tsx @@ -0,0 +1,51 @@ +import React from 'react' + +export type RadioOption = { + value: T + label: string + description?: string +} + +type RadioButtonSettingProps = { + id: string + options: Array> + value: T | undefined + onChange: (value: T) => void +} + +export default function RadioButtonSetting({ + id, + options, + value, + onChange, +}: RadioButtonSettingProps) { + const handleChange = (event: React.ChangeEvent) => { + onChange(event.target.value as T) + } + + return ( +
+ {options.map(option => ( + + ))} +
+ ) +} diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/settings-modal-body.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/settings-modal-body.tsx index 6c4082887a..3530d0dbc9 100644 --- a/services/web/frontend/js/features/ide-redesign/components/settings/settings-modal-body.tsx +++ b/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 ( { unfilled /> {entry.title} +
+ {entry.key === 'project_notifications' && ( + + + + + + )} ) diff --git a/services/web/frontend/js/features/ide-redesign/contexts/settings-modal-context.tsx b/services/web/frontend/js/features/ide-redesign/contexts/settings-modal-context.tsx index 884bc2ee15..bdfba12e0f 100644 --- a/services/web/frontend/js/features/ide-redesign/contexts/settings-modal-context.tsx +++ b/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 = ({ 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 = ({ }, ], }, + + { + key: 'project_notifications', + title: t('project_notifications'), + icon: 'notifications' as const, + sections: [ + { + key: 'general', + settings: [ + { + key: 'projectNotifications', + component: , + }, + ], + }, + ], + hidden: !hasEmailNotifications, + }, + { key: 'account_settings', title: t('account_settings'), @@ -242,7 +265,12 @@ export const SettingsModalProvider: FC = ({ href: '/user/subscription', }, ], - [t, overallTheme, hasDarkModePdf] + [t, overallTheme, hasDarkModePdf, hasEmailNotifications] + ) + + const settingsTabs = useMemo( + () => allSettingsTabs.filter(tab => !tab.hidden), + [allSettingsTabs] ) const settingToTabMap = useMemo(() => { diff --git a/services/web/frontend/stylesheets/pages/editor/settings.scss b/services/web/frontend/stylesheets/pages/editor/settings.scss index 61af7d9035..0ba97d6baa 100644 --- a/services/web/frontend/stylesheets/pages/editor/settings.scss +++ b/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); +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 6ab4119ae6..f9865a90a1 100644 --- a/services/web/locales/en.json +++ b/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__.", "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 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.", "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__ the owner of <1>__project__?", "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", "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.",