Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Rnwood.Smtp4dev/ApiModel/GitHubRelease.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Rnwood.Smtp4dev.ApiModel
{
public class GitHubRelease
{
public string TagName { get; set; }
public string Name { get; set; }
public string Body { get; set; }
public bool Prerelease { get; set; }
public string PublishedAt { get; set; }
public string HtmlUrl { get; set; }
}
}
2 changes: 2 additions & 0 deletions Rnwood.Smtp4dev/ApiModel/Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ public class Server
public bool DisableHtmlValidation { get; set; }
public bool DisableHtmlCompatibilityCheck { get; set; }
public string CommandValidationExpression { get; set; }
public bool DisableWhatsNewNotifications { get; set; }
public bool DisableUpdateNotifications { get; set; }
}

}
13 changes: 13 additions & 0 deletions Rnwood.Smtp4dev/ApiModel/UpdateCheckResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Collections.Generic;

namespace Rnwood.Smtp4dev.ApiModel
{
public class UpdateCheckResult
{
public bool UpdateAvailable { get; set; }
public List<GitHubRelease> NewReleases { get; set; } = new List<GitHubRelease>();
public string CurrentVersion { get; set; }
public bool ShowWhatsNew { get; set; }
public string LastSeenVersion { get; set; }
}
}
8 changes: 8 additions & 0 deletions Rnwood.Smtp4dev/ClientApp/src/ApiClient/GitHubRelease.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default class GitHubRelease {
tagName: string;
name: string;
body: string;
prerelease: boolean;
publishedAt: string;
htmlUrl: string;
}
9 changes: 9 additions & 0 deletions Rnwood.Smtp4dev/ClientApp/src/ApiClient/UpdateCheckResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import GitHubRelease from "./GitHubRelease";

export default class UpdateCheckResult {
updateAvailable: boolean;
newReleases: GitHubRelease[];
currentVersion: string;
showWhatsNew: boolean;
lastSeenVersion: string;
}
26 changes: 26 additions & 0 deletions Rnwood.Smtp4dev/ClientApp/src/ApiClient/UpdatesController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import axios from "axios";
import UpdateCheckResult from "./UpdateCheckResult";

export default class UpdatesController {
constructor() {}

public checkForUpdates_url(username: string | null = null): string {
let url = "api/Updates/check";
if (username) {
url += `?username=${encodeURIComponent(username)}`;
}
return url;
}

public async checkForUpdates(username: string | null = null): Promise<UpdateCheckResult> {
return (await axios.get(this.checkForUpdates_url(username))).data as UpdateCheckResult;
}

public markVersionAsSeen_url(username: string, version: string): string {
return `api/Updates/mark-seen?username=${encodeURIComponent(username)}&version=${encodeURIComponent(version)}`;
}

public async markVersionAsSeen(username: string, version: string): Promise<void> {
await axios.post(this.markVersionAsSeen_url(username, version));
}
}
101 changes: 101 additions & 0 deletions Rnwood.Smtp4dev/ClientApp/src/UpdateNotificationManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import UpdatesController from "./ApiClient/UpdatesController";
import UpdateCheckResult from "./ApiClient/UpdateCheckResult";

export default class UpdateNotificationManager {
private updatesController: UpdatesController;
private lastCheckDate: Date | null = null;
private checkIntervalMs = 24 * 60 * 60 * 1000; // 24 hours
private intervalId: any = null;
private onUpdateAvailableCallback: ((result: UpdateCheckResult) => void) | null = null;
private onWhatsNewCallback: ((result: UpdateCheckResult) => void) | null = null;

constructor() {
this.updatesController = new UpdatesController();
}

public async checkForUpdates(): Promise<UpdateCheckResult> {
const username = this.getUsername();
const result = await this.updatesController.checkForUpdates(username);
this.lastCheckDate = new Date();
this.saveLastCheckDate();

if (result.updateAvailable && this.onUpdateAvailableCallback) {
this.onUpdateAvailableCallback(result);
}

if (result.showWhatsNew && this.onWhatsNewCallback) {
this.onWhatsNewCallback(result);
}

return result;
}

public async markVersionAsSeen(version: string): Promise<void> {
const username = this.getUsername();
await this.updatesController.markVersionAsSeen(username, version);
this.saveSeenVersion(version);
}

public onUpdateAvailable(callback: (result: UpdateCheckResult) => void): void {
this.onUpdateAvailableCallback = callback;
}

public onWhatsNew(callback: (result: UpdateCheckResult) => void): void {
this.onWhatsNewCallback = callback;
}

public startPeriodicCheck(): void {
// Check immediately if we haven't checked today
const lastCheck = this.getLastCheckDate();
if (!lastCheck || this.isDayOld(lastCheck)) {
this.checkForUpdates();
}

// Set up periodic checks
if (this.intervalId) {
clearInterval(this.intervalId);
}

this.intervalId = setInterval(() => {
this.checkForUpdates();
}, this.checkIntervalMs);
}

public stopPeriodicCheck(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}

private getUsername(): string {
// Try to get username from storage or use anonymous
return localStorage.getItem('smtp4dev_username') || 'anonymous';
}

private getLastCheckDate(): Date | null {
const stored = localStorage.getItem('smtp4dev_last_update_check');
if (stored) {
return new Date(stored);
}
return null;
}

private saveLastCheckDate(): void {
localStorage.setItem('smtp4dev_last_update_check', new Date().toISOString());
}

private isDayOld(date: Date): boolean {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
return diffMs >= this.checkIntervalMs;
}

private saveSeenVersion(version: string): void {
localStorage.setItem('smtp4dev_last_seen_version', version);
}

public getSeenVersion(): string | null {
return localStorage.getItem('smtp4dev_last_seen_version');
}
}
62 changes: 61 additions & 1 deletion Rnwood.Smtp4dev/ClientApp/src/components/home/home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
</el-dropdown>
</el-header>
<settingsdialog v-model="settingsVisible" :connection="connection" v-on:closed="showSettings(false)" />
<whatsnewdialog
:visible="whatsNewVisible"
:update-check-result="updateCheckResult"
@close="whatsNewVisible = false"
@dismiss="dismissWhatsNew"
@marked-read="onMarkedRead" />
<el-main class="fill vfillpanel">
<el-tabs id="maintabs" class="fill" v-model="activeTabId" type="border-card">
<el-tab-pane label="Messages" name="messages" class="vfillpanel">
Expand Down Expand Up @@ -95,10 +101,13 @@
import HubConnectionManager from "@/HubConnectionManager";
import ServerStatus from "@/components/serverstatus.vue";
import SettingsDialog from "@/components/settingsdialog.vue";
import WhatsNewDialog from "@/components/whatsnewdialog.vue";
import HubConnectionStatus from "@/components/hubconnectionstatus.vue";
import { Splitpanes, Pane } from 'splitpanes';
import 'splitpanes/dist/splitpanes.css';
import { ElIcon } from "element-plus";
import { ElIcon, ElNotification } from "element-plus";
import UpdateNotificationManager from "@/UpdateNotificationManager";
import UpdateCheckResult from "@/ApiClient/UpdateCheckResult";

