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
+}