@@ -19,7 +19,7 @@ import { Commands } from './commands';
1919// import { competitions } from './pages/competitions';
2020import { notebookPlugin } from './pages/notebook' ;
2121// import { helpPlugin } from './pages/help';
22- import { generateDefaultNotebookName } from './notebook-name ' ;
22+ import { generateDefaultNotebookName , isNotebookEmpty } from './notebook-utils ' ;
2323import {
2424 IViewOnlyNotebookTracker ,
2525 viewOnlyNotebookFactoryPlugin ,
@@ -87,14 +87,22 @@ async function showShareDialog(sharingService: SharingService, notebookContent:
8787async function handleNotebookSharing (
8888 notebookPanel : NotebookPanel | ViewOnlyNotebookPanel ,
8989 sharingService : SharingService ,
90- manual : boolean
90+ manual : boolean ,
91+ onManualSave : ( ) => void
9192) {
9293 const notebookContent = notebookPanel . context . model . toJSON ( ) as INotebookContent ;
9394
9495 const isViewOnly = notebookContent . metadata ?. isSharedNotebook === true ;
9596 const sharedId = notebookContent . metadata ?. sharedId as string | undefined ;
9697 const defaultName = generateDefaultNotebookName ( ) ;
9798
99+ // Mark that the user has performed at least one manual save in this session.
100+ // We do this early in the manual flow for clarity; the local save already happened
101+ // in the command handlers and this flag only affects reminder wording.
102+ if ( manual && ! isViewOnly ) {
103+ onManualSave ( ) ;
104+ }
105+
98106 try {
99107 if ( isViewOnly ) {
100108 // Skip CKHub sync for view-only notebooks
@@ -137,22 +145,6 @@ async function handleNotebookSharing(
137145 }
138146}
139147
140- /**
141- * Helper to start the save reminder timer. Clears any existing timer
142- * and sets a new one to show the notification after 5 minutes.
143- */
144- function startSaveReminder ( currentTimeout : number | null ) : number {
145- if ( currentTimeout ) {
146- window . clearTimeout ( currentTimeout ) ;
147- }
148- return window . setTimeout ( ( ) => {
149- Notification . info (
150- "It's been 5 minutes since you've been working on this notebook. Make sure to save the link to your notebook to edit your work later." ,
151- { autoClose : 8000 }
152- ) ;
153- } , 300 * 1000 ) ; // once after 5 minutes
154- }
155-
156148/**
157149 * JUPYTEREVERYWHERE EXTENSION
158150 */
@@ -185,7 +177,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
185177 // Skip auto-sync if it's a manual share.
186178 return ;
187179 }
188- await handleNotebookSharing ( widget , sharingService , false ) ;
180+ await handleNotebookSharing ( widget , sharingService , false , ( ) => { } ) ;
189181 }
190182 } ) ;
191183 } ) ;
@@ -296,9 +288,21 @@ const plugin: JupyterFrontEndPlugin<void> = {
296288 }
297289 } ) ;
298290
291+ // Track user time, and show a reminder to save the notebook once after
292+ // five minutes of editing (i.e., once it becomes non-empty and dirty)
293+ // using a toast notification.
294+ let saveReminderTimeout : number | null = null ;
295+ let isSaveReminderScheduled = false ; // a 5-minute timer is scheduled, but it hasn't fired yet
296+ let hasShownSaveReminder = false ; // we've already shown the toast once for this notebook
297+ let hasManuallySaved = false ; // whether the user has manually saved at least once in this session
298+
299299 /**
300300 * Add custom Share notebook command
301301 */
302+ const markManualSave = ( ) => {
303+ hasManuallySaved = true ;
304+ } ;
305+
302306 commands . addCommand ( Commands . shareNotebookCommand , {
303307 label : 'Share Notebook' ,
304308 execute : async ( ) => {
@@ -318,7 +322,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
318322 // Save the notebook before we share it.
319323 await notebookPanel . context . save ( ) ;
320324
321- await handleNotebookSharing ( notebookPanel , sharingService , true ) ;
325+ await handleNotebookSharing ( notebookPanel , sharingService , true , markManualSave ) ;
322326 } catch ( error ) {
323327 console . error ( 'Error in share command:' , error ) ;
324328 }
@@ -344,7 +348,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
344348 }
345349 manuallySharing . add ( panel ) ;
346350 await panel . context . save ( ) ;
347- await handleNotebookSharing ( panel , sharingService , true ) ;
351+ await handleNotebookSharing ( panel , sharingService , true , markManualSave ) ;
348352 }
349353 } ) ;
350354
@@ -476,25 +480,100 @@ const plugin: JupyterFrontEndPlugin<void> = {
476480 }
477481 }
478482 } ) ;
479- // Track user time, and show a reminder to save the notebook after
480- // five minutes using a toast notification.
481- // Then reset the timer when the notebook is saved manually.
482- let saveReminderTimeout : number | null = null ;
483+
484+ /**
485+ * Helper to start the save reminder timer. Clears any existing timer
486+ * and sets a new one to show the notification after 5 minutes.
487+ */
488+ function startSaveReminder ( currentTimeout : number | null , onFire : ( ) => void ) : number {
489+ if ( currentTimeout ) {
490+ window . clearTimeout ( currentTimeout ) ;
491+ }
492+ return window . setTimeout ( ( ) => {
493+ const message = hasManuallySaved
494+ ? "It's been 5 minutes since you last saved this notebook. Make sure to save the link to your notebook to edit your work later."
495+ : "It's been 5 minutes since you've been working on this notebook. Make sure to save the link to your notebook to edit your work later." ;
496+
497+ Notification . info ( message , { autoClose : 8000 } ) ;
498+ onFire ( ) ;
499+ } , 300 * 1000 ) ; // once after 5 minutes
500+ }
483501
484502 tracker . widgetAdded . connect ( ( _ , panel ) => {
485503 if ( saveReminderTimeout ) {
486504 window . clearTimeout ( saveReminderTimeout ) ;
505+ saveReminderTimeout = null ;
487506 }
507+ isSaveReminderScheduled = false ;
508+ hasShownSaveReminder = false ;
509+
510+ const maybeScheduleSaveReminder = ( ) => {
511+ if ( hasShownSaveReminder ) {
512+ return ;
513+ }
514+
515+ const content = panel . context . model . toJSON ( ) as INotebookContent ;
516+ // Skip for view-only notebooks
517+ if ( panel . context . model . readOnly || content . metadata ?. isSharedNotebook === true ) {
518+ return ;
519+ }
520+ // Schedule after the notebook becomes non-empty
521+ if ( isNotebookEmpty ( content ) ) {
522+ return ;
523+ }
524+ if ( isSaveReminderScheduled ) {
525+ return ;
526+ }
527+
528+ isSaveReminderScheduled = true ;
529+ saveReminderTimeout = startSaveReminder ( saveReminderTimeout , ( ) => {
530+ hasShownSaveReminder = true ;
531+ isSaveReminderScheduled = false ;
532+ } ) ;
533+ } ;
488534
489- panel . context . ready . then ( ( ) => {
490- saveReminderTimeout = startSaveReminder ( saveReminderTimeout ) ;
535+ // After the model is ready, check immediately and on any content change.
536+ void panel . context . ready . then ( ( ) => {
537+ // We cover the case where the notebook loads already non-empty, say,
538+ // if the user uploads a notebook into the application.
539+ maybeScheduleSaveReminder ( ) ;
540+ panel . context . model . contentChanged . connect ( ( ) => {
541+ maybeScheduleSaveReminder ( ) ; // schedule when first content appears
542+ } ) ;
491543
544+ // Reset the reminder timer whenever the user saves manually.
545+ // We clear any pending timer and wait for the next edit (dirty state)
546+ // to schedule a fresh 5-minute reminder.
492547 panel . context . saveState . connect ( ( _ , state ) => {
493548 if ( state === 'completed' ) {
494- saveReminderTimeout = startSaveReminder ( saveReminderTimeout ) ;
549+ if ( saveReminderTimeout ) {
550+ window . clearTimeout ( saveReminderTimeout ) ;
551+ saveReminderTimeout = null ;
552+ }
553+ isSaveReminderScheduled = false ;
554+ hasShownSaveReminder = false ;
555+ // Note: we do not reschedule here; it will be scheduled on the next content change
556+ // once the notebook becomes dirty again.
495557 }
496558 } ) ;
497559 } ) ;
560+
561+ // If a view-only notebook is opened or becomes active, ensure no reminder can fire.
562+ readonlyTracker . widgetAdded . connect ( ( ) => {
563+ if ( saveReminderTimeout ) {
564+ window . clearTimeout ( saveReminderTimeout ) ;
565+ saveReminderTimeout = null ;
566+ }
567+ isSaveReminderScheduled = false ;
568+ hasShownSaveReminder = false ;
569+ } ) ;
570+
571+ panel . disposed . connect ( ( ) => {
572+ if ( saveReminderTimeout ) {
573+ window . clearTimeout ( saveReminderTimeout ) ;
574+ saveReminderTimeout = null ;
575+ }
576+ } ) ;
498577 } ) ;
499578 }
500579} ;
0 commit comments