diff --git a/services/web/app/views/project/editor/meta.pug b/services/web/app/views/project/editor/meta.pug index 73c610ee8b..0c9c72eb76 100644 --- a/services/web/app/views/project/editor/meta.pug +++ b/services/web/app/views/project/editor/meta.pug @@ -39,6 +39,7 @@ meta(name="ol-showPersonalAccessToken", data-type="boolean" content=showPersonal meta(name="ol-isReviewPanelReact", data-type="boolean" content=isReviewPanelReact) meta(name="ol-hasTrackChangesFeature", data-type="boolean" content=hasTrackChangesFeature) meta(name="ol-mathJax3Path" content=mathJax3Path) +meta(name="ol-completedTutorials", data-type="json" content=user.completedTutorials) - var fileActionI18n = ['edited', 'renamed', 'created', 'deleted'].reduce((acc, i) => {acc[i] = translate('file_action_' + i); return acc}, {}) meta(name="ol-fileActionI18n" data-type="json" content=fileActionI18n) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index fbe8793efb..eeecf87e52 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -798,6 +798,9 @@ "quoted_text_in": "", "raw_logs": "", "raw_logs_description": "", + "react_history_tutorial_content": "", + "react_history_tutorial_learn_more": "", + "react_history_tutorial_title": "", "reactivate_subscription": "", "read_only": "", "read_only_token": "", diff --git a/services/web/frontend/js/features/history/components/change-list/all-history-list.tsx b/services/web/frontend/js/features/history/components/change-list/all-history-list.tsx index e1c7d576b0..807155cfb4 100644 --- a/services/web/frontend/js/features/history/components/change-list/all-history-list.tsx +++ b/services/web/frontend/js/features/history/components/change-list/all-history-list.tsx @@ -3,10 +3,23 @@ import HistoryVersion from './history-version' import LoadingSpinner from '../../../../shared/components/loading-spinner' import { OwnerPaywallPrompt } from './owner-paywall-prompt' import { NonOwnerPaywallPrompt } from './non-owner-paywall-prompt' -import { isVersionSelected } from '../../utils/history-details' +import { + isVersionSelected, + ItemSelectionState, +} 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 getMeta from '../../../../utils/meta' + +type CompletedTutorials = { + 'react-history-buttons-tutorial': Date +} + +const unselectedStates: ItemSelectionState[] = [ + 'aboveSelected', + 'belowSelected', +] function AllHistoryList() { const { id: currentUserId } = useUserContext() @@ -84,6 +97,27 @@ function AllHistoryList() { } }, [updatesLoadingState]) + const completedTutorials: CompletedTutorials = getMeta( + 'ol-completedTutorials' + ) + + // only show tutorial popover if they havent dismissed ("completed") it yet + const hasCompletedHistTutorial = Boolean( + completedTutorials?.['react-history-buttons-tutorial'] + ) + + // only show tutorial popover on the first icon + const firstUnselectedIndex = visibleUpdates.findIndex(update => { + const selectionState = isVersionSelected( + selection, + update.fromV, + update.toV + ) + return unselectedStates.includes(selectionState) + }) + + const [showTutorial, setShowTutorial] = useState(!hasCompletedHistTutorial) + return (
@@ -110,6 +144,9 @@ function AllHistoryList() { selected === 'aboveSelected' || selected === 'belowSelected') + const hasTutorialOverlay = + index === firstUnselectedIndex && showTutorial + return ( ) })} diff --git a/services/web/frontend/js/features/history/components/change-list/history-version.tsx b/services/web/frontend/js/features/history/components/change-list/history-version.tsx index 3e99b0cea6..8f3fd1ccaf 100644 --- a/services/web/frontend/js/features/history/components/change-list/history-version.tsx +++ b/services/web/frontend/js/features/history/components/change-list/history-version.tsx @@ -1,3 +1,15 @@ +import { + useRef, + useCallback, + memo, + useEffect, + useState, + ReactNode, +} from 'react' +import { Popover, Overlay } from 'react-bootstrap' +import { useTranslation, Trans } from 'react-i18next' +import Close from '../../../../shared/components/close' +import MaterialIcon from '../../../../shared/components/material-icon' import HistoryVersionDetails from './history-version-details' import TagTooltip from './tag-tooltip' import Changes from './changes' @@ -13,10 +25,11 @@ import { ItemSelectionState, } from '../../utils/history-details' import { ActiveDropdown } from '../../hooks/use-dropdown-active-item' -import { memo, useCallback } from 'react' +import useAsync from '../../../../shared/hooks/use-async' import { HistoryContextValue } from '../../context/types/history-context-value' import VersionDropdownContent from './dropdown/version-dropdown-content' import CompareItems from './dropdown/menu-item/compare-items' +import { completeHistoryTutorial } from '../../services/api' import CompareVersionDropdown from './dropdown/compare-version-dropdown' import { CompareVersionDropdownContentAllHistory } from './dropdown/compare-version-dropdown-content' @@ -35,6 +48,8 @@ type HistoryVersionProps = { compareDropdownActive: boolean setActiveDropdownItem: ActiveDropdown['setActiveDropdownItem'] closeDropdownForItem: ActiveDropdown['closeDropdownForItem'] + hasTutorialOverlay?: boolean + setShowTutorial: (show: boolean) => void } function HistoryVersion({ @@ -52,8 +67,97 @@ function HistoryVersion({ compareDropdownActive, setActiveDropdownItem, closeDropdownForItem, + hasTutorialOverlay = false, + setShowTutorial, }: HistoryVersionProps) { const orderedLabels = orderBy(update.labels, ['created_at'], ['desc']) + const iconRef = useRef(null) + + const { runAsync } = useAsync() + + const { t } = useTranslation() + + const [popover, setPopover] = useState(null) + // wait for the layout to settle before showing popover, to avoid a flash/ instant move + const [layoutSettled, setLayoutSettled] = useState(false) + + useEffect(() => { + if (iconRef.current && hasTutorialOverlay && layoutSettled) { + const dismissModal = () => { + setShowTutorial(false) + runAsync(completeHistoryTutorial()).catch(console.error) + } + + const compareIcon = ( + + ) + + setPopover( + + + {t('react_history_tutorial_title')}{' '} + dismissModal()} /> + + } + className="dark-themed" + > + + + {' '} + {t('react_history_tutorial_learn_more')} + + + + ) + } + }, [hasTutorialOverlay, runAsync, setShowTutorial, t, layoutSettled]) + + // give the components time to position before showing popover so we dont get a instant position change + useEffect(() => { + const timer = window.setTimeout(() => { + setLayoutSettled(true) + }, 500) + + return () => clearTimeout(timer) + }, [setLayoutSettled]) + + useEffect(() => { + let timer: number | null = null + + const handleResize = () => { + // Hide popover when a resize starts, then waiting for a gap of 500ms + // with no resizes before making it reappear + if (timer) { + window.clearTimeout(timer) + } else { + setShowTutorial(false) + } + timer = window.setTimeout(() => { + timer = null + setShowTutorial(true) + }, 500) + } + + // only need a listener on the component that actually has the popover + if (hasTutorialOverlay) { + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + } + }, [hasTutorialOverlay, setShowTutorial]) const closeDropdown = useCallback(() => { closeDropdownForItem(update, 'moreOptions') @@ -63,6 +167,7 @@ function HistoryVersion({ return ( <> + {hasTutorialOverlay && popover} {showDivider ? (
+
{selected !== 'withinSelected' ? ( + variant?: 'light' | 'dark' } -function Close({ onDismiss }: CloseProps) { +function Close({ onDismiss, variant = 'light' }: CloseProps) { const { t } = useTranslation() return ( - diff --git a/services/web/frontend/stylesheets/app/base.less b/services/web/frontend/stylesheets/app/base.less index 3bb8925191..58a7d03e27 100644 --- a/services/web/frontend/stylesheets/app/base.less +++ b/services/web/frontend/stylesheets/app/base.less @@ -21,6 +21,16 @@ text-shadow: none; } +.light.close { + color: #000; +} + +.dark.close { + color: #fff; + opacity: 1; + text-shadow: none; +} + .clickable { cursor: pointer; } diff --git a/services/web/frontend/stylesheets/app/editor/history-react.less b/services/web/frontend/stylesheets/app/editor/history-react.less index 0fdf79ab37..2d26469b62 100644 --- a/services/web/frontend/stylesheets/app/editor/history-react.less +++ b/services/web/frontend/stylesheets/app/editor/history-react.less @@ -406,6 +406,11 @@ history-root { color: @neutral-90; } +.history-dropdown-icon-inverted { + color: @neutral-10; + vertical-align: top; +} + .history-compare-btn { line-height: 1; padding: 0; diff --git a/services/web/frontend/stylesheets/components/popovers.less b/services/web/frontend/stylesheets/components/popovers.less index 9a55c2b561..6b71b2b0c4 100755 --- a/services/web/frontend/stylesheets/components/popovers.less +++ b/services/web/frontend/stylesheets/components/popovers.less @@ -137,3 +137,54 @@ } } } + +.popover.dark-themed { + .popover-title { + color: @popover-dark-color; + background-color: @popover-dark-bg; + border-bottom: 0; + font-family: Lato, sans-serif; + font-weight: bold; + } + + .popover-content { + color: @popover-dark-color; + background-color: @popover-dark-bg; + } + + a { + color: #97b6e5; + } + + padding: 0; + border: 1px solid @popover-dark-border-color; + max-width: @popover-dark-max-width; +} + +.popover.dark-themed { + &.top > .arrow { + border-top-color: @popover-dark-arrow-outer-color; + &:after { + border-top-color: @popover-dark-arrow-color; + } + } + &.right > .arrow { + border-right-color: @popover-dark-arrow-outer-color; + &:after { + border-right-color: @popover-dark-arrow-color; + } + } + &.bottom > .arrow { + border-bottom-color: @popover-dark-arrow-outer-color; + &:after { + border-bottom-color: @popover-dark-arrow-color; + } + } + + &.left > .arrow { + border-left-color: @popover-dark-arrow-outer-color; + &:after { + border-left-color: @popover-dark-arrow-color; + } + } +} diff --git a/services/web/frontend/stylesheets/core/variables.less b/services/web/frontend/stylesheets/core/variables.less index f14ea5c2b2..9fc342595d 100644 --- a/services/web/frontend/stylesheets/core/variables.less +++ b/services/web/frontend/stylesheets/core/variables.less @@ -595,6 +595,27 @@ 20% ); +//== Popovers dark theme +// +//## +//** Popover body background color +@popover-dark-bg: #000; +//** Popover font color +@popover-dark-color: #fff; +//** Popover maximum width +@popover-dark-max-width: 512px; +//** Popover border color +@popover-dark-border-color: #000; + +//** Popover title background color +@popover-dark-title-bg: #000; + +//** Popover arrow color +@popover-dark-arrow-color: #000; + +//** Popover outer arrow color +@popover-dark-arrow-outer-color: fadein(@popover-dark-border-color, 5%); + //== Labels // //## diff --git a/services/web/frontend/stylesheets/variables/all.less b/services/web/frontend/stylesheets/variables/all.less index 3f98db36c9..1567f05d8d 100644 --- a/services/web/frontend/stylesheets/variables/all.less +++ b/services/web/frontend/stylesheets/variables/all.less @@ -532,6 +532,27 @@ 20% ); +//== Popovers dark theme +// +//## +//** Popover body background color +@popover-dark-bg: #000; +//** Popover font color +@popover-dark-color: #fff; +//** Popover maximum width +@popover-dark-max-width: 512px; +//** Popover border color +@popover-dark-border-color: #000; + +//** Popover title background color +@popover-dark-title-bg: #000; + +//** Popover arrow color +@popover-dark-arrow-color: #000; + +//** Popover outer arrow color +@popover-dark-arrow-outer-color: fadein(@popover-dark-border-color, 5%); + //== Labels // //## diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 6c4e542753..deee3ad602 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1299,6 +1299,9 @@ "quoted_text_in": "Quoted text in", "raw_logs": "Raw logs", "raw_logs_description": "Raw logs from the LaTeX compiler", + "react_history_tutorial_content": "To compare a range of versions, use the <0> on the versions you want at the start and end of the range. To add a label or to download a version use the options in the three-dot menu. Learn more about using Overleaf History.", + "react_history_tutorial_learn_more": "Learn more about using Overleaf History.", + "react_history_tutorial_title": "History actions have a new home", "reactivate_subscription": "Reactivate your subscription", "read_only": "Read Only", "read_only_token": "Read-Only Token", diff --git a/services/web/test/frontend/features/history/components/change-list.spec.tsx b/services/web/test/frontend/features/history/components/change-list.spec.tsx index 21cde521ec..aa2981a045 100644 --- a/services/web/test/frontend/features/history/components/change-list.spec.tsx +++ b/services/web/test/frontend/features/history/components/change-list.spec.tsx @@ -51,6 +51,9 @@ describe('change list', function () { cy.intercept('GET', '/project/*/filetree/diff*', { body: { diff: [{ pathname: 'main.tex' }, { pathname: 'name.tex' }] }, }).as('diff') + window.metaAttributesCache.set('ol-completedTutorials', { + 'react-history-buttons-tutorial': Date.now(), + }) }) describe('toggle switch', function () {