Explorar o código

Implement grammarly warning alert on cm6 users

GitOrigin-RevId: ed272bbc385faa69811ec1891075906cdca1c984
M Fahru %!s(int64=3) %!d(string=hai) anos
pai
achega
d70e0b1c0e

+ 2 - 0
services/web/app/views/project/editor.pug

@@ -65,6 +65,8 @@ block content
 						span.sr-only #{translate("close")}
 						span.sr-only #{translate("close")}
 					.system-message-content(ng-bind-html="htmlContent")
 					.system-message-content(ng-bind-html="htmlContent")
 
 
+			grammarly-warning()
+
 		include ./editor/main
 		include ./editor/main
 
 
 	script(type="text/ng-template", id="genericMessageModalTemplate")
 	script(type="text/ng-template", id="genericMessageModalTemplate")

+ 65 - 0
services/web/frontend/js/features/source-editor/components/grammarly-warning.tsx

@@ -0,0 +1,65 @@
+import { useCallback, useEffect, useState } from 'react'
+import { Button } from 'react-bootstrap'
+import customLocalStorage from '../../../infrastructure/local-storage'
+import useScopeValue from '../../../shared/hooks/use-scope-value'
+import grammarlyExtensionPresent from '../../../shared/utils/grammarly'
+import getMeta from '../../../utils/meta'
+
+export default function GrammarlyWarning() {
+  const [show, setShow] = useState(false)
+  const [newSourceEditor] = useScopeValue('editor.newSourceEditor')
+  const [showRichText] = useScopeValue('editor.showRichText')
+  const grammarly = grammarlyExtensionPresent()
+  const hasDismissedGrammarlyWarning = customLocalStorage.getItem(
+    'editor.has_dismissed_grammarly_warning'
+  )
+
+  useEffect(() => {
+    const showGrammarlyWarning =
+      !hasDismissedGrammarlyWarning &&
+      grammarly &&
+      newSourceEditor &&
+      !showRichText
+
+    if (showGrammarlyWarning) {
+      setShow(true)
+    }
+  }, [grammarly, hasDismissedGrammarlyWarning, newSourceEditor, showRichText])
+
+  const handleClose = useCallback(() => {
+    setShow(false)
+    customLocalStorage.setItem('editor.has_dismissed_grammarly_warning', true)
+  }, [])
+
+  if (!getMeta('ol-showNewSourceEditorOption')) {
+    return null
+  }
+
+  if (!show) {
+    return null
+  }
+
+  return (
+    <div className="alert alert-info grammarly-warning" role="alert">
+      <Button
+        className="close"
+        data-dismiss="alert"
+        aria-label="Close"
+        onClick={handleClose}
+      >
+        <span aria-hidden="true">&times;</span>
+      </Button>
+      <div className="warning-content">
+        A browser extension, for example Grammarly, may be slowing down
+        Overleaf.{' '}
+        <a
+          className="warning-link"
+          href="/learn/how-to/Use_Grammarly_with_Overleaf#Performance_issues"
+          target="_blank"
+        >
+          Find out how to avoid this
+        </a>
+      </div>
+    </div>
+  )
+}

+ 9 - 0
services/web/frontend/js/features/source-editor/controllers/grammarly-warning-controller.js

@@ -0,0 +1,9 @@
+import App from '../../../base'
+import { react2angular } from 'react2angular'
+import { rootContext } from '../../../shared/context/root-context'
+import GrammarlyWarning from '../components/grammarly-warning'
+
+App.component(
+  'grammarlyWarning',
+  react2angular(rootContext.use(GrammarlyWarning))
+)

+ 1 - 0
services/web/frontend/js/ide.js

@@ -65,6 +65,7 @@ import './features/pdf-preview/controllers/pdf-preview-controller'
 import './features/share-project-modal/controllers/react-share-project-modal-controller'
 import './features/share-project-modal/controllers/react-share-project-modal-controller'
 import './features/source-editor/controllers/editor-switch-controller'
 import './features/source-editor/controllers/editor-switch-controller'
 import './features/source-editor/controllers/cm6-switch-away-survey-controller'
 import './features/source-editor/controllers/cm6-switch-away-survey-controller'
+import './features/source-editor/controllers/grammarly-warning-controller'
 import { cleanupServiceWorker } from './utils/service-worker-cleanup'
 import { cleanupServiceWorker } from './utils/service-worker-cleanup'
 import { reportCM6Perf } from './infrastructure/cm6-performance'
 import { reportCM6Perf } from './infrastructure/cm6-performance'
 
 

+ 29 - 0
services/web/frontend/stylesheets/app/editor.less

@@ -827,3 +827,32 @@ CodeMirror
     font-size: @font-size-small;
     font-size: @font-size-small;
   }
   }
 }
 }
+
+.grammarly-warning {
+  width: 500px;
+
+  &.alert.alert-info {
+    padding: @padding-sm;
+  }
+
+  .btn.close {
+    background-color: transparent;
+    color: @white;
+    opacity: 1;
+  }
+
+  .warning-content {
+    padding-right: @alert-padding;
+    font-size: @font-size-small;
+    margin-right: 32px;
+
+    .warning-link {
+      font-weight: 700;
+      text-decoration: none;
+
+      &:hover {
+        text-decoration: underline;
+      }
+    }
+  }
+}

+ 184 - 0
services/web/test/frontend/features/source-editor/components/grammarly-warning.test.js

