Bläddra i källkod

Merge pull request #31329 from overleaf/mj-dark-mode-notification

[web] Introduce notification for dashboard dark mode

GitOrigin-RevId: da5045d412ddc87a4b18823a4a5fa3192fe15c89
Mathias Jakobsen 6 månader sedan
förälder
incheckning
498c89c6ed
20 ändrade filer med 369 tillägg och 89 borttagningar
  1. 1 0
      services/web/app/src/Features/Tutorial/TutorialController.mjs
  2. 9 0
      services/web/frontend/extracted-translations.json
  3. 2 2
      services/web/frontend/js/features/history/components/change-list/all-history-list.tsx
  4. 2 2
      services/web/frontend/js/features/ide-react/components/editor-survey.tsx
  5. 45 41
      services/web/frontend/js/features/ide-react/context/react-context-root.tsx
  6. 2 2
      services/web/frontend/js/features/ide-redesign/components/new-editor-opt-out-intro-modal.tsx
  7. 2 2
      services/web/frontend/js/features/ide-redesign/components/old-editor-warning-tooltip.tsx
  8. 2 2
      services/web/frontend/js/features/ide-redesign/components/tooltip-promo.tsx
  9. 2 2
      services/web/frontend/js/features/monthly-texlive/rolling-compile-image-changed-alert.tsx
  10. 6 3
      services/web/frontend/js/features/project-list/components/project-list-root.tsx
  11. 25 2
      services/web/frontend/js/features/project-list/components/sidebar/sidebar-ds-nav.tsx
  12. 102 0
      services/web/frontend/js/features/project-list/components/sidebar/themed-project-dashboard-notification.tsx
  13. 38 0
      services/web/frontend/js/features/project-list/components/sidebar/use-themed-dashboard-intro.ts
  14. 0 24
      services/web/frontend/js/shared/context/editor-context.tsx
  15. 64 0
      services/web/frontend/js/shared/context/tutorial-context.tsx
  16. 2 2
      services/web/frontend/js/shared/hooks/promotions/use-tutorial.tsx
  17. 33 0
      services/web/frontend/stylesheets/pages/project-list-ds-nav.scss
  18. 9 0
      services/web/locales/en.json
  19. 2 1
      services/web/test/frontend/features/workbench/chat.spec.tsx
  20. 21 4
      services/web/test/frontend/helpers/editor-providers.tsx

+ 1 - 0
services/web/app/src/Features/Tutorial/TutorialController.mjs

@@ -31,6 +31,7 @@ const VALID_KEYS = [
   'old-editor-warning-tooltip',
   'old-editor-warning-tooltip-2',
   'workbench-rail-popover',
+  'themed-dashboard-intro',
 ]
 
 async function completeTutorial(req, res, next) {

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

@@ -14,6 +14,7 @@
   "Pricing": "",
   "Solutions": "",
   "a_custom_size_has_been_used_in_the_latex_code": "",
+  "a_dashboard_that_follows_your_lead": "",
   "a_file_with_that_name_already_exists_and_will_be_overriden": "",
   "a_more_comprehensive_list_of_keyboard_shortcuts": "",
   "a_new_reference_was_added": "",
@@ -601,6 +602,7 @@
   "failed_to_send_managed_user_invite_to_email": "",
   "failed_to_send_sso_link_invite_to_email": "",
   "fair_usage_policy_applies": "",
+  "fancy_going_dark": "",
   "fast": "",
   "fast_draft": "",
   "feature_enabled_or_disabled": "",
@@ -761,6 +763,7 @@
   "go_to_writefull": "",
   "good_news_you_already_purchased_this_add_on": "",
   "good_news_you_are_already_receiving_this_add_on_via_writefull": "",
+  "got_it": "",
   "got_questions": "",
   "group_admin": "",
   "group_audit_logs": "",
@@ -1091,6 +1094,7 @@
   "math_inline": "",
   "maximum_files_uploaded_together": "",
   "maybe_later": "",
+  "meet_the_new_dark_dashboard": "",
   "members_added": "",
   "members_management": "",
   "mendeley": "",
@@ -2219,11 +2223,15 @@
   "wed_love_you_to_stay": "",
   "welcome_to_overleaf_opening_workspace": "",
   "welcome_to_sl": "",
+  "welcome_to_the_dark_side": "",
   "well_be_here_when_youre_ready": "",
   "were_making_some_changes_to_project_sharing_this_means_you_will_be_visible": "",
   "were_performing_maintenance": "",
+  "weve_given_your_dashboard_a_sleek_new_dark_theme_for_more_comfortable_late_night_research_prefer_the_light_switch_back_anytime_right_here": "",
   "weve_hit_a_problem_try_starting_a_new_chat": "",
   "weve_made_it_easier_to_find_and_use_the_tools_you_need_today": "",
+  "weve_matched_your_dashboard_theme_to_your_editor_preferences_but_you_can_change_that_here_anytime": "",
+  "weve_set_your_dashboard_to_dark_mode_to_help_you_stay_focused_if_youre_a_fan_of_a_lighter_look_you_can_easily_switch_themes_here": "",
   "what_did_you_find_most_helpful": "",
   "what_do_you_need_help_with": "",
   "what_does_this_mean_for_you": "",
@@ -2308,6 +2316,7 @@
   "your_current_plan_gives_you": "",
   "your_current_plan_supports_up_to_x_licenses": "",
   "your_current_project_will_revert_to_the_version_from_time": "",
+  "your_dashboard_is_set_to_match_your_system_theme_automatically_want_a_different_look_pick_your_favorite_theme_here": "",
   "your_email_is_confirmed": "",
   "your_feedback_matters_answer_two_quick_questions": "",
   "your_git_access_info": "",

+ 2 - 2
services/web/frontend/js/features/history/components/change-list/all-history-list.tsx

@@ -7,13 +7,13 @@ import { isVersionSelected } from '../../utils/history-details'
 import { useUserContext } from '../../../../shared/context/user-context'
 import useDropdownActiveItem from '../../hooks/use-dropdown-active-item'
 import { useHistoryContext } from '../../context/history-context'
-import { useEditorContext } from '../../../../shared/context/editor-context'
 import OLPopover from '@/shared/components/ol/ol-popover'
 import OLOverlay from '@/shared/components/ol/ol-overlay'
 import Close from '@/shared/components/close'
 import { Trans, useTranslation } from 'react-i18next'
 import MaterialIcon from '@/shared/components/material-icon'
 import useTutorial from '@/shared/hooks/promotions/use-tutorial'
+import { useTutorialContext } from '@/shared/context/tutorial-context'
 
 function AllHistoryList() {
   const { id: currentUserId } = useUserContext()
@@ -91,7 +91,7 @@ function AllHistoryList() {
     }
   }, [updatesLoadingState])
 
