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