mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-11 22:49:29 +08:00
316 lines
8.6 KiB
Go
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
|
|
}
|