From 440814564ce796e5e55b121ae769949ff82c3c00 Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
Date: Wed, 26 Feb 2020 19:44:47 +0100
Subject: [PATCH] Improve notifications

---
 api/graphql/generated.go                      | 48 +++++++++++++++++++
 api/graphql/models/generated.go               |  7 ++-
 api/graphql/schema.graphql                    |  4 ++
 api/scanner/album_scanner.go                  | 21 ++++++--
 api/scanner/photo_scanner.go                  | 16 ++++++-
 .../components/messages/SubscriptionsHook.js  | 23 +++++++++
 6 files changed, 112 insertions(+), 7 deletions(-)

diff --git a/api/graphql/generated.go b/api/graphql/generated.go
index 4bfb0c2..9617102 100644
--- a/api/graphql/generated.go
+++ b/api/graphql/generated.go
@@ -90,6 +90,7 @@ type ComplexityRoot struct {
 		Negative func(childComplexity int) int
 		Positive func(childComplexity int) int
 		Progress func(childComplexity int) int
+		Timeout  func(childComplexity int) int
 		Type     func(childComplexity int) int
 	}
 
@@ -504,6 +505,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Notification.Progress(childComplexity), true
 
+	case "Notification.timeout":
+		if e.complexity.Notification.Timeout == nil {
+			break
+		}
+
+		return e.complexity.Notification.Timeout(childComplexity), true
+
 	case "Notification.type":
 		if e.complexity.Notification.Type == nil {
 			break
@@ -1091,6 +1099,8 @@ type Subscription {
 enum NotificationType {
   Message
   Progress
+  "Close a notification with a given key"
+  Close
 }
 
 type Notification {
@@ -1101,6 +1111,8 @@ type Notification {
   progress: Float
   positive: Boolean!
   negative: Boolean!
+  "Time in milliseconds before the notification will close"
+  timeout: Int
 }
 
 type AuthorizeResult {
@@ -2921,6 +2933,40 @@ func (ec *executionContext) _Notification_negative(ctx context.Context, field gr
 	return ec.marshalNBoolean2bool(ctx, field.Selections, res)
 }
 
+func (ec *executionContext) _Notification_timeout(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) {
+	ctx = ec.Tracer.StartFieldExecution(ctx, field)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+		ec.Tracer.EndFieldExecution(ctx)
+	}()
+	rctx := &graphql.ResolverContext{
+		Object:   "Notification",
+		Field:    field,
+		Args:     nil,
+		IsMethod: false,
+	}
+	ctx = graphql.WithResolverContext(ctx, rctx)
+	ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Timeout, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*int)
+	rctx.Result = res
+	ctx = ec.Tracer.StartFieldChildExecution(ctx)
+	return ec.marshalOInt2ᚖint(ctx, field.Selections, res)
+}
+
 func (ec *executionContext) _Photo_id(ctx context.Context, field graphql.CollectedField, obj *models.Photo) (ret graphql.Marshaler) {
 	ctx = ec.Tracer.StartFieldExecution(ctx, field)
 	defer func() {
@@ -6404,6 +6450,8 @@ func (ec *executionContext) _Notification(ctx context.Context, sel ast.Selection
 			if out.Values[i] == graphql.Null {
 				invalids++
 			}
+		case "timeout":
+			out.Values[i] = ec._Notification_timeout(ctx, field, obj)
 		default:
 			panic("unknown field " + strconv.Quote(field.Name))
 		}
diff --git a/api/graphql/models/generated.go b/api/graphql/models/generated.go
index c806d17..7f6c3d9 100644
--- a/api/graphql/models/generated.go
+++ b/api/graphql/models/generated.go
@@ -29,6 +29,8 @@ type Notification struct {
 	Progress *float64         `json:"progress"`
 	Positive bool             `json:"positive"`
 	Negative bool             `json:"negative"`
+	// Time in milliseconds before the notification will close
+	Timeout *int `json:"timeout"`
 }
 
 type PhotoDownload struct {
@@ -55,16 +57,19 @@ type NotificationType string
 const (
 	NotificationTypeMessage  NotificationType = "Message"
 	NotificationTypeProgress NotificationType = "Progress"
+	// Close a notification with a given key
+	NotificationTypeClose NotificationType = "Close"
 )
 
 var AllNotificationType = []NotificationType{
 	NotificationTypeMessage,
 	NotificationTypeProgress,
+	NotificationTypeClose,
 }
 
 func (e NotificationType) IsValid() bool {
 	switch e {
-	case NotificationTypeMessage, NotificationTypeProgress:
+	case NotificationTypeMessage, NotificationTypeProgress, NotificationTypeClose:
 		return true
 	}
 	return false
diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql
index f73236c..aa26b29 100644
--- a/api/graphql/schema.graphql
+++ b/api/graphql/schema.graphql
@@ -93,6 +93,8 @@ type Subscription {
 enum NotificationType {
   Message
   Progress
+  "Close a notification with a given key"
+  Close
 }
 
 type Notification {
@@ -103,6 +105,8 @@ type Notification {
   progress: Float
   positive: Boolean!
   negative: Boolean!
+  "Time in milliseconds before the notification will close"
+  timeout: Int
 }
 
 type AuthorizeResult {
diff --git a/api/scanner/album_scanner.go b/api/scanner/album_scanner.go
index af5bdd8..b9610b1 100644
--- a/api/scanner/album_scanner.go
+++ b/api/scanner/album_scanner.go
@@ -79,6 +79,7 @@ func ScanUser(database *sql.DB, userId int) error {
 func scan(database *sql.DB, user *models.User) {
 
 	notifyKey := utils.GenerateToken()
+	processKey := utils.GenerateToken()
 
 	notification.BroadcastNotification(&models.Notification{
 		Key:     notifyKey,
@@ -166,7 +167,7 @@ func scan(database *sql.DB, user *models.User) {
 					continue
 				}
 
-				if err := ScanPhoto(tx, photoPath, albumId, content_type); err != nil {
+				if err := ScanPhoto(tx, photoPath, albumId, content_type, processKey); err != nil {
 					ScannerError("processing image %s: %s", photoPath, err)
 					tx.Rollback()
 					continue
@@ -199,6 +200,11 @@ func scan(database *sql.DB, user *models.User) {
 		Positive: true,
 	})
 
+	notification.BroadcastNotification(&models.Notification{
+		Key:  processKey,
+		Type: models.NotificationTypeClose,
+	})
+
 	log.Println("Done scanning")
 }
 
@@ -317,20 +323,27 @@ func cleanupCache(database *sql.DB, scanned_albums []interface{}, user *models.U
 		var album_id int
 		rows.Scan(&album_id)
 		deleted_ids = append(deleted_ids, album_id)
+		deleted_albums++
 		cache_path := path.Join("./image-cache", strconv.Itoa(album_id))
 		err := os.RemoveAll(cache_path)
 		if err != nil {
 			ScannerError("Could not delete unused cache folder: %s\n%s\n", cache_path, err)
-		} else {
-			deleted_albums++
 		}
 	}
 
 	if len(deleted_ids) > 0 {
 		albums_questions = strings.Repeat("?,", len(deleted_ids))[:len(deleted_ids)*2-1]
+
 		if _, err := database.Exec("DELETE FROM album WHERE album_id IN ("+albums_questions+")", deleted_ids...); err != nil {
-			log.Printf("ERROR: Could not delete old albums from database:\n%s\n", err)
+			ScannerError("Could not delete old albums from database:\n%s\n", err)
 		}
+
+		notification.BroadcastNotification(&models.Notification{
+			Key:     utils.GenerateToken(),
+			Type:    models.NotificationTypeMessage,
+			Header:  "Deleted old albums",
+			Content: fmt.Sprintf("Deleted %d albums, that was not found", len(deleted_ids)),
+		})
 	}
 
 	log.Printf("Deleted %d unused albums from cache", deleted_albums)
diff --git a/api/scanner/photo_scanner.go b/api/scanner/photo_scanner.go
index c74cb20..91667de 100644
--- a/api/scanner/photo_scanner.go
+++ b/api/scanner/photo_scanner.go
@@ -2,12 +2,15 @@ package scanner
 
 import (
 	"database/sql"
-	"github.com/viktorstrate/photoview/api/graphql/models"
+	"fmt"
 	"log"
 	"path"
+
+	"github.com/viktorstrate/photoview/api/graphql/models"
+	"github.com/viktorstrate/photoview/api/graphql/notification"
 )
 
-func ScanPhoto(tx *sql.Tx, photoPath string, albumId int, content_type *string) error {
+func ScanPhoto(tx *sql.Tx, photoPath string, albumId int, content_type *string, notificationKey string) error {
 
 	log.Printf("Scanning image: %s\n", photoPath)
 
@@ -25,6 +28,15 @@ func ScanPhoto(tx *sql.Tx, photoPath string, albumId int, content_type *string)
 		}
 	}
 
+	notifyTimeout := 5000
+	notification.BroadcastNotification(&models.Notification{
+		Key:     notificationKey,
+		Type:    models.NotificationTypeMessage,
+		Header:  "Scanning photo",
+		Content: fmt.Sprintf("Scanning image at %s", photoPath),
+		Timeout: &notifyTimeout,
+	})
+
 	result, err := tx.Exec("INSERT INTO photo (title, path, album_id) VALUES (?, ?, ?)", photoName, photoPath, albumId)
 	if err != nil {
 		log.Printf("ERROR: Could not insert photo into database")
diff --git a/ui/src/components/messages/SubscriptionsHook.js b/ui/src/components/messages/SubscriptionsHook.js
index beffa8f..f654f08 100644
--- a/ui/src/components/messages/SubscriptionsHook.js
+++ b/ui/src/components/messages/SubscriptionsHook.js
@@ -13,10 +13,13 @@ const notificationSubscription = gql`
       progress
       positive
       negative
+      timeout
     }
   }
 `
 
+let messageTimeoutHandles = new Map()
+
 const SubscriptionsHook = ({ messages, setMessages }) => {
   if (!localStorage.getItem('token')) {
     return null
@@ -46,9 +49,15 @@ const SubscriptionsHook = ({ messages, setMessages }) => {
 
     const msg = data.notification
 
+    if (msg.type == 'Close') {
+      setMessages(messages => messages.filter(m => m.key != msg.key))
+      return
+    }
+
     const newNotification = {
       key: msg.key,
       type: msg.type.toLowerCase(),
+      timeout: msg.timeout,
       props: {
         header: msg.header,
         content: msg.content,
@@ -57,6 +66,20 @@ const SubscriptionsHook = ({ messages, setMessages }) => {
       },
     }
 
+    if (msg.timeout) {
+      // Clear old timeout, to replace it with the new one
+      if (messageTimeoutHandles.get(msg.key)) {
+        const timeoutHandle = messageTimeoutHandles.get(msg.key)
+        clearTimeout(timeoutHandle)
+      }
+
+      const timeoutHandle = setTimeout(() => {
+        setMessages(messages => messages.filter(m => m.key != msg.key))
+      }, msg.timeout)
+
+      messageTimeoutHandles.set(msg.key, timeoutHandle)
+    }
+
     const notifyIndex = newMessages.findIndex(
       msg => msg.key == newNotification.key
     )