@@ -0,0 +1,184 @@
+import sinon from 'sinon'
+import fetchMock from 'fetch-mock'
+import { expect } from 'chai'
+import { fireEvent, screen, waitFor } from '@testing-library/react'
+import { renderWithEditorContext } from '../../../helpers/render-with-context'
+import GrammarlyWarning from '../../../../../frontend/js/features/source-editor/components/grammarly-warning'
+import * as grammarlyModule from '../../../../../frontend/js/shared/utils/grammarly'
+import localStorage from '../../../../../frontend/js/infrastructure/local-storage'
+
+describe('<GrammarlyWarning />', function () {
+  let grammarlyStub
+
+  before(function () {
+    window.metaAttributesCache = new Map()
+  })
+
+  beforeEach(function () {
+    grammarlyStub = sinon.stub(grammarlyModule, 'default')
+  })
+
+  afterEach(function () {
+    window.metaAttributesCache = new Map()
+    grammarlyStub.restore()
+    fetchMock.reset()
+    localStorage.clear()
+  })
+
+  it('shows warning when grammarly is available', async function () {
+    grammarlyStub.returns(true)
+    window.metaAttributesCache.set('ol-showNewSourceEditorOption', true)
+
+    renderWithEditorContext(<GrammarlyWarning />, {
+      scope: {
+        editor: {
+          newSourceEditor: true,
+        },
+      },
+    })
+
+    await screen.findByText(
+      'A browser extension, for example Grammarly, may be slowing down Overleaf.'
+    )
+    await screen.findByRole('button', { name: 'Close' })
+    await screen.findByRole('link', { name: 'Find out how to avoid this' })
+  })
+
+  it('does not show warning when grammarly is not available', async function () {
+    grammarlyStub.returns(false)
+    window.metaAttributesCache.set('ol-showNewSourceEditorOption', true)
+
+    renderWithEditorContext(<GrammarlyWarning />, {
+      scope: {
+        editor: {
+          newSourceEditor: true,
+        },
+      },
+    })
+
+    await waitFor(() => {
+      expect(
+        screen.queryByText(
+          'A browser extension, for example Grammarly, may be slowing down Overleaf.'
+        )
+      ).to.not.exist
+    })
+  })
+
+  it('does not show warning when user has dismissed the warning', async function () {
+    grammarlyStub.returns(true)
+    localStorage.setItem('editor.has_dismissed_grammarly_warning', true)
+    window.metaAttributesCache.set('ol-showNewSourceEditorOption', true)
+
+    renderWithEditorContext(<GrammarlyWarning />, {
+      scope: {
+        editor: {
+          newSourceEditor: true,
+        },
+      },
+    })
+
+    await waitFor(() => {
+      expect(
+        screen.queryByText(
+          'A browser extension, for example Grammarly, may be slowing down Overleaf.'
+        )
+      ).to.not.exist
+    })
+  })
+
+  it('does not show warning when user does not have CM6', async function () {
+    grammarlyStub.returns(true)
+    window.metaAttributesCache.set('ol-showNewSourceEditorOption', false)
+
+    renderWithEditorContext(<GrammarlyWarning />)
+
+    await waitFor(() => {
+      expect(
+        screen.queryByText(
+          'A browser extension, for example Grammarly, may be slowing down Overleaf.'
+        )
+      ).to.not.exist
+    })
+  })
+
+  it('does not show warning when user have ace as their preference', async function () {
+    grammarlyStub.returns(true)
+    window.metaAttributesCache.set('ol-showNewSourceEditorOption', true)
+
+    renderWithEditorContext(<GrammarlyWarning />, {
+      scope: {
+        editor: {
+          newSourceEditor: false,
+        },
+      },
+    })
+
+    await waitFor(() => {
+      expect(
+        screen.queryByText(
+          'A browser extension, for example Grammarly, may be slowing down Overleaf.'
+        )
+      ).to.not.exist
+    })
+  })
+
+  it('does not show warning when user have rich text as their preference', async function () {
+    grammarlyStub.returns(true)
+    window.metaAttributesCache.set('ol-showNewSourceEditorOption', true)
+
+    renderWithEditorContext(<GrammarlyWarning />, {
+      scope: {
+        editor: {
+          newSourceEditor: true,
+          showRichText: true,
+        },
+      },
+    })
+
+    await waitFor(() => {
+      expect(
+        screen.queryByText(
+          'A browser extension, for example Grammarly, may be slowing down Overleaf.'
+        )
+      ).to.not.exist
+    })
+  })
+
+  it('hides warning if close button is pressed', async function () {
+    grammarlyStub.returns(true)
+    window.metaAttributesCache.set('ol-showNewSourceEditorOption', true)
+
+    renderWithEditorContext(<GrammarlyWarning />, {
+      scope: {
+        editor: {
+          newSourceEditor: true,
+        },
+      },
+    })
+
+    const warningText =
+      'A browser extension, for example Grammarly, may be slowing down Overleaf.'
+
+    await screen.findByText(warningText)
+
+    const hasDismissedGrammarlyWarning = localStorage.getItem(
+      'editor.has_dismissed_grammarly_warning'
+    )
+
+    expect(hasDismissedGrammarlyWarning).to.equal(null)
+
+    const closeButton = screen.getByRole('button', { name: 'Close' })
+    fireEvent.click(closeButton)
+
+    expect(screen.queryByText(warningText)).to.not.exist
+
+    await waitFor(() => {
+      const hasDismissedGrammarlyWarning = localStorage.getItem(
+        'editor.has_dismissed_grammarly_warning'
+      )
+
+      expect(hasDismissedGrammarlyWarning).to.equal(true)
+    })
+  })
+})