feat: run scheduled native notifications
This commit is contained in:
parent
96dd499e0b
commit
e33c63d3ad
|
|
@ -39,10 +39,15 @@ import (
|
|||
|
||||
var newSyncClient = syncsvc.NewClient
|
||||
var emitFrontendEvent = runtime.EventsEmit
|
||||
var initializeNativeNotifications = runtime.InitializeNotifications
|
||||
var cleanupNativeNotifications = runtime.CleanupNotifications
|
||||
var sendNativeNotification = runtime.SendNotification
|
||||
|
||||
type notificationService interface {
|
||||
Replace(pluginID string, requests []notifications.Request) error
|
||||
Clear(pluginID string) error
|
||||
Start(ctx context.Context)
|
||||
Stop()
|
||||
}
|
||||
|
||||
const pluginEventRuntimeName = "verstak:plugin-event"
|
||||
|
|
@ -192,6 +197,43 @@ func (a *App) Startup(ctx context.Context) {
|
|||
log.Printf("[api] App.Startup: initialized with %d plugins", len(a.plugins))
|
||||
}
|
||||
|
||||
// DomReady initializes the native notification runtime before starting schedules.
|
||||
func (a *App) DomReady(ctx context.Context) {
|
||||
if a.notifications == nil {
|
||||
return
|
||||
}
|
||||
if err := initializeNativeNotifications(ctx); err != nil {
|
||||
log.Printf("[api] native notifications unavailable: %v", err)
|
||||
return
|
||||
}
|
||||
a.notifications.Start(ctx)
|
||||
}
|
||||
|
||||
// Shutdown stops scheduled delivery before releasing native notification resources.
|
||||
func (a *App) Shutdown(ctx context.Context) {
|
||||
if a.notifications != nil {
|
||||
a.notifications.Stop()
|
||||
}
|
||||
cleanupNativeNotifications(ctx)
|
||||
}
|
||||
|
||||
// NativeNotificationSender delivers scheduler items through the Wails runtime.
|
||||
type NativeNotificationSender struct{}
|
||||
|
||||
// NewNativeNotificationSender creates the adapter used by the core scheduler.
|
||||
func NewNativeNotificationSender() notifications.Sender {
|
||||
return NativeNotificationSender{}
|
||||
}
|
||||
|
||||
// Send shows a native system notification for a scheduled plugin reminder.
|
||||
func (NativeNotificationSender) Send(ctx context.Context, item notifications.Item) error {
|
||||
return sendNativeNotification(ctx, runtime.NotificationOptions{
|
||||
ID: "verstak:" + item.PluginID + ":" + item.ID,
|
||||
Title: item.Title,
|
||||
Body: item.Body,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *App) ensureBrowserInboxSubscriptions() {
|
||||
if a.eventBus == nil || a.storage == nil {
|
||||
a.browserInboxEnabled.Store(false)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
|
||||
"github.com/verstak/verstak-desktop/internal/core/appsettings"
|
||||
"github.com/verstak/verstak-desktop/internal/core/browserreceiver"
|
||||
"github.com/verstak/verstak-desktop/internal/core/capability"
|
||||
|
|
@ -31,8 +33,12 @@ import (
|
|||
type fakeNotificationScheduler struct {
|
||||
replaceCalls int
|
||||
clearCalls int
|
||||
startCalls int
|
||||
stopCalls int
|
||||
pluginID string
|
||||
requests []notifications.Request
|
||||
onStart func()
|
||||
onStop func()
|
||||
}
|
||||
|
||||
func (s *fakeNotificationScheduler) Replace(pluginID string, requests []notifications.Request) error {
|
||||
|
|
@ -48,6 +54,20 @@ func (s *fakeNotificationScheduler) Clear(pluginID string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *fakeNotificationScheduler) Start(context.Context) {
|
||||
s.startCalls++
|
||||
if s.onStart != nil {
|
||||
s.onStart()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *fakeNotificationScheduler) Stop() {
|
||||
s.stopCalls++
|
||||
if s.onStop != nil {
|
||||
s.onStop()
|
||||
}
|
||||
}
|
||||
|
||||
func newLocalHTTPTestServer(t *testing.T, handler http.Handler) *httptest.Server {
|
||||
t.Helper()
|
||||
|
||||
|
|
@ -205,6 +225,87 @@ func TestReplaceAndClearPluginNotificationsStayWithinPluginNamespace(t *testing.
|
|||
}
|
||||
}
|
||||
|
||||
func TestDomReadyInitializesNotificationsBeforeScheduler(t *testing.T) {
|
||||
oldInitialize := initializeNativeNotifications
|
||||
defer func() { initializeNativeNotifications = oldInitialize }()
|
||||
|
||||
order := []string{}
|
||||
initializeNativeNotifications = func(context.Context) error {
|
||||
order = append(order, "initialize")
|
||||
return nil
|
||||
}
|
||||
app, scheduler := newNotificationsTestApp(plugin.Manifest{ID: "notifications.test"})
|
||||
scheduler.onStart = func() { order = append(order, "start") }
|
||||
|
||||
app.DomReady(context.Background())
|
||||
|
||||
if got, want := strings.Join(order, ","), "initialize,start"; got != want {
|
||||
t.Fatalf("notification startup order = %q, want %q", got, want)
|
||||
}
|
||||
if scheduler.startCalls != 1 {
|
||||
t.Fatalf("scheduler start calls = %d, want 1", scheduler.startCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomReadyDoesNotStartSchedulerWhenNotificationInitializationFails(t *testing.T) {
|
||||
oldInitialize := initializeNativeNotifications
|
||||
defer func() { initializeNativeNotifications = oldInitialize }()
|
||||
|
||||
initializeNativeNotifications = func(context.Context) error { return fmt.Errorf("unavailable") }
|
||||
app, scheduler := newNotificationsTestApp(plugin.Manifest{ID: "notifications.test"})
|
||||
|
||||
app.DomReady(context.Background())
|
||||
|
||||
if scheduler.startCalls != 0 {
|
||||
t.Fatalf("scheduler start calls = %d, want 0 when native notifications are unavailable", scheduler.startCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShutdownStopsSchedulerBeforeCleaningUpNotifications(t *testing.T) {
|
||||
oldCleanup := cleanupNativeNotifications
|
||||
defer func() { cleanupNativeNotifications = oldCleanup }()
|
||||
|
||||
order := []string{}
|
||||
cleanupNativeNotifications = func(context.Context) { order = append(order, "cleanup") }
|
||||
app, scheduler := newNotificationsTestApp(plugin.Manifest{ID: "notifications.test"})
|
||||
scheduler.onStop = func() { order = append(order, "stop") }
|
||||
|
||||
app.Shutdown(context.Background())
|
||||
|
||||
if got, want := strings.Join(order, ","), "stop,cleanup"; got != want {
|
||||
t.Fatalf("notification shutdown order = %q, want %q", got, want)
|
||||
}
|
||||
if scheduler.stopCalls != 1 {
|
||||
t.Fatalf("scheduler stop calls = %d, want 1", scheduler.stopCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeNotificationSenderUsesStablePluginScopedID(t *testing.T) {
|
||||
oldSend := sendNativeNotification
|
||||
defer func() { sendNativeNotification = oldSend }()
|
||||
|
||||
var gotTitle, gotBody, gotID string
|
||||
sendNativeNotification = func(_ context.Context, options runtime.NotificationOptions) error {
|
||||
gotID = options.ID
|
||||
gotTitle = options.Title
|
||||
gotBody = options.Body
|
||||
return nil
|
||||
}
|
||||
|
||||
err := NewNativeNotificationSender().Send(context.Background(), notifications.Item{
|
||||
PluginID: "verstak.todo",
|
||||
ID: "task-42",
|
||||
Title: "Todo reminder",
|
||||
Body: "Prepare alpha release",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Send() error = %v", err)
|
||||
}
|
||||
if gotID != "verstak:verstak.todo:task-42" || gotTitle != "Todo reminder" || gotBody != "Prepare alpha release" {
|
||||
t.Fatalf("native notification = id=%q title=%q body=%q", gotID, gotTitle, gotBody)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPluginFrontendInfo_KnownPluginWithFrontend verifies that
|
||||
// GetPluginFrontendInfo returns correct metadata for a plugin with a frontend.
|
||||
func TestGetPluginFrontendInfo_KnownPluginWithFrontend(t *testing.T) {
|
||||
|
|
|
|||
6
main.go
6
main.go
|
|
@ -4,6 +4,7 @@ import (
|
|||
"embed"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
|
|
@ -16,6 +17,7 @@ import (
|
|||
"github.com/verstak/verstak-desktop/internal/core/contribution"
|
||||
"github.com/verstak/verstak-desktop/internal/core/events"
|
||||
corefiles "github.com/verstak/verstak-desktop/internal/core/files"
|
||||
"github.com/verstak/verstak-desktop/internal/core/notifications"
|
||||
"github.com/verstak/verstak-desktop/internal/core/permissions"
|
||||
"github.com/verstak/verstak-desktop/internal/core/plugin"
|
||||
"github.com/verstak/verstak-desktop/internal/core/pluginstate"
|
||||
|
|
@ -91,6 +93,7 @@ func main() {
|
|||
"verstak/core/events/v1",
|
||||
"verstak/core/files/v1",
|
||||
"verstak/core/workbench/v1",
|
||||
"verstak/core/notifications/v1",
|
||||
}
|
||||
if err := capRegistry.Register(corePluginID, coreCaps); err != nil {
|
||||
log.Fatalf("[main] failed to register core capabilities: %v", err)
|
||||
|
|
@ -232,6 +235,7 @@ func main() {
|
|||
})
|
||||
}
|
||||
app = api.NewApp(capRegistry, contribRegistry, permRegistry, eventBus, plugins, vaultService, storageService, filesService, appSettingsMgr, pluginStateMgr, workspaceMgr, syncService, browserReceiver, debugEnabled)
|
||||
app.SetNotificationService(notifications.New(vaultService, api.NewNativeNotificationSender(), time.Now))
|
||||
if browserReceiver != nil {
|
||||
browserReceiverServer, err := browserreceiver.Start(browserreceiver.DefaultAddr, browserReceiver)
|
||||
if err != nil {
|
||||
|
|
@ -251,6 +255,8 @@ func main() {
|
|||
MinHeight: 600,
|
||||
WindowStartState: options.Normal,
|
||||
OnStartup: app.Startup,
|
||||
OnDomReady: app.DomReady,
|
||||
OnShutdown: app.Shutdown,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue