1+ import { difference } from 'lodash' ;
12import debounce from 'lodash/debounce' ;
23import {
34 AbstractTextComponent ,
45 App ,
56 ItemView ,
7+ Notice ,
68 Platform ,
79 Plugin ,
810 PluginSettingTab ,
@@ -19,6 +21,7 @@ import PluginUpdateManager from './components/PluginUpdateManager';
1921import RibbonIcon from './components/RibbonIcon' ;
2022import UpdateStatusIcon from './components/UpdateStatusIcon' ;
2123import initiatePluginSettings from './domain/initiatePluginSettings' ;
24+ import pluginFilter from './domain/pluginFilter' ;
2225import { DEFAULT_PLUGIN_SETTINGS , PluginSettings } from './domain/pluginSettings' ;
2326import { RESET_ACTION , store } from './state' ;
2427import { cleanupDismissedPluginVersions } from './state/actionProducers/cleanupDismissedPluginVersions' ;
@@ -51,6 +54,7 @@ export default class PluginUpdateCheckerPlugin extends Plugin {
5154 private fileOpenCallback : ( file : TFile | null ) => any ;
5255 private activeLeafChangeCallback : ( leaf : WorkspaceLeaf | null ) => any ;
5356 private releasePollingIntervalTimerId : number | undefined ;
57+ private previousFilteredUpdates : string [ ] = [ ] ; // Store previous updates for comparison
5458
5559 async onload ( ) {
5660 this . registerView (
@@ -156,6 +160,77 @@ export default class PluginUpdateCheckerPlugin extends Plugin {
156160 ) ;
157161 }
158162
163+ showNotificationOnNewUpdate ( ) {
164+ const state = store . getState ( ) ;
165+ const pluginSettings = this . settings ;
166+ const releases = state . releases . releases ;
167+ const installed = state . obsidian . pluginManifests ;
168+ const enabledPlugins = state . obsidian . enabledPlugins ;
169+
170+ const filteredUpdates = pluginFilter (
171+ { } ,
172+ pluginSettings ,
173+ installed ,
174+ enabledPlugins ,
175+ releases
176+ ) ;
177+
178+ // If the setting is not enabled or there are no releases, return
179+ if ( ! this . settings . showNotificationOnNewUpdate || filteredUpdates . length === 0 ) return ;
180+
181+ // Create a list of pluginId-versionNumber to memory.
182+ // We will only show notification when there is new different pluginId-versionNumber in the list with the previous one.
183+ // This is the only way to avoid showing the notification multiple times without saving to disk
184+ // But if this plugin is disabled and re-enabled, the notification will be shown again
185+ const currentUpdatesKeys = filteredUpdates . map (
186+ ( update ) => `${ update . getPluginId ( ) } -${ update . getLatestVersionNumber ( ) } `
187+ ) ;
188+
189+ // If the updates are the same as before, don't show notification
190+ // Lodash difference(new, old) will return the new items that are not in the old list.
191+ // If the length is 0, it means there is no new update.
192+ if ( difference ( currentUpdatesKeys , this . previousFilteredUpdates ) . length === 0 ) {
193+ // We still want to update this list
194+ // There is a case when user has downgraded any plguin from file system and wanted to check update again.
195+ this . previousFilteredUpdates = currentUpdatesKeys ;
196+ return ;
197+ }
198+
199+ // Update the previous updates
200+ this . previousFilteredUpdates = currentUpdatesKeys ;
201+
202+ // Convert the string to a fragment to allow for HTML elements
203+ const stringToFragment = ( string : string ) => {
204+ const wrapper = document . createElement ( 'template' ) ;
205+ wrapper . innerHTML = string ;
206+ return wrapper . content ;
207+ } ;
208+
209+ // Show a notice with a button to view updates for 15 seconds
210+ const trackerButtonID = 'tracker-notification-button' ;
211+ new Notice (
212+ stringToFragment (
213+ `You have ${ filteredUpdates . length } plugin update${
214+ filteredUpdates . length > 1 ? 's' : ''
215+ } available.<br/><a id="${ trackerButtonID } ">View Updates</a>`
216+ ) ,
217+ 15000
218+ ) ;
219+
220+ const buttonEl = document . getElementById ( trackerButtonID ) ;
221+
222+ if ( buttonEl ) {
223+ // Bind the method to preserve the this context when running from top level Redux store middleware
224+ buttonEl . addEventListener ( 'click' , this . showPluginUpdateManagerView . bind ( this ) ) ;
225+
226+ // Clean up the button and listener after 15 seconds
227+ setTimeout ( ( ) => {
228+ buttonEl . removeEventListener ( 'click' , this . showPluginUpdateManagerView . bind ( this ) ) ;
229+ buttonEl . remove ( ) ;
230+ } , 15000 ) ;
231+ }
232+ }
233+
159234 updateRibonIconVisibilty ( ) {
160235 const isShownOnPlatform = Platform . isMobile || SHOW_RIBBON_ICON_ALL_PLATFORMS ;
161236
@@ -375,6 +450,26 @@ class PluginUpdateCheckerSettingsTab extends PluginSettingTab {
375450 )
376451 . setDynamicTooltip ( )
377452 ) ;
453+ new Setting ( containerEl )
454+ . setName ( 'Show Notification on New Update' )
455+ . setDesc (
456+ 'Show a notification when a new update is available. Useful on mobile since the plugin update icon is less visible.'
457+ )
458+ . addToggle ( ( toggle ) =>
459+ toggle
460+ . setValue ( this . plugin . settings . showNotificationOnNewUpdate )
461+ . onChange ( async ( showNotificationOnNewUpdate ) => {
462+ await this . plugin . saveSettings ( {
463+ ...this . plugin . settings ,
464+ showNotificationOnNewUpdate,
465+ } ) ;
466+
467+ if ( showNotificationOnNewUpdate ) {
468+ // Show notification immediately
469+ this . plugin . showNotificationOnNewUpdate ( ) ;
470+ }
471+ } )
472+ ) ;
378473 new Setting ( containerEl )
379474 . setName ( 'Show on Mobile' )
380475 . setDesc (
0 commit comments