mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
linux?
This commit is contained in:
parent
5ad3d73001
commit
e86cf68b17
1 changed files with 446 additions and 102 deletions
|
|
@ -4,24 +4,79 @@ package notifications
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"git.sr.ht/~whereswaldon/shout"
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
var NotificationLock sync.RWMutex
|
||||
var NotificationCategories = make(map[string]NotificationCategory)
|
||||
var Notifier shout.Notifier
|
||||
var appName string
|
||||
const (
|
||||
dbusObjectPath = "/org/freedesktop/Notifications"
|
||||
dbusNotificationsInterface = "org.freedesktop.Notifications"
|
||||
signalNotificationClosed = "org.freedesktop.Notifications.NotificationClosed"
|
||||
signalActionInvoked = "org.freedesktop.Notifications.ActionInvoked"
|
||||
callGetCapabilities = "org.freedesktop.Notifications.GetCapabilities"
|
||||
callCloseNotification = "org.freedesktop.Notifications.CloseNotification"
|
||||
|
||||
// Creates a new Notifications Service.
|
||||
MethodNotifySend = "notify-send"
|
||||
MethodDbus = "dbus"
|
||||
MethodKdialog = "kdialog"
|
||||
|
||||
notifyChannelBufferSize = 25
|
||||
)
|
||||
|
||||
var (
|
||||
notificationLock sync.RWMutex
|
||||
notificationCategories = make(map[string]NotificationCategory)
|
||||
appName string
|
||||
initOnce sync.Once
|
||||
)
|
||||
|
||||
type closedReason uint32
|
||||
|
||||
func (r closedReason) string() string {
|
||||
switch r {
|
||||
case 1:
|
||||
return "expired"
|
||||
case 2:
|
||||
return "dismissed-by-user"
|
||||
case 3:
|
||||
return "closed-by-call"
|
||||
case 4:
|
||||
return "unknown"
|
||||
case 5:
|
||||
return "activated-by-user"
|
||||
default:
|
||||
return "other"
|
||||
}
|
||||
}
|
||||
|
||||
// internalNotifier handles the actual notification sending via dbus or command line
|
||||
type notificationContext struct {
|
||||
ID string
|
||||
SystemID uint32
|
||||
Actions map[string]string // Maps action keys to display labels
|
||||
UserData map[string]interface{} // The original user data
|
||||
}
|
||||
|
||||
type internalNotifier struct {
|
||||
sync.Mutex
|
||||
method string
|
||||
dbusConn *dbus.Conn
|
||||
sendPath string
|
||||
activeNotifs map[string]uint32 // Maps our notification IDs to system IDs
|
||||
contexts map[string]*notificationContext // Stores notification contexts by our ID
|
||||
}
|
||||
|
||||
var notifier *internalNotifier
|
||||
|
||||
// New creates a new Notifications Service
|
||||
func New() *Service {
|
||||
if NotificationService == nil {
|
||||
NotificationService = &Service{}
|
||||
|
|
@ -37,78 +92,236 @@ func (ns *Service) ServiceStartup(ctx context.Context, options application.Servi
|
|||
fmt.Printf("Failed to load notification categories: %v\n", err)
|
||||
}
|
||||
|
||||
conn, err := dbus.SessionBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to D-Bus session bus: %v", err)
|
||||
notifier = &internalNotifier{
|
||||
activeNotifs: make(map[string]uint32),
|
||||
contexts: make(map[string]*notificationContext),
|
||||
}
|
||||
|
||||
var iconPath string
|
||||
|
||||
Notifier, err = shout.NewNotifier(conn, appName, iconPath, func(notificationID, action string, platformData map[string]dbus.Variant, target, notifierResponse dbus.Variant, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n---------- NOTIFICATION RESPONSE DEBUG ----------\n")
|
||||
fmt.Printf("notificationID: %s\n", notificationID)
|
||||
fmt.Printf("action: %s\n", action)
|
||||
fmt.Printf("platformData: %+v\n", platformData)
|
||||
fmt.Printf("target: %+v (signature: %s)\n", target, target.Signature())
|
||||
fmt.Printf("response: %+v (signature: %s)\n", notifierResponse, notifierResponse.Signature())
|
||||
fmt.Printf("----------------------------------------------\n")
|
||||
|
||||
response := NotificationResponse{
|
||||
ID: notificationID,
|
||||
ActionIdentifier: action,
|
||||
}
|
||||
|
||||
if target.Signature().String() == "s" {
|
||||
var targetStr string
|
||||
if err := target.Store(&targetStr); err == nil {
|
||||
var userInfo map[string]interface{}
|
||||
userInfoStr, err := base64.StdEncoding.DecodeString(targetStr)
|
||||
if err != nil {
|
||||
if err := json.Unmarshal([]byte(targetStr), &userInfo); err == nil {
|
||||
response.UserInfo = userInfo
|
||||
}
|
||||
}
|
||||
if err := json.Unmarshal(userInfoStr, &userInfo); err == nil {
|
||||
response.UserInfo = userInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if notifierResponse.Signature().String() == "s" {
|
||||
// var userText string
|
||||
// if err := notifierResponse.Store(&userText); err == nil {
|
||||
// response.UserText = userText
|
||||
// }
|
||||
// }
|
||||
|
||||
if NotificationService != nil {
|
||||
NotificationService.handleNotificationResponse(response)
|
||||
}
|
||||
var err error
|
||||
initOnce.Do(func() {
|
||||
err = notifier.init()
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create notifier: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
// ServiceShutdown is called when the service is unloaded
|
||||
func (ns *Service) ServiceShutdown() error {
|
||||
if notifier != nil && notifier.dbusConn != nil {
|
||||
notifier.dbusConn.Close()
|
||||
}
|
||||
return saveCategories()
|
||||
}
|
||||
|
||||
// Initialize the notifier and choose the best available notification method
|
||||
func (n *internalNotifier) init() error {
|
||||
var err error
|
||||
|
||||
checkDbus := func() (*dbus.Conn, error) {
|
||||
conn, err := dbus.SessionBusPrivate()
|
||||
if err != nil {
|
||||
return conn, err
|
||||
}
|
||||
|
||||
if err = conn.Auth(nil); err != nil {
|
||||
return conn, err
|
||||
}
|
||||
|
||||
if err = conn.Hello(); err != nil {
|
||||
return conn, err
|
||||
}
|
||||
|
||||
obj := conn.Object(dbusNotificationsInterface, dbusObjectPath)
|
||||
call := obj.Call(callGetCapabilities, 0)
|
||||
if call.Err != nil {
|
||||
return conn, call.Err
|
||||
}
|
||||
|
||||
var ret []string
|
||||
err = call.Store(&ret)
|
||||
if err != nil {
|
||||
return conn, err
|
||||
}
|
||||
|
||||
// Add a listener for notification signals
|
||||
err = conn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath(dbusObjectPath),
|
||||
dbus.WithMatchInterface(dbusNotificationsInterface),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// Try dbus first
|
||||
n.dbusConn, err = checkDbus()
|
||||
if err == nil {
|
||||
n.method = MethodDbus
|
||||
// Start the dbus signal listener
|
||||
go n.startDBusListener()
|
||||
return nil
|
||||
}
|
||||
if n.dbusConn != nil {
|
||||
n.dbusConn.Close()
|
||||
n.dbusConn = nil
|
||||
}
|
||||
|
||||
// Try notify-send
|
||||
send, err := exec.LookPath("notify-send")
|
||||
if err == nil {
|
||||
n.sendPath = send
|
||||
n.method = MethodNotifySend
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try sw-notify-send
|
||||
send, err = exec.LookPath("sw-notify-send")
|
||||
if err == nil {
|
||||
n.sendPath = send
|
||||
n.method = MethodNotifySend
|
||||
return nil
|
||||
}
|
||||
|
||||
// No method available
|
||||
n.method = "none"
|
||||
n.sendPath = ""
|
||||
|
||||
return errors.New("no notification method is available")
|
||||
}
|
||||
|
||||
// startDBusListener listens for DBus signals for notification actions and closures
|
||||
func (n *internalNotifier) startDBusListener() {
|
||||
signal := make(chan *dbus.Signal, notifyChannelBufferSize)
|
||||
n.dbusConn.Signal(signal)
|
||||
|
||||
for s := range signal {
|
||||
if s == nil || len(s.Body) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch s.Name {
|
||||
case signalNotificationClosed:
|
||||
systemID := s.Body[0].(uint32)
|
||||
reason := closedReason(s.Body[1].(uint32)).string()
|
||||
n.handleNotificationClosed(systemID, reason)
|
||||
case signalActionInvoked:
|
||||
systemID := s.Body[0].(uint32)
|
||||
actionKey := s.Body[1].(string)
|
||||
n.handleActionInvoked(systemID, actionKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleNotificationClosed processes notification closed signals
|
||||
func (n *internalNotifier) handleNotificationClosed(systemID uint32, reason string) {
|
||||
// Find our notification ID for this system ID
|
||||
var notifID string
|
||||
var userData map[string]interface{}
|
||||
|
||||
n.Lock()
|
||||
for id, sysID := range n.activeNotifs {
|
||||
if sysID == systemID {
|
||||
notifID = id
|
||||
// Get the user data from context if available
|
||||
if ctx, exists := n.contexts[id]; exists {
|
||||
userData = ctx.UserData
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
n.Unlock()
|
||||
|
||||
if notifID != "" && NotificationService != nil {
|
||||
response := NotificationResponse{
|
||||
ID: notifID,
|
||||
ActionIdentifier: DefaultActionIdentifier,
|
||||
UserInfo: userData,
|
||||
}
|
||||
|
||||
// Add reason to UserInfo or create it if none exists
|
||||
if response.UserInfo == nil {
|
||||
response.UserInfo = map[string]interface{}{
|
||||
"reason": reason,
|
||||
}
|
||||
} else {
|
||||
response.UserInfo["reason"] = reason
|
||||
}
|
||||
|
||||
NotificationService.handleNotificationResponse(response)
|
||||
|
||||
// Clean up the context
|
||||
n.Lock()
|
||||
delete(n.contexts, notifID)
|
||||
delete(n.activeNotifs, notifID)
|
||||
n.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// handleActionInvoked processes action invoked signals
|
||||
func (n *internalNotifier) handleActionInvoked(systemID uint32, actionKey string) {
|
||||
// Find our notification ID and context for this system ID
|
||||
var notifID string
|
||||
var ctx *notificationContext
|
||||
|
||||
n.Lock()
|
||||
for id, sysID := range n.activeNotifs {
|
||||
if sysID == systemID {
|
||||
notifID = id
|
||||
ctx = n.contexts[id]
|
||||
break
|
||||
}
|
||||
}
|
||||
n.Unlock()
|
||||
|
||||
if notifID != "" && NotificationService != nil {
|
||||
// First, send the action response with the user data
|
||||
response := NotificationResponse{
|
||||
ID: notifID,
|
||||
ActionIdentifier: actionKey,
|
||||
}
|
||||
|
||||
// Include the user data if we have it
|
||||
if ctx != nil {
|
||||
response.UserInfo = ctx.UserData
|
||||
}
|
||||
|
||||
NotificationService.handleNotificationResponse(response)
|
||||
|
||||
// Then, trigger a closed event with "activated-by-user" reason
|
||||
closeResponse := NotificationResponse{
|
||||
ID: notifID,
|
||||
ActionIdentifier: DefaultActionIdentifier,
|
||||
}
|
||||
|
||||
// Include the same user data in the close response
|
||||
if ctx != nil {
|
||||
closeResponse.UserInfo = ctx.UserData
|
||||
} else {
|
||||
closeResponse.UserInfo = map[string]interface{}{}
|
||||
}
|
||||
|
||||
// Add the reason to the user info
|
||||
closeResponse.UserInfo["reason"] = closedReason(5).string() // "activated-by-user"
|
||||
|
||||
NotificationService.handleNotificationResponse(closeResponse)
|
||||
|
||||
// Clean up the context
|
||||
n.Lock()
|
||||
delete(n.contexts, notifID)
|
||||
delete(n.activeNotifs, notifID)
|
||||
n.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// CheckBundleIdentifier is a Linux stub that always returns true.
|
||||
// (bundle identifiers are macOS-specific)
|
||||
func CheckBundleIdentifier() bool {
|
||||
func (ns *Service) CheckBundleIdentifier() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// RequestUserNotificationAuthorization is a Linux stub that always returns true, nil.
|
||||
// RequestUserNotificationAuthorization is a Linux stub that always returns true.
|
||||
// (user authorization is macOS-specific)
|
||||
func (ns *Service) RequestUserNotificationAuthorization() (bool, error) {
|
||||
return true, nil
|
||||
|
|
@ -122,77 +335,191 @@ func (ns *Service) CheckNotificationAuthorization() (bool, error) {
|
|||
|
||||
// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body.
|
||||
func (ns *Service) SendNotification(options NotificationOptions) error {
|
||||
notification := shout.Notification{
|
||||
Title: options.Title,
|
||||
Body: options.Body,
|
||||
Priority: shout.Normal,
|
||||
DefaultAction: DefaultActionIdentifier,
|
||||
if notifier == nil {
|
||||
return errors.New("notification service not initialized")
|
||||
}
|
||||
|
||||
if options.Data != nil {
|
||||
jsonData, err := json.Marshal(options.Data)
|
||||
if err == nil {
|
||||
notification.DefaultActionTarget = dbus.MakeVariant(base64.StdEncoding.EncodeToString(jsonData))
|
||||
notifier.Lock()
|
||||
defer notifier.Unlock()
|
||||
|
||||
var (
|
||||
systemID uint32
|
||||
err error
|
||||
)
|
||||
|
||||
switch notifier.method {
|
||||
case MethodDbus:
|
||||
systemID, err = notifier.sendViaDbus(options, nil)
|
||||
case MethodNotifySend:
|
||||
systemID, err = notifier.sendViaNotifySend(options)
|
||||
default:
|
||||
err = errors.New("no notification method is available")
|
||||
}
|
||||
|
||||
if err == nil && systemID > 0 {
|
||||
// Store the system ID mapping
|
||||
notifier.activeNotifs[options.ID] = systemID
|
||||
|
||||
// Create and store the notification context
|
||||
ctx := ¬ificationContext{
|
||||
ID: options.ID,
|
||||
SystemID: systemID,
|
||||
UserData: options.Data,
|
||||
}
|
||||
notifier.contexts[options.ID] = ctx
|
||||
}
|
||||
|
||||
return Notifier.Send(options.ID, notification)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendNotificationWithActions sends a notification with additional actions and inputs.
|
||||
// SendNotificationWithActions sends a notification with additional actions.
|
||||
func (ns *Service) SendNotificationWithActions(options NotificationOptions) error {
|
||||
NotificationLock.RLock()
|
||||
category, exists := NotificationCategories[options.CategoryID]
|
||||
NotificationLock.RUnlock()
|
||||
if notifier == nil {
|
||||
return errors.New("notification service not initialized")
|
||||
}
|
||||
|
||||
notificationLock.RLock()
|
||||
category, exists := notificationCategories[options.CategoryID]
|
||||
notificationLock.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return ns.SendNotification(options)
|
||||
}
|
||||
|
||||
notification := shout.Notification{
|
||||
Title: options.Title,
|
||||
Body: options.Body,
|
||||
Priority: shout.Normal,
|
||||
DefaultAction: DefaultActionIdentifier,
|
||||
notifier.Lock()
|
||||
defer notifier.Unlock()
|
||||
|
||||
var (
|
||||
systemID uint32
|
||||
err error
|
||||
)
|
||||
|
||||
switch notifier.method {
|
||||
case MethodDbus:
|
||||
systemID, err = notifier.sendViaDbus(options, &category)
|
||||
case MethodNotifySend:
|
||||
// notify-send doesn't support actions, fall back to basic notification
|
||||
systemID, err = notifier.sendViaNotifySend(options)
|
||||
default:
|
||||
err = errors.New("no notification method is available")
|
||||
}
|
||||
|
||||
if options.Data != nil {
|
||||
jsonData, err := json.Marshal(options.Data)
|
||||
if err == nil {
|
||||
notification.DefaultActionTarget = dbus.MakeVariant(base64.StdEncoding.EncodeToString(jsonData))
|
||||
if err == nil && systemID > 0 {
|
||||
// Store the system ID mapping
|
||||
notifier.activeNotifs[options.ID] = systemID
|
||||
|
||||
// Create and store the notification context with actions
|
||||
ctx := ¬ificationContext{
|
||||
ID: options.ID,
|
||||
SystemID: systemID,
|
||||
UserData: options.Data,
|
||||
Actions: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
for _, action := range category.Actions {
|
||||
var targetStr string = ""
|
||||
if options.Data != nil {
|
||||
if jsonData, err := json.Marshal(options.Data); err == nil {
|
||||
targetStr = base64.StdEncoding.EncodeToString(jsonData)
|
||||
// Store action mappings
|
||||
if exists {
|
||||
for _, action := range category.Actions {
|
||||
ctx.Actions[action.ID] = action.Title
|
||||
}
|
||||
}
|
||||
notification.Buttons = append(notification.Buttons, shout.Button{
|
||||
Label: action.Title,
|
||||
Action: action.ID,
|
||||
Target: targetStr,
|
||||
})
|
||||
|
||||
notifier.contexts[options.ID] = ctx
|
||||
}
|
||||
|
||||
return Notifier.Send(options.ID, notification)
|
||||
return err
|
||||
}
|
||||
|
||||
// sendViaDbus sends a notification via dbus
|
||||
func (n *internalNotifier) sendViaDbus(options NotificationOptions, category *NotificationCategory) (result uint32, err error) {
|
||||
// Prepare actions
|
||||
var actions []string
|
||||
if category != nil {
|
||||
for _, action := range category.Actions {
|
||||
actions = append(actions, action.ID, action.Title)
|
||||
}
|
||||
}
|
||||
|
||||
// Default timeout (-1 means use system default)
|
||||
timeout := int32(-1)
|
||||
|
||||
// Prepare hints
|
||||
hints := map[string]dbus.Variant{
|
||||
// Normal urgency by default
|
||||
"urgency": dbus.MakeVariant(byte(1)),
|
||||
}
|
||||
|
||||
// Add user data to hints if available
|
||||
if options.Data != nil {
|
||||
if userData, err := json.Marshal(options.Data); err == nil {
|
||||
hints["x-wails-user-data"] = dbus.MakeVariant(string(userData))
|
||||
}
|
||||
}
|
||||
|
||||
// Send the notification
|
||||
obj := n.dbusConn.Object(dbusNotificationsInterface, dbusObjectPath)
|
||||
dbusArgs := []interface{}{
|
||||
appName, // App name
|
||||
uint32(0), // Replaces ID (0 means new notification)
|
||||
"", // App icon (empty for now)
|
||||
options.Title, // Title
|
||||
options.Body, // Body
|
||||
actions, // Actions
|
||||
hints, // Hints
|
||||
timeout, // Timeout
|
||||
}
|
||||
|
||||
call := obj.Call("org.freedesktop.Notifications.Notify", 0, dbusArgs...)
|
||||
if call.Err != nil {
|
||||
return 0, fmt.Errorf("dbus notification error: %v", call.Err)
|
||||
}
|
||||
|
||||
err = call.Store(&result)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// sendViaNotifySend sends a notification via notify-send command
|
||||
func (n *internalNotifier) sendViaNotifySend(options NotificationOptions) (uint32, error) {
|
||||
args := []string{
|
||||
options.Title,
|
||||
options.Body,
|
||||
}
|
||||
|
||||
// Add icon if eventually supported
|
||||
// if options.Icon != "" { ... }
|
||||
|
||||
// Add urgency (normal by default)
|
||||
args = append(args, "--urgency=normal")
|
||||
|
||||
// Execute the command
|
||||
cmd := exec.Command(n.sendPath, args...)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("notify-send error: %v", err)
|
||||
}
|
||||
|
||||
// notify-send doesn't return IDs, so we use 0
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
|
||||
func (ns *Service) RegisterNotificationCategory(category NotificationCategory) error {
|
||||
NotificationLock.Lock()
|
||||
NotificationCategories[category.ID] = category
|
||||
NotificationLock.Unlock()
|
||||
notificationLock.Lock()
|
||||
notificationCategories[category.ID] = category
|
||||
notificationLock.Unlock()
|
||||
|
||||
return saveCategories()
|
||||
}
|
||||
|
||||
// RemoveNotificationCategory removes a previously registered NotificationCategory.
|
||||
func (ns *Service) RemoveNotificationCategory(categoryId string) error {
|
||||
NotificationLock.Lock()
|
||||
delete(NotificationCategories, categoryId)
|
||||
NotificationLock.Unlock()
|
||||
notificationLock.Lock()
|
||||
delete(notificationCategories, categoryId)
|
||||
notificationLock.Unlock()
|
||||
|
||||
return saveCategories()
|
||||
}
|
||||
|
||||
|
|
@ -222,7 +549,24 @@ func (ns *Service) RemoveDeliveredNotification(_ string) error {
|
|||
|
||||
// RemoveNotification removes a notification by ID (Linux-specific)
|
||||
func (ns *Service) RemoveNotification(identifier string) error {
|
||||
return Notifier.Remove(identifier)
|
||||
if notifier == nil || notifier.method != MethodDbus || notifier.dbusConn == nil {
|
||||
return errors.New("dbus not available for closing notifications")
|
||||
}
|
||||
|
||||
// Get the system ID for this notification
|
||||
notifier.Lock()
|
||||
systemID, exists := notifier.activeNotifs[identifier]
|
||||
notifier.Unlock()
|
||||
|
||||
if !exists {
|
||||
return nil // Already closed or unknown
|
||||
}
|
||||
|
||||
// Call CloseNotification on dbus
|
||||
obj := notifier.dbusConn.Object(dbusNotificationsInterface, dbusObjectPath)
|
||||
call := obj.Call(callCloseNotification, 0, systemID)
|
||||
|
||||
return call.Err
|
||||
}
|
||||
|
||||
// getConfigFilePath returns the path to the configuration file for storing notification categories
|
||||
|
|
@ -247,9 +591,9 @@ func saveCategories() error {
|
|||
return err
|
||||
}
|
||||
|
||||
NotificationLock.RLock()
|
||||
data, err := json.Marshal(NotificationCategories)
|
||||
NotificationLock.RUnlock()
|
||||
notificationLock.RLock()
|
||||
data, err := json.Marshal(notificationCategories)
|
||||
notificationLock.RUnlock()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal notification categories: %v", err)
|
||||
|
|
@ -287,9 +631,9 @@ func loadCategories() error {
|
|||
return fmt.Errorf("failed to unmarshal notification categories: %v", err)
|
||||
}
|
||||
|
||||
NotificationLock.Lock()
|
||||
NotificationCategories = categories
|
||||
NotificationLock.Unlock()
|
||||
notificationLock.Lock()
|
||||
notificationCategories = categories
|
||||
notificationLock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue