diff --git a/CHANGELOG.md b/CHANGELOG.md
index 753e6d0f9..543588ed7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -76,6 +76,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
- Image tags with placeholder urls (`https://`) will be replaced with a placeholder frame.
- Images that are currently uploading will be rendered as "uploading".
- Code blocks with `plantuml` as language are rendered as [PlantUML](https://plantuml.com/) diagram using a configured render server.
+- File based motd that supports markdown without html.
### Changed
diff --git a/cypress/integration/motd.spec.ts b/cypress/integration/motd.spec.ts
index 89f62b709..f61e336b2 100644
--- a/cypress/integration/motd.spec.ts
+++ b/cypress/integration/motd.spec.ts
@@ -6,17 +6,18 @@
const MOTD_LOCAL_STORAGE_KEY = 'motd.lastModified'
const MOCK_LAST_MODIFIED = 'mockETag'
-const motdMockContent = 'This is the mock Motd call'
+const motdMockContent = 'This is the **mock** Motd call'
+const motdMockHtml = 'This is the mock Motd call'
describe('Motd', () => {
- const mockExistingMotd = (useEtag?: boolean) => {
- cy.intercept('GET', '/mock-backend/public/motd.txt', {
+ const mockExistingMotd = (useEtag?: boolean, content = motdMockContent) => {
+ cy.intercept('GET', '/mock-backend/public/motd.md', {
statusCode: 200,
headers: { [useEtag ? 'etag' : 'Last-Modified']: MOCK_LAST_MODIFIED },
- body: motdMockContent
+ body: content
})
- cy.intercept('HEAD', '/mock-backend/public/motd.txt', {
+ cy.intercept('HEAD', '/mock-backend/public/motd.md', {
statusCode: 200,
headers: { [useEtag ? 'etag' : 'Last-Modified']: MOCK_LAST_MODIFIED }
})
@@ -29,13 +30,19 @@ describe('Motd', () => {
it('shows the correct alert Motd text', () => {
mockExistingMotd()
cy.visitHome()
- cy.getByCypressId('motd').contains(motdMockContent)
+ cy.getByCypressId('motd').find('.markdown-body').should('contain.html', motdMockHtml)
+ })
+
+ it("doesn't allow html in the motd", () => {
+ mockExistingMotd(false, '')
+ cy.visitHome()
+ cy.getByCypressId('motd').find('.markdown-body').should('have.html', '
<iframe></iframe>
\n')
})
it('can be dismissed using etag', () => {
mockExistingMotd(true)
cy.visitHome()
- cy.getByCypressId('motd').contains(motdMockContent)
+ cy.getByCypressId('motd').find('.markdown-body').should('contain.html', motdMockHtml)
cy.getByCypressId('motd-dismiss')
.click()
.then(() => {
@@ -47,7 +54,7 @@ describe('Motd', () => {
it('can be dismissed', () => {
mockExistingMotd()
cy.visitHome()
- cy.getByCypressId('motd').contains(motdMockContent)
+ cy.getByCypressId('motd').find('.markdown-body').should('contain.html', motdMockHtml)
cy.getByCypressId('motd-dismiss')
.click()
.then(() => {
@@ -59,7 +66,7 @@ describe('Motd', () => {
it("won't show again after dismiss and reload", () => {
mockExistingMotd()
cy.visitHome()
- cy.getByCypressId('motd').contains(motdMockContent)
+ cy.getByCypressId('motd').find('.markdown-body').should('contain.html', motdMockHtml)
cy.getByCypressId('motd-dismiss')
.click()
.then(() => {
@@ -74,16 +81,16 @@ describe('Motd', () => {
it('will show again after reload without dismiss', () => {
mockExistingMotd()
cy.visitHome()
- cy.getByCypressId('motd').contains(motdMockContent)
+ cy.getByCypressId('motd').find('.markdown-body').should('contain.html', motdMockHtml)
cy.reload()
cy.get('main').should('exist')
- cy.getByCypressId('motd').contains(motdMockContent)
+ cy.getByCypressId('motd').find('.markdown-body').should('contain.html', motdMockHtml)
})
it("won't show again after dismiss and page navigation", () => {
mockExistingMotd()
cy.visitHome()
- cy.getByCypressId('motd').contains(motdMockContent)
+ cy.getByCypressId('motd').find('.markdown-body').should('contain.html', motdMockHtml)
cy.getByCypressId('motd-dismiss')
.click()
.then(() => {
diff --git a/cypress/support/config.ts b/cypress/support/config.ts
index 040a576ec..99aa0f2bb 100644
--- a/cypress/support/config.ts
+++ b/cypress/support/config.ts
@@ -68,11 +68,11 @@ Cypress.Commands.add('loadConfig', (additionalConfig?: Partial) =
beforeEach(() => {
cy.loadConfig()
- cy.intercept('GET', '/mock-backend/public/motd.txt', {
+ cy.intercept('GET', '/mock-backend/public/motd.md', {
body: '404 Not Found!',
statusCode: 404
})
- cy.intercept('HEAD', '/mock-backend/public/motd.txt', {
+ cy.intercept('HEAD', '/mock-backend/public/motd.md', {
statusCode: 404
})
})
diff --git a/netlify/intro.md b/netlify/intro.md
index 1dd350d05..8437de0e4 100644
--- a/netlify/intro.md
+++ b/netlify/intro.md
@@ -1,10 +1,4 @@
-
-
-:::warning
+:::info
What you see is an UI-Test! It's filled with dummy data, not connected to a backend and no data will be saved.
:::
diff --git a/netlify/motd.txt.license b/netlify/intro.md.license
similarity index 100%
rename from netlify/motd.txt.license
rename to netlify/intro.md.license
diff --git a/netlify/motd.md b/netlify/motd.md
new file mode 100644
index 000000000..04abdc441
--- /dev/null
+++ b/netlify/motd.md
@@ -0,0 +1,6 @@
+This demo is hosted by [netlify](https://netlify.com).
+Please check their [privacy policy](https://netlify.com/privacy) as well as [our privacy policy](https://hedgedoc.org/privacy-policy).
+
+:::info
+What you see is an UI-Test! It's filled with dummy data, not connected to a backend and no data will be saved.
+:::
diff --git a/netlify/motd.md.license b/netlify/motd.md.license
new file mode 100644
index 000000000..078e5a9ac
--- /dev/null
+++ b/netlify/motd.md.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
+
+SPDX-License-Identifier: CC0-1.0
diff --git a/netlify/motd.txt b/netlify/motd.txt
deleted file mode 100644
index 87cd750fb..000000000
--- a/netlify/motd.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-This demo is hosted by netlify.
-Please check their privacy policy (https://netlify.com/privacy) as well as ours (https://hedgedoc.org/privacy-policy).
diff --git a/netlify/patch-files.sh b/netlify/patch-files.sh
index 63db6ca74..96b497c86 100644
--- a/netlify/patch-files.sh
+++ b/netlify/patch-files.sh
@@ -7,8 +7,8 @@ set -e
echo 'Patch intro.md to include netlify banner.'
cp netlify/intro.md public/mock-backend/public/intro.md
-echo 'Patch motd.txt to include privacy policy.'
-cp netlify/motd.txt public/mock-backend/public/motd.txt
+echo 'Patch motd.md to include privacy policy.'
+cp netlify/motd.md public/mock-backend/public/motd.md
echo 'Patch version.json to include git hash'
jq ".version = \"0.0.0+${GITHUB_SHA:0:8}\"" src/version.json > src/_version.json
mv src/_version.json src/version.json
diff --git a/public/mock-backend/public/motd.txt b/public/mock-backend/public/motd.md
similarity index 77%
rename from public/mock-backend/public/motd.txt
rename to public/mock-backend/public/motd.md
index b0a5168f3..974a3143f 100644
--- a/public/mock-backend/public/motd.txt
+++ b/public/mock-backend/public/motd.md
@@ -1 +1,2 @@
This is the test motd text
+:smile:
diff --git a/src/components/application-loader/initializers/fetch-motd.ts b/src/components/application-loader/initializers/fetch-motd.ts
index b764e0a1f..0d7e1c3bb 100644
--- a/src/components/application-loader/initializers/fetch-motd.ts
+++ b/src/components/application-loader/initializers/fetch-motd.ts
@@ -17,12 +17,12 @@ const log = new Logger('Motd')
* To check if the motd has changed the "last modified" header from the request
* will be compared to the saved value from the browser's local storage.
*
- * @param customizeAssetsUrl the URL where the motd.txt can be found.
+ * @param customizeAssetsUrl the URL where the motd.md can be found.
* @return A promise that gets resolved if the motd was fetched successfully.
*/
export const fetchMotd = async (customizeAssetsUrl: string): Promise => {
const cachedLastModified = window.localStorage.getItem(MOTD_LOCAL_STORAGE_KEY)
- const motdUrl = `${customizeAssetsUrl}motd.txt`
+ const motdUrl = `${customizeAssetsUrl}motd.md`
if (cachedLastModified) {
const response = await fetch(motdUrl, {
@@ -48,7 +48,7 @@ export const fetchMotd = async (customizeAssetsUrl: string): Promise => {
const lastModified = response.headers.get('Last-Modified') || response.headers.get('etag')
if (!lastModified) {
- log.warn("'Last-Modified' or 'Etag' not found for motd.txt!")
+ log.warn("'Last-Modified' or 'Etag' not found for motd.md!")
}
const motdText = await response.text()
diff --git a/src/components/common/motd-modal/motd-modal.tsx b/src/components/common/motd-modal/motd-modal.tsx
index 82f213e33..288a1ac88 100644
--- a/src/components/common/motd-modal/motd-modal.tsx
+++ b/src/components/common/motd-modal/motd-modal.tsx
@@ -4,13 +4,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import React, { Fragment, useCallback, useMemo } from 'react'
+import React, { Suspense, useCallback } from 'react'
import { Button, Modal } from 'react-bootstrap'
import { CommonModal } from '../modals/common-modal'
import { Trans, useTranslation } from 'react-i18next'
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { dismissMotd } from '../../../redux/motd/methods'
import { cypressId } from '../../../utils/cypress-attribute'
+import { WaitSpinner } from '../wait-spinner/wait-spinner'
+
+const MotdRenderer = React.lazy(() => import('./motd-renderer'))
/**
* Reads the motd from the global application state and shows it in a modal.
@@ -21,23 +24,6 @@ export const MotdModal: React.FC = () => {
useTranslation()
const motdState = useApplicationState((state) => state.motd)
- const domContent = useMemo(() => {
- if (!motdState) {
- return null
- }
- let index = 0
- return motdState.text
- ?.split('\n')
- .map((line) => {line})
- .reduce((previousLine, currentLine, currentLineIndex) => (
-
- {previousLine}
-
- {currentLine}
-
- ))
- }, [motdState])
-
const dismiss = useCallback(() => {
if (!motdState) {
return
@@ -49,8 +35,12 @@ export const MotdModal: React.FC = () => {
return null
} else {
return (
-
- {domContent}
+
+
+ }>
+
+
+