Skip to content

Commit 1ffea31

Browse files
ckt1031swar8080
andauthored
Add notification for new plugin updates (#147)
* Add notification for new plugin updates and related settings * Integrate plugin filtering for update notifications * Remove console * Update description of mobile notification section in settings tab Co-authored-by: Steven Swartz <stevenswartz@live.ca> * Implement update notification suppression to avoid duplicate alerts for the same plugin updates. Store previous updates for comparison and enhance settings description for clarity. * Fix duplicated notifications after updating a plugin before next period of auto check * Show update notification immediately when user first enable after starting up the tracker plguin and when there are updates available * Refactor update notification logic to use Lodash's difference function --------- Co-authored-by: Steven Swartz <stevenswartz@live.ca>
1 parent 793794f commit 1ffea31

File tree

6 files changed

+127
-3
lines changed

6 files changed

+127
-3
lines changed

src/domain/initiatePluginSettings.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe('initiatePluginSettings', () => {
2525
},
2626
},
2727
showIconOnMobile: !DEFAULT_PLUGIN_SETTINGS.showIconOnMobile,
28+
showNotificationOnNewUpdate: !DEFAULT_PLUGIN_SETTINGS.showNotificationOnNewUpdate,
2829
excludeBetaVersions: !DEFAULT_PLUGIN_SETTINGS.excludeBetaVersions,
2930
excludeDisabledPlugins: !DEFAULT_PLUGIN_SETTINGS.excludeDisabledPlugins,
3031
minUpdateCountToShowIcon: DEFAULT_PLUGIN_SETTINGS.minUpdateCountToShowIcon + 1,

src/domain/pluginSettings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type PluginSettings = {
44
excludeBetaVersions: boolean;
55
excludeDisabledPlugins: boolean;
66
showIconOnMobile: boolean;
7+
showNotificationOnNewUpdate: boolean;
78
// Deprecated for minUpdateCountToShowIcon
89
hideIconIfNoUpdatesAvailable?: boolean;
910
minUpdateCountToShowIcon: number;
@@ -26,6 +27,7 @@ export const DEFAULT_PLUGIN_SETTINGS: PluginSettings = {
2627
daysToSuppressNewUpdates: 0,
2728
dismissedVersionsByPluginId: {},
2829
showIconOnMobile: true,
30+
showNotificationOnNewUpdate: false,
2931
excludeBetaVersions: true,
3032
excludeDisabledPlugins: false,
3133
minUpdateCountToShowIcon: 0,

src/main.tsx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { difference } from 'lodash';
12
import debounce from 'lodash/debounce';
23
import {
34
AbstractTextComponent,
45
App,
56
ItemView,
7+
Notice,
68
Platform,
79
Plugin,
810
PluginSettingTab,
@@ -19,6 +21,7 @@ import PluginUpdateManager from './components/PluginUpdateManager';
1921
import RibbonIcon from './components/RibbonIcon';
2022
import UpdateStatusIcon from './components/UpdateStatusIcon';
2123
import initiatePluginSettings from './domain/initiatePluginSettings';
24+
import pluginFilter from './domain/pluginFilter';
2225
import { DEFAULT_PLUGIN_SETTINGS, PluginSettings } from './domain/pluginSettings';
2326
import { RESET_ACTION, store } from './state';
2427
import { 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(

src/state/actionProducers/dismissPluginVersions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { State } from '..';
66
import {
77
DismissedPluginVersion,
88
PluginDismissedVersions,
9-
PluginSettings,
9+
PluginSettings
1010
} from '../../domain/pluginSettings';
1111
import { groupById } from '../../domain/util/groupById';
1212
import { ObsidianState } from '../obsidianReducer';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import PluginUpdateCheckerPlugin from 'src/main';
2+
import { store } from '..';
3+
import { ObsidianApp } from '../obsidianReducer';
4+
import { fetchReleases } from './fetchReleases';
5+
6+
export const showUpdateNotificationMiddleware = () => (next: any) => (action: any) => {
7+
const result = next(action);
8+
9+
if (action.type === fetchReleases.fulfilled.type) {
10+
const state = store.getState();
11+
const thisPluginId = state.obsidian.thisPluginId;
12+
13+
const app = window.app as ObsidianApp;
14+
const plugin = app.plugins?.plugins?.[thisPluginId] as PluginUpdateCheckerPlugin;
15+
16+
if (plugin) {
17+
// Bind the method to preserve the this context
18+
plugin.showNotificationOnNewUpdate.bind(plugin)();
19+
}
20+
}
21+
22+
return result;
23+
};

src/state/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { AnyAction, combineReducers, configureStore, ThunkAction } from '@reduxjs/toolkit';
22
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
33
import logger from 'redux-logger';
4+
import { showUpdateNotificationMiddleware } from './actionProducers/showUpdateNotification';
45
import ObsidianReducer, { ObsidianState } from './obsidianReducer';
56
import ReleaseReducer, { ReleaseState } from './releasesReducer';
67

@@ -19,10 +20,12 @@ export const store = configureStore({
1920
return reducers(state, action);
2021
},
2122
middleware: (getDefaultMiddleware) => {
23+
const middleware = getDefaultMiddleware();
2224
if (process.env.OBSIDIAN_APP_ENABLE_REDUX_LOGGER === 'true') {
23-
return getDefaultMiddleware().concat(logger);
25+
middleware.push(logger);
2426
}
25-
return getDefaultMiddleware();
27+
middleware.push(showUpdateNotificationMiddleware);
28+
return middleware;
2629
},
2730
});
2831

0 commit comments

Comments
 (0)