Merge pull request #15592 from overleaf/td-ide-page-more-modals

React IDE page: add out-of-sync modal and project deleted modal

GitOrigin-RevId: 6081a40c3009e8ebc5b3f9752c4059af3d52966d
This commit is contained in:
Tim Down 2023-11-06 13:11:06 +00:00 committed by Copybot
parent ecf7befe88
commit c3afce73c1
13 changed files with 257 additions and 25 deletions

View file

@ -466,6 +466,7 @@
"helps_us_tailor_your_experience": "", "helps_us_tailor_your_experience": "",
"hide_configuration": "", "hide_configuration": "",
"hide_document_preamble": "", "hide_document_preamble": "",
"hide_local_file_contents": "",
"hide_outline": "", "hide_outline": "",
"history": "", "history": "",
"history_add_label": "", "history_add_label": "",
@ -790,6 +791,8 @@
"organize_projects": "", "organize_projects": "",
"other_logs_and_files": "", "other_logs_and_files": "",
"other_output_files": "", "other_output_files": "",
"out_of_sync": "",
"out_of_sync_detail": "",
"output_file": "", "output_file": "",
"overall_theme": "", "overall_theme": "",
"overleaf": "", "overleaf": "",
@ -843,6 +846,7 @@
"please_select_a_project": "", "please_select_a_project": "",
"please_select_an_output_file": "", "please_select_an_output_file": "",
"please_set_main_file": "", "please_set_main_file": "",
"please_wait": "",
"plus_additional_collaborators_document_history_track_changes_and_more": "", "plus_additional_collaborators_document_history_track_changes_and_more": "",
"plus_more": "", "plus_more": "",
"plus_upgraded_accounts_receive": "", "plus_upgraded_accounts_receive": "",
@ -875,6 +879,8 @@
"project_not_linked_to_github": "", "project_not_linked_to_github": "",
"project_ownership_transfer_confirmation_1": "", "project_ownership_transfer_confirmation_1": "",
"project_ownership_transfer_confirmation_2": "", "project_ownership_transfer_confirmation_2": "",
"project_renamed_or_deleted": "",
"project_renamed_or_deleted_detail": "",
"project_synced_with_git_repo_at": "", "project_synced_with_git_repo_at": "",
"project_synchronisation": "", "project_synchronisation": "",
"project_timed_out_enable_stop_on_first_error": "", "project_timed_out_enable_stop_on_first_error": "",
@ -936,6 +942,7 @@
"reject": "", "reject": "",
"reject_all": "", "reject_all": "",
"relink_your_account": "", "relink_your_account": "",
"reload_editor": "",
"remote_service_error": "", "remote_service_error": "",
"remove": "", "remove": "",
"remove_collaborator": "", "remove_collaborator": "",
@ -1068,6 +1075,7 @@
"show_in_code": "", "show_in_code": "",
"show_in_pdf": "", "show_in_pdf": "",
"show_less": "", "show_less": "",
"show_local_file_contents": "",
"show_outline": "", "show_outline": "",
"show_x_more": "", "show_x_more": "",
"show_x_more_projects": "", "show_x_more_projects": "",
@ -1362,6 +1370,7 @@
"welcome_to_sl": "", "welcome_to_sl": "",
"were_in_the_process_of_reducing_compile_timeout_which_may_affect_this_project": "", "were_in_the_process_of_reducing_compile_timeout_which_may_affect_this_project": "",
"were_in_the_process_of_reducing_compile_timeout_which_may_affect_your_project": "", "were_in_the_process_of_reducing_compile_timeout_which_may_affect_your_project": "",
"were_performing_maintenance": "",
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_this_project": "", "weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_this_project": "",
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_your_project": "", "weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_your_project": "",
"what_does_this_mean": "", "what_does_this_mean": "",

View file

