From 9497726a7c7a2dada8b113c7e9293a1c8dd13e71 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Tue, 28 Mar 2023 12:22:52 +0200 Subject: [PATCH] feat(frontend): show indicator in document title for background changes Signed-off-by: Tilman Vatteroth --- ...ontent-been-changed-in-background.spec.tsx | 56 +++++++++++++++++++ ...down-content-been-changed-in-background.ts | 30 ++++++++++ .../note-and-app-title-head.tsx | 6 +- .../common/use-is-document-visible.spec.tsx | 24 ++++++++ .../hooks/common/use-is-document-visible.ts | 28 ++++++++++ 5 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/editor-page/head-meta-properties/hooks/use-has-markdown-content-been-changed-in-background.spec.tsx create mode 100644 frontend/src/components/editor-page/head-meta-properties/hooks/use-has-markdown-content-been-changed-in-background.ts create mode 100644 frontend/src/hooks/common/use-is-document-visible.spec.tsx create mode 100644 frontend/src/hooks/common/use-is-document-visible.ts diff --git a/frontend/src/components/editor-page/head-meta-properties/hooks/use-has-markdown-content-been-changed-in-background.spec.tsx b/frontend/src/components/editor-page/head-meta-properties/hooks/use-has-markdown-content-been-changed-in-background.spec.tsx new file mode 100644 index 000000000..a97c6b54c --- /dev/null +++ b/frontend/src/components/editor-page/head-meta-properties/hooks/use-has-markdown-content-been-changed-in-background.spec.tsx @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import * as UseIsDocumentVisibleModule from '../../../../hooks/common/use-is-document-visible' +import * as UseNoteMarkdownContent from '../../../../hooks/common/use-note-markdown-content' +import { useHasMarkdownContentBeenChangedInBackground } from './use-has-markdown-content-been-changed-in-background' +import { render } from '@testing-library/react' +import React, { Fragment } from 'react' + +jest.mock('../../../../hooks/common/use-is-document-visible') +jest.mock('../../../../hooks/common/use-note-markdown-content') + +describe('use has markdown content been changed in background', () => { + const TestComponent: React.FC = () => { + const visible = useHasMarkdownContentBeenChangedInBackground() + return {String(visible)} + } + + let documentVisible = true + let noteContent = 'content' + + beforeEach(() => { + jest.spyOn(UseIsDocumentVisibleModule, 'useIsDocumentVisible').mockImplementation(() => documentVisible) + jest.spyOn(UseNoteMarkdownContent, 'useNoteMarkdownContent').mockImplementation(() => noteContent) + }) + + it('returns the correct value', () => { + documentVisible = true + noteContent = 'content1' + const view = render() + expect(view.container.textContent).toBe('false') + expect(view.container.textContent).toBe('false') //intentionally no change + + noteContent = 'content2' + view.rerender() + expect(view.container.textContent).toBe('false') + + documentVisible = false + view.rerender() + expect(view.container.textContent).toBe('false') + + noteContent = 'content3' + view.rerender() + expect(view.container.textContent).toBe('true') + + noteContent = 'content2' + view.rerender() + expect(view.container.textContent).toBe('true') + + documentVisible = true + view.rerender() + expect(view.container.textContent).toBe('false') + }) +}) diff --git a/frontend/src/components/editor-page/head-meta-properties/hooks/use-has-markdown-content-been-changed-in-background.ts b/frontend/src/components/editor-page/head-meta-properties/hooks/use-has-markdown-content-been-changed-in-background.ts new file mode 100644 index 000000000..99f1b6988 --- /dev/null +++ b/frontend/src/components/editor-page/head-meta-properties/hooks/use-has-markdown-content-been-changed-in-background.ts @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { useIsDocumentVisible } from '../../../../hooks/common/use-is-document-visible' +import { useNoteMarkdownContent } from '../../../../hooks/common/use-note-markdown-content' +import { useEffect, useRef, useState } from 'react' + +/** + * Determines if the markdown content has been changed while the browser tab hasn't been active. + */ +export const useHasMarkdownContentBeenChangedInBackground = (): boolean => { + const [backgroundChangesHappened, setBackgroundChangesHappened] = useState(false) + const documentVisible = useIsDocumentVisible() + const currentContent = useNoteMarkdownContent() + const lastContent = useRef('') + + useEffect(() => { + if (lastContent.current === currentContent || documentVisible) { + lastContent.current = currentContent + setBackgroundChangesHappened(false) + return + } + lastContent.current = currentContent + setBackgroundChangesHappened(true) + }, [currentContent, documentVisible]) + + return backgroundChangesHappened +} diff --git a/frontend/src/components/editor-page/head-meta-properties/note-and-app-title-head.tsx b/frontend/src/components/editor-page/head-meta-properties/note-and-app-title-head.tsx index ae1625f8f..3de403c28 100644 --- a/frontend/src/components/editor-page/head-meta-properties/note-and-app-title-head.tsx +++ b/frontend/src/components/editor-page/head-meta-properties/note-and-app-title-head.tsx @@ -5,6 +5,7 @@ */ import { useAppTitle } from '../../../hooks/common/use-app-title' import { useNoteTitle } from '../../../hooks/common/use-note-title' +import { useHasMarkdownContentBeenChangedInBackground } from './hooks/use-has-markdown-content-been-changed-in-background' import Head from 'next/head' import React, { useMemo } from 'react' @@ -14,10 +15,11 @@ import React, { useMemo } from 'react' export const NoteAndAppTitleHead: React.FC = () => { const noteTitle = useNoteTitle() const appTitle = useAppTitle() + const showDot = useHasMarkdownContentBeenChangedInBackground() const noteAndAppTitle = useMemo(() => { - return noteTitle + ' - ' + appTitle - }, [appTitle, noteTitle]) + return (showDot ? '(•) ' : '') + noteTitle + ' - ' + appTitle + }, [appTitle, noteTitle, showDot]) return ( diff --git a/frontend/src/hooks/common/use-is-document-visible.spec.tsx b/frontend/src/hooks/common/use-is-document-visible.spec.tsx new file mode 100644 index 000000000..3f306fb39 --- /dev/null +++ b/frontend/src/hooks/common/use-is-document-visible.spec.tsx @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { useIsDocumentVisible } from './use-is-document-visible' +import { fireEvent, render } from '@testing-library/react' +import React, { Fragment } from 'react' + +describe('use is document visible', () => { + const TestComponent: React.FC = () => { + const visible = useIsDocumentVisible() + return {String(visible)} + } + + it('returns the correct value', () => { + const view = render() + expect(view.container.textContent).toBe('true') + fireEvent(window, new Event('blur')) + expect(view.container.textContent).toBe('false') + fireEvent(window, new Event('focus')) + expect(view.container.textContent).toBe('true') + }) +}) diff --git a/frontend/src/hooks/common/use-is-document-visible.ts b/frontend/src/hooks/common/use-is-document-visible.ts new file mode 100644 index 000000000..d2a790574 --- /dev/null +++ b/frontend/src/hooks/common/use-is-document-visible.ts @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { useEffect, useState } from 'react' + +/** + * Uses the browsers visiblity API to determine if the tab is active or now. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API + */ +export const useIsDocumentVisible = (): boolean => { + const [documentVisible, setDocumentVisible] = useState(true) + + useEffect(() => { + const onFocus = () => setDocumentVisible(true) + const onBlur = () => setDocumentVisible(false) + window.addEventListener('focus', onFocus) + window.addEventListener('blur', onBlur) + return () => { + document.removeEventListener('focus', onFocus) + document.removeEventListener('blur', onBlur) + } + }, []) + + return documentVisible +}