diff --git a/services/web/app/src/Features/Project/ProjectHelper.js b/services/web/app/src/Features/Project/ProjectHelper.js
index cdb03bca3a..3ba5716a31 100644
--- a/services/web/app/src/Features/Project/ProjectHelper.js
+++ b/services/web/app/src/Features/Project/ProjectHelper.js
@@ -167,6 +167,7 @@ function getAllowedImagesForUser(user) {
return {
...image,
allowed: _imageAllowed(user, image),
+ rolling: image.monthlyExperimental,
}
})
diff --git a/services/web/app/src/Features/Tutorial/TutorialController.mjs b/services/web/app/src/Features/Tutorial/TutorialController.mjs
index 543bf00bf0..df9183e094 100644
--- a/services/web/app/src/Features/Tutorial/TutorialController.mjs
+++ b/services/web/app/src/Features/Tutorial/TutorialController.mjs
@@ -21,6 +21,7 @@ const VALID_KEYS = [
'ide-redesign-new-survey-promo',
'ide-redesign-beta-intro',
'ide-redesign-labs-user-beta-promo',
+ 'rolling-compile-image-changed',
]
async function completeTutorial(req, res, next) {
diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js
index 5b8d4a1e04..8d0913f752 100644
--- a/services/web/config/settings.defaults.js
+++ b/services/web/config/settings.defaults.js
@@ -1006,6 +1006,7 @@ module.exports = {
v1ImportDataScreen: [],
snapshotUtils: [],
usGovBanner: [],
+ rollingBuildsUpdatedAlert: [],
offlineModeToolbarButtons: [],
settingsEntries: [],
autoCompleteExtensions: [],
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index a7d66badaf..7d0ccd15b3 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -21,6 +21,7 @@
"a_new_reference_was_added_from_provider": "",
"a_new_reference_was_added_to_file": "",
"a_new_reference_was_added_to_file_from_provider": "",
+ "a_new_version_of_the_rolling_texlive_build_released": "",
"about_to_archive_projects": "",
"about_to_delete_cert": "",
"about_to_delete_projects": "",
@@ -1616,6 +1617,7 @@
"showing_x_results_of_total": "",
"sign_up": "",
"simple_search_mode": "",
+ "since_this_project_is_set_to_the_rolling_build": "",
"single_sign_on_sso": "",
"size": "",
"something_not_right": "",
diff --git a/services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx b/services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx
index 13db236193..7e37a6f770 100644
--- a/services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx
+++ b/services/web/frontend/js/features/ide-react/components/alerts/alerts.tsx
@@ -2,9 +2,16 @@ import { useTranslation } from 'react-i18next'
import { LostConnectionAlert } from './lost-connection-alert'
import { useConnectionContext } from '@/features/ide-react/context/connection-context'
import { debugging } from '@/utils/debugging'
+import { ElementType } from 'react'
import { createPortal } from 'react-dom'
import { useGlobalAlertsContainer } from '@/features/ide-react/context/global-alerts-context'
import OLNotification from '@/shared/components/ol/ol-notification'
+import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
+
+const rollingBuildsUpdatedAlert: Array<{
+ import: { default: ElementType }
+ path: string
+}> = importOverleafModules('rollingBuildsUpdatedAlert')
export function Alerts() {
const { t } = useTranslation()
@@ -23,6 +30,12 @@ export function Alerts() {
return createPortal(
<>
+ {rollingBuildsUpdatedAlert.map(
+ ({ import: { default: Component }, path }) => (
+
+ )
+ )}
+
{connectionState.forceDisconnected &&
// hide "disconnected" banner when displaying out of sync modal
connectionState.error !== 'out-of-sync' ? (
diff --git a/services/web/frontend/js/features/monthly-texlive/labs-widget.tsx b/services/web/frontend/js/features/monthly-texlive/labs-widget.tsx
index f0c99547be..fd0ee6d9b1 100644
--- a/services/web/frontend/js/features/monthly-texlive/labs-widget.tsx
+++ b/services/web/frontend/js/features/monthly-texlive/labs-widget.tsx
@@ -1,9 +1,13 @@
-import { useState } from 'react'
+import { useCallback, useState } from 'react'
import LabsExperimentWidget from '../../shared/components/labs/labs-experiments-widget'
import { isInExperiment } from '@/utils/labs-utils'
import { useTranslation } from 'react-i18next'
import MaterialIcon from '@/shared/components/material-icon'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
+import { postJSON } from '@/infrastructure/fetch-json'
+import { debugConsole } from '@/utils/debugging'
+
+export const TUTORIAL_KEY = 'rolling-compile-image-changed'
const MonthlyTexliveLabsWidget = ({
labsProgram,
@@ -15,6 +19,18 @@ const MonthlyTexliveLabsWidget = ({
const { t } = useTranslation()
const [optedIn, setOptedIn] = useState(isInExperiment('monthly-texlive'))
+ const optInWithCompletedTutorial = useCallback(
+ async (shouldOptIn: boolean) => {
+ try {
+ await postJSON(`/tutorial/${TUTORIAL_KEY}/complete`)
+ } catch (err) {
+ debugConsole.error(err)
+ }
+ setOptedIn(shouldOptIn)
+ },
+ [setOptedIn]
+ )
+
const monthlyTexLiveSplitTestEnabled = isSplitTestEnabled('monthly-texlive')
if (!monthlyTexLiveSplitTestEnabled) {
return null
@@ -35,7 +51,7 @@ const MonthlyTexliveLabsWidget = ({
labsEnabled={labsProgram}
setErrorMessage={setErrorMessage}
optedIn={optedIn}
- setOptedIn={setOptedIn}
+ setOptedIn={optInWithCompletedTutorial}
title={t('rolling_texlive_build')}
/>
)
diff --git a/services/web/frontend/js/features/monthly-texlive/rolling-compile-image-changed-alert.tsx b/services/web/frontend/js/features/monthly-texlive/rolling-compile-image-changed-alert.tsx
new file mode 100644
index 0000000000..b5ae6454ba
--- /dev/null
+++ b/services/web/frontend/js/features/monthly-texlive/rolling-compile-image-changed-alert.tsx
@@ -0,0 +1,44 @@
+import { useTutorial } from '@/shared/hooks/promotions/use-tutorial'
+
+import { useEditorContext } from '@/shared/context/editor-context'
+import { useProjectSettingsContext } from '../editor-left-menu/context/project-settings-context'
+
+import OLNotification from '@/shared/components/ol/ol-notification'
+import getMeta from '@/utils/meta'
+import { useTranslation } from 'react-i18next'
+import { useCallback } from 'react'
+
+export const TUTORIAL_KEY = 'rolling-compile-image-changed'
+const rollingImages = getMeta('ol-imageNames')
+ .filter(img => img.rolling)
+ .map(img => img.imageName)
+
+const RollingCompileImageChangedAlert = () => {
+ const { completeTutorial } = useTutorial(TUTORIAL_KEY)
+
+ const { inactiveTutorials } = useEditorContext()
+ const { imageName } = useProjectSettingsContext()
+ const { t } = useTranslation()
+
+ const onClose = useCallback(() => {
+ completeTutorial({ event: 'promo-click', action: 'complete' })
+ }, [completeTutorial])
+
+ const onRollingBuild = imageName && rollingImages.includes(imageName)
+ if (inactiveTutorials.includes(TUTORIAL_KEY) || !onRollingBuild) {
+ return null
+ }
+
+ return (
+
+ )
+}
+
+export default RollingCompileImageChangedAlert
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 0662befbec..2f8275684d 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -22,6 +22,7 @@
"a_new_reference_was_added_from_provider": "A new reference was added from __provider__",
"a_new_reference_was_added_to_file": "A new reference was added to <0>__filePath__0>",
"a_new_reference_was_added_to_file_from_provider": "A new reference was added to <0>__filePath__0> from __provider__",
+ "a_new_version_of_the_rolling_texlive_build_released": "A new version of the Rolling TeX Live build has been released.",
"about": "About",
"about_to_archive_projects": "You are about to archive the following projects:",
"about_to_delete_cert": "You are about to delete the following certificate:",
@@ -1936,7 +1937,6 @@
"right": "Right",
"ro": "Romanian",
"role": "Role",
- "rolling_texlive_build": "Rolling TexLive Build",
"ru": "Russian",
"saml": "SAML",
"saml_auth_error": "Sorry, your identity provider responded with an error. Please contact your administrator for more information.",
@@ -2087,6 +2087,7 @@
"sign_up_for_free": "Sign up for free",
"sign_up_for_free_account": "Sign up for a free account and receive regular updates",
"simple_search_mode": "Simple search",
+ "since_this_project_is_set_to_the_rolling_build": "Since this project is set to use the rolling build, new compiles will automatically use the newest version.",
"single_sign_on_sso": "Single Sign-On (SSO)",
"site_description": "An online LaTeX editor that’s easy to use. No installation, real-time collaboration, version control, hundreds of LaTeX templates, and more.",
"site_wide_option_available": "Site-wide option available",
@@ -2291,7 +2292,6 @@
"test": "Test",
"test_configuration": "Test configuration",
"test_configuration_successful": "Test configuration successful",
- "test_more_recent_versions_of_texlive": "Test more recent versions of TexLive before they get turned into our annual release. This experiment adds the compiler option for a monthly build of the current TexLive version. This experimental version is primarily for testing changes to TexLive, packages, and previewing new accessibility features before they’re compiled into our annual TexLive release. Note that these images change month to month, and are untested by our team. Important projects should continue to be built on our yearly release to avoid issues that may occur when the monthly build changes.",
"tex_live_version": "TeX Live version",
"texgpt": "TeXGPT",
"text": "Text",
diff --git a/services/web/scripts/reset_rolling_build_update_notification.mjs b/services/web/scripts/reset_rolling_build_update_notification.mjs
new file mode 100644
index 0000000000..89044949fa
--- /dev/null
+++ b/services/web/scripts/reset_rolling_build_update_notification.mjs
@@ -0,0 +1,40 @@
+import { scriptRunner } from './lib/ScriptRunner.mjs'
+import { db } from '../app/src/infrastructure/mongodb.js'
+import minimist from 'minimist'
+const argv = minimist(process.argv.slice(2))
+
+async function resetTutorials() {
+ const commit = argv.commit !== undefined
+
+ const users = await db.users
+ .find(
+ {
+ 'completedTutorials.rolling-compile-image-changed.state': 'completed',
+ },
+ { readPreference: 'secondaryPreferred' }
+ )
+ .toArray()
+
+ if (!commit) {
+ console.log(
+ `would have removed rolling-compile-image-changed tutorial for ${users.length} users`
+ )
+ return
+ }
+
+ await db.users.updateMany(
+ { _id: { $in: users.map(user => user._id) } },
+ {
+ $unset: { 'completedTutorials.rolling-compile-image-changed': '' },
+ }
+ )
+ console.log(`updated ${users.length} users`)
+}
+
+try {
+ await scriptRunner(resetTutorials)
+ process.exit(0)
+} catch (error) {
+ console.error(error)
+ process.exit(1)
+}
diff --git a/services/web/types/project-settings.ts b/services/web/types/project-settings.ts
index f3decf62fa..bf1230ee2a 100644
--- a/services/web/types/project-settings.ts
+++ b/services/web/types/project-settings.ts
@@ -5,6 +5,7 @@ export type ImageName = {
imageDesc: string
imageName: string
allowed: boolean
+ rolling?: boolean
}
export type DocId = Brand