Add release notes viewer

- New dedicated view displays release notes when plugin updates
- Automatically opens after version updates
- Available via command palette and settings
- Uses native Obsidian markdown rendering
- Auto-generation system loads correct version automatically
- Fully internationalized
This commit is contained in:
callumalpass 2025-10-05 13:41:49 +11:00
parent 88ae6e0ce9
commit 4f28dfa98a
14 changed files with 249 additions and 3 deletions

3
.gitignore vendored
View file

@ -16,6 +16,9 @@ main.js
# It's built from source files in styles/ directory during CI/build process.
styles.css
# Don't include the auto-generated release notes import file
src/releaseNotes.ts
# Exclude sourcemaps
*.map

View file

@ -25,6 +25,17 @@ Example:
-->
## Added
- Added release notes viewer
- New view displays release notes in a dedicated tab when plugin updates
- Automatically opens after updating to a new version
- Access anytime via command palette "TaskNotes: View release notes" or Settings > General
- Release notes are rendered using native Obsidian markdown rendering
- Includes link to view all releases on GitHub
- Fully internationalized with i18n support
- Auto-generation system ensures correct version is loaded without manual updates
## Fixed
- (#814) Fixed markdown links in projects field not being recognized on Project notes

View file

@ -1,6 +1,7 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import { readFileSync } from "fs";
const banner =
`/*
@ -11,6 +12,20 @@ if you want to view the source, please visit the github repository of this plugi
const prod = (process.argv[2] === "production");
// Plugin to import markdown files as strings
const markdownPlugin = {
name: 'markdown',
setup(build) {
build.onLoad({ filter: /\.md$/ }, async (args) => {
const text = readFileSync(args.path, 'utf8');
return {
contents: `export default ${JSON.stringify(text)}`,
loader: 'js',
};
});
}
};
const context = await esbuild.context({
banner: {
js: banner,
@ -39,6 +54,7 @@ const context = await esbuild.context({
treeShaking: true,
outfile: "main.js",
minify: prod,
plugins: [markdownPlugin],
});
if (prod) {

View file

@ -0,0 +1,20 @@
import { readFileSync, writeFileSync } from "fs";
// Read current version from manifest.json
const manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const version = manifest.version;
// Generate the TypeScript file that imports the current version's release notes
const content = `// Auto-generated file - do not edit manually
// This file is regenerated during the build process to import the current version's release notes
import releaseNotes from "../docs/releases/${version}.md";
export const CURRENT_VERSION = "${version}";
export const CURRENT_RELEASE_NOTES = releaseNotes;
`;
// Write to src/releaseNotes.ts
writeFileSync("src/releaseNotes.ts", content);
console.log(`✓ Generated release notes import for version ${version}`);

View file

@ -127,6 +127,9 @@
"views.stats.sections.projectBreakdown": "c856b0f89a0d1f51f02f82726206cb07cd1ae95e",
"views.stats.sections.dateRange": "6bb4b674b3232e32feb264e11f19ee1dbed354a3",
"views.stats.filters.minTime": "77f4b7a081b20f78557dd404e14d91b89b52a27c",
"views.releaseNotes.title": "5939771f391859041bc57e5e05b04235b1ef8ee7",
"views.releaseNotes.header": "5939771f391859041bc57e5e05b04235b1ef8ee7",
"views.releaseNotes.viewAllLink": "b37e5d2fb2faa630817557713df6363f4bd0f5cc",
"settings.tabs.general": "9239ee2cda84eca4c3440e2a7b50148af67da3d4",
"settings.tabs.taskProperties": "770bd2ee4e0a455df6d1332def9c6c58f44994bc",
"settings.tabs.defaults": "809ab386aa1c870ee286190b1af3eace7f0bfe30",
@ -845,6 +848,7 @@
"commands.pauseResumePomodoro": "1ac6f3fd60f918b25cfb95be70b9d43f3b3281bb",
"commands.refreshCache": "0b56ae2b187bdf931d24258272f284d7046ad543",
"commands.exportAllTasksIcs": "3f3714587b4802efeb6baf380a59615b8faf0c10",
"commands.viewReleaseNotes": "04c2072af97cf70c22fbf9efa9b62eb2574d23d6",
"modals.task.titlePlaceholder": "c8196f1d1039c2fb290add52ee990c571173d6f1",
"modals.task.titleLabel": "768e0c1c69573fb588f61f1308a015c11468e05f",
"modals.task.titleDetailedPlaceholder": "da3700738242c039de32d3f5e3a0ba10bc28ba0b",

View file

@ -512,6 +512,9 @@
"source": "77f4b7a081b20f78557dd404e14d91b89b52a27c",
"translation": "a6bce6cf45ab6987a0491268052e0770559f80a4"
},
"views.releaseNotes.title": null,
"views.releaseNotes.header": null,
"views.releaseNotes.viewAllLink": null,
"settings.tabs.general": {
"source": "9239ee2cda84eca4c3440e2a7b50148af67da3d4",
"translation": "21a7b4ecae6d99d7c9ae0fe00f0cc0a33f1e2b6f"
@ -3372,6 +3375,7 @@
"source": "3f3714587b4802efeb6baf380a59615b8faf0c10",
"translation": "2b3a3551e913a0618f09a1378c63fa6b832fe6cf"
},
"commands.viewReleaseNotes": null,
"modals.task.titlePlaceholder": {
"source": "c8196f1d1039c2fb290add52ee990c571173d6f1",
"translation": "03f97eff6f24910100617f3fcbc4665e2e65cd7e"
@ -6042,6 +6046,9 @@
"source": "77f4b7a081b20f78557dd404e14d91b89b52a27c",
"translation": "01c1479a894b4e8a3b6e679bd3630c1389c7cd60"
},
"views.releaseNotes.title": null,
"views.releaseNotes.header": null,
"views.releaseNotes.viewAllLink": null,
"settings.tabs.general": {
"source": "9239ee2cda84eca4c3440e2a7b50148af67da3d4",
"translation": "9239ee2cda84eca4c3440e2a7b50148af67da3d4"
@ -8902,6 +8909,7 @@
"source": "3f3714587b4802efeb6baf380a59615b8faf0c10",
"translation": "8f93640a115b91031d8f53e89543cc0bf04e300b"
},
"commands.viewReleaseNotes": null,
"modals.task.titlePlaceholder": {
"source": "c8196f1d1039c2fb290add52ee990c571173d6f1",
"translation": "dee0035d1027b937237c8bbdd6e77288df356ff4"
@ -11572,6 +11580,9 @@
"source": "77f4b7a081b20f78557dd404e14d91b89b52a27c",
"translation": "b6a500e983b64d611aab3a9d5b2ec65fd4511f10"
},
"views.releaseNotes.title": null,
"views.releaseNotes.header": null,
"views.releaseNotes.viewAllLink": null,
"settings.tabs.general": {
"source": "9239ee2cda84eca4c3440e2a7b50148af67da3d4",
"translation": "5376df989fac86247d5cd9f983d11ece28e75fcd"
@ -14432,6 +14443,7 @@
"source": "3f3714587b4802efeb6baf380a59615b8faf0c10",
"translation": "e4c5963ce721f4bb182d474d1a9c0c604325c274"
},
"commands.viewReleaseNotes": null,
"modals.task.titlePlaceholder": {
"source": "c8196f1d1039c2fb290add52ee990c571173d6f1",
"translation": "4ad6efca173a22d4bf622d5729c3ad13612e6902"
@ -17036,6 +17048,9 @@
"source": "77f4b7a081b20f78557dd404e14d91b89b52a27c",
"translation": "7deced4ee8b6e75d5dcafff58c7bd89905c98fd6"
},
"views.releaseNotes.title": null,
"views.releaseNotes.header": null,
"views.releaseNotes.viewAllLink": null,
"settings.tabs.general": {
"source": "9239ee2cda84eca4c3440e2a7b50148af67da3d4",
"translation": "f0aaccbc0dcda37daa045cb4cda85177bedafdb0"
@ -19890,6 +19905,7 @@
"source": "3f3714587b4802efeb6baf380a59615b8faf0c10",
"translation": "4f7b9505464a8b039fee24745afe2a1c951b81d3"
},
"commands.viewReleaseNotes": null,
"modals.task.titlePlaceholder": {
"source": "c8196f1d1039c2fb290add52ee990c571173d6f1",
"translation": "0e65840f941f2c5e030cf553e4e5a3cc315fe73d"
@ -22419,6 +22435,9 @@
"source": "77f4b7a081b20f78557dd404e14d91b89b52a27c",
"translation": "a82231aecd1ee2ec08d32031cf19b6f6a4f2842e"
},
"views.releaseNotes.title": null,
"views.releaseNotes.header": null,
"views.releaseNotes.viewAllLink": null,
"settings.tabs.general": {
"source": "9239ee2cda84eca4c3440e2a7b50148af67da3d4",
"translation": "fdd17d17b54069e4f68253b0af0b1a50d389b199"
@ -25246,6 +25265,7 @@
"source": "3f3714587b4802efeb6baf380a59615b8faf0c10",
"translation": "1ce699b6bccf33a45ff58320f4dfeb2593c3111e"
},
"commands.viewReleaseNotes": null,
"modals.task.titlePlaceholder": {
"source": "c8196f1d1039c2fb290add52ee990c571173d6f1",
"translation": "fb99b9f5be8916e4a7b038e3d1d9d9087f8a5653"
@ -27850,6 +27870,9 @@
"source": "77f4b7a081b20f78557dd404e14d91b89b52a27c",
"translation": "227f72b88ebec3e37158e855352776b82662a27c"
},
"views.releaseNotes.title": null,
"views.releaseNotes.header": null,
"views.releaseNotes.viewAllLink": null,
"settings.tabs.general": {
"source": "9239ee2cda84eca4c3440e2a7b50148af67da3d4",
"translation": "f1484fa78b69777c083f5b19555ee4d232d70163"
@ -30695,6 +30718,7 @@
"source": "3f3714587b4802efeb6baf380a59615b8faf0c10",
"translation": "f4fa2d18fff78b531c07f83bbc7753c26dbc05d9"
},
"commands.viewReleaseNotes": null,
"modals.task.titlePlaceholder": {
"source": "c8196f1d1039c2fb290add52ee990c571173d6f1",
"translation": "37d9b0a0a59872345ce5e206027f8eccf793e6fa"

View file

@ -4,12 +4,12 @@
"description": "Note-based task management with calendar, pomodoro and time-tracking integration.",
"main": "main.js",
"scripts": {
"build": "npm run build-css && tsc -noEmit --skipLibCheck && node esbuild.config.mjs production",
"build": "npm run build-css && node generate-release-notes-import.mjs && tsc -noEmit --skipLibCheck && node esbuild.config.mjs production",
"dev": "npm run build && npm run copy-files",
"dev:watch": "npm run build-css && node esbuild.config.mjs",
"dev:watch": "npm run build-css && node generate-release-notes-import.mjs && node esbuild.config.mjs",
"copy-files": "node copy-files.mjs",
"build-css": "node build-css.mjs",
"version": "node version-bump.mjs && git add manifest.json versions.json",
"version": "node version-bump.mjs && node generate-release-notes-import.mjs && git add manifest.json versions.json src/releaseNotes.ts",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",

View file

@ -204,6 +204,11 @@ export const en: TranslationTree = {
minTime: "Min Time (minutes)",
},
},
releaseNotes: {
title: "What's new in TaskNotes {version}",
header: "What's new in TaskNotes {version}",
viewAllLink: "View all release notes on GitHub →",
},
},
settings: {
tabs: {
@ -1496,6 +1501,7 @@ export const en: TranslationTree = {
pauseResumePomodoro: "Pause/resume pomodoro timer",
refreshCache: "Refresh cache",
exportAllTasksIcs: "Export all tasks as ICS file",
viewReleaseNotes: "View release notes",
},
modals: {
task: {

View file

@ -94,6 +94,8 @@ import { AutoExportService } from "./services/AutoExportService";
// Type-only import for HTTPAPIService (actual import is dynamic on desktop only)
import type { HTTPAPIService } from "./services/HTTPAPIService";
import { createI18nService, I18nService, TranslationKey } from "./i18n";
import { ReleaseNotesView, RELEASE_NOTES_VIEW_TYPE } from "./views/ReleaseNotesView";
import { CURRENT_RELEASE_NOTES } from "./releaseNotes";
interface TranslatedCommandDefinition {
id: string;
@ -461,6 +463,11 @@ export default class TaskNotesPlugin extends Plugin {
this.registerView(KANBAN_VIEW_TYPE, (leaf) => new KanbanView(leaf, this));
this.registerView(
RELEASE_NOTES_VIEW_TYPE,
(leaf) => new ReleaseNotesView(leaf, this, CURRENT_RELEASE_NOTES, this.manifest.version)
);
// Register essential editor extensions (now safe after layout ready)
this.registerEditorExtension(createTaskLinkOverlay(this));
@ -639,6 +646,9 @@ export default class TaskNotesPlugin extends Plugin {
// Migration check was moved to early startup - just show prompts here if needed
await this.showMigrationPromptsIfNeeded();
// Check for version updates and show release notes if needed
await this.checkForVersionUpdate();
} catch (error) {
console.error("Error during lazy service initialization:", error);
}
@ -886,6 +896,36 @@ export default class TaskNotesPlugin extends Plugin {
}
}
/**
* Check for version updates and show release notes if needed
*/
private async checkForVersionUpdate(): Promise<void> {
try {
const currentVersion = this.manifest.version;
const lastSeenVersion = this.settings.lastSeenVersion;
// If this is a new install or version has changed, show release notes
if (lastSeenVersion && lastSeenVersion !== currentVersion) {
// Show release notes after a delay to ensure UI is ready
setTimeout(async () => {
await this.activateReleaseNotesView();
// Update lastSeenVersion immediately after showing the release notes
// This ensures they only show once per version
this.settings.lastSeenVersion = currentVersion;
await this.saveSettings();
}, 1500); // Slightly longer delay than migration to avoid conflicts
}
// Update lastSeenVersion if it hasn't been set yet (new install)
if (!lastSeenVersion) {
this.settings.lastSeenVersion = currentVersion;
await this.saveSettings();
}
} catch (error) {
console.error("Error checking for version update:", error);
}
}
/**
* Public method for views to wait for migration completion
*/
@ -1429,6 +1469,13 @@ export default class TaskNotesPlugin extends Plugin {
}
},
},
{
id: "view-release-notes",
nameKey: "commands.viewReleaseNotes",
callback: async () => {
await this.activateReleaseNotesView();
},
},
];
this.registerCommands();
@ -1560,6 +1607,10 @@ export default class TaskNotesPlugin extends Plugin {
return this.activateView(KANBAN_VIEW_TYPE);
}
async activateReleaseNotesView() {
return this.activateView(RELEASE_NOTES_VIEW_TYPE);
}
/**
* Open and activate the search pane with a tag query
* (Renamed from openSearchPaneWithTag for cleaner API)

5
src/markdown.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
// Type declarations for importing markdown files as strings
declare module "*.md" {
const content: string;
export default content;
}

View file

@ -238,6 +238,8 @@ export const DEFAULT_SETTINGS: TaskNotesSettings = {
customPriorities: DEFAULT_PRIORITIES,
// Migration defaults
recurrenceMigrated: false,
// Release notes defaults
lastSeenVersion: undefined,
// Status bar defaults
showTrackedTasksInStatusBar: false,
// Time tracking defaults

View file

@ -1,4 +1,5 @@
// import { TAbstractFile } from 'obsidian';
import { Setting } from "obsidian";
import TaskNotesPlugin from "../../main";
import {
createSectionHeader,
@ -226,4 +227,20 @@ export function renderGeneralTab(
save();
},
});
// Release Notes Section
createSectionHeader(container, "Release Notes");
createHelpText(container, `Current version: ${plugin.manifest.version}`);
new Setting(container)
.setName("View release notes")
.setDesc("See what's new in the latest version of TaskNotes")
.addButton((button) =>
button
.setButtonText("View release notes")
.setCta()
.onClick(async () => {
await plugin.activateReleaseNotesView();
})
);
}

View file

@ -85,6 +85,8 @@ export interface TaskNotesSettings {
customPriorities: PriorityConfig[];
// Migration tracking
recurrenceMigrated?: boolean;
// Release notes tracking
lastSeenVersion?: string;
// Status bar settings
showTrackedTasksInStatusBar: boolean;
// Time tracking settings

View file

@ -0,0 +1,85 @@
import { ItemView, WorkspaceLeaf, MarkdownRenderer } from "obsidian";
import TaskNotesPlugin from "../main";
export const RELEASE_NOTES_VIEW_TYPE = "tasknotes-release-notes";
const GITHUB_RELEASES_URL = "https://github.com/callumalpass/tasknotes/releases";
export class ReleaseNotesView extends ItemView {
plugin: TaskNotesPlugin;
private releaseNotes: string;
private version: string;
constructor(leaf: WorkspaceLeaf, plugin: TaskNotesPlugin, releaseNotes: string, version: string) {
super(leaf);
this.plugin = plugin;
this.releaseNotes = releaseNotes;
this.version = version;
}
getViewType(): string {
return RELEASE_NOTES_VIEW_TYPE;
}
getDisplayText(): string {
return this.plugin.i18n.translate("views.releaseNotes.title", { version: this.version });
}
getIcon(): string {
return "book-open";
}
async onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.addClass("tasknotes-release-notes-view");
// Create a container for the markdown content
const container = contentEl.createDiv({ cls: "tasknotes-release-notes-container" });
container.style.padding = "20px";
container.style.maxWidth = "900px";
container.style.margin = "0 auto";
// Header with version
const header = container.createEl("div", { cls: "release-notes-header" });
header.style.marginBottom = "20px";
header.createEl("h1", {
text: this.plugin.i18n.translate("views.releaseNotes.header", { version: this.version })
});
// Markdown content
const markdownContainer = container.createEl("div", { cls: "release-notes-content" });
markdownContainer.style.marginBottom = "30px";
// Render the markdown
await MarkdownRenderer.render(
this.plugin.app,
this.releaseNotes,
markdownContainer,
"",
this as any
);
// Footer with link to all releases
const footer = container.createEl("div", { cls: "release-notes-footer" });
footer.style.borderTop = "1px solid var(--background-modifier-border)";
footer.style.paddingTop = "20px";
footer.style.textAlign = "center";
const link = footer.createEl("a", {
text: this.plugin.i18n.translate("views.releaseNotes.viewAllLink"),
href: GITHUB_RELEASES_URL,
});
link.style.color = "var(--text-accent)";
link.style.textDecoration = "none";
link.addEventListener("click", (e) => {
e.preventDefault();
window.open(GITHUB_RELEASES_URL, "_blank");
});
}
async onClose() {
const { contentEl } = this;
contentEl.empty();
}
}