@ -21,7 +21,7 @@ export function Alerts() {
return ( return (
<div className="global-alerts"> <div className="global-alerts">
{connectionState.forceDisconnected ? ( {connectionState.forceDisconnected ? (
<Alert bsStyle="warning" className="small"> <Alert bsStyle="danger" className="small">
<strong>{t('disconnected')}</strong> <strong>{t('disconnected')}</strong>
</Alert> </Alert>
) : null} ) : null}

View file

@ -10,6 +10,7 @@ import EditorNavigationToolbar from '@/features/ide-react/components/editor-navi
import ChatPane from '@/features/chat/components/chat-pane' import ChatPane from '@/features/chat/components/chat-pane'
import { useLayoutEventTracking } from '@/features/ide-react/hooks/use-layout-event-tracking' import { useLayoutEventTracking } from '@/features/ide-react/hooks/use-layout-event-tracking'
import useSocketListeners from '@/features/ide-react/hooks/use-socket-listeners' import useSocketListeners from '@/features/ide-react/hooks/use-socket-listeners'
import { useModalsContext } from '@/features/ide-react/context/modals-context'
import { useOpenFile } from '@/features/ide-react/hooks/use-open-file' import { useOpenFile } from '@/features/ide-react/hooks/use-open-file'
// This is filled with placeholder content while the real content is migrated // This is filled with placeholder content while the real content is migrated
@ -25,7 +26,19 @@ export default function IdePage() {
useOpenFile() useOpenFile()
const [leftColumnDefaultSize, setLeftColumnDefaultSize] = useState(20) const [leftColumnDefaultSize, setLeftColumnDefaultSize] = useState(20)
const { registerUserActivity } = useConnectionContext() const { connectionState, registerUserActivity } = useConnectionContext()
const { showLockEditorMessageModal } = useModalsContext()
// Show modal when editor is forcefully disconnected
useEffect(() => {
if (connectionState.forceDisconnected) {
showLockEditorMessageModal(connectionState.forcedDisconnectDelay)
}
}, [
connectionState.forceDisconnected,
connectionState.forcedDisconnectDelay,
showLockEditorMessageModal,
])
// Inform the connection manager when the user is active // Inform the connection manager when the user is active
const listener = useCallback( const listener = useCallback(

View file

@ -0,0 +1,48 @@
import { useTranslation } from 'react-i18next'
import { Modal } from 'react-bootstrap'
import AccessibleModal from '@/shared/components/accessible-modal'
import { useEffect, useState } from 'react'
export type LockEditorMessageModalProps = {
delay: number // In seconds
show: boolean
}
function LockEditorMessageModal({ delay, show }: LockEditorMessageModalProps) {
const { t } = useTranslation()
const [secondsUntilRefresh, setSecondsUntilRefresh] = useState(0)
useEffect(() => {
if (show) {
setSecondsUntilRefresh(delay)
const timer = window.setInterval(() => {
setSecondsUntilRefresh(seconds => Math.max(0, seconds - 1))
}, 1000)
return () => {
window.clearInterval(timer)
}
}
}, [show, delay])
return (
<AccessibleModal
show={show}
// It's not possible to hide this modal, but it's a required prop
onHide={() => {}}
className="lock-editor-modal"
backdrop={false}
keyboard={false}
>
<Modal.Header>
<Modal.Title>{t('please_wait')}</Modal.Title>
</Modal.Header>
<Modal.Body>
{t('were_performing_maintenance', { seconds: secondsUntilRefresh })}
</Modal.Body>
</AccessibleModal>
)
}
export default LockEditorMessageModal

View file

@ -0,0 +1,81 @@
import { Trans, useTranslation } from 'react-i18next'
import { Button, Modal } from 'react-bootstrap'
import AccessibleModal from '@/shared/components/accessible-modal'
import { useState } from 'react'
import { useLocation } from '@/shared/hooks/use-location'
export type OutOfSyncModalProps = {
editorContent: string
show: boolean
onHide: () => void
}
function OutOfSyncModal({ editorContent, show, onHide }: OutOfSyncModalProps) {
const { t } = useTranslation()
const location = useLocation()
const [editorContentShown, setEditorContentShown] = useState(false)
const editorContentRows = (editorContent.match(/\n/g)?.length || 0) + 1
// Reload the page to avoid staying in an inconsistent state.
// https://github.com/overleaf/issues/issues/3694
function done() {
onHide()
location.reload()
}
return (
<AccessibleModal
show={show}
onHide={done}
className="out-of-sync-modal"
backdrop={false}
keyboard={false}
>
<Modal.Header closeButton>
<Modal.Title>{t('out_of_sync')}</Modal.Title>
</Modal.Header>
<Modal.Body className="modal-body-share">
<Trans
i18nKey="out_of_sync_detail"
components={[
// eslint-disable-next-line react/jsx-key
<br />,
// eslint-disable-next-line jsx-a11y/anchor-has-content,react/jsx-key
<a
target="_blank"
rel="noopener noreferrer"
href="/learn/Kb/Editor_out_of_sync_problems"
/>,
]}
/>
</Modal.Body>
<Modal.Body>
<Button
bsStyle="info"
onClick={() => setEditorContentShown(shown => !shown)}
>
{editorContentShown
? t('hide_local_file_contents')
: t('show_local_file_contents')}
</Button>
{editorContentShown ? (
<div className="text-preview">
<textarea
className="scroll-container"
readOnly
rows={editorContentRows}
value={editorContent}
/>
</div>
) : null}
</Modal.Body>
<Modal.Footer>
<Button bsStyle="info" onClick={done}>
{t('reload_editor')}
</Button>
</Modal.Footer>
</AccessibleModal>
)
}
export default OutOfSyncModal

View file

@ -24,6 +24,7 @@ const initialState: ConnectionState = {
inactiveDisconnect: false, inactiveDisconnect: false,
lastConnectionAttempt: 0, lastConnectionAttempt: 0,
reconnectAt: null, reconnectAt: null,
forcedDisconnectDelay: 0,
error: '', error: '',
} }
@ -181,9 +182,10 @@ export class ConnectionManager extends Emitter<Events> {
this.changeState({ this.changeState({
...this.state, ...this.state,
forceDisconnected: true, forceDisconnected: true,
forcedDisconnectDelay: delay,
error, error,
}) })
setTimeout(() => this.disconnect(), delay * 1000) setTimeout(() => this.disconnect(), 1000)
} }
private onJoinProjectResponse({ private onJoinProjectResponse({

View file

@ -13,6 +13,7 @@ export type ConnectionState = {
forceDisconnected: boolean forceDisconnected: boolean
inactiveDisconnect: boolean inactiveDisconnect: boolean
reconnectAt: number | null reconnectAt: number | null
forcedDisconnectDelay: number
lastConnectionAttempt: number lastConnectionAttempt: number
error: '' | ConnectionError error: '' | ConnectionError
} }

View file

@ -11,6 +11,7 @@ import { ConnectionState } from '../connection/types/connection-state'
import { ConnectionManager } from '@/features/ide-react/connection/connection-manager' import { ConnectionManager } from '@/features/ide-react/connection/connection-manager'
import { Socket } from '@/features/ide-react/connection/types/socket' import { Socket } from '@/features/ide-react/connection/types/socket'
import { secondsUntil } from '@/features/ide-react/connection/utils' import { secondsUntil } from '@/features/ide-react/connection/utils'
import { useLocation } from '@/shared/hooks/use-location'
type ConnectionContextValue = { type ConnectionContextValue = {
socket: Socket socket: Socket
@ -28,6 +29,8 @@ const ConnectionContext = createContext<ConnectionContextValue | undefined>(
) )
export const ConnectionProvider: FC = ({ children }) => { export const ConnectionProvider: FC = ({ children }) => {
const location = useLocation()
const [connectionManager] = useState(() => new ConnectionManager()) const [connectionManager] = useState(() => new ConnectionManager())
const [connectionState, setConnectionState] = useState( const [connectionState, setConnectionState] = useState(
connectionManager.state connectionManager.state
@ -69,6 +72,24 @@ export const ConnectionProvider: FC = ({ children }) => {
connectionManager.disconnect() connectionManager.disconnect()
}, [connectionManager]) }, [connectionManager])
// Reload the page on force disconnect. Doing this in React-land means that we
// can use useLocation(), which provides mockable location methods
useEffect(() => {
if (connectionState.forceDisconnected) {
const timer = window.setTimeout(
() => location.reload(),
connectionState.forcedDisconnectDelay * 1000
)
return () => {
window.clearTimeout(timer)
}
}
}, [
connectionState.forceDisconnected,
connectionState.forcedDisconnectDelay,
location,
])
const value = useMemo<ConnectionContextValue>( const value = useMemo<ConnectionContextValue>(
() => ({ () => ({
socket: connectionManager.socket, socket: connectionManager.socket,

View file

@ -115,7 +115,8 @@ export const EditorManagerProvider: FC = ({ children }) => {
const { reportError, eventEmitter, eventLog } = useIdeReactContext() const { reportError, eventEmitter, eventLog } = useIdeReactContext()
const { socket, disconnect } = useConnectionContext() const { socket, disconnect } = useConnectionContext()
const { view, setView } = useLayoutContext() const { view, setView } = useLayoutContext()
const { showGenericMessageModal, genericModalVisible } = useModalsContext() const { showGenericMessageModal, genericModalVisible, showOutOfSyncModal } =
useModalsContext()
const [showSymbolPalette, setShowSymbolPalette] = useScopeValue<boolean>( const [showSymbolPalette, setShowSymbolPalette] = useScopeValue<boolean>(
'editor.showSymbolPalette' 'editor.showSymbolPalette'
@ -550,13 +551,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
// Tell the user about the error state. // Tell the user about the error state.
setIsInErrorState(true) setIsInErrorState(true)
showOutOfSyncModal(editorContent || '')
// TODO: MIGRATION: Show out-of-sync modal
// this.ide.showOutOfSyncModal(
// 'Out of sync',
// "Sorry, this file has gone out of sync and we need to do a full refresh. <br> <a target='_blank' rel='noopener noreferrer' href='/learn/Kb/Editor_out_of_sync_problems'>Please see this help guide for more information</a>",
// editorContent
// )
// Do not forceReopen the document. // Do not forceReopen the document.
return return
@ -581,6 +576,7 @@ export const EditorManagerProvider: FC = ({ children }) => {
reportError, reportError,
setIsInErrorState, setIsInErrorState,
showGenericMessageModal, showGenericMessageModal,
showOutOfSyncModal,
t, t,
]) ])

View file

@ -104,9 +104,9 @@ export const IdeReactProvider: FC = ({ children }) => {
user_id: getMeta('ol-user_id'), user_id: getMeta('ol-user_id'),
project_id: projectId, project_id: projectId,
// @ts-ignore // @ts-ignore
client_id: socket.socket.sessionid, client_id: socket.socket?.sessionid,
// @ts-ignore // @ts-ignore
transport: socket.socket.transport, transport: socket.socket?.transport?.name,
client_now: new Date(), client_now: new Date(),
} }

View file

@ -20,6 +20,8 @@ import useSocketListener from '@/features/ide-react/hooks/use-socket-listener'
import useEventListener from '@/shared/hooks/use-event-listener' import useEventListener from '@/shared/hooks/use-event-listener'
import { FileTreeFindResult } from '@/features/ide-react/types/file-tree' import { FileTreeFindResult } from '@/features/ide-react/types/file-tree'
import { Project } from '../../../../../types/project' import { Project } from '../../../../../types/project'
import { useModalsContext } from '@/features/ide-react/context/modals-context'
import { useTranslation } from 'react-i18next'
type DocumentMetadata = { type DocumentMetadata = {
labels: string[] labels: string[]
@ -45,12 +47,14 @@ const MetadataContext = createContext<MetadataContextValue | undefined>(
) )
export const MetadataProvider: FC = ({ children }) => { export const MetadataProvider: FC = ({ children }) => {
const { t } = useTranslation()
const ide = useIdeContext() const ide = useIdeContext()
const { eventEmitter, projectId } = useIdeReactContext() const { eventEmitter, projectId } = useIdeReactContext()
const { socket } = useConnectionContext() const { socket } = useConnectionContext()
const { onlineUsersCount } = useOnlineUsersContext() const { onlineUsersCount } = useOnlineUsersContext()
const { permissionsLevel } = useEditorContext() const { permissionsLevel } = useEditorContext()
const { currentDocument } = useEditorManagerContext() const { currentDocument } = useEditorManagerContext()
const { showGenericMessageModal } = useModalsContext()
const [documents, setDocuments] = useState<DocumentsMetadata>({}) const [documents, setDocuments] = useState<DocumentsMetadata>({})
@ -180,17 +184,10 @@ export const MetadataProvider: FC = ({ children }) => {
useEffect(() => { useEffect(() => {
const handleProjectJoined = ({ project }: { project: Project }) => { const handleProjectJoined = ({ project }: { project: Project }) => {
if (project.deletedByExternalDataSource) { if (project.deletedByExternalDataSource) {
// TODO: MIGRATION: Show generic message modal here showGenericMessageModal(
/* t('project_renamed_or_deleted'),
ide.showGenericMessageModal( t('project_renamed_or_deleted_detail')
'Project Renamed or Deleted',
`\
This project has either been renamed or deleted by an external data source such as Dropbox.
We don't want to delete your data on Overleaf, so this project still contains your history and collaborators.
If the project has been renamed please look in your project list for a new project under the new name.\
`
) )
*/
} }
window.setTimeout(() => { window.setTimeout(() => {
if (permissionsLevel !== 'readOnly') { if (permissionsLevel !== 'readOnly') {
@ -204,7 +201,13 @@ If the project has been renamed please look in your project list for a new proje
return () => { return () => {
eventEmitter.off('project:joined', handleProjectJoined) eventEmitter.off('project:joined', handleProjectJoined)
} }
}, [eventEmitter, loadProjectMetaFromServer, permissionsLevel]) }, [
eventEmitter,
loadProjectMetaFromServer,
permissionsLevel,
showGenericMessageModal,
t,
])
const value = useMemo<MetadataContextValue>( const value = useMemo<MetadataContextValue>(
() => ({ () => ({

View file

@ -9,6 +9,10 @@ import {
import GenericMessageModal, { import GenericMessageModal, {
GenericMessageModalOwnProps, GenericMessageModalOwnProps,
} from '@/features/ide-react/components/modals/generic-message-modal' } from '@/features/ide-react/components/modals/generic-message-modal'
import OutOfSyncModal, {
OutOfSyncModalProps,
} from '@/features/ide-react/components/modals/out-of-sync-modal'
import LockEditorMessageModal from '@/features/ide-react/components/modals/lock-editor-message-modal'
type ModalsContextValue = { type ModalsContextValue = {
genericModalVisible: boolean genericModalVisible: boolean
@ -16,6 +20,10 @@ type ModalsContextValue = {
title: GenericMessageModalOwnProps['title'], title: GenericMessageModalOwnProps['title'],
message: GenericMessageModalOwnProps['message'] message: GenericMessageModalOwnProps['message']
) => void ) => void
showOutOfSyncModal: (
editorContent: OutOfSyncModalProps['editorContent']
) => void
showLockEditorMessageModal: (delay: number) => void
} }
const ModalsContext = createContext<ModalsContextValue | undefined>(undefined) const ModalsContext = createContext<ModalsContextValue | undefined>(undefined)
@ -25,6 +33,18 @@ export const ModalsContextProvider: FC = ({ children }) => {
const [genericMessageModalData, setGenericMessageModalData] = const [genericMessageModalData, setGenericMessageModalData] =
useState<GenericMessageModalOwnProps>({ title: '', message: '' }) useState<GenericMessageModalOwnProps>({ title: '', message: '' })
const [shouldShowOutOfSyncModal, setShouldShowOutOfSyncModal] =
useState(false)
const [outOfSyncModalData, setOutOfSyncModalData] = useState({
editorContent: '',
})
const [shouldShowLockEditorModal, setShouldShowLockEditorModal] =
useState(false)
const [lockEditorModalData, setLockEditorModalData] = useState({
delay: 0,
})
const handleHideGenericModal = useCallback(() => { const handleHideGenericModal = useCallback(() => {
setShowGenericModal(false) setShowGenericModal(false)
}, []) }, [])
@ -40,12 +60,33 @@ export const ModalsContextProvider: FC = ({ children }) => {
[] []
) )
const handleHideOutOfSyncModal = useCallback(() => {
setShouldShowOutOfSyncModal(false)
}, [])
const showOutOfSyncModal = useCallback((editorContent: string) => {
setOutOfSyncModalData({ editorContent })
setShouldShowOutOfSyncModal(true)
}, [])
const showLockEditorMessageModal = useCallback((delay: number) => {
setLockEditorModalData({ delay })
setShouldShowLockEditorModal(true)
}, [])
const value = useMemo<ModalsContextValue>( const value = useMemo<ModalsContextValue>(
() => ({ () => ({
showGenericMessageModal, showGenericMessageModal,
genericModalVisible: showGenericModal, genericModalVisible: showGenericModal,
showOutOfSyncModal,
showLockEditorMessageModal,
}), }),
[showGenericMessageModal, showGenericModal] [
showGenericMessageModal,
showGenericModal,
showOutOfSyncModal,
showLockEditorMessageModal,
]
) )
return ( return (
@ -56,6 +97,15 @@ export const ModalsContextProvider: FC = ({ children }) => {
onHide={handleHideGenericModal} onHide={handleHideGenericModal}
{...genericMessageModalData} {...genericMessageModalData}
/> />
<OutOfSyncModal
{...outOfSyncModalData}
show={shouldShowOutOfSyncModal}
onHide={handleHideOutOfSyncModal}
/>
<LockEditorMessageModal
{...lockEditorModalData}
show={shouldShowLockEditorModal}
/>
</ModalsContext.Provider> </ModalsContext.Provider>
) )
} }

View file

@ -740,6 +740,7 @@
"helps_us_tailor_your_experience": "This helps us tailor your Overleaf experience.", "helps_us_tailor_your_experience": "This helps us tailor your Overleaf experience.",
"hide_configuration": "Hide configuration", "hide_configuration": "Hide configuration",
"hide_document_preamble": "Hide document preamble", "hide_document_preamble": "Hide document preamble",
"hide_local_file_contents": "Hide Local File Contents",
"hide_outline": "Hide File outline", "hide_outline": "Hide File outline",
"history": "History", "history": "History",
"history_add_label": "Add label", "history_add_label": "Add label",
@ -1227,6 +1228,8 @@
"other_output_files": "Download other output files", "other_output_files": "Download other output files",
"other_sessions": "Other Sessions", "other_sessions": "Other Sessions",
"our_values": "Our values", "our_values": "Our values",
"out_of_sync": "Out of sync",
"out_of_sync_detail": "Sorry, this file has gone out of sync and we need to do a full refresh.<0 /><1>Please see this help guide for more information</1>",
"output_file": "Output file", "output_file": "Output file",
"over": "over", "over": "over",
"over_x_templates_easy_getting_started": "There are thousands of __templates__ in our template gallery, so its really easy to get started, whether youre writing a journal article, thesis, CV or something else.", "over_x_templates_easy_getting_started": "There are thousands of __templates__ in our template gallery, so its really easy to get started, whether youre writing a journal article, thesis, CV or something else.",
@ -1309,6 +1312,7 @@
"please_select_an_output_file": "Please Select an Output File", "please_select_an_output_file": "Please Select an Output File",
"please_set_a_password": "Please set a password", "please_set_a_password": "Please set a password",
"please_set_main_file": "Please choose the main file for this project in the project menu. ", "please_set_main_file": "Please choose the main file for this project in the project menu. ",
"please_wait": "Please wait",
"plus_additional_collaborators_document_history_track_changes_and_more": "(plus additional collaborators, document history, track changes, and more).", "plus_additional_collaborators_document_history_track_changes_and_more": "(plus additional collaborators, document history, track changes, and more).",
"plus_more": "plus more", "plus_more": "plus more",
"plus_upgraded_accounts_receive": "Plus with an upgraded account you get", "plus_upgraded_accounts_receive": "Plus with an upgraded account you get",
@ -1360,6 +1364,8 @@
"project_owner_plus_10": "Project author + 10", "project_owner_plus_10": "Project author + 10",
"project_ownership_transfer_confirmation_1": "Are you sure you want to make <0>__user__</0> the owner of <1>__project__</1>?", "project_ownership_transfer_confirmation_1": "Are you sure you want to make <0>__user__</0> the owner of <1>__project__</1>?",
"project_ownership_transfer_confirmation_2": "This action cannot be undone. The new owner will be notified and will be able to change project access settings (including removing your own access).", "project_ownership_transfer_confirmation_2": "This action cannot be undone. The new owner will be notified and will be able to change project access settings (including removing your own access).",
"project_renamed_or_deleted": "Project Renamed or Deleted",
"project_renamed_or_deleted_detail": "This project has either been renamed or deleted by an external data source such as Dropbox. We dont want to delete your data on Overleaf, so this project still contains your history and collaborators. If the project has been renamed please look in your project list for a new project under the new name.",
"project_synced_with_git_repo_at": "This project is synced with the GitHub repository at", "project_synced_with_git_repo_at": "This project is synced with the GitHub repository at",
"project_synchronisation": "Project Synchronisation", "project_synchronisation": "Project Synchronisation",
"project_timed_out_enable_stop_on_first_error": "<0>Enable “Stop on first error”</0> to help you find and fix errors right away.", "project_timed_out_enable_stop_on_first_error": "<0>Enable “Stop on first error”</0> to help you find and fix errors right away.",
@ -1625,6 +1631,7 @@
"show_in_code": "Show in code", "show_in_code": "Show in code",
"show_in_pdf": "Show in PDF", "show_in_pdf": "Show in PDF",
"show_less": "show less", "show_less": "show less",
"show_local_file_contents": "Show Local File Contents",
"show_outline": "Show File outline", "show_outline": "Show File outline",
"show_x_more": "Show __x__ more", "show_x_more": "Show __x__ more",
"show_x_more_projects": "Show __x__ more projects", "show_x_more_projects": "Show __x__ more projects",
@ -2027,6 +2034,7 @@
"welcome_to_sl": "Welcome to __appName__!", "welcome_to_sl": "Welcome to __appName__!",
"were_in_the_process_of_reducing_compile_timeout_which_may_affect_this_project": "Were in the process of <0>reducing the compile timeout limit</0> on our free plan, which may affect this project in future.", "were_in_the_process_of_reducing_compile_timeout_which_may_affect_this_project": "Were in the process of <0>reducing the compile timeout limit</0> on our free plan, which may affect this project in future.",
"were_in_the_process_of_reducing_compile_timeout_which_may_affect_your_project": "Were in the process of <0>reducing the compile timeout limit</0> on our free plan, which may affect your project in future.", "were_in_the_process_of_reducing_compile_timeout_which_may_affect_your_project": "Were in the process of <0>reducing the compile timeout limit</0> on our free plan, which may affect your project in future.",
"were_performing_maintenance": "Were performing maintenance on Overleaf and you need to wait a moment. Sorry for any inconvenience. The editor will refresh automatically in __seconds__ seconds.",
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_this_project": "Weve recently <0>reduced the compile timeout limit</0> on our free plan, which may have affected this project.", "weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_this_project": "Weve recently <0>reduced the compile timeout limit</0> on our free plan, which may have affected this project.",
"weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_your_project": "Weve recently <0>reduced the compile timeout limit</0> on our free plan, which may have affected your project.", "weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_your_project": "Weve recently <0>reduced the compile timeout limit</0> on our free plan, which may have affected your project.",
"what_does_this_mean": "What does this mean?", "what_does_this_mean": "What does this mean?",