-  const { inactiveTutorials } = useEditorContext()
+  const { inactiveTutorials } = useTutorialContext()
   const {
     showPopup: showHistoryTutorial,
     tryShowingPopup: tryShowingHistoryTutorial,

+ 2 - 2
services/web/frontend/js/features/ide-react/components/editor-survey.tsx

@@ -5,12 +5,12 @@ import OLFormGroup from '@/shared/components/ol/ol-form-group'
 import OLIconButton from '@/shared/components/ol/ol-icon-button'
 import { OLToast } from '@/shared/components/ol/ol-toast'
 import { OLToastContainer } from '@/shared/components/ol/ol-toast-container'
-import { useEditorContext } from '@/shared/context/editor-context'
 import useTutorial from '@/shared/hooks/promotions/use-tutorial'
 import { memo, useCallback, useEffect, useMemo, useState } from 'react'
 import { sendMB } from '@/infrastructure/event-tracking'
 import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
 import { useTranslation } from 'react-i18next'
+import { useTutorialContext } from '@/shared/context/tutorial-context'
 
 type EditorSurveyPage = 'ease-of-use' | 'meets-my-needs' | 'thank-you'
 
@@ -28,7 +28,7 @@ const EditorSurveyContent = () => {
   const [easeOfUse, setEaseOfUse] = useState<number | null>(null)
   const [meetsMyNeeds, setMeetsMyNeeds] = useState<number | null>(null)
   const [page, setPage] = useState<EditorSurveyPage>('ease-of-use')
-  const { inactiveTutorials } = useEditorContext()
+  const { inactiveTutorials } = useTutorialContext()
   const hasCompletedSurvey = inactiveTutorials.includes(TUTORIAL_KEY)
   const newEditor = useIsNewEditorEnabled()
 

+ 45 - 41
services/web/frontend/js/features/ide-react/context/react-context-root.tsx

@@ -32,6 +32,7 @@ import { CommandRegistryProvider } from './command-registry-context'
 import { NewEditorTourProvider } from '@/features/ide-redesign/contexts/new-editor-tour-context'
 import { EditorSelectionProvider } from '@/shared/context/editor-selection-context'
 import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
+import { TutorialProvider } from '@/shared/context/tutorial-context'
 
 const rootContextProviders = importOverleafModules('rootContextProviders') as {
   import: { default: ElementType }
@@ -76,6 +77,7 @@ export const ReactContextRoot: FC<
     UserFeaturesProvider,
     NewEditorTourProvider,
     EditorSelectionProvider,
+    TutorialProvider,
     ...providers,
   }
 
@@ -105,47 +107,49 @@ export const ReactContextRoot: FC<
                         <Providers.EditorViewProvider>
                           <Providers.EditorOpenDocProvider>
                             <Providers.EditorProvider>
-                              <Providers.FileTreeDataProvider>
-                                <Providers.FileTreePathProvider>
-                                  <Providers.UserFeaturesProvider>
-                                    <Providers.PermissionsProvider>
-                                      <Providers.RailProvider>
-                                        <Providers.LayoutProvider>
-                                          <Providers.NewEditorTourProvider>
-                                            <Providers.ProjectSettingsProvider>
-                                              <Providers.EditorManagerProvider>
-                                                <Providers.ReferencesProvider>
-                                                  <Providers.LocalCompileProvider>
-                                                    <Providers.DetachCompileProvider>
-                                                      <Providers.ChatProvider>
-                                                        <Providers.FileTreeOpenProvider>
-                                                          <Providers.OnlineUsersProvider>
-                                                            <Providers.MetadataProvider>
-                                                              <Providers.OutlineProvider>
-                                                                <Providers.CommandRegistryProvider>
-                                                                  <Providers.EditorSelectionProvider>
-                                                                    {
-                                                                      childrenWrappedWithDynamicProviders
-                                                                    }
-                                                                  </Providers.EditorSelectionProvider>
-                                                                </Providers.CommandRegistryProvider>
-                                                              </Providers.OutlineProvider>
-                                                            </Providers.MetadataProvider>
-                                                          </Providers.OnlineUsersProvider>
-                                                        </Providers.FileTreeOpenProvider>
-                                                      </Providers.ChatProvider>
-                                                    </Providers.DetachCompileProvider>
-                                                  </Providers.LocalCompileProvider>
-                                                </Providers.ReferencesProvider>
-                                              </Providers.EditorManagerProvider>
-                                            </Providers.ProjectSettingsProvider>
-                                          </Providers.NewEditorTourProvider>
-                                        </Providers.LayoutProvider>
-                                      </Providers.RailProvider>
-                                    </Providers.PermissionsProvider>
-                                  </Providers.UserFeaturesProvider>
-                                </Providers.FileTreePathProvider>
-                              </Providers.FileTreeDataProvider>
+                              <Providers.TutorialProvider>
+                                <Providers.FileTreeDataProvider>
+                                  <Providers.FileTreePathProvider>
+                                    <Providers.UserFeaturesProvider>
+                                      <Providers.PermissionsProvider>
+                                        <Providers.RailProvider>
+                                          <Providers.LayoutProvider>
+                                            <Providers.NewEditorTourProvider>
+                                              <Providers.ProjectSettingsProvider>
+                                                <Providers.EditorManagerProvider>
+                                                  <Providers.ReferencesProvider>
+                                                    <Providers.LocalCompileProvider>
+                                                      <Providers.DetachCompileProvider>
+                                                        <Providers.ChatProvider>
+                                                          <Providers.FileTreeOpenProvider>
+                                                            <Providers.OnlineUsersProvider>
+                                                              <Providers.MetadataProvider>
+                                                                <Providers.OutlineProvider>
+                                                                  <Providers.CommandRegistryProvider>
+                                                                    <Providers.EditorSelectionProvider>
+                                                                      {
+                                                                        childrenWrappedWithDynamicProviders
+                                                                      }
+                                                                    </Providers.EditorSelectionProvider>
+                                                                  </Providers.CommandRegistryProvider>
+                                                                </Providers.OutlineProvider>
+                                                              </Providers.MetadataProvider>
+                                                            </Providers.OnlineUsersProvider>
+                                                          </Providers.FileTreeOpenProvider>
+                                                        </Providers.ChatProvider>
+                                                      </Providers.DetachCompileProvider>
+                                                    </Providers.LocalCompileProvider>
+                                                  </Providers.ReferencesProvider>
+                                                </Providers.EditorManagerProvider>
+                                              </Providers.ProjectSettingsProvider>
+                                            </Providers.NewEditorTourProvider>
+                                          </Providers.LayoutProvider>
+                                        </Providers.RailProvider>
+                                      </Providers.PermissionsProvider>
+                                    </Providers.UserFeaturesProvider>
+                                  </Providers.FileTreePathProvider>
+                                </Providers.FileTreeDataProvider>
+                              </Providers.TutorialProvider>
                             </Providers.EditorProvider>
                           </Providers.EditorOpenDocProvider>
                         </Providers.EditorViewProvider>

+ 2 - 2
services/web/frontend/js/features/ide-redesign/components/new-editor-opt-out-intro-modal.tsx

@@ -9,15 +9,15 @@ import { useCallback, useEffect, useState } from 'react'
 import useTutorial from '@/shared/hooks/promotions/use-tutorial'
 import OLButton from '@/shared/components/ol/ol-button'
 import { useTranslation } from 'react-i18next'
-import { useEditorContext } from '@/shared/context/editor-context'
 import { useIsNewToNewEditor } from '../utils/new-editor-utils'
 import { useNewEditorTourContext } from '../contexts/new-editor-tour-context'
 import promoVideo from './new-editor-promo-video.mp4'
+import { useTutorialContext } from '@/shared/context/tutorial-context'
 
 const TUTORIAL_KEY = 'new-editor-intro-2'
 
 export default function NewEditorOptOutIntroModal() {
-  const { inactiveTutorials } = useEditorContext()
+  const { inactiveTutorials } = useTutorialContext()
   const {
     tryShowingPopup,
     showPopup: showModal,

+ 2 - 2
services/web/frontend/js/features/ide-redesign/components/old-editor-warning-tooltip.tsx

@@ -5,8 +5,8 @@ import { useTranslation } from 'react-i18next'
 import useTutorial from '@/shared/hooks/promotions/use-tutorial'
 import { useCallback, useEffect, useState } from 'react'
 import { useSwitchEnableNewEditorState } from '../hooks/use-switch-enable-new-editor-state'
-import { useEditorContext } from '@/shared/context/editor-context'
 import { canUseNewEditor } from '../utils/new-editor-utils'
+import { useTutorialContext } from '@/shared/context/tutorial-context'
 
 const TUTORIAL_KEY = 'old-editor-warning-tooltip-2'
 
@@ -15,7 +15,7 @@ export default function OldEditorWarningTooltip({
 }: {
   target: HTMLElement | null
 }) {
-  const { inactiveTutorials } = useEditorContext()
+  const { inactiveTutorials } = useTutorialContext()
   const { t } = useTranslation()
   const { loading, setEditorRedesignStatus } = useSwitchEnableNewEditorState()
 

+ 2 - 2
services/web/frontend/js/features/ide-redesign/components/tooltip-promo.tsx

@@ -1,5 +1,5 @@
 import Close from '@/shared/components/close'
-import { useEditorContext } from '@/shared/context/editor-context'
+import { useTutorialContext } from '@/shared/context/tutorial-context'
 import useTutorial from '@/shared/hooks/promotions/use-tutorial'
 import { isSplitTestEnabled } from '@/utils/splitTestUtils'
 import classNames from 'classnames'
@@ -26,7 +26,7 @@ export default function TooltipPromotion({
   placement?: OverlayProps['placement']
   splitTestName?: string
 }) {
-  const { inactiveTutorials } = useEditorContext()
+  const { inactiveTutorials } = useTutorialContext()
   const { showPopup, tryShowingPopup, hideUntilReload, dismissTutorial } =
     useTutorial(tutorialKey, eventData)
 

+ 2 - 2
services/web/frontend/js/features/monthly-texlive/rolling-compile-image-changed-alert.tsx

@@ -1,17 +1,17 @@
 import useTutorial from '@/shared/hooks/promotions/use-tutorial'
-import { useEditorContext } from '@/shared/context/editor-context'
 import { useProjectContext } from '@/shared/context/project-context'
 import OLNotification from '@/shared/components/ol/ol-notification'
 import { useTranslation, Trans } from 'react-i18next'
 import { useCallback } from 'react'
 import { onRollingBuild } from '@/shared/utils/rolling-build'
+import { useTutorialContext } from '@/shared/context/tutorial-context'
 
 export const TUTORIAL_KEY = 'rolling-compile-image-changed'
 
 const RollingCompileImageChangedAlert = () => {
   const { completeTutorial } = useTutorial(TUTORIAL_KEY)
   const { project } = useProjectContext()
-  const { inactiveTutorials } = useEditorContext()
+  const { inactiveTutorials } = useTutorialContext()
 
   const { t } = useTranslation()
 

+ 6 - 3
services/web/frontend/js/features/project-list/components/project-list-root.tsx

@@ -21,6 +21,7 @@ import { DsNavStyleProvider } from '@/features/project-list/components/use-is-ds
 import CookieBanner from '@/shared/components/cookie-banner'
 import useThemedPage from '@/shared/hooks/use-themed-page'
 import { UserSettingsProvider } from '@/shared/context/user-settings-context'
+import { TutorialProvider } from '@/shared/context/tutorial-context'
 
 function ProjectListRoot() {
   const { isReady } = useWaitForI18n()
@@ -37,9 +38,11 @@ export function ProjectListRootInner() {
     <ProjectListProvider>
       <ColorPickerProvider>
         <SplitTestProvider>
-          <UserSettingsProvider>
-            <ProjectListPageContent />
-          </UserSettingsProvider>
+          <TutorialProvider>
+            <UserSettingsProvider>
+              <ProjectListPageContent />
+            </UserSettingsProvider>
+          </TutorialProvider>
         </SplitTestProvider>
       </ColorPickerProvider>
     </ProjectListProvider>

+ 25 - 2
services/web/frontend/js/features/project-list/components/sidebar/sidebar-ds-nav.tsx

@@ -18,6 +18,8 @@ import { useScrolled } from '@/features/project-list/components/sidebar/use-scro
 import { useSendProjectListMB } from '@/features/project-list/components/project-list-events'
 import { SurveyWidgetDsNav } from '@/features/project-list/components/survey-widget-ds-nav'
 import { useFeatureFlag } from '@/shared/context/split-test-context'
+import { ThemedProjectDashboardNotification } from './themed-project-dashboard-notification'
+import { useThemedDashboardIntro } from './use-themed-dashboard-intro'
 
 function SidebarDsNav() {
   const { t } = useTranslation()
@@ -38,6 +40,12 @@ function SidebarDsNav() {
   ) as NavbarDropdownItemData
   const { containerRef, scrolledUp, scrolledDown } = useScrolled()
   const themedDsNav = useFeatureFlag('themed-project-dashboard')
+  const {
+    completeThemedDashboardIntro,
+    dismissThemedDashboardIntro,
+    targetRef,
+    showingThemedDashboardIntro,
+  } = useThemedDashboardIntro()
 
   return (
     <div
@@ -131,6 +139,12 @@ function SidebarDsNav() {
                       item: 'account',
                       location: 'sidebar',
                     })
+                    if (showingThemedDashboardIntro) {
+                      completeThemedDashboardIntro({
+                        action: 'complete',
+                        event: 'promo-click',
+                      })
+                    }
                   }
                 }}
                 role="menu"
@@ -144,7 +158,7 @@ function SidebarDsNav() {
                     }}
                     hidden={showAccountDropdown}
                   >
-                    <div>
+                    <div ref={targetRef}>
                       <User size={24} />
                     </div>
                   </OLTooltip>
@@ -166,7 +180,16 @@ function SidebarDsNav() {
                   />
                 </Dropdown.Menu>
               </Dropdown>
-              <UserProvider>{contactUsModal}</UserProvider>
+              <UserProvider>
+                {themedDsNav && (
+                  <ThemedProjectDashboardNotification
+                    target={targetRef.current}
+                    show={showingThemedDashboardIntro}
+                    onDismiss={dismissThemedDashboardIntro}
+                  />
+                )}
+                {contactUsModal}
+              </UserProvider>
             </>
           )}
         </nav>

+ 102 - 0
services/web/frontend/js/features/project-list/components/sidebar/themed-project-dashboard-notification.tsx

@@ -0,0 +1,102 @@
+import Close from '@/shared/components/close'
+import OLButton from '@/shared/components/ol/ol-button'
+import { useUserContext } from '@/shared/context/user-context'
+import { useUserSettingsContext } from '@/shared/context/user-settings-context'
+import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme'
+import { Overlay, Popover } from 'react-bootstrap'
+import { useTranslation } from 'react-i18next'
+
+// TODO: Update this before release
+const NEW_USER_CUTOFF_DATE = new Date('2026-02-15')
+
+type ThemedProjectDashboardNotificationProps = {
+  target: HTMLElement | null
+  show: boolean
+  onDismiss: () => void
+}
+export const ThemedProjectDashboardNotification = ({
+  target,
+  show,
+  onDismiss,
+}: ThemedProjectDashboardNotificationProps) => {
+  const theme = useActiveOverallTheme('themed-project-dashboard')
+  const {
+    userSettings: { overallTheme },
+  } = useUserSettingsContext()
+  const { signUpDate: signUpDateString } = useUserContext()
+  const signUpDate = signUpDateString ? new Date(signUpDateString) : new Date(0)
+  const isNewUser = signUpDate > NEW_USER_CUTOFF_DATE
+  const { t } = useTranslation()
+
+  if (!target) {
+    return null
+  }
+
+  if (!show) {
+    return null
+  }
+
+  let content
+  switch (true) {
+    case overallTheme === 'system':
+      content = {
+        header: t('a_dashboard_that_follows_your_lead'),
+        body: t(
+          'your_dashboard_is_set_to_match_your_system_theme_automatically_want_a_different_look_pick_your_favorite_theme_here'
+        ),
+      }
+      break
+    case isNewUser && theme === 'dark':
+      content = {
+        header: t('meet_the_new_dark_dashboard'),
+        body: t(
+          'weve_set_your_dashboard_to_dark_mode_to_help_you_stay_focused_if_youre_a_fan_of_a_lighter_look_you_can_easily_switch_themes_here'
+        ),
+      }
+      break
+    case theme === 'dark':
+      content = {
+        header: t('welcome_to_the_dark_side'),
+        body: t(
+          'weve_given_your_dashboard_a_sleek_new_dark_theme_for_more_comfortable_late_night_research_prefer_the_light_switch_back_anytime_right_here'
+        ),
+      }
+      break
+    case theme === 'light':
+      content = {
+        header: t('fancy_going_dark'),
+        body: t(
+          'weve_matched_your_dashboard_theme_to_your_editor_preferences_but_you_can_change_that_here_anytime'
+        ),
+      }
+      break
+    default:
+      content = { header: '', body: '' }
+  }
+
+  if (!content.header || !content.body) {
+    return null
+  }
+
+  return (
+    <Overlay show placement="right" target={target} onHide={onDismiss}>
+      <Popover className="themed-dashboard-intro-popover">
+        <Popover.Header>
+          {content.header}
+          <Close
+            variant={theme === 'light' ? 'dark' : 'light'}
+            onDismiss={onDismiss}
+          />
+        </Popover.Header>
+        <Popover.Body>
+          {content.body}
+          <div className="d-flex justify-content-end">
+            <OLButton onClick={onDismiss} variant="link">
+              {t('got_it')}
+            </OLButton>
+          </div>
+        </Popover.Body>
+      </Popover>
+    </Overlay>
+  )
+}

+ 38 - 0
services/web/frontend/js/features/project-list/components/sidebar/use-themed-dashboard-intro.ts

@@ -0,0 +1,38 @@
+import { useFeatureFlag } from '@/shared/context/split-test-context'
+import { useTutorialContext } from '@/shared/context/tutorial-context'
+import useTutorial from '@/shared/hooks/promotions/use-tutorial'
+import { useCallback, useEffect, useRef } from 'react'
+
+const THEMED_DASHBOARD_TUTORIAL_KEY = 'themed-dashboard-intro'
+
+export const useThemedDashboardIntro = () => {
+  const themedDsNav = useFeatureFlag('themed-project-dashboard')
+  const targetRef = useRef<HTMLDivElement | null>(null)
+  const { inactiveTutorials } = useTutorialContext()
+  const {
+    tryShowingPopup: tryShowingPopupThemedDashboardIntro,
+    showPopup: showingThemedDashboardIntro,
+    completeTutorial: completeThemedDashboardIntro,
+    dismissTutorial,
+  } = useTutorial(THEMED_DASHBOARD_TUTORIAL_KEY, {
+    name: THEMED_DASHBOARD_TUTORIAL_KEY,
+  })
+  const dismissThemedDashboardIntro = useCallback(() => {
+    dismissTutorial()
+  }, [dismissTutorial])
+  useEffect(() => {
+    if (
+      themedDsNav &&
+      !inactiveTutorials.includes(THEMED_DASHBOARD_TUTORIAL_KEY)
+    ) {
+      tryShowingPopupThemedDashboardIntro()
+    }
+  }, [inactiveTutorials, tryShowingPopupThemedDashboardIntro, themedDsNav])
+
+  return {
+    targetRef,
+    showingThemedDashboardIntro,
+    completeThemedDashboardIntro,
+    dismissThemedDashboardIntro,
+  }
+}

+ 0 - 24
services/web/frontend/js/shared/context/editor-context.tsx

@@ -29,10 +29,6 @@ export const EditorContext = createContext<
       isProjectOwner: boolean
       isRestrictedTokenMember?: boolean
       isPendingEditor: boolean
-      deactivateTutorial: (tutorial: string) => void
-      inactiveTutorials: string[]
-      currentPopup: string | null
-      setCurrentPopup: Dispatch<SetStateAction<string | null>>
       hasPremiumSuggestion: boolean
       setHasPremiumSuggestion: (value: boolean) => void
       setPremiumSuggestionResetDate: (date: Date) => void
@@ -77,11 +73,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
     )
   }, [])
 
-  const [inactiveTutorials, setInactiveTutorials] = useState(
-    () => getMeta('ol-inactiveTutorials') || []
-  )
-
-  const [currentPopup, setCurrentPopup] = useState<string | null>(null)
   const [hasPremiumSuggestion, setHasPremiumSuggestion] = useState<boolean>(
     () => {
       return Boolean(
@@ -111,13 +102,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
     [members, userId]
   )
 
-  const deactivateTutorial = useCallback(
-    (tutorialKey: string) => {
-      setInactiveTutorials([...inactiveTutorials, tutorialKey])
-    },
-    [inactiveTutorials]
-  )
-
   const renameProject = useCallback(
     (newName: string) => {
       const oldName = projectName
@@ -180,10 +164,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
       isRestrictedTokenMember: getMeta('ol-isRestrictedTokenMember'),
       isPendingEditor,
       insertSymbol,
-      inactiveTutorials,
-      deactivateTutorial,
-      currentPopup,
-      setCurrentPopup,
       hasPremiumSuggestion,
       setHasPremiumSuggestion,
       premiumSuggestionResetDate,
@@ -201,10 +181,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
       renameProject,
       isPendingEditor,
       insertSymbol,
-      inactiveTutorials,
-      deactivateTutorial,
-      currentPopup,
-      setCurrentPopup,
       hasPremiumSuggestion,
       setHasPremiumSuggestion,
       premiumSuggestionResetDate,

+ 64 - 0
services/web/frontend/js/shared/context/tutorial-context.tsx

@@ -0,0 +1,64 @@
+import getMeta from '@/utils/meta'
+import {
+  createContext,
+  Dispatch,
+  FC,
+  SetStateAction,
+  useCallback,
+  useContext,
+  useMemo,
+  useState,
+} from 'react'
+
+export const TutorialContext = createContext<
+  | {
+      deactivateTutorial: (tutorial: string) => void
+      inactiveTutorials: string[]
+      currentPopup: string | null
+      setCurrentPopup: Dispatch<SetStateAction<string | null>>
+    }
+  | undefined
+>(undefined)
+
+export const TutorialProvider: FC<React.PropsWithChildren> = ({ children }) => {
+  const [inactiveTutorials, setInactiveTutorials] = useState(
+    () => getMeta('ol-inactiveTutorials') || []
+  )
+
+  const [currentPopup, setCurrentPopup] = useState<string | null>(null)
+
+  const deactivateTutorial = useCallback(
+    (tutorialKey: string) => {
+      setInactiveTutorials([...inactiveTutorials, tutorialKey])
+    },
+    [inactiveTutorials]
+  )
+
+  const value = useMemo(
+    () => ({
+      deactivateTutorial,
+      inactiveTutorials,
+      currentPopup,
+      setCurrentPopup,
+    }),
+    [deactivateTutorial, inactiveTutorials, currentPopup, setCurrentPopup]
+  )
+
+  return (
+    <TutorialContext.Provider value={value}>
+      {children}
+    </TutorialContext.Provider>
+  )
+}
+
+export function useTutorialContext() {
+  const context = useContext(TutorialContext)
+
+  if (!context) {
+    throw new Error(
+      'useTutorialContext is only available inside TutorialProvider'
+    )
+  }
+
+  return context
+}

+ 2 - 2
services/web/frontend/js/shared/hooks/promotions/use-tutorial.tsx

@@ -2,7 +2,7 @@ import { useCallback, useState } from 'react'
 import * as eventTracking from '@/infrastructure/event-tracking'
 import { postJSON } from '@/infrastructure/fetch-json'
 import { debugConsole } from '@/utils/debugging'
-import { useEditorContext } from '@/shared/context/editor-context'
+import { useTutorialContext } from '@/shared/context/tutorial-context'
 
 type CompleteTutorialParams = {
   event: string
@@ -22,7 +22,7 @@ const useTutorial = (
   const [showPopup, setShowPopup] = useState(false)
 
   const { deactivateTutorial, currentPopup, setCurrentPopup } =
-    useEditorContext()
+    useTutorialContext()
 
   const completeTutorial = useCallback(
     async (

+ 33 - 0
services/web/frontend/stylesheets/pages/project-list-ds-nav.scss

@@ -4,6 +4,11 @@
   --theme-toggle-selected-background: var(--green-20);
   --ds-nav-content-bg-secondary: var(--bg-light-secondary);
   --table-icon-bg-hover: 27 34 44;
+  --themed-dashboard-popover-bg: var(--bg-dark-primary);
+  --themed-dashboard-popover-color: var(--content-primary-dark);
+  --themed-dashboard-popover-link-color: var(--link-ui-dark);
+  --themed-dashboard-popover-link-hover-color: var(--link-ui-hover-dark);
+  --themed-dashboard-popover-link-visited-color: var(--link-ui-visited-dark);
 
   @include theme('default') {
     --ds-nav-active-bg: var(--green-70);
@@ -11,6 +16,11 @@
     --theme-toggle-selected-background: var(--green-70);
     --ds-nav-content-bg-secondary: var(--bg-dark-secondary);
     --table-icon-bg-hover: 255 255 255;
+    --themed-dashboard-popover-bg: var(--bg-light-primary);
+    --themed-dashboard-popover-color: var(--content-primary);
+    --themed-dashboard-popover-link-color: var(--link-ui);
+    --themed-dashboard-popover-link-hover-color: var(--link-ui-hover);
+    --themed-dashboard-popover-link-visited-color: var(--link-ui-visited);
   }
 }
 
@@ -420,3 +430,26 @@ body {
     background-color: var(--theme-toggle-selected-background);
   }
 }
+
+.themed-dashboard-intro-popover {
+  --bs-popover-bg: var(--themed-dashboard-popover-bg);
+  --bs-popover-header-bg: var(--themed-dashboard-popover-bg);
+  --bs-popover-body-color: var(--themed-dashboard-popover-color);
+  --bs-popover-header-color: var(--themed-dashboard-popover-color);
+
+  .btn-link {
+    color: var(--bs-popover-body-color);
+  }
+
+  a {
+    color: var(--themed-dashboard-popover-link-color);
+
+    &:visited {
+      color: var(--themed-dashboard-popover-link-hover-color);
+    }
+
+    &:hover {
+      color: var(--themed-dashboard-popover-link-visited-color);
+    }
+  }
+}

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

@@ -15,6 +15,7 @@
   "Terms": "Terms",
   "Universities": "Universities",
   "a_custom_size_has_been_used_in_the_latex_code": "A custom size has been used in the LaTeX code.",
+  "a_dashboard_that_follows_your_lead": "A dashboard that follows your lead",
   "a_file_with_that_name_already_exists_and_will_be_overriden": "A file with that name already exists. That file will be overwritten.",
   "a_more_comprehensive_list_of_keyboard_shortcuts": "A more comprehensive list of keyboard shortcuts can be found in <0>this __appName__ project template</0>",
   "a_new_reference_was_added": "A new reference was added",
@@ -771,6 +772,7 @@
   "failed_to_send_managed_user_invite_to_email": "Failed to send Managed User invite to <0>__email__</0>. Please try again later.",
   "failed_to_send_sso_link_invite_to_email": "Failed to send SSO invite reminder to <0>__email__</0>. Please try again later.",
   "fair_usage_policy_applies": "Fair usage policy applies.",
+  "fancy_going_dark": "Fancy going dark?",
   "faq_how_does_free_trial_works_answer": "You get full access to your chosen __appName__ plan during your __len__-day free trial. There is no obligation to continue beyond the trial. Your card will be charged at the end of your __len__ day trial unless you cancel before then. You can cancel via your subscription settings.",
   "fast": "Fast",
   "fast_draft": "Fast [draft]",
@@ -974,6 +976,7 @@
   "go_to_writefull": "Go to Writefull",
   "good_news_you_already_purchased_this_add_on": "Good news! You already have this add-on, so no need to pay again.",
   "good_news_you_are_already_receiving_this_add_on_via_writefull": "Good news! You already have this add-on via your Writefull subscription. No need to pay again.",
+  "got_it": "Got it",
   "got_questions": "Got questions?",
   "great_for_getting_started": "Great for getting started",
   "great_for_small_teams_and_departments": "Great for small teams and departments",
@@ -1413,6 +1416,7 @@
   "maximum_files_uploaded_together": "Maximum __max__ files uploaded together",
   "may": "May",
   "maybe_later": "Maybe later",
+  "meet_the_new_dark_dashboard": "Meet the new dark dashboard",
   "member_picker": "Select number of users for group plan",
   "members_added": "Member(s) added.",
   "members_management": "Members management",
@@ -2774,11 +2778,15 @@
   "wed_love_you_to_stay": "We’d love you to stay",
   "welcome_to_overleaf_opening_workspace": "Welcome to Overleaf. Opening your workspace.",
   "welcome_to_sl": "Welcome to __appName__",
+  "welcome_to_the_dark_side": "Welcome to the dark side",
   "well_be_here_when_youre_ready": "We’ll be here when you’re ready to dive back in! 🦆",
   "were_making_some_changes_to_project_sharing_this_means_you_will_be_visible": "We’re making some <0>changes to project sharing</0>. This means, as someone with edit access, your name and email address will be visible to the project owner and other editors.",
   "were_performing_maintenance": "We’re performing maintenance on Overleaf and you need to wait a moment. Sorry for any inconvenience. The editor will refresh automatically in __seconds__ seconds.",
+  "weve_given_your_dashboard_a_sleek_new_dark_theme_for_more_comfortable_late_night_research_prefer_the_light_switch_back_anytime_right_here": "We’ve given your dashboard a sleek new dark theme for more comfortable late-night research. Prefer the light? Switch back anytime right here.",
   "weve_hit_a_problem_try_starting_a_new_chat": "We’ve hit a problem. Try starting a new chat.",
   "weve_made_it_easier_to_find_and_use_the_tools_you_need_today": "The new editor design makes it easier to find and use the tools you need today, while making space for the new features you’ll love tomorrow.",
+  "weve_matched_your_dashboard_theme_to_your_editor_preferences_but_you_can_change_that_here_anytime": "We’ve matched your dashboard theme to your editor preferences, but you can change that here anytime.",
+  "weve_set_your_dashboard_to_dark_mode_to_help_you_stay_focused_if_youre_a_fan_of_a_lighter_look_you_can_easily_switch_themes_here": "We’ve set your dashboard to dark mode to help you stay focused. If you’re a fan of a lighter look, you can easily switch themes here.",
   "what_did_you_find_most_helpful": "What did you find most helpful?",
   "what_do_you_need": "What do you need?",
   "what_do_you_need_help_with": "What do you need help with?",
@@ -2876,6 +2884,7 @@
   "your_current_plan_gives_you": "By pausing your subscription, you’ll be able to access your premium features faster when you need them again.",
   "your_current_plan_supports_up_to_x_licenses": "Your current plan supports up to __users__ licenses.",
   "your_current_project_will_revert_to_the_version_from_time": "Your current project will revert to the version from __timestamp__",
+  "your_dashboard_is_set_to_match_your_system_theme_automatically_want_a_different_look_pick_your_favorite_theme_here": "Your dashboard is set to match your system theme automatically. Want a different look? Pick your favorite theme here.",
   "your_email_is_confirmed": "Your email is confirmed.",
   "your_feedback_matters_answer_two_quick_questions": "Your feedback matters! Answer two quick questions.",
   "your_git_access_info": "Your Git authentication tokens should be entered whenever you’re prompted for a password.",

+ 2 - 1
services/web/test/frontend/features/workbench/chat.spec.tsx

@@ -4,6 +4,7 @@ import { EditorProviders } from '../../helpers/editor-providers'
 import { EditorView } from '@codemirror/view'
 import type { FC, PropsWithChildren } from 'react'
 import { EditorViewContext } from '@/features/ide-react/context/editor-view-context'
+import { TutorialProvider } from '@/shared/context/tutorial-context'
 
 describe('Workbench', { scrollBehavior: false }, function () {
   beforeEach(function () {
@@ -34,7 +35,7 @@ describe('Workbench', { scrollBehavior: false }, function () {
     return (
       <EditorProviders
         features={{ aiErrorAssistant: aiAssistEnabled }}
-        providers={{ EditorViewProvider }}
+        providers={{ EditorViewProvider, TutorialProvider }}
       >
         <div style={{ backgroundColor: '#1b222c' }}>{children}</div>
       </EditorProviders>

+ 21 - 4
services/web/test/frontend/helpers/editor-providers.tsx

@@ -55,6 +55,7 @@ import { DetachCompileContext } from '@/shared/context/detach-compile-context'
 import { type CompileContext } from '@/shared/context/local-compile-context'
 import { EditorContext } from '@/shared/context/editor-context'
 import { Cobranding } from '@ol-types/cobranding'
+import { TutorialContext } from '@/shared/context/tutorial-context'
 
 // these constants can be imported in tests instead of
 // using magic strings
@@ -248,6 +249,7 @@ export function EditorProviders({
     LayoutProvider: makeLayoutProvider(layoutContext),
     ProjectProvider: makeProjectProvider(project),
     ReferencesProvider: makeReferencesProvider(),
+    TutorialProvider: makeTutorialProvider(),
     ...providers,
   }
 
@@ -284,10 +286,6 @@ export function makeEditorProvider({
       isProjectOwner,
       renameProject,
       isPendingEditor: false,
-      deactivateTutorial: () => {},
-      inactiveTutorials: [],
-      currentPopup: null,
-      setCurrentPopup: () => {},
       hasPremiumSuggestion: false,
       setHasPremiumSuggestion: () => {},
       premiumSuggestionResetDate: new Date(),
@@ -307,6 +305,25 @@ export function makeEditorProvider({
   return EditorProvider
 }
 
+export const makeTutorialProvider = (opts?: {
+  inactiveTutorials: string[]
+}) => {
+  const TutorialProvider: FC<PropsWithChildren> = ({ children }) => {
+    const value = {
+      deactivateTutorial: () => {},
+      inactiveTutorials: opts?.inactiveTutorials ?? [],
+      currentPopup: null,
+      setCurrentPopup: () => {},
+    }
+    return (
+      <TutorialContext.Provider value={value}>
+        {children}
+      </TutorialContext.Provider>
+    )
+  }
+  return TutorialProvider
+}
+
 const makeReferencesProvider = () => {
   const ReferencesProvider: FC<PropsWithChildren> = ({ children }) => {
     return (