@Component({
components: {
Expand All @@ -109,6 +118,7 @@
hubconnstatus: HubConnectionStatus,
serverstatus: ServerStatus,
settingsdialog: SettingsDialog,
whatsnewdialog: WhatsNewDialog,
splitpanes: Splitpanes,
pane: Pane,
VersionInfo,
Expand All @@ -123,6 +133,10 @@
connection: HubConnectionManager | null = null;

settingsVisible: boolean = false;
whatsNewVisible: boolean = false;
updateCheckResult: UpdateCheckResult | null = null;
updateManager = new UpdateNotificationManager();
updateNotification: any = null;

showSettings(visible: boolean) {
this.settingsVisible = visible;
Expand Down Expand Up @@ -185,12 +199,58 @@
async mounted() {
this.connection = new HubConnectionManager("hubs/notifications")
this.connection.start();

// Setup update notification callbacks
this.updateManager.onWhatsNew((result) => {
this.updateCheckResult = result;
this.whatsNewVisible = true;
});

this.updateManager.onUpdateAvailable((result) => {
this.showUpdateNotification(result);
});

// Start periodic update checks
this.updateManager.startPeriodicCheck();
}

destroyed() {
if (this.connection) {
this.connection.stop();
}
this.updateManager.stopPeriodicCheck();
if (this.updateNotification) {
this.updateNotification.close();
}
}

showUpdateNotification(result: UpdateCheckResult) {
if (this.updateNotification) {
this.updateNotification.close();
}

this.updateNotification = ElNotification({
title: 'Update Available',
message: `A new version (${result.newReleases[0]?.tagName}) is available. Click to view release notes.`,
type: 'warning',
duration: 0, // Don't auto-dismiss
onClick: () => {
this.updateCheckResult = result;
this.whatsNewVisible = true;
}
});
}

dismissWhatsNew() {
this.whatsNewVisible = false;
}

async onMarkedRead() {
this.whatsNewVisible = false;
if (this.updateNotification) {
this.updateNotification.close();
this.updateNotification = null;
}
}

}
Expand Down
12 changes: 12 additions & 0 deletions Rnwood.Smtp4dev/ClientApp/src/components/settingsdialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@

<el-switch v-model="server.disableHtmlCompatibilityCheck" :disabled="server.lockedSettings.disableHtmlCompatibilityCheck" />
</el-form-item>

<el-form-item label="Disable 'What's New' notifications" prop="server.disableWhatsNewNotifications">
<el-icon v-if="server.lockedSettings.disableWhatsNewNotifications" :title="`Locked: ${server.lockedSettings.disableWhatsNewNotifications}`"><Lock /></el-icon>

<el-switch v-model="server.disableWhatsNewNotifications" :disabled="server.lockedSettings.disableWhatsNewNotifications" />
</el-form-item>

<el-form-item label="Disable update notifications" prop="server.disableUpdateNotifications">
<el-icon v-if="server.lockedSettings.disableUpdateNotifications" :title="`Locked: ${server.lockedSettings.disableUpdateNotifications}`"><Lock /></el-icon>

<el-switch v-model="server.disableUpdateNotifications" :disabled="server.lockedSettings.disableUpdateNotifications" />
</el-form-item>
</el-tab-pane>
<el-tab-pane label="User Settings" v-if="clientSettings">
<el-form-item label="Page size">
Expand Down
Loading