5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-11 22:49:29 +08:00
wails/v3/pkg/services/notifications/notifications_windows.go
Zach Botterman be716fc0ee fix input id
2025-02-24 16:47:19 -08:00

316 lines
8.6 KiB
Go

//go:build windows
package notifications
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"git.sr.ht/~jackmordaunt/go-toast/v2"
"github.com/wailsapp/wails/v3/pkg/application"
"golang.org/x/sys/windows/registry"
)
var NotificationLock sync.RWMutex
var NotificationCategories = make(map[string]NotificationCategory)
const (
defaultAction = "defaultActionIdentifier"
dataSeparator = ":::"
)
func New() *Service {
return &Service{}
}
// ServiceName returns the name of the service
func (ns *Service) ServiceName() string {
return "github.com/wailsapp/wails/v3/services/notifications"
}
// ServiceStartup is called when the service is loaded
// Sets an activation callback to emit an event when notifications are interacted with.
func (ns *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
toast.SetAppData(toast.AppData{
AppID: "Notifications",
GUID: "{8F2E1A3D-C497-42B6-9E5D-72F8A169B051}",
IconPath: "C:\\Users\\Zach\\Development\\notifications_demo\\build\\appicon.ico",
ActivationExe: "C:\\Users\\Zach\\Development\\notifications_demo\\bin\\Notifications.exe",
})
toast.SetActivationCallback(func(args string, data []toast.UserData) {
println("HERE!")
actionIdentifier, userInfo := parseNotificationResponse(args)
response := NotificationResponse{
Name: "notification",
Data: NotificationResponseData{
ActionIdentifier: actionIdentifier,
},
}
if userInfo != "" {
jsonStr := strings.ReplaceAll(userInfo, "'", "\"")
var userInfoMap map[string]interface{}
if err := json.Unmarshal([]byte(jsonStr), &userInfoMap); err == nil {
response.Data.UserInfo = userInfoMap
}
}
if userText, found := getUserText(data); found {
response.Data.UserText = userText
}
application.Get().EmitEvent("notificationResponse", response)
})
return loadCategoriesFromRegistry()
}
// ServiceShutdown is called when the service is unloaded
func (ns *Service) ServiceShutdown() error {
return saveCategoriesToRegistry()
}
// CheckBundleIdentifier is a Windows stub that always returns true.
// (bundle identifiers are macOS-specific)
func CheckBundleIdentifier() bool {
return true
}
// RequestUserNotificationAuthorization is a Windows stub that always returns true, nil.
// (user authorization is macOS-specific)
func (ns *Service) RequestUserNotificationAuthorization() (bool, error) {
return true, nil
}
// CheckNotificationAuthorization is a Windows stub that always returns true.
// (user authorization is macOS-specific)
func (ns *Service) CheckNotificationAuthorization() bool {
return true
}
// SendNotification sends a basic notification with a name, title, and body. All other options are ignored on Windows.
// (subtitle and category id are only available on macOS)
func (ns *Service) SendNotification(options NotificationOptions) error {
n := toast.Notification{
AppID: options.ID,
Title: options.Title,
Body: options.Body,
ActivationArguments: defaultAction,
Audio: toast.IM,
}
if options.Data != nil {
jsonData, err := json.Marshal(options.Data)
if err == nil {
n.ActivationArguments = defaultAction + dataSeparator + strings.ReplaceAll(string(jsonData), "\"", "'")
}
}
err := n.Push()
if err != nil {
return err
}
return nil
}
// SendNotificationWithActions sends a notification with additional actions and inputs.
// A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category.
// If a NotificationCategory is not registered a basic notification will be sent.
// (subtitle and category id are only available on macOS)
func (ns *Service) SendNotificationWithActions(options NotificationOptions) error {
NotificationLock.RLock()
nCategory := NotificationCategories[options.CategoryID]
NotificationLock.RUnlock()
n := toast.Notification{
AppID: options.ID,
Title: options.Title,
Body: options.Body,
ActivationArguments: defaultAction,
Audio: toast.IM,
}
for _, action := range nCategory.Actions {
n.Actions = append(n.Actions, toast.Action{
Content: action.Title,
Arguments: action.ID,
})
}
if nCategory.HasReplyField {
n.Inputs = append(n.Inputs, toast.Input{
ID: "userText",
Title: nCategory.ReplyButtonTitle,
Placeholder: nCategory.ReplyPlaceholder,
})
n.Actions = append(n.Actions, toast.Action{
Content: nCategory.ReplyButtonTitle,
Arguments: "TEXT_REPLY",
InputID: "userText",
})
}
if options.Data != nil {
jsonData, err := json.Marshal(options.Data)
if err == nil {
n.ActivationArguments = defaultAction + dataSeparator + strings.ReplaceAll(string(jsonData), "\"", "'")
for index := range n.Actions {
n.Actions[index].Arguments = n.Actions[index].Arguments + dataSeparator + strings.ReplaceAll(string(jsonData), "\"", "'")
}
}
}
err := n.Push()
if err != nil {
return err
}
return nil
}
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
// Registering a category with the same name as a previously registered NotificationCategory will override it.
func (ns *Service) RegisterNotificationCategory(category NotificationCategory) error {
NotificationLock.Lock()
NotificationCategories[category.ID] = NotificationCategory{
ID: category.ID,
Actions: category.Actions,
HasReplyField: bool(category.HasReplyField),
ReplyPlaceholder: category.ReplyPlaceholder,
ReplyButtonTitle: category.ReplyButtonTitle,
}
NotificationLock.Unlock()
return saveCategoriesToRegistry()
}
// RemoveNotificationCategory removes a previously registered NotificationCategory.
func (ns *Service) RemoveNotificationCategory(categoryId string) error {
NotificationLock.Lock()
delete(NotificationCategories, categoryId)
NotificationLock.Unlock()
return saveCategoriesToRegistry()
}
// RemoveAllPendingNotifications is a Windows stub that always returns nil.
// (macOS-specific)
func (ns *Service) RemoveAllPendingNotifications() error {
return nil
}
// RemovePendingNotification is a Windows stub that always returns nil.
// (macOS-specific)
func (ns *Service) RemovePendingNotification(_ string) error {
return nil
}
// RemoveAllDeliveredNotifications is a Windows stub that always returns nil.
// (macOS-specific)
func (ns *Service) RemoveAllDeliveredNotifications() error {
return nil
}
// RemoveDeliveredNotification is a Windows stub that always returns nil.
// (macOS-specific)
func (ns *Service) RemoveDeliveredNotification(_ string) error {
return nil
}
func parseNotificationResponse(response string) (action string, data string) {
parts := strings.Split(response, dataSeparator)
if len(parts) == 1 {
return parts[0], ""
}
return parts[0], parts[1]
}
// Is there a better way for me to grab this from the Wails config?
func getExeName() string {
executable, err := os.Executable()
if err != nil {
return ""
}
return strings.TrimSuffix(filepath.Base(executable), filepath.Ext(executable))
}
func saveCategoriesToRegistry() error {
appName := getExeName()
if appName == "" {
return fmt.Errorf("failed to save categories to registry: empty executable name")
}
registryPath := fmt.Sprintf(`SOFTWARE\%s\NotificationCategories`, appName)
key, _, err := registry.CreateKey(
registry.CURRENT_USER,
registryPath,
registry.ALL_ACCESS,
)
if err != nil {
return err
}
defer key.Close()
NotificationLock.RLock()
data, err := json.Marshal(NotificationCategories)
NotificationLock.RUnlock()
if err != nil {
return err
}
return key.SetStringValue("Categories", string(data))
}
func loadCategoriesFromRegistry() error {
appName := getExeName()
if appName == "" {
return fmt.Errorf("failed to save categories to registry: empty executable name")
}
registryPath := fmt.Sprintf(`SOFTWARE\%s\NotificationCategories`, appName)
key, err := registry.OpenKey(
registry.CURRENT_USER,
registryPath,
registry.QUERY_VALUE,
)
if err != nil {
if err == registry.ErrNotExist {
return nil
}
return err
}
defer key.Close()
data, _, err := key.GetStringValue("Categories")
if err != nil {
return err
}
categories := make(map[string]NotificationCategory)
if err := json.Unmarshal([]byte(data), &categories); err != nil {
return err
}
NotificationLock.Lock()
NotificationCategories = categories
NotificationLock.Unlock()
return nil
}
func getUserText(data []toast.UserData) (string, bool) {
for _, d := range data {
if d.Key == "userText" {
return d.Value, true
}
}
return "", false
}